From 93e3f0b9c8fa5a82b8c905b661f74cffbd54603f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 20:00:34 +0200 Subject: [PATCH 01/78] Update image.dart Implement toByteData() --- .../lib/src/engine/compositor/image.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 61ed3c13c866d..e6953aa7c1402 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -72,7 +72,23 @@ class CkImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final js.JsObject imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit['AlphaType']['Premul'], + 'colorType': canvasKit['ColorType']['RGBA_8888'], + 'width': width, + 'height': height, + }); + bytes = skImage.callMethod('readPixels', [imageInfo, 0, 0]); + } else { + final js.JsObject skData = skImage.callMethod('encodeToData'); //defaults to PNG 100% + bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + } + + final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + return Future.value(data); } } From cc827d63ab80d9c45537e3a4e1892c1938d5ddc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 20:02:04 +0200 Subject: [PATCH 02/78] Update html_image_codec.dart Implement toByteData() --- lib/web_ui/lib/src/engine/html_image_codec.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 cc8b30c1d27ef..e68d7778651c3 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -158,7 +158,13 @@ class HtmlImage implements ui.Image { // `Picture`s. /// Returns an error message on failure, null on success. String _toByteData(int format, Callback callback) { - callback(null); - return 'Image.toByteData is not supported in Flutter for Web'; + if (imgElement.src.startsWith('data:')) { + final data = UriData.fromUri(Uri.parse(imgElement.src)); + callback(data.contentAsBytes()); + return null; + } else { + callback(null); + return 'Data URI not found'; + } } } From 6b9fd732b49a13893900178eaa30c082dd005b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 20:23:02 +0200 Subject: [PATCH 03/78] rebase --- lib/web_ui/lib/src/engine/compositor/picture.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index 49d6fea0c3731..6f2d3af3ce467 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -22,8 +22,12 @@ class CkPicture implements ui.Picture { @override Future toImage(int width, int height) { - throw UnsupportedError( - 'Picture.toImage not yet implemented for CanvasKit and HTML'); + final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); + final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); + skCanvas.callMethod('drawPicture', [skPicture]); + final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); + skSurface.callMethod('dispose'); + return SkImage(skImage); } } From 352b31a01a876710a153a02612c9a5e6170773ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 20:34:08 +0200 Subject: [PATCH 04/78] Update image.dart Null-aware. --- lib/web_ui/lib/src/engine/compositor/image.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index e6953aa7c1402..aaa261ab09e79 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -81,9 +81,9 @@ class CkImage implements ui.Image { 'width': width, 'height': height, }); - bytes = skImage.callMethod('readPixels', [imageInfo, 0, 0]); + bytes = skImage!.callMethod('readPixels', [imageInfo, 0, 0]); } else { - final js.JsObject skData = skImage.callMethod('encodeToData'); //defaults to PNG 100% + final js.JsObject skData = skImage!.callMethod('encodeToData'); //defaults to PNG 100% bytes = canvasKit.callMethod('getSkDataBytes', [skData]); } From 4ea7ef2a51a9cb6e49f9e1eb3f59b9614ed44d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 21:01:08 +0200 Subject: [PATCH 05/78] Update picture.dart Sk* to Ck* --- lib/web_ui/lib/src/engine/compositor/picture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index 6f2d3af3ce467..dc73812429c61 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -27,7 +27,7 @@ class CkPicture implements ui.Picture { skCanvas.callMethod('drawPicture', [skPicture]); final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); skSurface.callMethod('dispose'); - return SkImage(skImage); + return CkImage(skImage); } } From f99c8b2d49baa2f6a2e46358fbee25bb8815932b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 23:38:12 +0200 Subject: [PATCH 06/78] Update picture.dart --- lib/web_ui/lib/src/engine/compositor/picture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index dc73812429c61..fc955dfe6c8a9 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -24,7 +24,7 @@ class CkPicture implements ui.Picture { Future toImage(int width, int height) { final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); - skCanvas.callMethod('drawPicture', [skPicture]); + skCanvas.callMethod('drawPicture', [skPicture.skiaObject]); final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); skSurface.callMethod('dispose'); return CkImage(skImage); From 1e7188f1fbef1d98b04faf79168f3ae323e24552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 23:54:57 +0200 Subject: [PATCH 07/78] Update picture.dart Null-aware --- lib/web_ui/lib/src/engine/compositor/picture.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index fc955dfe6c8a9..74d149615d090 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -21,10 +21,10 @@ class CkPicture implements ui.Picture { } @override - Future toImage(int width, int height) { + Future toImage(int width, int height) async { final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); - skCanvas.callMethod('drawPicture', [skPicture.skiaObject]); + skCanvas.callMethod('drawPicture', [skPicture.skiaObject!]); final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); skSurface.callMethod('dispose'); return CkImage(skImage); From 4017e40007c478430d35256f267dba5e088075cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sat, 11 Jul 2020 00:13:00 +0200 Subject: [PATCH 08/78] Update html_image_codec.dart --- lib/web_ui/lib/src/engine/html_image_codec.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 e68d7778651c3..227e5ff74184d 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -154,14 +154,12 @@ class HtmlImage implements ui.Image { } } - // TODO(het): Support this for asset images and images generated from - // `Picture`s. - /// Returns an error message on failure, null on success. + // Returns an error message on failure. String _toByteData(int format, Callback callback) { if (imgElement.src.startsWith('data:')) { final data = UriData.fromUri(Uri.parse(imgElement.src)); callback(data.contentAsBytes()); - return null; + return ''; } else { callback(null); return 'Data URI not found'; From ed811cf0ce5bc2044c3be145304b62cc121f2e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sat, 11 Jul 2020 01:11:34 +0200 Subject: [PATCH 09/78] Update image.dart Type arguments --- lib/web_ui/lib/src/engine/compositor/image.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index aaa261ab09e79..df3ac0d88866d 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -75,7 +75,7 @@ class CkImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final js.JsObject imageInfo = js.JsObject.jsify({ + final js.JsObject imageInfo = js.JsObject.jsify({ 'alphaType': canvasKit['AlphaType']['Premul'], 'colorType': canvasKit['ColorType']['RGBA_8888'], 'width': width, @@ -84,7 +84,7 @@ class CkImage implements ui.Image { bytes = skImage!.callMethod('readPixels', [imageInfo, 0, 0]); } else { final js.JsObject skData = skImage!.callMethod('encodeToData'); //defaults to PNG 100% - bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + bytes = canvasKit.callMethod('getSkDataBytes', [skData]); } final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); From d6dc4100fa7bdbb5a684c7eaaf2039feae410880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sun, 12 Jul 2020 14:29:33 +0200 Subject: [PATCH 10/78] Update image.dart Same for CkAnimatedImage --- .../lib/src/engine/compositor/image.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index df3ac0d88866d..02448ee989a93 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -48,7 +48,23 @@ class CkAnimatedImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final js.JsObject imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit['AlphaType']['Premul'], + 'colorType': canvasKit['ColorType']['RGBA_8888'], + 'width': width, + 'height': height, + }); + bytes = _skAnimatedImage!.callMethod('readPixels', [imageInfo, 0, 0]); + } else { + final js.JsObject skData = _skAnimatedImage!.callMethod('encodeToData'); //defaults to PNG 100% + bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + } + + final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + return Future.value(data); } } From c2c70e4fa751542b9699005a9a283a4e6f20ba85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Thu, 23 Jul 2020 12:42:27 +0200 Subject: [PATCH 11/78] Revert --- lib/web_ui/lib/src/engine/compositor/picture.dart | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index 74d149615d090..56d4525070c03 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -6,11 +6,10 @@ part of engine; class CkPicture implements ui.Picture { - final SkiaObject skiaObject; + final SkiaObject skiaObject; final ui.Rect? cullRect; - CkPicture(SkPicture picture, this.cullRect) - : skiaObject = SkPictureSkiaObject(picture); + CkPicture(SkPicture picture, this.cullRect); @override int get approximateBytesUsed => 0; @@ -30,12 +29,3 @@ class CkPicture implements ui.Picture { return CkImage(skImage); } } - -class SkPictureSkiaObject extends OneShotSkiaObject { - SkPictureSkiaObject(SkPicture picture) : super(picture); - - @override - void delete() { - rawSkiaObject?.delete(); - } -} From 8b1ed1a6eba3a7b808dddd203631e78efacf97b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Fri, 24 Jul 2020 23:42:23 +0200 Subject: [PATCH 12/78] Tested by local engine --- .../lib/src/engine/html_image_codec.dart | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) 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 227e5ff74184d..b9f4c03d54c7a 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -133,13 +133,13 @@ class HtmlImage implements ui.Image { final int height; @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - return futurize((Callback callback) { - return _toByteData(format.index, (Uint8List? encoded) { - callback(encoded?.buffer.asByteData()); - }); - }); + Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + if (imgElement.src.startsWith('data:')) { + final data = UriData.fromUri(Uri.parse(imgElement.src)); + return Future.value(data.contentAsBytes().buffer.asByteData()); + } else { + return Future.value(null); + } } // Returns absolutely positioned actual image element on first call and @@ -153,16 +153,4 @@ class HtmlImage implements ui.Image { return imgElement; } } - - // Returns an error message on failure. - String _toByteData(int format, Callback callback) { - if (imgElement.src.startsWith('data:')) { - final data = UriData.fromUri(Uri.parse(imgElement.src)); - callback(data.contentAsBytes()); - return ''; - } else { - callback(null); - return 'Data URI not found'; - } - } } From bad4d4553a7eaf4bbcd19bf56014e6746eda5d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sat, 25 Jul 2020 00:49:35 +0200 Subject: [PATCH 13/78] Add files via upload --- .../engine/picture_to_image_test.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart diff --git a/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart b/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart new file mode 100644 index 0000000000000..ef53d4c643a3f --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart @@ -0,0 +1,52 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Picture to Image', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Image testImage = await _drawTestPictureWithCircle(region); + builder.addImage(Offset.zero, testImage); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('picture_to_image.png', region: region, write: true); + }); +} + +Future _drawTestPictureWithCircle(Rect region) async { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00); + final Picture picture = pictureRecorder.endRecording(); + return picture.toImage(region.width, region.height); +} From 22dbcf7250cd029cd081cf6b63ebee88043e5841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sat, 25 Jul 2020 00:53:31 +0200 Subject: [PATCH 14/78] Delete picture_to_image_test.dart --- .../engine/picture_to_image_test.dart | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart diff --git a/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart b/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart deleted file mode 100644 index ef53d4c643a3f..0000000000000 --- a/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart +++ /dev/null @@ -1,52 +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. - -// @dart = 2.6 -import 'dart:html' as html; - -import 'package:ui/ui.dart'; -import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; - -import 'package:web_engine_tester/golden_tester.dart'; - -void main() async { - final Rect region = Rect.fromLTWH(0, 0, 500, 500); - - setUp(() async { - debugShowClipLayers = true; - SurfaceSceneBuilder.debugForgetFrameScene(); - for (html.Node scene in html.document.querySelectorAll('flt-scene')) { - scene.remove(); - } - - await webOnlyInitializePlatform(); - webOnlyFontCollection.debugRegisterTestFonts(); - await webOnlyFontCollection.ensureFontsLoaded(); - }); - - test('Convert Picture to Image', () async { - final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); - final Image testImage = await _drawTestPictureWithCircle(region); - builder.addImage(Offset.zero, testImage); - - html.document.body.append(builder - .build() - .webOnlyRootElement); - - await matchGoldenFile('picture_to_image.png', region: region, write: true); - }); -} - -Future _drawTestPictureWithCircle(Rect region) async { - final EnginePictureRecorder recorder = PictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(region); - canvas.drawOval( - region, - Paint() - ..style = PaintingStyle.fill - ..color = Color(0xFF00FF00); - final Picture picture = pictureRecorder.endRecording(); - return picture.toImage(region.width, region.height); -} From 5eb7b8ae1e1b41f40dbd73b3198c9674929cc6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sat, 25 Jul 2020 19:42:21 +0200 Subject: [PATCH 15/78] Test #1 --- .../engine/canvas_to_picture_test.dart | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart new file mode 100644 index 0000000000000..7f3675ce06314 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -0,0 +1,51 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Canvas to Picture', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = await _drawTestPictureWithCircle(region); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + }); +} + +Picture _drawTestPictureWithCircle(Rect region) { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00); + return pictureRecorder.endRecording(); +} From 1feae026a19015b87518d534a23de65ce0def5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sun, 26 Jul 2020 14:51:10 +0200 Subject: [PATCH 16/78] Test to read only --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index 7f3675ce06314..c305f0903fe68 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -35,7 +35,7 @@ void main() async { .build() .webOnlyRootElement); - await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + await matchGoldenFile('canvas_to_picture.png', region: region); }); } From 4c9001b9b21f0f785704d00aabb08d373b7608ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 21:03:01 +0200 Subject: [PATCH 17/78] Finally --- lib/web_ui/lib/src/engine/compositor/picture.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index 56d4525070c03..aa20763c993a3 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -6,10 +6,11 @@ part of engine; class CkPicture implements ui.Picture { - final SkiaObject skiaObject; + final SkiaObject skiaObject; final ui.Rect? cullRect; - CkPicture(SkPicture picture, this.cullRect); +CkPicture(SkPicture picture, this.cullRect) + : skiaObject = SkPictureSkiaObject(picture); @override int get approximateBytesUsed => 0; @@ -29,3 +30,12 @@ class CkPicture implements ui.Picture { return CkImage(skImage); } } + +class SkPictureSkiaObject extends OneShotSkiaObject { + SkPictureSkiaObject(SkPicture picture) : super(picture); + + @override + void delete() { + rawSkiaObject?.delete(); + } +} \ No newline at end of file From a2ff894bdf2aef58a406ae300ed2f151a787b2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 21:08:25 +0200 Subject: [PATCH 18/78] Sorry, spaces --- lib/web_ui/lib/src/engine/compositor/picture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index aa20763c993a3..76c4fb9869220 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -9,7 +9,7 @@ class CkPicture implements ui.Picture { final SkiaObject skiaObject; final ui.Rect? cullRect; -CkPicture(SkPicture picture, this.cullRect) + CkPicture(SkPicture picture, this.cullRect) : skiaObject = SkPictureSkiaObject(picture); @override From 0c7249629df31f2526151e1155cce40259823523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 21:18:00 +0200 Subject: [PATCH 19/78] Test, finally --- .../test/golden_tests/engine/canvas_to_picture_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index c305f0903fe68..604bef4d3bd2c 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -46,6 +46,6 @@ Picture _drawTestPictureWithCircle(Rect region) { region, Paint() ..style = PaintingStyle.fill - ..color = Color(0xFF00FF00); - return pictureRecorder.endRecording(); + ..color = Color(0xFF00FF00)); + return recorder.endRecording(); } From 91b12cafde41b0ce7f47b31ea86c88821353c12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 23:54:40 +0200 Subject: [PATCH 20/78] @JS refactoring --- .../src/engine/compositor/canvaskit_api.dart | 74 +++++++++++++++++++ .../lib/src/engine/compositor/image.dart | 24 +++--- .../lib/src/engine/compositor/picture.dart | 10 +-- .../lib/src/engine/html_image_codec.dart | 4 +- 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index ab51ab4134a44..2d4361c060f5a 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -30,6 +30,8 @@ class CanvasKit { external SkBlurStyleEnum get BlurStyle; external SkTileModeEnum get TileMode; external SkFillTypeEnum get FillType; + external SkAlphaTypeEnum get AlphaType; + external SkColorTypeEnum get ColorType; external SkPathOpEnum get PathOp; external SkClipOpEnum get ClipOp; external SkPointModeEnum get PointMode; @@ -59,6 +61,13 @@ class CanvasKit { external SkParagraphBuilderNamespace get ParagraphBuilder; external SkParagraphStyle ParagraphStyle(SkParagraphStyleProperties properties); external SkTextStyle TextStyle(SkTextStyleProperties properties); + external SkSurface MakeSurface( + int width, + int height, + ); + external Uint8List getSkDataBytes( + SkData skData, + ); // Text decoration enum is embedded in the CanvasKit object itself. external int get NoDecoration; @@ -121,6 +130,7 @@ class SkSurface { external int width(); external int height(); external void dispose(); + external SkImage makeImageSnapshot(); } @JS() @@ -617,6 +627,66 @@ SkTileMode toSkTileMode(ui.TileMode mode) { return _skTileModes[mode.index]; } +@JS() +class SkAlphaTypeEnum { + external SkAlphaType get Opaque; + external SkAlphaType get Premul; + external SkAlphaType get Unpremul; +} + +@JS() +class SkAlphaType { + external int get value; +} + +final List _skAlphaTypes = [ + canvasKit.AlphaType.Opaque, + canvasKit.AlphaType.Premul, + canvasKit.AlphaType.Unpremul, +]; + +//SkAlphaType toSkAlphaType(ui.AlphaType alphaType) { +// return _skAlphaTypes[alphaType.index]; +//} + +@JS() +class SkColorTypeEnum { + external SkColorType get Alpha_8; + external SkColorType get RGB_565; + external SkColorType get ARGB_4444; + external SkColorType get RGBA_8888; + external SkColorType get RGB_888x; + external SkColorType get BGRA_8888; + external SkColorType get RGBA_1010102; + external SkColorType get RGB_101010x; + external SkColorType get Gray_8; + external SkColorType get RGBA_F16; + external SkColorType get RGBA_F32; +} + +@JS() +class SkColorType { + external int get value; +} + +final List _skColorTypes = [ + canvasKit.ColorType.Alpha_8, + canvasKit.ColorType.RGB_565, + canvasKit.ColorType.ARGB_4444, + canvasKit.ColorType.RGBA_8888, + canvasKit.ColorType.RGB_888x, + canvasKit.ColorType.BGRA_8888, + canvasKit.ColorType.RGBA_1010102, + canvasKit.ColorType.RGB_101010x, + canvasKit.ColorType.Gray_8, + canvasKit.ColorType.RGBA_F16, + canvasKit.ColorType.RGBA_F32, +]; + +//SkColorType toSkColorType(ui.ColorType colorType) { +// return _skColorTypes[colorType.index]; +//} + @JS() class SkAnimatedImage { external int getFrameCount(); @@ -626,6 +696,8 @@ class SkAnimatedImage { external SkImage getCurrentFrame(); external int width(); external int height(); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); /// Deletes the C++ object. /// @@ -639,6 +711,8 @@ class SkImage { external int width(); external int height(); external SkShader makeShader(SkTileMode tileModeX, SkTileMode tileModeY); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); } @JS() diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 02448ee989a93..f4be47d682c14 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -51,16 +51,16 @@ class CkAnimatedImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final js.JsObject imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit['AlphaType']['Premul'], - 'colorType': canvasKit['ColorType']['RGBA_8888'], + final SkImageInfo imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit.AlphaType.Premul, + 'colorType': canvasKit.ColorType.RGBA_8888, 'width': width, 'height': height, }); - bytes = _skAnimatedImage!.callMethod('readPixels', [imageInfo, 0, 0]); + bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { - final js.JsObject skData = _skAnimatedImage!.callMethod('encodeToData'); //defaults to PNG 100% - bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% + bytes = canvasKit.getSkDataBytes(skData); } final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); @@ -91,16 +91,16 @@ class CkImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final js.JsObject imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit['AlphaType']['Premul'], - 'colorType': canvasKit['ColorType']['RGBA_8888'], + final SkImageInfo imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit.AlphaType.Premul, + 'colorType': canvasKit.ColorType.RGBA_8888, 'width': width, 'height': height, }); - bytes = skImage!.callMethod('readPixels', [imageInfo, 0, 0]); + bytes = skImage.readPixels(imageInfo, 0, 0); } else { - final js.JsObject skData = skImage!.callMethod('encodeToData'); //defaults to PNG 100% - bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + final SkData skData = skImage.encodeToData(); //defaults to PNG 100% + bytes = canvasKit.getSkDataBytes(skData); } final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index 76c4fb9869220..fdc7c1c5d62a3 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -22,11 +22,11 @@ class CkPicture implements ui.Picture { @override Future toImage(int width, int height) async { - final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); - final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); - skCanvas.callMethod('drawPicture', [skPicture.skiaObject!]); - final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); - skSurface.callMethod('dispose'); + final SkSurface skSurface = canvasKit.MakeSurface(width, height); + final SkCanvas skCanvas = skSurface.getCanvas(); + skCanvas.drawPicture(skiaObject.skiaObject); + final SkImage skImage = skSurface.makeImageSnapshot(); + skSurface.dispose(); return CkImage(skImage); } } 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 b9f4c03d54c7a..2d68b45ce801d 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -134,8 +134,8 @@ class HtmlImage implements ui.Image { @override Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - if (imgElement.src.startsWith('data:')) { - final data = UriData.fromUri(Uri.parse(imgElement.src)); + if (imgElement.src?.startsWith('data:') == true) { + final data = UriData.fromUri(Uri.parse(imgElement.src!)); return Future.value(data.contentAsBytes().buffer.asByteData()); } else { return Future.value(null); From 9eeb7e0b4f6483eb1e802fe8821275eff7a0b7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 00:18:17 +0200 Subject: [PATCH 21/78] @JS refactoring --- .../lib/src/engine/compositor/image.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index f4be47d682c14..b527ad86be767 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -51,12 +51,12 @@ class CkAnimatedImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit.AlphaType.Premul, - 'colorType': canvasKit.ColorType.RGBA_8888, - 'width': width, - 'height': height, - }); + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + width: width, + height: height, + ); bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% @@ -91,12 +91,12 @@ class CkImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit.AlphaType.Premul, - 'colorType': canvasKit.ColorType.RGBA_8888, - 'width': width, - 'height': height, - }); + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + width: width, + height: height, + ); bytes = skImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% From 6efd7633f559b0b9ea87f49ce412d51a0c9801df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 11:43:18 +0200 Subject: [PATCH 22/78] SkData and SkImageInfo --- .../src/engine/compositor/canvaskit_api.dart | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 2d4361c060f5a..1a2db6b3e8d72 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -645,7 +645,7 @@ final List _skAlphaTypes = [ canvasKit.AlphaType.Unpremul, ]; -//SkAlphaType toSkAlphaType(ui.AlphaType alphaType) { +// TODO SkAlphaType toSkAlphaType(ui.AlphaType alphaType) { // return _skAlphaTypes[alphaType.index]; //} @@ -683,7 +683,7 @@ final List _skColorTypes = [ canvasKit.ColorType.RGBA_F32, ]; -//SkColorType toSkColorType(ui.ColorType colorType) { +// TODO SkColorType toSkColorType(ui.ColorType colorType) { // return _skColorTypes[colorType.index]; //} @@ -1611,3 +1611,41 @@ class SkFontMgrNamespace { // TODO(yjbanov): can this be made non-null? It returns null in our unit-tests right now. external SkFontMgr? FromData(List fonts); } + +@JS() +class SkData { + external int size(); + external bool isEmpty(); + external Uint8List bytes(); +} + +@JS() +@anonymous +class SkImageInfo { + external SkAlphaType get AlphaType; + external int get BitsPerPixel; + external int get BytesPerPixel; + external int get BytesSize; + external int get BytesSize64; + external SkColorSpace get ColorSpace; + external SkColorType get ColorType; + external int get Height; + external bool get IsEmpty; + external bool get IsOpaque; + external SkRect get Rect; + external int get RowBytes; + external int get RowBytes64; + // TODO external SkSize get Size; + external int get Width; + external factory SkImageInfo({ + required int width, + required int height, + SkAlphaType alphaType, + SkColorSpace colorSpace, + SkColorType colorType, + }); + external SkImageInfo WithAlphaType(SkAlphaType alphaType); + external SkImageInfo WithColorSpace(SkColorSpace colorSpace); + external SkImageInfo WithColorType(SkColorType colorType); + external SkImageInfo WithSize(int width, int height); +} From 22d2f96bdd78c958de4b31fd1239b130fd2fd487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 12:08:01 +0200 Subject: [PATCH 23/78] Unused items --- .../src/engine/compositor/canvaskit_api.dart | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 1a2db6b3e8d72..420fedc38ccb1 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -639,11 +639,11 @@ class SkAlphaType { external int get value; } -final List _skAlphaTypes = [ - canvasKit.AlphaType.Opaque, - canvasKit.AlphaType.Premul, - canvasKit.AlphaType.Unpremul, -]; +// TODO final List _skAlphaTypes = [ +// canvasKit.AlphaType.Opaque, +// canvasKit.AlphaType.Premul, +// canvasKit.AlphaType.Unpremul, +//]; // TODO SkAlphaType toSkAlphaType(ui.AlphaType alphaType) { // return _skAlphaTypes[alphaType.index]; @@ -669,19 +669,19 @@ class SkColorType { external int get value; } -final List _skColorTypes = [ - canvasKit.ColorType.Alpha_8, - canvasKit.ColorType.RGB_565, - canvasKit.ColorType.ARGB_4444, - canvasKit.ColorType.RGBA_8888, - canvasKit.ColorType.RGB_888x, - canvasKit.ColorType.BGRA_8888, - canvasKit.ColorType.RGBA_1010102, - canvasKit.ColorType.RGB_101010x, - canvasKit.ColorType.Gray_8, - canvasKit.ColorType.RGBA_F16, - canvasKit.ColorType.RGBA_F32, -]; +// TODO final List _skColorTypes = [ +// canvasKit.ColorType.Alpha_8, +// canvasKit.ColorType.RGB_565, +// canvasKit.ColorType.ARGB_4444, +// canvasKit.ColorType.RGBA_8888, +// canvasKit.ColorType.RGB_888x, +// canvasKit.ColorType.BGRA_8888, +// canvasKit.ColorType.RGBA_1010102, +// canvasKit.ColorType.RGB_101010x, +// canvasKit.ColorType.Gray_8, +// canvasKit.ColorType.RGBA_F16, +// canvasKit.ColorType.RGBA_F32, +//]; // TODO SkColorType toSkColorType(ui.ColorType colorType) { // return _skColorTypes[colorType.index]; From f1d49bed53a6509d09e2b7ff1ba1e08643b39a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 12:50:50 +0200 Subject: [PATCH 24/78] Test to write PNG --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index 604bef4d3bd2c..fe6f69cf37652 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -35,7 +35,7 @@ void main() async { .build() .webOnlyRootElement); - await matchGoldenFile('canvas_to_picture.png', region: region); + await matchGoldenFile('canvas_to_picture.png', region: region, write: true); }); } From 6675e13d526a1da1342153bc4136be8bd40fedcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 4 Aug 2020 21:55:50 +0200 Subject: [PATCH 25/78] Remove comments --- .../src/engine/compositor/canvaskit_api.dart | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 420fedc38ccb1..85cf64d25c1ba 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -4,7 +4,7 @@ /// Bindings for CanvasKit JavaScript API. /// -/// Prefer keeping the originl CanvasKit names so it is easier to locate +/// Prefer keeping the original CanvasKit names so it is easier to locate /// the API behind these bindings in the Skia source code. // @dart = 2.10 @@ -639,16 +639,6 @@ class SkAlphaType { external int get value; } -// TODO final List _skAlphaTypes = [ -// canvasKit.AlphaType.Opaque, -// canvasKit.AlphaType.Premul, -// canvasKit.AlphaType.Unpremul, -//]; - -// TODO SkAlphaType toSkAlphaType(ui.AlphaType alphaType) { -// return _skAlphaTypes[alphaType.index]; -//} - @JS() class SkColorTypeEnum { external SkColorType get Alpha_8; @@ -669,24 +659,6 @@ class SkColorType { external int get value; } -// TODO final List _skColorTypes = [ -// canvasKit.ColorType.Alpha_8, -// canvasKit.ColorType.RGB_565, -// canvasKit.ColorType.ARGB_4444, -// canvasKit.ColorType.RGBA_8888, -// canvasKit.ColorType.RGB_888x, -// canvasKit.ColorType.BGRA_8888, -// canvasKit.ColorType.RGBA_1010102, -// canvasKit.ColorType.RGB_101010x, -// canvasKit.ColorType.Gray_8, -// canvasKit.ColorType.RGBA_F16, -// canvasKit.ColorType.RGBA_F32, -//]; - -// TODO SkColorType toSkColorType(ui.ColorType colorType) { -// return _skColorTypes[colorType.index]; -//} - @JS() class SkAnimatedImage { external int getFrameCount(); @@ -1635,7 +1607,6 @@ class SkImageInfo { external SkRect get Rect; external int get RowBytes; external int get RowBytes64; - // TODO external SkSize get Size; external int get Width; external factory SkImageInfo({ required int width, From ced934ca25136a9dc377157213aab27503755f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 4 Aug 2020 22:31:50 +0200 Subject: [PATCH 26/78] Add comments --- lib/web_ui/lib/src/engine/compositor/image.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index b527ad86be767..8274917244f87 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -63,6 +63,7 @@ class CkAnimatedImage implements ui.Image { bytes = canvasKit.getSkDataBytes(skData); } + // Without copying, the data would go away immediately with the returned SkData final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); return Future.value(data); } @@ -103,6 +104,7 @@ class CkImage implements ui.Image { bytes = canvasKit.getSkDataBytes(skData); } + // Without copying, the data would go away immediately with the returned SkData final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); return Future.value(data); } From 7b78b27158a2356a89de01d71811201eebe576b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 4 Aug 2020 22:57:42 +0200 Subject: [PATCH 27/78] CK test --- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 4787834f19899..cea475245b78c 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1175,6 +1175,21 @@ void _canvasTests() { 20, ); }); + + test('toImage.toByteData', () { + final SkPictureRecorder otherRecorder = SkPictureRecorder(); + final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + )); + otherCanvas.drawRect(0, 0, 1, 1, SkPaint()); + final SkPicture picture = otherRecorder.finishRecordingAsPicture(); + final SkImage image = picture.toImage(); + final Uint8List data = image.toByteData(); + expect(data, isNotNull); + }); } final Uint8List kTransparentImage = Uint8List.fromList([ From 11c4073e26ec2d15f3ab7fc497109fe4323c8ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Thu, 6 Aug 2020 00:18:16 +0200 Subject: [PATCH 28/78] Comment about copying --- lib/web_ui/lib/src/engine/compositor/image.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 8274917244f87..de7f9c22ae919 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -60,11 +60,11 @@ class CkAnimatedImage implements ui.Image { bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% - bytes = canvasKit.getSkDataBytes(skData); + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); } - // Without copying, the data would go away immediately with the returned SkData - final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + final ByteData data = bytes.buffer.asByteData(0, bytes.length); return Future.value(data); } } @@ -101,11 +101,11 @@ class CkImage implements ui.Image { bytes = skImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% - bytes = canvasKit.getSkDataBytes(skData); + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); } - // Without copying, the data would go away immediately with the returned SkData - final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + final ByteData data = bytes.buffer.asByteData(0, bytes.length); return Future.value(data); } } From 72ef5052a5789783ef51170c9876b927b904c328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 20:00:34 +0200 Subject: [PATCH 29/78] Update image.dart Implement toByteData() --- .../lib/src/engine/compositor/image.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 85ac7971e8b41..839db66ace3e6 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -69,7 +69,23 @@ class CkAnimatedImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final js.JsObject imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit['AlphaType']['Premul'], + 'colorType': canvasKit['ColorType']['RGBA_8888'], + 'width': width, + 'height': height, + }); + bytes = skImage.callMethod('readPixels', [imageInfo, 0, 0]); + } else { + final js.JsObject skData = skImage.callMethod('encodeToData'); //defaults to PNG 100% + bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + } + + final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + return Future.value(data); } } From 1afe1404498abb74331ec749e22f569b6791afeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 20:02:04 +0200 Subject: [PATCH 30/78] Update html_image_codec.dart Implement toByteData() --- lib/web_ui/lib/src/engine/html_image_codec.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 a42f4b4f1c746..8ac0f3db30a98 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -154,7 +154,13 @@ class HtmlImage implements ui.Image { // `Picture`s. /// Returns an error message on failure, null on success. String _toByteData(int format, Callback callback) { - callback(null); - return 'Image.toByteData is not supported in Flutter for Web'; + if (imgElement.src.startsWith('data:')) { + final data = UriData.fromUri(Uri.parse(imgElement.src)); + callback(data.contentAsBytes()); + return null; + } else { + callback(null); + return 'Data URI not found'; + } } } From 18642e46cbeb5ee91c20fe5564215b5aa3f35fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 20:23:02 +0200 Subject: [PATCH 31/78] rebase --- lib/web_ui/lib/src/engine/compositor/picture.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index bcce6fe1b7bef..e7dc7ef111b20 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -22,8 +22,12 @@ class CkPicture implements ui.Picture { @override Future toImage(int width, int height) { - throw UnsupportedError( - 'Picture.toImage not yet implemented for CanvasKit and HTML'); + final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); + final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); + skCanvas.callMethod('drawPicture', [skPicture]); + final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); + skSurface.callMethod('dispose'); + return SkImage(skImage); } } From a20d80daad58ef8dbabda18cd9fa9fa093354d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 20:34:08 +0200 Subject: [PATCH 32/78] Update image.dart Null-aware. --- lib/web_ui/lib/src/engine/compositor/image.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 839db66ace3e6..dbe0f5520d8cf 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -78,9 +78,9 @@ class CkAnimatedImage implements ui.Image { 'width': width, 'height': height, }); - bytes = skImage.callMethod('readPixels', [imageInfo, 0, 0]); + bytes = skImage!.callMethod('readPixels', [imageInfo, 0, 0]); } else { - final js.JsObject skData = skImage.callMethod('encodeToData'); //defaults to PNG 100% + final js.JsObject skData = skImage!.callMethod('encodeToData'); //defaults to PNG 100% bytes = canvasKit.callMethod('getSkDataBytes', [skData]); } From e3147c533eb4be182eaa1e6f4803dfb88f40b217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 21:01:08 +0200 Subject: [PATCH 33/78] Update picture.dart Sk* to Ck* --- lib/web_ui/lib/src/engine/compositor/picture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index e7dc7ef111b20..a8bbbef8a1610 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -27,7 +27,7 @@ class CkPicture implements ui.Picture { skCanvas.callMethod('drawPicture', [skPicture]); final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); skSurface.callMethod('dispose'); - return SkImage(skImage); + return CkImage(skImage); } } From ae8ba90349ffe5c40d7fe559808b2c536e79e66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 23:38:12 +0200 Subject: [PATCH 34/78] Update picture.dart --- lib/web_ui/lib/src/engine/compositor/picture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index a8bbbef8a1610..ee9fbf9b70c56 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -24,7 +24,7 @@ class CkPicture implements ui.Picture { Future toImage(int width, int height) { final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); - skCanvas.callMethod('drawPicture', [skPicture]); + skCanvas.callMethod('drawPicture', [skPicture.skiaObject]); final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); skSurface.callMethod('dispose'); return CkImage(skImage); From abf4f78a910406ca2dc77b6def31436838a67591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 10 Jul 2020 23:54:57 +0200 Subject: [PATCH 35/78] Update picture.dart Null-aware --- lib/web_ui/lib/src/engine/compositor/picture.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index ee9fbf9b70c56..a13a37a7e2be7 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -21,10 +21,10 @@ class CkPicture implements ui.Picture { } @override - Future toImage(int width, int height) { + Future toImage(int width, int height) async { final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); - skCanvas.callMethod('drawPicture', [skPicture.skiaObject]); + skCanvas.callMethod('drawPicture', [skPicture.skiaObject!]); final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); skSurface.callMethod('dispose'); return CkImage(skImage); From 2abe68f2f753cb4c01ce7290642be6cd2a8c8397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sat, 11 Jul 2020 00:13:00 +0200 Subject: [PATCH 36/78] Update html_image_codec.dart --- lib/web_ui/lib/src/engine/html_image_codec.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 8ac0f3db30a98..e8df8b9fb2e92 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -150,14 +150,12 @@ class HtmlImage implements ui.Image { } } - // TODO(het): Support this for asset images and images generated from - // `Picture`s. - /// Returns an error message on failure, null on success. + // Returns an error message on failure. String _toByteData(int format, Callback callback) { if (imgElement.src.startsWith('data:')) { final data = UriData.fromUri(Uri.parse(imgElement.src)); callback(data.contentAsBytes()); - return null; + return ''; } else { callback(null); return 'Data URI not found'; From 0b765d859b7018251780e35dba4045c8981d8c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sat, 11 Jul 2020 01:11:34 +0200 Subject: [PATCH 37/78] Update image.dart Type arguments --- lib/web_ui/lib/src/engine/compositor/image.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index dbe0f5520d8cf..47257762b1b69 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -72,7 +72,7 @@ class CkAnimatedImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final js.JsObject imageInfo = js.JsObject.jsify({ + final js.JsObject imageInfo = js.JsObject.jsify({ 'alphaType': canvasKit['AlphaType']['Premul'], 'colorType': canvasKit['ColorType']['RGBA_8888'], 'width': width, @@ -81,7 +81,7 @@ class CkAnimatedImage implements ui.Image { bytes = skImage!.callMethod('readPixels', [imageInfo, 0, 0]); } else { final js.JsObject skData = skImage!.callMethod('encodeToData'); //defaults to PNG 100% - bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + bytes = canvasKit.callMethod('getSkDataBytes', [skData]); } final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); From 11090043db7877c28e30865356a990f8c1cd58ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sun, 12 Jul 2020 14:29:33 +0200 Subject: [PATCH 38/78] Update image.dart Same for CkAnimatedImage --- .../lib/src/engine/compositor/image.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 47257762b1b69..f2900b0d78d35 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -109,7 +109,23 @@ class CkImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final js.JsObject imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit['AlphaType']['Premul'], + 'colorType': canvasKit['ColorType']['RGBA_8888'], + 'width': width, + 'height': height, + }); + bytes = _skAnimatedImage!.callMethod('readPixels', [imageInfo, 0, 0]); + } else { + final js.JsObject skData = _skAnimatedImage!.callMethod('encodeToData'); //defaults to PNG 100% + bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + } + + final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + return Future.value(data); } } From 17a2ef5c966efab6c9e11f3a9c6146d20e50fc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Thu, 23 Jul 2020 12:42:27 +0200 Subject: [PATCH 39/78] Revert --- lib/web_ui/lib/src/engine/compositor/picture.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index a13a37a7e2be7..fdc7c1c5d62a3 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -22,11 +22,11 @@ class CkPicture implements ui.Picture { @override Future toImage(int width, int height) async { - final js.JsObject skSurface = canvasKit.callMethod('MakeSurface', [width, height]); - final js.JsObject skCanvas = skSurface.callMethod('getCanvas'); - skCanvas.callMethod('drawPicture', [skPicture.skiaObject!]); - final js.JsObject skImage = skSurface.callMethod('makeImageSnapshot'); - skSurface.callMethod('dispose'); + final SkSurface skSurface = canvasKit.MakeSurface(width, height); + final SkCanvas skCanvas = skSurface.getCanvas(); + skCanvas.drawPicture(skiaObject.skiaObject); + final SkImage skImage = skSurface.makeImageSnapshot(); + skSurface.dispose(); return CkImage(skImage); } } @@ -36,6 +36,6 @@ class SkPictureSkiaObject extends OneShotSkiaObject { @override void delete() { - rawSkiaObject.delete(); + rawSkiaObject?.delete(); } -} +} \ No newline at end of file From 0c2aac9adb0c6ddeb39fb02ae751665f6111eed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Fri, 24 Jul 2020 23:42:23 +0200 Subject: [PATCH 40/78] Tested by local engine --- .../lib/src/engine/html_image_codec.dart | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) 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 e8df8b9fb2e92..ec2ffbfdae081 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -129,13 +129,13 @@ class HtmlImage implements ui.Image { final int height; @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - return futurize((Callback callback) { - return _toByteData(format.index, (Uint8List? encoded) { - callback(encoded?.buffer.asByteData()); - }); - }); + Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + if (imgElement.src.startsWith('data:')) { + final data = UriData.fromUri(Uri.parse(imgElement.src)); + return Future.value(data.contentAsBytes().buffer.asByteData()); + } else { + return Future.value(null); + } } // Returns absolutely positioned actual image element on first call and @@ -149,16 +149,4 @@ class HtmlImage implements ui.Image { return imgElement; } } - - // Returns an error message on failure. - String _toByteData(int format, Callback callback) { - if (imgElement.src.startsWith('data:')) { - final data = UriData.fromUri(Uri.parse(imgElement.src)); - callback(data.contentAsBytes()); - return ''; - } else { - callback(null); - return 'Data URI not found'; - } - } } From 301ae90b6a88d42e16ca2f735696995926d697e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sat, 25 Jul 2020 00:49:35 +0200 Subject: [PATCH 41/78] Add files via upload --- .../engine/picture_to_image_test.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart diff --git a/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart b/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart new file mode 100644 index 0000000000000..ef53d4c643a3f --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart @@ -0,0 +1,52 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Picture to Image', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Image testImage = await _drawTestPictureWithCircle(region); + builder.addImage(Offset.zero, testImage); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('picture_to_image.png', region: region, write: true); + }); +} + +Future _drawTestPictureWithCircle(Rect region) async { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00); + final Picture picture = pictureRecorder.endRecording(); + return picture.toImage(region.width, region.height); +} From fbfd6752dc117b0862d1a1c8a71f557245e4d776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sat, 25 Jul 2020 00:53:31 +0200 Subject: [PATCH 42/78] Delete picture_to_image_test.dart --- .../engine/picture_to_image_test.dart | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart diff --git a/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart b/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart deleted file mode 100644 index ef53d4c643a3f..0000000000000 --- a/lib/web_ui/test/golden_tests/engine/picture_to_image_test.dart +++ /dev/null @@ -1,52 +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. - -// @dart = 2.6 -import 'dart:html' as html; - -import 'package:ui/ui.dart'; -import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; - -import 'package:web_engine_tester/golden_tester.dart'; - -void main() async { - final Rect region = Rect.fromLTWH(0, 0, 500, 500); - - setUp(() async { - debugShowClipLayers = true; - SurfaceSceneBuilder.debugForgetFrameScene(); - for (html.Node scene in html.document.querySelectorAll('flt-scene')) { - scene.remove(); - } - - await webOnlyInitializePlatform(); - webOnlyFontCollection.debugRegisterTestFonts(); - await webOnlyFontCollection.ensureFontsLoaded(); - }); - - test('Convert Picture to Image', () async { - final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); - final Image testImage = await _drawTestPictureWithCircle(region); - builder.addImage(Offset.zero, testImage); - - html.document.body.append(builder - .build() - .webOnlyRootElement); - - await matchGoldenFile('picture_to_image.png', region: region, write: true); - }); -} - -Future _drawTestPictureWithCircle(Rect region) async { - final EnginePictureRecorder recorder = PictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(region); - canvas.drawOval( - region, - Paint() - ..style = PaintingStyle.fill - ..color = Color(0xFF00FF00); - final Picture picture = pictureRecorder.endRecording(); - return picture.toImage(region.width, region.height); -} From b49583bfd2975f7d31e1585f0492ac5933a7e588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sat, 25 Jul 2020 19:42:21 +0200 Subject: [PATCH 43/78] Test #1 --- .../engine/canvas_to_picture_test.dart | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart new file mode 100644 index 0000000000000..7f3675ce06314 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -0,0 +1,51 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Canvas to Picture', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = await _drawTestPictureWithCircle(region); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + }); +} + +Picture _drawTestPictureWithCircle(Rect region) { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00); + return pictureRecorder.endRecording(); +} From 26a49fb0d1a16201d0376d40e405a5212805d3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sun, 26 Jul 2020 14:51:10 +0200 Subject: [PATCH 44/78] Test to read only --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index 7f3675ce06314..c305f0903fe68 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -35,7 +35,7 @@ void main() async { .build() .webOnlyRootElement); - await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + await matchGoldenFile('canvas_to_picture.png', region: region); }); } From b23ca62965a9f45c1ef974e69c3a8e114cc6b48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 21:18:00 +0200 Subject: [PATCH 45/78] Test, finally --- .../test/golden_tests/engine/canvas_to_picture_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index c305f0903fe68..604bef4d3bd2c 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -46,6 +46,6 @@ Picture _drawTestPictureWithCircle(Rect region) { region, Paint() ..style = PaintingStyle.fill - ..color = Color(0xFF00FF00); - return pictureRecorder.endRecording(); + ..color = Color(0xFF00FF00)); + return recorder.endRecording(); } From 41d664aa3aec120ce53530c758b430796cc3bd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 28 Jul 2020 23:54:40 +0200 Subject: [PATCH 46/78] @JS refactoring --- .../src/engine/compositor/canvaskit_api.dart | 74 +++++++++++++++++++ .../lib/src/engine/compositor/image.dart | 55 +++++--------- .../lib/src/engine/html_image_codec.dart | 4 +- 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index b9170ec0ed62c..4cca890e662ce 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -30,6 +30,8 @@ class CanvasKit { external SkBlurStyleEnum get BlurStyle; external SkTileModeEnum get TileMode; external SkFillTypeEnum get FillType; + external SkAlphaTypeEnum get AlphaType; + external SkColorTypeEnum get ColorType; external SkPathOpEnum get PathOp; external SkClipOpEnum get ClipOp; external SkPointModeEnum get PointMode; @@ -61,6 +63,13 @@ class CanvasKit { external SkParagraphStyle ParagraphStyle( SkParagraphStyleProperties properties); external SkTextStyle TextStyle(SkTextStyleProperties properties); + external SkSurface MakeSurface( + int width, + int height, + ); + external Uint8List getSkDataBytes( + SkData skData, + ); // Text decoration enum is embedded in the CanvasKit object itself. external int get NoDecoration; @@ -127,6 +136,7 @@ class SkSurface { external int width(); external int height(); external void dispose(); + external SkImage makeImageSnapshot(); } @JS() @@ -621,6 +631,66 @@ SkTileMode toSkTileMode(ui.TileMode mode) { return _skTileModes[mode.index]; } +@JS() +class SkAlphaTypeEnum { + external SkAlphaType get Opaque; + external SkAlphaType get Premul; + external SkAlphaType get Unpremul; +} + +@JS() +class SkAlphaType { + external int get value; +} + +final List _skAlphaTypes = [ + canvasKit.AlphaType.Opaque, + canvasKit.AlphaType.Premul, + canvasKit.AlphaType.Unpremul, +]; + +//SkAlphaType toSkAlphaType(ui.AlphaType alphaType) { +// return _skAlphaTypes[alphaType.index]; +//} + +@JS() +class SkColorTypeEnum { + external SkColorType get Alpha_8; + external SkColorType get RGB_565; + external SkColorType get ARGB_4444; + external SkColorType get RGBA_8888; + external SkColorType get RGB_888x; + external SkColorType get BGRA_8888; + external SkColorType get RGBA_1010102; + external SkColorType get RGB_101010x; + external SkColorType get Gray_8; + external SkColorType get RGBA_F16; + external SkColorType get RGBA_F32; +} + +@JS() +class SkColorType { + external int get value; +} + +final List _skColorTypes = [ + canvasKit.ColorType.Alpha_8, + canvasKit.ColorType.RGB_565, + canvasKit.ColorType.ARGB_4444, + canvasKit.ColorType.RGBA_8888, + canvasKit.ColorType.RGB_888x, + canvasKit.ColorType.BGRA_8888, + canvasKit.ColorType.RGBA_1010102, + canvasKit.ColorType.RGB_101010x, + canvasKit.ColorType.Gray_8, + canvasKit.ColorType.RGBA_F16, + canvasKit.ColorType.RGBA_F32, +]; + +//SkColorType toSkColorType(ui.ColorType colorType) { +// return _skColorTypes[colorType.index]; +//} + @JS() class SkAnimatedImage { external int getFrameCount(); @@ -631,6 +701,8 @@ class SkAnimatedImage { external SkImage getCurrentFrame(); external int width(); external int height(); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); /// Deletes the C++ object. /// @@ -644,6 +716,8 @@ class SkImage { external int width(); external int height(); external SkShader makeShader(SkTileMode tileModeX, SkTileMode tileModeY); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); } @JS() diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index f2900b0d78d35..a2116b9536eb4 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -5,36 +5,15 @@ // @dart = 2.10 part of engine; -/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia. +/// Instantiates a [ui.Codec] backed by an `SkImage` from Skia. void skiaInstantiateImageCodec(Uint8List list, Callback callback, [int? width, int? height, int? format, int? rowBytes]) { - final SkAnimatedImage skAnimatedImage = - canvasKit.MakeAnimatedImageFromEncoded(list); + final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(list); final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); callback(codec); } -/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after requesting from URI. -void skiaInstantiateWebImageCodec(String src, Callback callback, - WebOnlyImageCodecChunkCallback? chunkCallback) { - chunkCallback?.call(0, 100); - //TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported. - html.HttpRequest.request( - src, - responseType: "arraybuffer", - ).then((html.HttpRequest response) { - chunkCallback?.call(100, 100); - final Uint8List list = - new Uint8List.view((response.response as ByteBuffer)); - final SkAnimatedImage skAnimatedImage = - canvasKit.MakeAnimatedImageFromEncoded(list); - final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); - final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); - callback(codec); - }); -} - /// A wrapper for `SkAnimatedImage`. class CkAnimatedImage implements ui.Image { final SkAnimatedImage _skAnimatedImage; @@ -72,19 +51,20 @@ class CkAnimatedImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final js.JsObject imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit['AlphaType']['Premul'], - 'colorType': canvasKit['ColorType']['RGBA_8888'], + final SkImageInfo imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit.AlphaType.Premul, + 'colorType': canvasKit.ColorType.RGBA_8888, 'width': width, 'height': height, }); - bytes = skImage!.callMethod('readPixels', [imageInfo, 0, 0]); + bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { - final js.JsObject skData = skImage!.callMethod('encodeToData'); //defaults to PNG 100% - bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); } - final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + final ByteData data = bytes.buffer.asByteData(0, bytes.length); return Future.value(data); } } @@ -112,19 +92,20 @@ class CkImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final js.JsObject imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit['AlphaType']['Premul'], - 'colorType': canvasKit['ColorType']['RGBA_8888'], + final SkImageInfo imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit.AlphaType.Premul, + 'colorType': canvasKit.ColorType.RGBA_8888, 'width': width, 'height': height, }); - bytes = _skAnimatedImage!.callMethod('readPixels', [imageInfo, 0, 0]); + bytes = skImage.readPixels(imageInfo, 0, 0); } else { - final js.JsObject skData = _skAnimatedImage!.callMethod('encodeToData'); //defaults to PNG 100% - bytes = canvasKit.callMethod('getSkDataBytes', [skData]); + final SkData skData = skImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); } - final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + final ByteData data = bytes.buffer.asByteData(0, bytes.length); return Future.value(data); } } 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 ec2ffbfdae081..6a0595d66ae50 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -130,8 +130,8 @@ class HtmlImage implements ui.Image { @override Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - if (imgElement.src.startsWith('data:')) { - final data = UriData.fromUri(Uri.parse(imgElement.src)); + if (imgElement.src?.startsWith('data:') == true) { + final data = UriData.fromUri(Uri.parse(imgElement.src!)); return Future.value(data.contentAsBytes().buffer.asByteData()); } else { return Future.value(null); From fcd1d2f148bb196a4eba012450bc105772b4ebd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 00:18:17 +0200 Subject: [PATCH 47/78] @JS refactoring --- .../lib/src/engine/compositor/image.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index a2116b9536eb4..de7f9c22ae919 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -51,12 +51,12 @@ class CkAnimatedImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit.AlphaType.Premul, - 'colorType': canvasKit.ColorType.RGBA_8888, - 'width': width, - 'height': height, - }); + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + width: width, + height: height, + ); bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% @@ -92,12 +92,12 @@ class CkImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit.AlphaType.Premul, - 'colorType': canvasKit.ColorType.RGBA_8888, - 'width': width, - 'height': height, - }); + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + width: width, + height: height, + ); bytes = skImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% From a29069f150e8868b2cc929ced6c15ce1079040c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 11:43:18 +0200 Subject: [PATCH 48/78] SkData and SkImageInfo --- .../src/engine/compositor/canvaskit_api.dart | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 4cca890e662ce..c34b952d2c855 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -649,10 +649,6 @@ final List _skAlphaTypes = [ canvasKit.AlphaType.Unpremul, ]; -//SkAlphaType toSkAlphaType(ui.AlphaType alphaType) { -// return _skAlphaTypes[alphaType.index]; -//} - @JS() class SkColorTypeEnum { external SkColorType get Alpha_8; @@ -687,10 +683,6 @@ final List _skColorTypes = [ canvasKit.ColorType.RGBA_F32, ]; -//SkColorType toSkColorType(ui.ColorType colorType) { -// return _skColorTypes[colorType.index]; -//} - @JS() class SkAnimatedImage { external int getFrameCount(); @@ -1629,6 +1621,7 @@ class SkFontMgrNamespace { } @JS() +<<<<<<< HEAD class TypefaceFontProviderNamespace { external TypefaceFontProvider Make(); } @@ -1703,3 +1696,40 @@ external Object? get _finalizationRegistryConstructor; /// Whether the current browser supports `FinalizationRegistry`. bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; + +class SkData { + external int size(); + external bool isEmpty(); + external Uint8List bytes(); +} + +@JS() +@anonymous +class SkImageInfo { + external SkAlphaType get AlphaType; + external int get BitsPerPixel; + external int get BytesPerPixel; + external int get BytesSize; + external int get BytesSize64; + external SkColorSpace get ColorSpace; + external SkColorType get ColorType; + external int get Height; + external bool get IsEmpty; + external bool get IsOpaque; + external SkRect get Rect; + external int get RowBytes; + external int get RowBytes64; + // TODO external SkSize get Size; + external int get Width; + external factory SkImageInfo({ + required int width, + required int height, + SkAlphaType alphaType, + SkColorSpace colorSpace, + SkColorType colorType, + }); + external SkImageInfo WithAlphaType(SkAlphaType alphaType); + external SkImageInfo WithColorSpace(SkColorSpace colorSpace); + external SkImageInfo WithColorType(SkColorType colorType); + external SkImageInfo WithSize(int width, int height); +} From 83a7399f5651c7d7191be3080b8194a7685f52ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 12:08:01 +0200 Subject: [PATCH 49/78] Unused items --- .../src/engine/compositor/canvaskit_api.dart | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index c34b952d2c855..4c5ac71f6c7b1 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -643,11 +643,11 @@ class SkAlphaType { external int get value; } -final List _skAlphaTypes = [ - canvasKit.AlphaType.Opaque, - canvasKit.AlphaType.Premul, - canvasKit.AlphaType.Unpremul, -]; +// TODO final List _skAlphaTypes = [ +// canvasKit.AlphaType.Opaque, +// canvasKit.AlphaType.Premul, +// canvasKit.AlphaType.Unpremul, +//]; @JS() class SkColorTypeEnum { @@ -669,19 +669,19 @@ class SkColorType { external int get value; } -final List _skColorTypes = [ - canvasKit.ColorType.Alpha_8, - canvasKit.ColorType.RGB_565, - canvasKit.ColorType.ARGB_4444, - canvasKit.ColorType.RGBA_8888, - canvasKit.ColorType.RGB_888x, - canvasKit.ColorType.BGRA_8888, - canvasKit.ColorType.RGBA_1010102, - canvasKit.ColorType.RGB_101010x, - canvasKit.ColorType.Gray_8, - canvasKit.ColorType.RGBA_F16, - canvasKit.ColorType.RGBA_F32, -]; +// TODO final List _skColorTypes = [ +// canvasKit.ColorType.Alpha_8, +// canvasKit.ColorType.RGB_565, +// canvasKit.ColorType.ARGB_4444, +// canvasKit.ColorType.RGBA_8888, +// canvasKit.ColorType.RGB_888x, +// canvasKit.ColorType.BGRA_8888, +// canvasKit.ColorType.RGBA_1010102, +// canvasKit.ColorType.RGB_101010x, +// canvasKit.ColorType.Gray_8, +// canvasKit.ColorType.RGBA_F16, +// canvasKit.ColorType.RGBA_F32, +//]; @JS() class SkAnimatedImage { From 99eb64cbffa2e2009eeb4eddd11b35f0cf48c08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Wed, 29 Jul 2020 12:50:50 +0200 Subject: [PATCH 50/78] Test to write PNG --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index 604bef4d3bd2c..fe6f69cf37652 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -35,7 +35,7 @@ void main() async { .build() .webOnlyRootElement); - await matchGoldenFile('canvas_to_picture.png', region: region); + await matchGoldenFile('canvas_to_picture.png', region: region, write: true); }); } From 4b28ee1c6fce0cdb0bcf96713a7e158376871e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 4 Aug 2020 21:55:50 +0200 Subject: [PATCH 51/78] Remove comments --- .../src/engine/compositor/canvaskit_api.dart | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 4c5ac71f6c7b1..9295a4f311c65 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -4,7 +4,7 @@ /// Bindings for CanvasKit JavaScript API. /// -/// Prefer keeping the originl CanvasKit names so it is easier to locate +/// Prefer keeping the original CanvasKit names so it is easier to locate /// the API behind these bindings in the Skia source code. // @dart = 2.10 @@ -643,12 +643,6 @@ class SkAlphaType { external int get value; } -// TODO final List _skAlphaTypes = [ -// canvasKit.AlphaType.Opaque, -// canvasKit.AlphaType.Premul, -// canvasKit.AlphaType.Unpremul, -//]; - @JS() class SkColorTypeEnum { external SkColorType get Alpha_8; @@ -669,20 +663,6 @@ class SkColorType { external int get value; } -// TODO final List _skColorTypes = [ -// canvasKit.ColorType.Alpha_8, -// canvasKit.ColorType.RGB_565, -// canvasKit.ColorType.ARGB_4444, -// canvasKit.ColorType.RGBA_8888, -// canvasKit.ColorType.RGB_888x, -// canvasKit.ColorType.BGRA_8888, -// canvasKit.ColorType.RGBA_1010102, -// canvasKit.ColorType.RGB_101010x, -// canvasKit.ColorType.Gray_8, -// canvasKit.ColorType.RGBA_F16, -// canvasKit.ColorType.RGBA_F32, -//]; - @JS() class SkAnimatedImage { external int getFrameCount(); @@ -1719,7 +1699,6 @@ class SkImageInfo { external SkRect get Rect; external int get RowBytes; external int get RowBytes64; - // TODO external SkSize get Size; external int get Width; external factory SkImageInfo({ required int width, From 10f4d4cb76da5da69c5e5aca814d960e1ddf0d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Tue, 4 Aug 2020 22:57:42 +0200 Subject: [PATCH 52/78] CK test --- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 4787834f19899..cea475245b78c 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1175,6 +1175,21 @@ void _canvasTests() { 20, ); }); + + test('toImage.toByteData', () { + final SkPictureRecorder otherRecorder = SkPictureRecorder(); + final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + )); + otherCanvas.drawRect(0, 0, 1, 1, SkPaint()); + final SkPicture picture = otherRecorder.finishRecordingAsPicture(); + final SkImage image = picture.toImage(); + final Uint8List data = image.toByteData(); + expect(data, isNotNull); + }); } final Uint8List kTransparentImage = Uint8List.fromList([ From a1346eaa6388dcb84ef2c155989314fd55ac4cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 11:17:53 +0200 Subject: [PATCH 53/78] Rebase manually --- .../lib/src/engine/compositor/image.dart | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index de7f9c22ae919..317623769f877 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -5,15 +5,36 @@ // @dart = 2.10 part of engine; -/// Instantiates a [ui.Codec] backed by an `SkImage` from Skia. +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia. void skiaInstantiateImageCodec(Uint8List list, Callback callback, [int? width, int? height, int? format, int? rowBytes]) { - final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(list); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(list); final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); callback(codec); } +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after requesting from URI. +void skiaInstantiateWebImageCodec(String src, Callback callback, + WebOnlyImageCodecChunkCallback? chunkCallback) { + chunkCallback?.call(0, 100); + //TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported. + html.HttpRequest.request( + src, + responseType: "arraybuffer", + ).then((html.HttpRequest response) { + chunkCallback?.call(100, 100); + final Uint8List list = + new Uint8List.view((response.response as ByteBuffer)); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(list); + final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); + final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); + callback(codec); + }); +} + /// A wrapper for `SkAnimatedImage`. class CkAnimatedImage implements ui.Image { final SkAnimatedImage _skAnimatedImage; @@ -51,20 +72,19 @@ class CkAnimatedImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = SkImageInfo( - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - width: width, - height: height, - ); + final SkImageInfo imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit.AlphaType.Premul, + 'colorType': canvasKit.ColorType.RGBA_8888, + 'width': width, + 'height': height, + }); bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% - // make a copy that we can return - bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + bytes = canvasKit.getSkDataBytes(skData); } - final ByteData data = bytes.buffer.asByteData(0, bytes.length); + final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); return Future.value(data); } } @@ -92,20 +112,19 @@ class CkImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = SkImageInfo( - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - width: width, - height: height, - ); + final SkImageInfo imageInfo = js.JsObject.jsify({ + 'alphaType': canvasKit.AlphaType.Premul, + 'colorType': canvasKit.ColorType.RGBA_8888, + 'width': width, + 'height': height, + }); bytes = skImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% - // make a copy that we can return - bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + bytes = canvasKit.getSkDataBytes(skData); } - final ByteData data = bytes.buffer.asByteData(0, bytes.length); + final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); return Future.value(data); } } From 4e41dfc70dc5e349fd2b3916f9ab9cca0504edcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 11:20:11 +0200 Subject: [PATCH 54/78] Rebase manually --- lib/web_ui/lib/src/engine/compositor/picture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index fdc7c1c5d62a3..9c80b7ed58e1d 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -36,6 +36,6 @@ class SkPictureSkiaObject extends OneShotSkiaObject { @override void delete() { - rawSkiaObject?.delete(); + rawSkiaObject.delete(); } } \ No newline at end of file From fb02673ff78ebcf370f59cbefd3b6e2dad00bc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 11:48:55 +0200 Subject: [PATCH 55/78] New test --- lib/web_ui/lib/src/engine/compositor/image.dart | 10 ++++++---- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 10 +++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 317623769f877..65e974f1a56d8 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -81,10 +81,11 @@ class CkAnimatedImage implements ui.Image { bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% - bytes = canvasKit.getSkDataBytes(skData); + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); } - final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + final ByteData data = bytes.buffer.asByteData(0, bytes.length); return Future.value(data); } } @@ -121,10 +122,11 @@ class CkImage implements ui.Image { bytes = skImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% - bytes = canvasKit.getSkDataBytes(skData); + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); } - final ByteData data = Uint8List.fromList(bytes).buffer.asByteData(0, bytes.length); + final ByteData data = bytes.buffer.asByteData(0, bytes.length); return Future.value(data); } } diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index cea475245b78c..c3bda48c02aa5 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1184,7 +1184,15 @@ void _canvasTests() { fRight: 1, fBottom: 1, )); - otherCanvas.drawRect(0, 0, 1, 1, SkPaint()); + otherCanvas.drawRect( + SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + ), + SkPaint(), + ); final SkPicture picture = otherRecorder.finishRecordingAsPicture(); final SkImage image = picture.toImage(); final Uint8List data = image.toByteData(); From 8c2790cf7584446834e70d3ce95b5217612b6197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 11:50:37 +0200 Subject: [PATCH 56/78] Newline --- lib/web_ui/lib/src/engine/compositor/picture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/compositor/picture.dart index 9c80b7ed58e1d..2a0d292ebd45b 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -38,4 +38,4 @@ class SkPictureSkiaObject extends OneShotSkiaObject { void delete() { rawSkiaObject.delete(); } -} \ No newline at end of file +} From 6a31d418e6f370f5107001ee465204f0e3d2b5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 11:52:27 +0200 Subject: [PATCH 57/78] Rebase dirt --- lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 8b179fd6bc7be..3669ae4c6e015 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -1601,8 +1601,6 @@ class SkFontMgrNamespace { } @JS() -<<<<<<< HEAD -<<<<<<< HEAD class TypefaceFontProviderNamespace { external TypefaceFontProvider Make(); } From dc77edc877b8507009f4f838bdd9d6e27c6c053d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 12:28:38 +0200 Subject: [PATCH 58/78] SkImageInfo --- .../lib/src/engine/compositor/image.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 65e974f1a56d8..d32876c62499c 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -72,12 +72,12 @@ class CkAnimatedImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit.AlphaType.Premul, - 'colorType': canvasKit.ColorType.RGBA_8888, - 'width': width, - 'height': height, - }); + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + width: width, + height: height, + ); bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% @@ -113,12 +113,12 @@ class CkImage implements ui.Image { Uint8List bytes; if (format == ui.ImageByteFormat.rawRgba) { - final SkImageInfo imageInfo = js.JsObject.jsify({ - 'alphaType': canvasKit.AlphaType.Premul, - 'colorType': canvasKit.ColorType.RGBA_8888, - 'width': width, - 'height': height, - }); + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + width: width, + height: height, + ); bytes = skImage.readPixels(imageInfo, 0, 0); } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% From 54e706c2a977a9ac75c050d3892ccfb78f030ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 13:05:00 +0200 Subject: [PATCH 59/78] New test --- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index c3bda48c02aa5..7214b6201d526 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1176,7 +1176,7 @@ void _canvasTests() { ); }); - test('toImage.toByteData', () { + test('toImage.toByteData', () async { final SkPictureRecorder otherRecorder = SkPictureRecorder(); final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( fLeft: 0, @@ -1193,10 +1193,12 @@ void _canvasTests() { ), SkPaint(), ); - final SkPicture picture = otherRecorder.finishRecordingAsPicture(); - final SkImage image = picture.toImage(); - final Uint8List data = image.toByteData(); - expect(data, isNotNull); + final CkPicture picture = otherRecorder.finishRecordingAsPicture(); + final CkImage image = await picture.toImage(1, 1); + final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + expect(rawData, isNotNull); + final ByteData pngData = await image.toByteData(format: ui.ImageByteFormat.png); + expect(pngData, isNotNull); }); } From d4a07a14aa663172b2a4e02f588bbf51f90a2266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 13:21:24 +0200 Subject: [PATCH 60/78] New test --- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 7214b6201d526..5bd087c4c85c2 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1193,7 +1193,7 @@ void _canvasTests() { ), SkPaint(), ); - final CkPicture picture = otherRecorder.finishRecordingAsPicture(); + final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture()); final CkImage image = await picture.toImage(1, 1); final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); expect(rawData, isNotNull); From 6db0a3ab50d81c869176809d57f619f805b6ff95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 7 Aug 2020 13:37:08 +0200 Subject: [PATCH 61/78] New test --- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 5bd087c4c85c2..bc5a2aadba499 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1193,7 +1193,7 @@ void _canvasTests() { ), SkPaint(), ); - final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture()); + final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture(), null); final CkImage image = await picture.toImage(1, 1); final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); expect(rawData, isNotNull); From 6daead76b84f564523d07a62010a80aeae9cded6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sat, 8 Aug 2020 00:14:33 +0200 Subject: [PATCH 62/78] Missing annotation --- lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 3669ae4c6e015..2e102959957e2 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -1676,6 +1676,7 @@ external Object? get _finalizationRegistryConstructor; /// Whether the current browser supports `FinalizationRegistry`. bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; +@JS() class SkData { external int size(); external bool isEmpty(); From ad715e139326eaa4708aa01dc24ad067443e2035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Thu, 13 Aug 2020 19:24:59 +0200 Subject: [PATCH 63/78] ColorSpace --- lib/web_ui/lib/src/engine/compositor/image.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index c4a6a2770d0d3..8bcb210d961ef 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -81,6 +81,7 @@ class CkAnimatedImage implements ui.Image { final SkImageInfo imageInfo = SkImageInfo( alphaType: canvasKit.AlphaType.Premul, colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, width: width, height: height, ); @@ -128,6 +129,7 @@ class CkImage implements ui.Image { final SkImageInfo imageInfo = SkImageInfo( alphaType: canvasKit.AlphaType.Premul, colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, width: width, height: height, ); From 69dd265c747b127cb4bb4042a415c04b3a5566cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Thu, 13 Aug 2020 19:35:30 +0200 Subject: [PATCH 64/78] SkImageInfo correct names --- .../src/engine/compositor/canvaskit_api.dart | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 0d670c8ea981a..59ad09503ed8a 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -1698,20 +1698,6 @@ class SkData { @JS() @anonymous class SkImageInfo { - external SkAlphaType get AlphaType; - external int get BitsPerPixel; - external int get BytesPerPixel; - external int get BytesSize; - external int get BytesSize64; - external SkColorSpace get ColorSpace; - external SkColorType get ColorType; - external int get Height; - external bool get IsEmpty; - external bool get IsOpaque; - external SkRect get Rect; - external int get RowBytes; - external int get RowBytes64; - external int get Width; external factory SkImageInfo({ required int width, required int height, @@ -1719,8 +1705,16 @@ class SkImageInfo { SkColorSpace colorSpace, SkColorType colorType, }); - external SkImageInfo WithAlphaType(SkAlphaType alphaType); - external SkImageInfo WithColorSpace(SkColorSpace colorSpace); - external SkImageInfo WithColorType(SkColorType colorType); - external SkImageInfo WithSize(int width, int height); + external SkAlphaType get alphaType(); + external SkColorSpace get colorSpace(); + external SkColorType get colorType(); + external int get height(); + external bool get isEmpty(); + external bool get isOpaque(); + external SkRect get bounds(); + external int get width(); + external SkImageInfo makehAlphaType(SkAlphaType alphaType); + external SkImageInfo makeColorSpace(SkColorSpace colorSpace); + external SkImageInfo makeColorType(SkColorType colorType); + external SkImageInfo makeWH(int width, int height); } From 078d6bc5d4cbab01c26c919fb7062724aff9bba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Thu, 13 Aug 2020 19:37:36 +0200 Subject: [PATCH 65/78] SkImageInfo correct names --- lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 59ad09503ed8a..1afb4be89f9f9 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -1713,7 +1713,7 @@ class SkImageInfo { external bool get isOpaque(); external SkRect get bounds(); external int get width(); - external SkImageInfo makehAlphaType(SkAlphaType alphaType); + external SkImageInfo makeAlphaType(SkAlphaType alphaType); external SkImageInfo makeColorSpace(SkColorSpace colorSpace); external SkImageInfo makeColorType(SkColorType colorType); external SkImageInfo makeWH(int width, int height); From 5dd02dd6498dc82951fe270ace9e98f9af1bc22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Thu, 13 Aug 2020 19:41:22 +0200 Subject: [PATCH 66/78] SkImageInfo correct names --- .../lib/src/engine/compositor/canvaskit_api.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 1afb4be89f9f9..1a2660b3c65a5 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -1705,14 +1705,14 @@ class SkImageInfo { SkColorSpace colorSpace, SkColorType colorType, }); - external SkAlphaType get alphaType(); - external SkColorSpace get colorSpace(); - external SkColorType get colorType(); - external int get height(); - external bool get isEmpty(); - external bool get isOpaque(); - external SkRect get bounds(); - external int get width(); + external SkAlphaType alphaType(); + external SkColorSpace colorSpace(); + external SkColorType colorType(); + external int height(); + external bool isEmpty(); + external bool isOpaque(); + external SkRect bounds(); + external int width(); external SkImageInfo makeAlphaType(SkAlphaType alphaType); external SkImageInfo makeColorSpace(SkColorSpace colorSpace); external SkImageInfo makeColorType(SkColorType colorType); From 5f4d5e7bec7fa3f92bed9447d1f3dd650875123d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Thu, 13 Aug 2020 22:26:02 +0200 Subject: [PATCH 67/78] SkImageInfo properties --- .../lib/src/engine/compositor/canvaskit_api.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 1a2660b3c65a5..dca0e1ffc7b85 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -1705,14 +1705,14 @@ class SkImageInfo { SkColorSpace colorSpace, SkColorType colorType, }); - external SkAlphaType alphaType(); - external SkColorSpace colorSpace(); - external SkColorType colorType(); - external int height(); - external bool isEmpty(); - external bool isOpaque(); - external SkRect bounds(); - external int width(); + external SkAlphaType get alphaType; + external SkColorSpace get colorSpace; + external SkColorType get colorType; + external int get height; + external bool get isEmpty; + external bool get isOpaque; + external SkRect get bounds; + external int get width; external SkImageInfo makeAlphaType(SkAlphaType alphaType); external SkImageInfo makeColorSpace(SkColorSpace colorSpace); external SkImageInfo makeColorType(SkColorType colorType); From 107ba3d5177788938d80a3e30ebffcfc90314e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Thu, 13 Aug 2020 22:36:15 +0200 Subject: [PATCH 68/78] Inactive test --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index fe6f69cf37652..967d58d3218c7 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -35,7 +35,7 @@ void main() async { .build() .webOnlyRootElement); - await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + //await matchGoldenFile('canvas_to_picture.png', region: region, write: true); }); } From 69ec1ccb1220718fc95e99a0a7eda4cdb437a9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Fri, 21 Aug 2020 08:28:14 +0200 Subject: [PATCH 69/78] Unused import --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index 967d58d3218c7..3c8d8c64210f9 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -9,8 +9,6 @@ import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; import 'package:test/test.dart'; -import 'package:web_engine_tester/golden_tester.dart'; - void main() async { final Rect region = Rect.fromLTWH(0, 0, 500, 500); From 6e1d95d4d5652ecfea0c6217a5ac0653cf60069c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sun, 23 Aug 2020 11:38:55 +0200 Subject: [PATCH 70/78] Squash 2 --- .cirrus.yml | 20 +- BUILD.gn | 16 +- DEPS | 53 +- build/generate_coverage.py | 8 +- ci/analyze.sh | 149 +- ci/bin/format.dart | 937 +++++++++++ ci/bin/lint.dart | 18 +- ci/build.sh | 53 +- ci/check_gn_format.py | 51 - ci/check_roll.sh | 35 +- ci/dev/README.md | 32 + ci/dev/prod_builders.json | 68 + ci/dev/try_builders.json | 74 + ci/firebase_testlab.sh | 30 +- ci/format.bat | 32 + ci/format.sh | 121 +- ci/licenses.sh | 199 ++- ci/licenses_golden/licenses_flutter | 146 +- ci/licenses_golden/licenses_fuchsia | 29 +- ci/licenses_golden/licenses_skia | 382 +++-- ci/licenses_golden/licenses_third_party | 316 +++- ci/lint.sh | 29 +- ci/pubspec.yaml | 7 +- ci/test.sh | 4 - common/config.gni | 119 +- common/settings.h | 21 +- flow/BUILD.gn | 67 +- flow/compositor_context.cc | 15 +- flow/compositor_context.h | 22 +- flow/embedded_views.cc | 4 + flow/embedded_views.h | 14 + flow/instrumentation.cc | 32 +- flow/instrumentation.h | 4 +- flow/layers/child_scene_layer.cc | 16 +- flow/layers/container_layer.cc | 68 +- flow/layers/fuchsia_layer_unittests.cc | 74 +- flow/layers/layer.cc | 52 +- flow/layers/layer.h | 17 +- flow/layers/layer_tree.cc | 90 +- flow/layers/layer_tree.h | 22 +- flow/layers/layer_tree_unittests.cc | 19 +- flow/layers/opacity_layer.h | 1 - flow/layers/performance_overlay_layer.cc | 12 +- flow/layers/physical_shape_layer.cc | 28 +- flow/layers/physical_shape_layer.h | 5 +- flow/layers/physical_shape_layer_unittests.cc | 4 +- flow/layers/picture_layer.cc | 16 +- flow/layers/picture_layer.h | 3 +- flow/layers/picture_layer_unittests.cc | 15 +- flow/layers/platform_view_layer.cc | 4 +- flow/layers/transform_layer.cc | 10 +- flow/matrix_decomposition.cc | 17 +- flow/matrix_decomposition_unittests.cc | 13 +- flow/paint_utils.cc | 1 - flow/raster_cache.cc | 56 +- flow/raster_cache.h | 40 +- flow/rtree_unittests.cc | 15 +- flow/scene_update_context.cc | 297 ++-- flow/scene_update_context.h | 209 +-- flow/skia_gpu_object.cc | 2 +- flow/skia_gpu_object.h | 4 +- flow/testing/layer_test.h | 9 +- flow/testing/mock_layer.cc | 1 - flow/testing/mock_layer.h | 2 - flow/testing/mock_layer_unittests.cc | 3 - flow/view_holder.cc | 15 +- flow/view_holder.h | 11 +- fml/file.cc | 11 +- fml/logging.cc | 3 +- fml/memory/ref_counted_unittest.cc | 16 +- fml/memory/weak_ptr_unittest.cc | 4 + fml/message_loop_task_queues.cc | 2 +- fml/message_loop_task_queues_unittests.cc | 7 + fml/raster_thread_merger.cc | 71 +- fml/raster_thread_merger.h | 36 +- fml/raster_thread_merger_unittests.cc | 93 ++ lib/io/dart_io.cc | 30 +- lib/io/dart_io.h | 4 +- lib/ui/BUILD.gn | 62 +- lib/ui/channel_buffers.dart | 2 +- lib/ui/compositing.dart | 12 +- lib/ui/compositing/scene.cc | 7 +- lib/ui/compositing/scene_builder.cc | 2 +- lib/ui/dart_ui.cc | 4 +- lib/ui/fixtures/ui_test.dart | 64 +- lib/ui/geometry.dart | 2 +- lib/ui/hooks.dart | 6 +- lib/ui/painting.dart | 284 +++- lib/ui/painting/canvas.cc | 13 +- lib/ui/painting/image.cc | 2 +- lib/ui/painting/multi_frame_codec.cc | 4 +- lib/ui/painting/multi_frame_codec.h | 5 +- lib/ui/painting/picture.cc | 2 +- lib/ui/text/font_collection.cc | 8 +- lib/ui/text/paragraph_builder.cc | 8 +- lib/ui/ui_dart_state.cc | 19 +- lib/ui/ui_dart_state.h | 13 +- lib/ui/window.dart | 99 +- lib/ui/window/platform_configuration.cc | 437 +++++ lib/ui/window/platform_configuration.h | 421 +++++ .../platform_configuration_unittests.cc | 139 ++ lib/ui/window/viewport_metrics.cc | 28 +- lib/ui/window/viewport_metrics.h | 36 +- lib/ui/window/window.cc | 417 +---- lib/ui/window/window.h | 81 +- lib/web_ui/CODE_CONVENTIONS.md | 62 + lib/web_ui/dev/README.md | 24 +- lib/web_ui/dev/browser_lock.yaml | 20 +- lib/web_ui/dev/chrome_installer.dart | 62 +- lib/web_ui/dev/common.dart | 4 +- lib/web_ui/dev/driver_manager.dart | 54 +- lib/web_ui/dev/driver_version.yaml | 1 - lib/web_ui/dev/felt_windows.bat | 6 +- lib/web_ui/dev/goldens_lock.yaml | 2 +- lib/web_ui/dev/macos_info.dart | 2 + lib/web_ui/dev/test_platform.dart | 31 +- lib/web_ui/dev/test_runner.dart | 187 ++- lib/web_ui/lib/assets/houdini_painter.js | 1069 ------------- lib/web_ui/lib/src/engine.dart | 112 +- lib/web_ui/lib/src/engine/bitmap_canvas.dart | 8 +- .../lib/src/engine/browser_detection.dart | 29 + .../{compositor => canvaskit}/canvas.dart | 7 +- .../canvaskit_api.dart | 282 +++- .../canvaskit_canvas.dart} | 27 +- .../color_filter.dart | 2 +- .../embedded_views.dart | 2 +- .../{compositor => canvaskit}/fonts.dart | 60 +- .../lib/src/engine/canvaskit/image.dart | 185 +++ .../image_filter.dart | 2 +- .../initialization.dart | 2 +- .../{compositor => canvaskit}/layer.dart | 0 .../layer_scene_builder.dart | 0 .../{compositor => canvaskit}/layer_tree.dart | 10 +- .../mask_filter.dart | 2 +- .../n_way_canvas.dart | 0 .../{compositor => canvaskit}/painting.dart | 12 +- .../{compositor => canvaskit}/path.dart | 4 +- .../path_metrics.dart | 0 .../{compositor => canvaskit}/picture.dart | 12 +- .../picture_recorder.dart | 0 .../platform_message.dart | 0 .../raster_cache.dart | 0 .../{compositor => canvaskit}/rasterizer.dart | 0 .../lib/src/engine/canvaskit/shader.dart | 186 +++ .../skia_object_cache.dart | 125 +- .../{compositor => canvaskit}/surface.dart | 47 +- .../{compositor => canvaskit}/text.dart | 60 +- .../{compositor => canvaskit}/util.dart | 0 .../{compositor => canvaskit}/vertices.dart | 57 +- .../viewport_metrics.dart | 0 lib/web_ui/lib/src/engine/color_filter.dart | 4 - .../lib/src/engine/compositor/image.dart | 117 -- lib/web_ui/lib/src/engine/engine_canvas.dart | 124 ++ lib/web_ui/lib/src/engine/houdini_canvas.dart | 370 ----- .../{surface => html}/backdrop_filter.dart | 0 .../src/engine/{surface => html}/canvas.dart | 18 +- .../src/engine/{surface => html}/clip.dart | 2 + .../debug_canvas_reuse_overlay.dart | 0 .../{surface => html}/image_filter.dart | 0 .../src/engine/{surface => html}/offset.dart | 0 .../src/engine/{surface => html}/opacity.dart | 0 .../engine/{surface => html}/painting.dart | 0 .../engine/{surface => html}/path/conic.dart | 0 .../engine/{surface => html}/path/cubic.dart | 0 .../engine/{surface => html}/path/path.dart | 6 - .../{surface => html}/path/path_metrics.dart | 0 .../{surface => html}/path/path_ref.dart | 2 +- .../{surface => html}/path/path_to_svg.dart | 0 .../{surface => html}/path/path_utils.dart | 12 +- .../{surface => html}/path/path_windings.dart | 18 +- .../{surface => html}/path/tangent.dart | 0 .../src/engine/{surface => html}/picture.dart | 483 +++--- .../{surface => html}/platform_view.dart | 0 .../{surface => html}/recording_canvas.dart | 319 +--- .../{surface => html}/render_vertices.dart | 6 +- .../src/engine/{surface => html}/scene.dart | 0 .../{surface => html}/scene_builder.dart | 5 +- .../lib/src/engine/{ => html}/shader.dart | 120 +- .../src/engine/{surface => html}/surface.dart | 0 .../{surface => html}/surface_stats.dart | 6 +- .../engine/{surface => html}/transform.dart | 0 .../lib/src/engine/html_image_codec.dart | 30 +- lib/web_ui/lib/src/engine/rrect_renderer.dart | 1 - .../lib/src/engine/text/line_breaker.dart | 179 ++- .../lib/src/engine/text/measurement.dart | 105 +- lib/web_ui/lib/src/engine/text/paragraph.dart | 135 +- lib/web_ui/lib/src/engine/text/ruler.dart | 28 + .../text_editing/text_capitalization.dart | 2 +- .../src/engine/text_editing/text_editing.dart | 289 +++- lib/web_ui/lib/src/engine/util.dart | 25 + lib/web_ui/lib/src/engine/window.dart | 24 +- lib/web_ui/lib/src/ui/annotations.dart | 30 - lib/web_ui/lib/src/ui/canvas.dart | 599 +------ lib/web_ui/lib/src/ui/channel_buffers.dart | 81 +- lib/web_ui/lib/src/ui/compositing.dart | 299 ---- lib/web_ui/lib/src/ui/geometry.dart | 981 +----------- lib/web_ui/lib/src/ui/hash_codes.dart | 52 +- lib/web_ui/lib/src/ui/initialization.dart | 24 +- lib/web_ui/lib/src/ui/lerp.dart | 2 - lib/web_ui/lib/src/ui/natives.dart | 17 +- lib/web_ui/lib/src/ui/painting.dart | 1419 +---------------- lib/web_ui/lib/src/ui/path.dart | 241 +-- lib/web_ui/lib/src/ui/path_metrics.dart | 108 -- lib/web_ui/lib/src/ui/pointer.dart | 240 +-- lib/web_ui/lib/src/ui/semantics.dart | 452 +----- lib/web_ui/lib/src/ui/test_embedding.dart | 14 +- lib/web_ui/lib/src/ui/text.dart | 1052 +----------- lib/web_ui/lib/src/ui/tile_mode.dart | 44 - lib/web_ui/lib/src/ui/window.dart | 751 +-------- lib/web_ui/pubspec.yaml | 1 + lib/web_ui/test/alarm_clock_test.dart | 5 + lib/web_ui/test/canvas_test.dart | 6 +- .../test/canvaskit/canvaskit_api_test.dart | 66 +- lib/web_ui/test/canvaskit/image_test.dart | 45 + .../test/canvaskit/path_metrics_test.dart | 5 + lib/web_ui/test/canvaskit/shader_test.dart | 81 + .../canvaskit/skia_objects_cache_test.dart | 19 +- lib/web_ui/test/canvaskit/test_data.dart | 29 + lib/web_ui/test/canvaskit/vertices_test.dart | 62 + lib/web_ui/test/clipboard_test.dart | 7 +- lib/web_ui/test/color_test.dart | 7 +- lib/web_ui/test/dom_renderer_test.dart | 7 +- .../test/engine/frame_reference_test.dart | 5 + lib/web_ui/test/engine/history_test.dart | 5 + .../engine/image/html_image_codec_test.dart | 29 +- lib/web_ui/test/engine/navigation_test.dart | 5 + lib/web_ui/test/engine/path_metrics_test.dart | 7 +- .../test/engine/pointer_binding_test.dart | 8 +- lib/web_ui/test/engine/profiler_test.dart | 5 + .../test/engine/recording_canvas_test.dart | 7 +- .../engine/semantics/accessibility_test.dart | 7 +- .../semantics/semantics_helper_test.dart | 8 +- .../test/engine/semantics/semantics_test.dart | 5 + .../engine/services/serialization_test.dart | 7 +- .../surface/path/path_iterator_test.dart | 90 ++ .../surface/path}/path_winding_test.dart | 7 +- .../engine/surface/scene_builder_test.dart | 12 +- .../test/engine/surface/surface_test.dart | 5 + lib/web_ui/test/engine/ulps_test.dart | 5 + lib/web_ui/test/engine/util_test.dart | 8 +- .../test/engine/web_experiments_test.dart | 5 + lib/web_ui/test/engine/window_test.dart | 5 + lib/web_ui/test/geometry_test.dart | 9 +- .../engine/backdrop_filter_golden_test.dart | 9 +- .../engine/canvas_arc_golden_test.dart | 11 +- .../engine/canvas_blend_golden_test.dart | 8 +- .../engine/canvas_clip_path_test.dart | 9 +- .../engine/canvas_context_test.dart | 9 +- .../engine/canvas_draw_image_golden_test.dart | 31 +- .../engine/canvas_draw_picture_test.dart | 9 +- .../engine/canvas_draw_points_test.dart | 10 +- .../engine/canvas_golden_test.dart | 9 +- .../engine/canvas_image_blend_mode_test.dart | 9 +- .../engine/canvas_lines_golden_test.dart | 9 +- .../engine/canvas_mask_filter_test.dart | 9 +- .../engine/canvas_rect_golden_test.dart | 9 +- .../engine/canvas_reuse_test.dart | 9 +- .../engine/canvas_rrect_golden_test.dart | 9 +- .../canvas_stroke_joins_golden_test.dart | 11 +- .../canvas_stroke_rects_golden_test.dart | 11 +- .../engine/canvas_to_picture_test.dart | 49 + .../engine/canvas_winding_rule_test.dart | 9 +- .../engine/compositing_golden_test.dart | 33 +- .../engine/conic_golden_test.dart | 9 +- .../engine/draw_vertices_golden_test.dart | 9 +- .../engine/linear_gradient_golden_test.dart | 9 +- .../multiline_text_clipping_golden_test.dart | 7 +- .../engine/path_metrics_test.dart | 9 +- .../engine/path_to_svg_golden_test.dart | 9 +- .../engine/path_transform_test.dart | 9 +- .../engine/picture_golden_test.dart | 7 +- .../engine/radial_gradient_golden_test.dart | 9 +- .../engine/recording_canvas_golden_test.dart | 9 +- .../test/golden_tests/engine/scuba.dart | 14 +- .../engine/shadow_golden_test.dart | 9 +- .../engine/text_overflow_golden_test.dart | 7 +- .../engine/text_placeholders_test.dart | 79 + .../engine/text_style_golden_test.dart | 9 +- .../golden_failure_smoke_test.dart | 5 + .../golden_success_smoke_test.dart | 7 +- lib/web_ui/test/gradient_test.dart | 9 +- lib/web_ui/test/hash_codes_test.dart | 5 + lib/web_ui/test/keyboard_test.dart | 8 +- lib/web_ui/test/locale_test.dart | 8 +- lib/web_ui/test/paragraph_builder_test.dart | 8 +- lib/web_ui/test/paragraph_test.dart | 49 +- lib/web_ui/test/path_test.dart | 5 + lib/web_ui/test/rect_test.dart | 8 +- lib/web_ui/test/rrect_test.dart | 8 +- .../test/text/font_collection_test.dart | 9 +- lib/web_ui/test/text/font_loading_test.dart | 7 +- lib/web_ui/test/text/line_breaker_test.dart | 89 +- lib/web_ui/test/text/measurement_test.dart | 10 +- lib/web_ui/test/text/word_breaker_test.dart | 5 + lib/web_ui/test/text_editing_test.dart | 415 ++++- lib/web_ui/test/text_test.dart | 7 +- lib/web_ui/test/title_test.dart | 7 +- lib/web_ui/test/window_test.dart | 5 + runtime/BUILD.gn | 44 +- runtime/dart_isolate.cc | 29 +- runtime/dart_isolate.h | 7 +- runtime/dart_isolate_unittests.cc | 89 +- runtime/dart_service_isolate.cc | 2 +- runtime/dart_vm.cc | 10 +- runtime/{window_data.cc => platform_data.cc} | 6 +- runtime/{window_data.h => platform_data.h} | 14 +- runtime/runtime_controller.cc | 158 +- runtime/runtime_controller.h | 53 +- runtime/service_protocol.cc | 36 +- runtime/service_protocol.h | 9 +- shell/common/BUILD.gn | 94 +- shell/common/animator.cc | 21 +- shell/common/animator.h | 1 + shell/common/animator_unittests.cc | 6 +- shell/common/engine.cc | 63 +- shell/common/engine.h | 40 +- shell/common/engine_unittests.cc | 234 +++ shell/common/pointer_data_dispatcher.h | 2 +- shell/common/rasterizer.cc | 80 +- shell/common/rasterizer.h | 85 +- shell/common/serialization_callbacks.cc | 4 +- shell/common/shell.cc | 190 +-- shell/common/shell.h | 46 +- shell/common/shell_benchmarks.cc | 6 +- shell/common/shell_test.cc | 62 +- shell/common/shell_test.h | 6 +- .../shell_test_external_view_embedder.cc | 29 +- .../shell_test_external_view_embedder.h | 24 +- shell/common/shell_test_platform_view_gl.cc | 2 +- shell/common/shell_test_platform_view_gl.h | 2 +- shell/common/shell_unittests.cc | 558 ++++++- shell/common/skp_shader_warmup_unittests.cc | 6 +- shell/common/switches.cc | 7 +- shell/common/switches.h | 15 +- shell/common/vsync_waiter.cc | 3 - shell/gpu/BUILD.gn | 61 +- shell/gpu/gpu.gni | 26 - shell/gpu/gpu_surface_gl.cc | 21 +- shell/gpu/gpu_surface_gl_delegate.h | 13 +- shell/platform/android/BUILD.gn | 4 + .../android/android_external_texture_gl.cc | 5 +- .../platform/android/android_shell_holder.cc | 23 +- shell/platform/android/android_shell_holder.h | 2 +- shell/platform/android/android_surface_gl.cc | 2 +- shell/platform/android/android_surface_gl.h | 2 +- .../external_view_embedder.cc | 5 + .../external_view_embedder.h | 2 + .../external_view_embedder_unittests.cc | 8 + .../android/FlutterFragmentActivity.java | 20 +- .../embedding/android/FlutterView.java | 2 +- .../engine/loader/ApplicationInfoLoader.java | 161 ++ .../engine/loader/FlutterApplicationInfo.java | 46 + .../engine/loader/FlutterLoader.java | 122 +- .../systemchannels/PlatformChannel.java | 16 +- .../systemchannels/TextInputChannel.java | 61 + .../plugin/common/JSONMethodCodec.java | 11 + .../flutter/plugin/common/MethodChannel.java | 15 +- .../io/flutter/plugin/common/MethodCodec.java | 14 + .../plugin/common/StandardMethodCodec.java | 18 + .../editing/InputConnectionAdaptor.java | 7 + .../plugin/editing/TextInputPlugin.java | 10 + .../plugin/platform/PlatformPlugin.java | 11 +- .../platform/PlatformViewsController.java | 166 +- .../io/flutter/view/AccessibilityBridge.java | 11 + .../android/io/flutter/view/FlutterView.java | 12 +- .../android/surface/android_surface_mock.cc | 2 +- .../android/surface/android_surface_mock.h | 2 +- .../test/io/flutter/FlutterTestSuite.java | 2 + .../android/FlutterFragmentActivityTest.java | 60 +- .../embedding/engine/PluginComponentTest.java | 8 +- .../loader/ApplicationInfoLoaderTest.java | 193 +++ .../systemchannels/PlatformChannelTest.java | 45 + .../common/StandardMethodCodecTest.java | 24 + .../editing/InputConnectionAdaptorTest.java | 312 ++++ .../plugin/editing/TextInputPluginTest.java | 80 + .../plugin/platform/PlatformPluginTest.java | 26 + .../platform/PlatformViewsControllerTest.java | 164 +- .../flutter/view/AccessibilityBridgeTest.java | 35 + .../common/cpp/client_wrapper/BUILD.gn | 56 +- .../basic_message_channel_unittests.cc | 5 +- .../client_wrapper/binary_messenger_impl.h | 50 + ...tream_wrappers.h => byte_buffer_streams.h} | 51 +- .../client_wrapper/core_implementations.cc | 150 ++ .../cpp/client_wrapper/core_wrapper_files.gni | 20 +- .../encodable_value_unittests.cc | 184 +-- .../client_wrapper/engine_method_result.cc | 46 +- .../include/flutter/byte_streams.h | 85 + .../include/flutter/encodable_value.h | 194 ++- .../include/flutter/event_channel.h | 4 +- .../include/flutter/event_sink.h | 33 +- .../include/flutter/method_result.h | 46 +- .../include/flutter/plugin_registrar.h | 11 +- .../flutter/standard_codec_serializer.h | 76 + .../include/flutter/standard_message_codec.h | 25 +- .../include/flutter/standard_method_codec.h | 27 +- .../method_channel_unittests.cc | 6 +- .../method_result_functions_unittests.cc | 7 +- .../cpp/client_wrapper/plugin_registrar.cc | 120 +- .../common/cpp/client_wrapper/publish.gni | 4 +- .../cpp/client_wrapper/standard_codec.cc | 398 +++-- .../standard_codec_serializer.h | 54 - .../standard_message_codec_unittests.cc | 89 +- .../standard_method_codec_unittests.cc | 37 +- .../testing/encodable_value_utils.cc | 89 -- .../testing/encodable_value_utils.h | 22 - .../testing/stub_flutter_api.cc | 8 - .../client_wrapper/testing/stub_flutter_api.h | 3 - .../testing/test_codec_extensions.cc | 80 + .../testing/test_codec_extensions.h | 89 ++ .../platform/common/cpp/json_method_codec.cc | 12 +- .../cpp/public/flutter_plugin_registrar.h | 13 - shell/platform/darwin/ios/BUILD.gn | 2 + .../darwin/ios/framework/Headers/Flutter.h | 46 - .../ios/framework/Headers/FlutterEngine.h | 53 +- .../framework/Headers/FlutterViewController.h | 32 +- .../framework/Source/FlutterDartProject.mm | 54 +- .../Source/FlutterDartProjectTest.mm | 85 + .../Source/FlutterDartProject_Internal.h | 6 +- .../ios/framework/Source/FlutterEngine.mm | 118 +- .../ios/framework/Source/FlutterEngineTest.mm | 22 +- .../framework/Source/FlutterEngine_Internal.h | 4 +- .../ios/framework/Source/FlutterEngine_Test.h | 10 + .../framework/Source/FlutterPlatformPlugin.mm | 14 + .../Source/FlutterPlatformPluginTest.mm | 51 + .../framework/Source/FlutterPlatformViews.mm | 119 +- .../Source/FlutterPlatformViewsTest.mm | 382 +++++ .../Source/FlutterPlatformViews_Internal.h | 49 +- .../Source/FlutterPlatformViews_Internal.mm | 118 +- .../Source/FlutterTextInputPlugin.mm | 49 +- .../Source/FlutterTextInputPluginTest.m | 343 ++-- .../framework/Source/FlutterViewController.mm | 54 +- .../Source/FlutterViewControllerTest.mm | 8 +- .../ios/framework/Source/SemanticsObject.mm | 1 + .../framework/Source/SemanticsObjectTest.mm | 1 + .../framework/Source/accessibility_bridge.h | 12 +- .../framework/Source/accessibility_bridge.mm | 54 +- .../Source/accessibility_bridge_ios.h | 7 + .../Source/accessibility_bridge_test.mm | 231 ++- .../darwin/ios/ios_external_texture_gl.h | 13 + .../darwin/ios/ios_external_texture_gl.mm | 108 +- .../darwin/ios/ios_external_texture_metal.h | 5 + .../darwin/ios/ios_external_texture_metal.mm | 162 +- shell/platform/darwin/ios/ios_surface.h | 5 +- shell/platform/darwin/ios/ios_surface.mm | 22 +- shell/platform/darwin/ios/ios_surface_gl.h | 2 +- shell/platform/darwin/ios/ios_surface_gl.mm | 4 +- .../platform/darwin/ios/ios_surface_metal.mm | 2 +- .../darwin/ios/ios_surface_software.mm | 2 +- .../platform/darwin/ios/platform_view_ios.mm | 10 +- .../macos/framework/Source/FlutterEngine.mm | 50 + .../framework/Source/FlutterViewController.mm | 1 + shell/platform/embedder/embedder.cc | 32 +- shell/platform/embedder/embedder.h | 123 +- shell/platform/embedder/embedder_engine.h | 1 - .../platform/embedder/embedder_surface_gl.cc | 4 +- shell/platform/embedder/embedder_surface_gl.h | 4 +- .../embedder/tests/embedder_config_builder.cc | 21 +- .../embedder/tests/embedder_config_builder.h | 6 + .../embedder/tests/embedder_test_context.cc | 7 +- .../embedder/tests/embedder_test_context.h | 6 +- .../embedder/tests/embedder_unittests.cc | 55 + .../fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart | 21 +- .../dart-pkg/zircon/lib/src/handle.dart | 1 - .../zircon/lib/src/handle_waiter.dart | 1 - .../dart-pkg/zircon/lib/src/system.dart | 91 +- .../fuchsia/dart-pkg/zircon/lib/zircon.dart | 1 - shell/platform/fuchsia/dart_runner/BUILD.gn | 3 + .../fuchsia/dart_runner/embedder/builtin.dart | 3 +- .../fuchsia/dart_runner/kernel/BUILD.gn | 2 +- shell/platform/fuchsia/flutter/BUILD.gn | 485 +++--- shell/platform/fuchsia/flutter/component.cc | 6 + .../fuchsia/flutter/compositor_context.cc | 180 ++- .../fuchsia/flutter/compositor_context.h | 36 +- shell/platform/fuchsia/flutter/engine.cc | 306 ++-- shell/platform/fuchsia/flutter/engine.h | 34 +- .../fuchsia/flutter/engine_flutter_runner.gni | 150 -- .../platform/fuchsia/flutter/kernel/BUILD.gn | 2 +- .../platform/fuchsia/flutter/platform_view.cc | 236 +-- .../platform/fuchsia/flutter/platform_view.h | 38 +- .../fuchsia/flutter/platform_view_unittest.cc | 285 +++- .../fuchsia/flutter/session_connection.cc | 57 +- .../fuchsia/flutter/session_connection.h | 54 +- shell/platform/fuchsia/flutter/surface.cc | 9 +- shell/platform/fuchsia/flutter/surface.h | 4 +- .../tests/session_connection_unittests.cc | 25 +- .../fuchsia/flutter/vulkan_surface.cc | 21 +- .../platform/fuchsia/flutter/vulkan_surface.h | 89 +- .../fuchsia/flutter/vulkan_surface_pool.cc | 96 +- .../fuchsia/flutter/vulkan_surface_pool.h | 33 +- .../flutter/vulkan_surface_producer.cc | 25 +- .../fuchsia/flutter/vulkan_surface_producer.h | 47 +- shell/platform/glfw/client_wrapper/BUILD.gn | 2 + .../include/flutter/plugin_registrar_glfw.h | 12 +- .../testing/stub_flutter_glfw_api.cc | 8 + .../testing/stub_flutter_glfw_api.h | 3 + shell/platform/glfw/flutter_glfw.cc | 3 + shell/platform/glfw/public/flutter_glfw.h | 16 + .../linux/fl_basic_message_channel.cc | 21 +- shell/platform/linux/fl_binary_messenger.cc | 21 +- shell/platform/linux/fl_engine.cc | 56 +- shell/platform/linux/fl_json_method_codec.cc | 9 +- shell/platform/linux/fl_method_channel.cc | 21 +- shell/platform/linux/fl_method_codec.cc | 3 +- shell/platform/linux/fl_renderer.cc | 96 +- shell/platform/linux/fl_renderer.h | 65 +- shell/platform/linux/fl_renderer_headless.cc | 15 +- shell/platform/linux/fl_renderer_x11.cc | 73 +- shell/platform/linux/fl_renderer_x11.h | 9 - .../linux/fl_standard_message_codec.cc | 113 +- .../linux/fl_standard_method_codec.cc | 37 +- shell/platform/linux/fl_string_codec.cc | 4 +- shell/platform/linux/fl_text_input_plugin.cc | 37 +- shell/platform/linux/fl_text_input_plugin.h | 4 +- shell/platform/linux/fl_value.cc | 88 +- shell/platform/linux/fl_view.cc | 41 +- shell/platform/linux/testing/mock_renderer.cc | 24 +- shell/platform/windows/BUILD.gn | 10 + .../platform/windows/angle_surface_manager.cc | 32 +- .../platform/windows/angle_surface_manager.h | 17 +- .../platform/windows/client_wrapper/BUILD.gn | 18 +- .../windows/client_wrapper/flutter_engine.cc | 83 + .../flutter_engine_unittests.cc | 106 ++ .../client_wrapper/flutter_view_controller.cc | 68 +- .../flutter_view_controller_unittests.cc | 45 +- .../include/flutter/dart_project.h | 1 + .../include/flutter/flutter_engine.h | 94 ++ .../include/flutter/flutter_view_controller.h | 38 +- .../flutter/plugin_registrar_windows.h | 80 +- .../plugin_registrar_windows_unittests.cc | 129 +- .../testing/stub_flutter_windows_api.cc | 111 +- .../testing/stub_flutter_windows_api.h | 50 +- shell/platform/windows/cursor_handler.cc | 21 +- .../windows/flutter_project_bundle.cc | 81 + .../platform/windows/flutter_project_bundle.h | 65 + shell/platform/windows/flutter_windows.cc | 358 ++--- .../windows/flutter_windows_engine.cc | 259 +++ .../platform/windows/flutter_windows_engine.h | 126 ++ .../platform/windows/flutter_windows_view.cc | 123 +- shell/platform/windows/flutter_windows_view.h | 46 +- .../platform/windows/public/flutter_windows.h | 167 +- shell/platform/windows/system_utils.h | 36 + .../windows/system_utils_unittests.cc | 75 + shell/platform/windows/system_utils_win32.cc | 93 ++ .../windows/win32_dpi_utils_unittests.cc | 4 + .../windows/win32_flutter_window_unittests.cc | 4 + .../windows/win32_platform_handler.cc | 13 +- .../win32_window_proc_delegate_manager.cc | 42 + .../win32_window_proc_delegate_manager.h | 58 + ..._window_proc_delegate_manager_unittests.cc | 168 ++ .../windows/win32_window_unittests.cc | 4 + shell/platform/windows/window_state.h | 76 +- shell/testing/tester_main.cc | 9 +- sky/packages/sky_engine/LICENSE | 155 +- testing/BUILD.gn | 14 +- testing/dart/canvas_test.dart | 93 +- .../dart/window_hooks_integration_test.dart | 8 - testing/dart/window_test.dart | 10 +- testing/fuchsia/run_tests.sh | 54 +- testing/fuchsia/test_fars | 4 - testing/ios/IosUnitTests/App/Info.plist | 20 + .../IosUnitTests.xcodeproj/project.pbxproj | 27 + testing/ios/IosUnitTests/run_tests.sh | 7 +- testing/run_tests.py | 15 +- testing/scenario_app/.gitignore | 3 + testing/scenario_app/README.md | 13 + testing/scenario_app/android/app/build.gradle | 5 +- .../flutter/scenariosui/ScreenshotUtil.java | 6 +- .../scenarios/TestableFlutterActivity.java | 19 +- .../scenarios/TextPlatformViewActivity.java | 18 +- testing/scenario_app/android/build.gradle | 3 +- .../scenario_app/android/gradle-home/.vpython | 11 + .../android/gradle-home/bin/python | 14 + ...atformTextureUiTests__testPlatformView.png | Bin 33464 -> 29557 bytes ...xtureUiTests__testPlatformViewClippath.png | Bin 30028 -> 19560 bytes ...xtureUiTests__testPlatformViewCliprect.png | Bin 21585 -> 18389 bytes ...tureUiTests__testPlatformViewCliprrect.png | Bin 23520 -> 20209 bytes ...xtureUiTests__testPlatformViewMultiple.png | Bin 53141 -> 25037 bytes ...atformViewMultipleBackgroundForeground.png | Bin 11251 -> 11452 bytes ...estPlatformViewMultipleWithoutOverlays.png | Bin 62517 -> 43916 bytes ...extureUiTests__testPlatformViewOpacity.png | Bin 31570 -> 22231 bytes ...TextureUiTests__testPlatformViewRotate.png | Bin 33464 -> 22559 bytes ...tureUiTests__testPlatformViewTransform.png | Bin 24157 -> 22950 bytes ...estPlatformViewTwoIntersectingOverlays.png | Bin 27067 -> 30760 bytes ....PlatformViewUiTests__testPlatformView.png | Bin 39235 -> 32713 bytes ...mViewUiTests__testPlatformViewClippath.png | Bin 28517 -> 24012 bytes ...mViewUiTests__testPlatformViewCliprect.png | Bin 21011 -> 18636 bytes ...ViewUiTests__testPlatformViewCliprrect.png | Bin 30269 -> 25946 bytes ...mViewUiTests__testPlatformViewMultiple.png | Bin 33777 -> 28040 bytes ...atformViewMultipleBackgroundForeground.png | Bin 13277 -> 12557 bytes ...estPlatformViewMultipleWithoutOverlays.png | Bin 63761 -> 50031 bytes ...rmViewUiTests__testPlatformViewOpacity.png | Bin 30299 -> 25349 bytes ...ormViewUiTests__testPlatformViewRotate.png | Bin 28736 -> 24657 bytes ...ViewUiTests__testPlatformViewTransform.png | Bin 33845 -> 26722 bytes ...estPlatformViewTwoIntersectingOverlays.png | Bin 40144 -> 33889 bytes testing/scenario_app/assemble_apk.sh | 42 +- .../build_and_run_android_tests.sh | 63 +- .../scenario_app/build_and_run_ios_tests.sh | 54 +- testing/scenario_app/compile_android_aot.sh | 56 +- testing/scenario_app/compile_android_jit.sh | 100 ++ testing/scenario_app/compile_ios_aot.sh | 60 +- testing/scenario_app/compile_ios_jit.sh | 52 +- testing/scenario_app/firebase_xctest.sh | 54 +- .../Scenarios.xcodeproj/project.pbxproj | 4 + .../Scenarios/FlutterEngine+ScenariosTest.m | 2 +- .../ios/Scenarios/Scenarios/Info.plist | 2 - .../ScenariosTests/FlutterEngineTest.m | 2 +- .../FlutterViewControllerInitialRouteTest.m | 84 + .../FlutterViewControllerTest.m | 9 +- .../Scenarios/ScenariosTests/ScenariosTests.m | 10 +- ...tform_view_clippath_iPhone 8_simulator.png | Bin 20295 -> 20863 bytes ...form_view_cliprrect_iPhone 8_simulator.png | Bin 19558 -> 19543 bytes testing/scenario_app/lib/main.dart | 8 + .../lib/src/initial_route_reply.dart | 30 + testing/scenario_app/lib/src/scenario.dart | 22 +- testing/scenario_app/lib/src/scenarios.dart | 13 +- testing/scenario_app/run_android_tests.sh | 41 +- testing/scenario_app/run_ios_tests.sh | 43 +- third_party/txt/src/txt/paragraph_txt.cc | 18 +- third_party/txt/src/txt/paragraph_txt.h | 2 + third_party/txt/tests/paragraph_unittests.cc | 29 + tools/android_lint/bin/main.dart | 4 +- tools/const_finder/lib/const_finder.dart | 24 +- .../const_finder/test/const_finder_test.dart | 17 + .../test/fixtures/lib/consts.dart | 8 + .../test/fixtures/lib/target.dart | 36 + tools/font-subset/main.cc | 17 +- tools/fuchsia/build_fuchsia_artifacts.py | 85 +- .../fuchsia/merge_and_upload_debug_symbols.py | 30 +- tools/gn | 16 +- vulkan/BUILD.gn | 5 +- vulkan/vulkan_application.cc | 9 +- vulkan/vulkan_application.h | 2 +- vulkan/vulkan_backbuffer.cc | 1 - vulkan/vulkan_command_buffer.cc | 1 - vulkan/vulkan_debug_report.cc | 4 +- vulkan/vulkan_device.cc | 1 - vulkan/vulkan_handle.cc | 1 - vulkan/vulkan_image.cc | 1 - vulkan/vulkan_interface.cc | 1 - vulkan/vulkan_native_surface.cc | 1 - vulkan/vulkan_proc_table.cc | 1 - vulkan/vulkan_surface.cc | 5 +- vulkan/vulkan_swapchain.cc | 14 +- vulkan/vulkan_utilities.cc | 1 - web_sdk/test/api_conform_test.dart | 2 +- .../web_engine_tester/lib/golden_tester.dart | 2 +- 646 files changed, 19921 insertions(+), 16075 deletions(-) create mode 100644 ci/bin/format.dart create mode 100644 ci/dev/README.md create mode 100644 ci/dev/prod_builders.json create mode 100644 ci/dev/try_builders.json create mode 100644 ci/format.bat delete mode 100755 ci/test.sh create mode 100644 lib/ui/window/platform_configuration.cc create mode 100644 lib/ui/window/platform_configuration.h create mode 100644 lib/ui/window/platform_configuration_unittests.cc create mode 100644 lib/web_ui/CODE_CONVENTIONS.md delete mode 100644 lib/web_ui/lib/assets/houdini_painter.js rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/canvas.dart (96%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/canvaskit_api.dart (83%) rename lib/web_ui/lib/src/engine/{compositor/canvas_kit_canvas.dart => canvaskit/canvaskit_canvas.dart} (95%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/color_filter.dart (96%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/embedded_views.dart (99%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/fonts.dart (69%) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/image.dart rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/image_filter.dart (92%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/initialization.dart (97%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/layer.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/layer_scene_builder.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/layer_tree.dart (92%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/mask_filter.dart (91%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/n_way_canvas.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/painting.dart (94%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/path.dart (99%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/path_metrics.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/picture.dart (65%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/picture_recorder.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/platform_message.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/raster_cache.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/rasterizer.dart (100%) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/shader.dart rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/skia_object_cache.dart (69%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/surface.dart (80%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/text.dart (90%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/util.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/vertices.dart (65%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/viewport_metrics.dart (100%) delete mode 100644 lib/web_ui/lib/src/engine/compositor/image.dart delete mode 100644 lib/web_ui/lib/src/engine/houdini_canvas.dart rename lib/web_ui/lib/src/engine/{surface => html}/backdrop_filter.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/canvas.dart (97%) rename lib/web_ui/lib/src/engine/{surface => html}/clip.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/debug_canvas_reuse_overlay.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/image_filter.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/offset.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/opacity.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/painting.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/conic.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/cubic.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_metrics.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_ref.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_to_svg.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_utils.dart (97%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_windings.dart (96%) rename lib/web_ui/lib/src/engine/{surface => html}/path/tangent.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/picture.dart (87%) rename lib/web_ui/lib/src/engine/{surface => html}/platform_view.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/recording_canvas.dart (87%) rename lib/web_ui/lib/src/engine/{surface => html}/render_vertices.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/scene.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/scene_builder.dart (98%) rename lib/web_ui/lib/src/engine/{ => html}/shader.dart (64%) rename lib/web_ui/lib/src/engine/{surface => html}/surface.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/surface_stats.dart (98%) rename lib/web_ui/lib/src/engine/{surface => html}/transform.dart (100%) create mode 100644 lib/web_ui/test/canvaskit/image_test.dart create mode 100644 lib/web_ui/test/canvaskit/shader_test.dart create mode 100644 lib/web_ui/test/canvaskit/test_data.dart create mode 100644 lib/web_ui/test/canvaskit/vertices_test.dart create mode 100644 lib/web_ui/test/engine/surface/path/path_iterator_test.dart rename lib/web_ui/test/{ => engine/surface/path}/path_winding_test.dart (99%) create mode 100644 lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart create mode 100644 lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart rename runtime/{window_data.cc => platform_data.cc} (63%) rename runtime/{window_data.h => platform_data.h} (81%) create mode 100644 shell/common/engine_unittests.cc create mode 100644 shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java create mode 100644 shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java create mode 100644 shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h rename shell/platform/common/cpp/client_wrapper/{byte_stream_wrappers.h => byte_buffer_streams.h} (58%) create mode 100644 shell/platform/common/cpp/client_wrapper/core_implementations.cc create mode 100644 shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h create mode 100644 shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h delete mode 100644 shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h delete mode 100644 shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc delete mode 100644 shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h create mode 100644 shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc create mode 100644 shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h create mode 100644 shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm create mode 100644 shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h create mode 100644 shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm delete mode 100644 shell/platform/fuchsia/flutter/engine_flutter_runner.gni create mode 100644 shell/platform/windows/client_wrapper/flutter_engine.cc create mode 100644 shell/platform/windows/client_wrapper/flutter_engine_unittests.cc create mode 100644 shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h create mode 100644 shell/platform/windows/flutter_project_bundle.cc create mode 100644 shell/platform/windows/flutter_project_bundle.h create mode 100644 shell/platform/windows/flutter_windows_engine.cc create mode 100644 shell/platform/windows/flutter_windows_engine.h create mode 100644 shell/platform/windows/system_utils.h create mode 100644 shell/platform/windows/system_utils_unittests.cc create mode 100644 shell/platform/windows/system_utils_win32.cc create mode 100644 shell/platform/windows/win32_window_proc_delegate_manager.cc create mode 100644 shell/platform/windows/win32_window_proc_delegate_manager.h create mode 100644 shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc create mode 100644 testing/scenario_app/android/gradle-home/.vpython create mode 100755 testing/scenario_app/android/gradle-home/bin/python create mode 100755 testing/scenario_app/compile_android_jit.sh create mode 100644 testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m create mode 100644 testing/scenario_app/lib/src/initial_route_reply.dart diff --git a/.cirrus.yml b/.cirrus.yml index 5731441e4e0be..f5ca07262b00b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -4,9 +4,13 @@ web_shard_template: &WEB_SHARD_TEMPLATE only_if: "changesInclude('.cirrus.yml', 'DEPS', 'lib/web_ui/**', 'web_sdk/**') || $CIRRUS_PR == ''" environment: # As of March 2020, the Web shards needed 16G of RAM and 4 CPUs to run all framework tests with goldens without flaking. + # The tests are encountering a flake in Chrome. Increasing the number of shards to decrease race conditions. + # Shard number kept at 8 for the engine since 12 shards exhausted the Cirrus limits. + # https://github.com/flutter/flutter/issues/62510 + WEB_SHARD_COUNT: 8 CPU: 4 MEMORY: 16G - WEB_SHARD_COUNT: 4 + CHROME_NO_SANDBOX: true compile_host_script: | cd $ENGINE_PATH/src ./flutter/tools/gn --unoptimized --full-dart-sdk @@ -108,7 +112,19 @@ task: - name: web_tests-2-linux << : *WEB_SHARD_TEMPLATE - - name: web_tests-3_last-linux # last Web shard must end with _last + - name: web_tests-3-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-4-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-5-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-6-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-7_last-linux # last Web shard must end with _last << : *WEB_SHARD_TEMPLATE - name: build_test diff --git a/BUILD.gn b/BUILD.gn index 90ca39d027844..0f1c68a8990c5 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -19,18 +19,12 @@ config("config") { cflags = [ "/WX" ] # Treat warnings as errors. } } -} -# This "fuchsia_legacy" configuration includes old, non-embedder API sources and -# defines the LEGACY_FUCHSIA_EMBEDDER symbol. This config and its associated -# template are both transitional and will be removed after the embedder API -# transition is complete. -# -# See `source_set_maybe_fuchsia_legacy` in //flutter/common/config.gni -# -# TODO(fxb/54041): Remove when no longer neccesary. -config("fuchsia_legacy") { - if (is_fuchsia) { + # This define is transitional and will be removed after the embedder API + # transition is complete. + # + # TODO(bugs.fuchsia.dev/54041): Remove when no longer neccesary. + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { defines = [ "LEGACY_FUCHSIA_EMBEDDER" ] } } diff --git a/DEPS b/DEPS index 892cd355c9f5f..c711df19fece4 100644 --- a/DEPS +++ b/DEPS @@ -26,7 +26,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', - 'skia_revision': '8cc118dce81392f94da2a05de41a48fb34f54b1f', + 'skia_revision': '370cbc70e080ada9dd75b416d019d91304e1b168', # When updating the Dart revision, ensure that all entries that are # dependencies of Dart are also updated to match the entries in the @@ -34,7 +34,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS. # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'bd528bfbd69deecd3e8ad21e634da495bf0c09bb', + 'dart_revision': '3367c66051ce0b17c9d895ae3600f0c107d7a0cf', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -42,23 +42,21 @@ vars = { 'dart_boringssl_gen_rev': '429ccb1877f7987a6f3988228bc2440e61293499', 'dart_boringssl_rev': '4dfd5af70191b068aebe567b8e29ce108cee85ce', 'dart_collection_rev': '583693680fc067e34ca5b72503df25e8b80579f9', - 'dart_dart2js_info_tag': '0.6.0', 'dart_dart_style_tag': '1.3.6', 'dart_http_retry_tag': '0.1.1', 'dart_http_throttle_tag': '1.0.2', 'dart_intl_tag': '0.16.1', - 'dart_linter_tag': '0.1.117', + 'dart_linter_tag': '0.1.118', 'dart_oauth2_tag': '1.6.0', 'dart_protobuf_rev': '3746c8fd3f2b0147623a8e3db89c3ff4330de760', - 'dart_pub_rev': '04b054b62cc437cf23451785fdc50e49cd9de139', + 'dart_pub_rev': 'cf9795f3bb209504c349e20501f0b4b8ae31530c', 'dart_pub_semver_tag': 'v1.4.4', 'dart_quiver-dart_tag': '246e754fe45cecb6aa5f3f13b4ed61037ff0d784', 'dart_resource_rev': 'f8e37558a1c4f54550aa463b88a6a831e3e33cd6', - 'dart_root_certificates_rev': '16ef64be64c7dfdff2b9f4b910726e635ccc519e', + 'dart_root_certificates_rev': '7e5ec82c99677a2e5b95ce296c4d68b0d3378ed8', 'dart_shelf_packages_handler_tag': '2.0.0', 'dart_shelf_proxy_tag': '0.1.0+7', 'dart_shelf_static_rev': 'v0.2.8', - 'dart_shelf_tag': '0.7.3+3', 'dart_shelf_web_socket_tag': '0.2.2+3', 'dart_sse_tag': 'e5cf68975e8e87171a3dc297577aa073454a91dc', 'dart_stack_trace_tag': 'd3813ca0a77348e0faf0d6af0cc17913e36afa39', @@ -107,7 +105,7 @@ allowed_hosts = [ ] deps = { - 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'fe3b46e595e7ce1350e11aa0c90365976051f4a3', + 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'a6c0959d1ac8cdfe6f9ff87892bc4905a73699fe', # Fuchsia compatibility # @@ -125,7 +123,7 @@ deps = { Var('fuchsia_git') + '/third_party/rapidjson' + '@' + 'ef3564c5c8824989393b87df25355baf35ff544b', 'src/third_party/harfbuzz': - Var('fuchsia_git') + '/third_party/harfbuzz' + '@' + 'f5c000538699a4e40649508a44f41d37035e6c35', + Var('fuchsia_git') + '/third_party/harfbuzz' + '@' + '9c55f4cf3313d68d68f68419e7a57fb0771fcf49', 'src/third_party/libcxx': Var('fuchsia_git') + '/third_party/libcxx' + '@' + '7524ef50093a376f334a62a7e5cebf5d238d4c99', @@ -161,9 +159,6 @@ deps = { # WARNING: Unused Dart dependencies in the list below till "WARNING:" marker are removed automatically - see create_updated_flutter_deps.py. - 'src/third_party/dart/pkg/analysis_server/language_model': - {'packages': [{'version': 'lIRt14qoA1Cocb8j3yw_Fx5cfYou2ddam6ArBm4AI6QC', 'package': 'dart/language_model'}], 'dep_type': 'cipd'}, - 'src/third_party/dart/third_party/pkg/args': Var('dart_git') + '/args.git' + '@' + Var('dart_args_tag'), @@ -177,28 +172,28 @@ deps = { Var('dart_git') + '/boolean_selector.git@665e6921ab246569420376f827bff4585dff0b14', 'src/third_party/dart/third_party/pkg/charcode': - Var('dart_git') + '/charcode.git@af1e2d59a9c383da94f99ea51dac4b93fb0626c4', + Var('dart_git') + '/charcode.git@4a685faba42d86ebd9d661eadd1e79d0a1c34c43', 'src/third_party/dart/third_party/pkg/cli_util': - Var('dart_git') + '/cli_util.git@0.1.4', + Var('dart_git') + '/cli_util.git@0.2.0', 'src/third_party/dart/third_party/pkg/collection': Var('dart_git') + '/collection.git' + '@' + Var('dart_collection_rev'), 'src/third_party/dart/third_party/pkg/convert': - Var('dart_git') + '/convert.git@49bde5b371eb5c2c8e721557cf762f17c75e49fc', + Var('dart_git') + '/convert.git@c1b01f832835d3d8a06b0b246a361c0eaab35d3c', 'src/third_party/dart/third_party/pkg/crypto': - Var('dart_git') + '/crypto.git@7422fb2f6584fe1839eb30bc4ca56e9f9760b801', + Var('dart_git') + '/crypto.git@f7c48b334b1386bc5ab0f706fbcd6df8496a87fc', 'src/third_party/dart/third_party/pkg/csslib': Var('dart_git') + '/csslib.git@451448a9ac03f87a8d0377fc0b411d8c388a6cb4', 'src/third_party/dart/third_party/pkg/dart2js_info': - Var('dart_git') + '/dart2js_info.git' + '@' + Var('dart_dart2js_info_tag'), + Var('dart_git') + '/dart2js_info.git@94ba36cb77067f28b75a4212e77b810a2d7385e9', 'src/third_party/dart/third_party/pkg/dartdoc': - Var('dart_git') + '/dartdoc.git@6d5396c2b4bc415ab9cb3d8212b87ecffd90a272', + Var('dart_git') + '/dartdoc.git@291ebc50072746bc59ccab59115a298915218428', 'src/third_party/dart/third_party/pkg/ffi': Var('dart_git') + '/ffi.git@454ab0f9ea6bd06942a983238d8a6818b1357edb', @@ -231,7 +226,7 @@ deps = { Var('dart_git') + '/intl.git' + '@' + Var('dart_intl_tag'), 'src/third_party/dart/third_party/pkg/json_rpc_2': - Var('dart_git') + '/json_rpc_2.git@d589e635d8ccb7cda6a804bd571f88abbabab146', + Var('dart_git') + '/json_rpc_2.git@995611cf006c927d51cc53cb28f1aa4356d5414f', 'src/third_party/dart/third_party/pkg/linter': Var('dart_git') + '/linter.git' + '@' + Var('dart_linter_tag'), @@ -240,13 +235,13 @@ deps = { Var('dart_git') + '/logging.git@9561ba016ae607747ae69b846c0e10958ca58ed4', 'src/third_party/dart/third_party/pkg/markdown': - Var('dart_git') + '/markdown.git@acaddfe74217f62498b5cf0cf5429efa6a700be3', + Var('dart_git') + '/markdown.git@dbeafd47759e7dd0a167602153bb9c49fb5e5fe7', 'src/third_party/dart/third_party/pkg/matcher': Var('dart_git') + '/matcher.git@9cae8faa7868bf3a88a7ba45eb0bd128e66ac515', 'src/third_party/dart/third_party/pkg/mime': - Var('dart_git') + '/mime.git@179b5e6a88f4b63f36dc1b8fcbc1e83e5e0cd3a7', + Var('dart_git') + '/mime.git@0.9.7', 'src/third_party/dart/third_party/pkg/mockito': Var('dart_git') + '/mockito.git@d39ac507483b9891165e422ec98d9fb480037c8b', @@ -282,7 +277,7 @@ deps = { Var('dart_git') + '/resource.git' + '@' + Var('dart_resource_rev'), 'src/third_party/dart/third_party/pkg/shelf': - Var('dart_git') + '/shelf.git' + '@' + Var('dart_shelf_tag'), + Var('dart_git') + '/shelf.git@289309adc6c39aab0a63db676d550c517fc1cc2d', 'src/third_party/dart/third_party/pkg/shelf_packages_handler': Var('dart_git') + '/shelf_packages_handler.git' + '@' + Var('dart_shelf_packages_handler_tag'), @@ -324,7 +319,7 @@ deps = { Var('dart_git') + '/term_glyph.git@6a0f9b6fb645ba75e7a00a4e20072678327a0347', 'src/third_party/dart/third_party/pkg/test': - Var('dart_git') + '/test.git@c6b3fe63eda87da1687580071cad1eefd575f851', + Var('dart_git') + '/test.git@e37a93bbeae23b215972d1659ac865d71287ff6a', 'src/third_party/dart/third_party/pkg/test_reflective_loader': Var('dart_git') + '/test_reflective_loader.git' + '@' + Var('dart_test_reflective_loader_tag'), @@ -354,7 +349,7 @@ deps = { Var('dart_git') + '/package_config.git@9c586d04bd26fef01215fd10e7ab96a3050cfa64', 'src/third_party/dart/tools/sdks': - {'packages': [{'version': 'version:2.10.0-0.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, + {'packages': [{'version': 'version:2.10.0-3.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, # WARNING: end of dart dependencies list that is cleaned up automatically - see create_updated_flutter_deps.py. @@ -430,7 +425,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/sdk/build-tools/${{platform}}', - 'version': 'version:29.0.1' + 'version': 'version:30.0.1' } ], 'condition': 'download_android_deps', @@ -441,7 +436,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/sdk/platform-tools/${{platform}}', - 'version': 'version:29.0.2' + 'version': 'version:30.0.4' } ], 'condition': 'download_android_deps', @@ -452,7 +447,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/sdk/platforms', - 'version': 'version:29r1' + 'version': 'version:30r2' } ], 'condition': 'download_android_deps', @@ -521,7 +516,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/mac-amd64', - 'version': 'T2xc0OuiKH4DXmYwnM0GRhzMP1k2DGJQ3yccE4ld2hoC' + 'version': 'KGZUxRgWxdxH_PR6zLSQrUHqKZ-vWeFDNZ76kG9nDDIC' } ], 'condition': 'host_os == "mac"', @@ -541,7 +536,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'd_5wDVmBdmLoZFwim5qFY4G9xXel2vAMP6s_DY65ulsC' + 'version': 'LhYt1i9FPQeIhMU1ReI1t1fk4Zx0F3uQwhrr4NkLpuMC' } ], 'condition': 'host_os == "linux"', diff --git a/build/generate_coverage.py b/build/generate_coverage.py index fb6ad9d74514c..6286d30a43612 100755 --- a/build/generate_coverage.py +++ b/build/generate_coverage.py @@ -42,7 +42,7 @@ def RemoveIfExists(path): def main(): parser = argparse.ArgumentParser(); - + parser.add_argument('-t', '--tests', nargs='+', dest='tests', required=True, help='The unit tests to run and gather coverage data on.') parser.add_argument('-o', '--output', dest='output', @@ -64,19 +64,19 @@ def main(): # Run all unit tests and collect raw profiles. for test in args.tests: absolute_test_path = os.path.abspath(test) - + if not os.path.exists(absolute_test_path): print("Path %s does not exist." % absolute_test_path) return -1 binaries.append(absolute_test_path) - + raw_profile = absolute_test_path + ".rawprofile" RemoveIfExists(raw_profile) print "Running test %s to gather profile." % os.path.basename(absolute_test_path) - + subprocess.check_call([absolute_test_path], env={ "LLVM_PROFILE_FILE": raw_profile }) diff --git a/ci/analyze.sh b/ci/analyze.sh index 536d933289865..6eb2a529c949a 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -1,81 +1,94 @@ #!/bin/bash -echo "Analyzing dart:ui library..." +# +# 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. + +set -e + +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH -echo "Using analyzer from `which dartanalyzer`" +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -L "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) -dartanalyzer --version +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +FLUTTER_DIR="$SRC_DIR/flutter" +DART_BIN="$SRC_DIR/third_party/dart/tools/sdks/dart-sdk/bin" +PUB="$DART_BIN/pub" +DART_ANALYZER="$DART_BIN/dartanalyzer" -RESULTS=`dartanalyzer \ - --options flutter/analysis_options.yaml \ - --enable-experiment=non-nullable \ - "$1out/host_debug_unopt/gen/sky/bindings/dart_ui/ui.dart" \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+out/host_debug_unopt/gen/sky/bindings/dart_ui/ui\.dart"` +echo "Using analyzer from $DART_ANALYZER" -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +"$DART_ANALYZER" --version + +function analyze() ( + local last_arg="${!#}" + local results + # Grep sets its return status to non-zero if it doesn't find what it's + # looking for. + set +e + results="$("$DART_ANALYZER" "$@" 2>&1 | + grep -Ev "No issues found!" | + grep -Ev "Analyzing.+$last_arg")" + set -e + echo "$results" + if [ -n "$results" ]; then + echo "Failed analysis of $last_arg" + return 1 + else + echo "Success: no issues found in $last_arg" + fi + return 0 +) + +echo "Analyzing dart:ui library..." +analyze \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + --enable-experiment=non-nullable \ + "$SRC_DIR/out/host_debug_unopt/gen/sky/bindings/dart_ui/ui.dart" echo "Analyzing flutter_frontend_server..." -RESULTS=`dartanalyzer \ - --packages=flutter/flutter_frontend_server/.dart_tool/package_config.json \ - --options flutter/analysis_options.yaml \ - flutter/flutter_frontend_server \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+frontend_server"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +analyze \ + --packages="$FLUTTER_DIR/flutter_frontend_server/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + "$FLUTTER_DIR/flutter_frontend_server" echo "Analyzing tools/licenses..." -(cd flutter/tools/licenses && pub get) -RESULTS=`dartanalyzer \ - --packages=flutter/tools/licenses/.dart_tool/package_config.json \ - --options flutter/tools/licenses/analysis_options.yaml \ - flutter/tools/licenses \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+tools/licenses"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +(cd "$FLUTTER_DIR/tools/licenses" && "$PUB" get) +analyze \ + --packages="$FLUTTER_DIR/tools/licenses/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/tools/licenses/analysis_options.yaml" \ + "$FLUTTER_DIR/tools/licenses" echo "Analyzing testing/dart..." -flutter/tools/gn --unoptimized -ninja -C out/host_debug_unopt sky_engine sky_services -(cd flutter/testing/dart && pub get) -RESULTS=`dartanalyzer \ - --packages=flutter/testing/dart/.dart_tool/package_config.json \ - --options flutter/analysis_options.yaml \ - flutter/testing/dart \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+testing/dart"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +"$FLUTTER_DIR/tools/gn" --unoptimized +ninja -C "$SRC_DIR/out/host_debug_unopt" sky_engine sky_services +(cd "$FLUTTER_DIR/testing/dart" && "$PUB" get) +analyze \ + --packages="$FLUTTER_DIR/testing/dart/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + "$FLUTTER_DIR/testing/dart" echo "Analyzing testing/scenario_app..." -(cd flutter/testing/scenario_app && pub get) -RESULTS=`dartanalyzer \ - --packages=flutter/testing/scenario_app/.dart_tool/package_config.json \ - --options flutter/analysis_options.yaml \ - flutter/testing/scenario_app \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+testing/scenario_app"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi \ No newline at end of file +(cd "$FLUTTER_DIR/testing/scenario_app" && "$PUB" get) +analyze \ + --packages="$FLUTTER_DIR/testing/scenario_app/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + "$FLUTTER_DIR/testing/scenario_app" diff --git a/ci/bin/format.dart b/ci/bin/format.dart new file mode 100644 index 0000000000000..1426edd54f97b --- /dev/null +++ b/ci/bin/format.dart @@ -0,0 +1,937 @@ +// 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 and fixes format on files with changes. +// +// Run with --help for usage. + +// TODO(gspencergoog): Support clang formatting on Windows. +// TODO(gspencergoog): Support Java formatting on Windows. +// TODO(gspencergoog): Convert to null safety. + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:isolate/isolate.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:process_runner/process_runner.dart'; +import 'package:process/process.dart'; + +class FormattingException implements Exception { + FormattingException(this.message, [this.result]); + + final String message; + final ProcessResult /*?*/ result; + + int get exitCode => result?.exitCode ?? -1; + + @override + String toString() { + final StringBuffer output = StringBuffer(runtimeType.toString()); + output.write(': $message'); + final String stderr = result?.stderr as String ?? ''; + if (stderr.isNotEmpty) { + output.write(':\n$stderr'); + } + return output.toString(); + } +} + +enum MessageType { + message, + error, + warning, +} + +enum FormatCheck { + clang, + java, + whitespace, + gn, +} + +FormatCheck nameToFormatCheck(String name) { + switch (name) { + case 'clang': + return FormatCheck.clang; + case 'java': + return FormatCheck.java; + case 'whitespace': + return FormatCheck.whitespace; + case 'gn': + return FormatCheck.gn; + } + assert(false, 'Unknown FormatCheck type $name'); + return null; +} + +String formatCheckToName(FormatCheck check) { + switch (check) { + case FormatCheck.clang: + return 'C++/ObjC'; + case FormatCheck.java: + return 'Java'; + case FormatCheck.whitespace: + return 'Trailing whitespace'; + case FormatCheck.gn: + return 'GN'; + } + assert(false, 'Unhandled FormatCheck type $check'); + return null; +} + +List formatCheckNames() { + List allowed; + if (!Platform.isWindows) { + allowed = FormatCheck.values; + } else { + allowed = [FormatCheck.gn, FormatCheck.whitespace]; + } + return allowed + .map((FormatCheck check) => check.toString().replaceFirst('$FormatCheck.', '')) + .toList(); +} + +Future _runGit( + List args, + ProcessRunner processRunner, { + bool failOk = false, +}) async { + final ProcessRunnerResult result = await processRunner.runProcess( + ['git', ...args], + failOk: failOk, + ); + return result.stdout; +} + +typedef MessageCallback = Function(String message, {MessageType type}); + +/// Base class for format checkers. +/// +/// Provides services that all format checkers need. +abstract class FormatChecker { + FormatChecker({ + ProcessManager /*?*/ processManager, + @required this.baseGitRef, + @required this.repoDir, + @required this.srcDir, + this.allFiles = false, + this.messageCallback, + }) : _processRunner = ProcessRunner( + defaultWorkingDirectory: repoDir, + processManager: processManager ?? const LocalProcessManager(), + ); + + /// Factory method that creates subclass format checkers based on the type of check. + factory FormatChecker.ofType( + FormatCheck check, { + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) { + switch (check) { + case FormatCheck.clang: + return ClangFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + case FormatCheck.java: + return JavaFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + case FormatCheck.whitespace: + return WhitespaceFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + case FormatCheck.gn: + return GnFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + } + assert(false, 'Unhandled FormatCheck type $check'); + return null; + } + + final ProcessRunner _processRunner; + final Directory srcDir; + final Directory repoDir; + final bool allFiles; + MessageCallback /*?*/ messageCallback; + final String baseGitRef; + + /// Override to provide format checking for a specific type. + Future checkFormatting(); + + /// Override to provide format fixing for a specific type. + Future fixFormatting(); + + @protected + void message(String string) => messageCallback?.call(string, type: MessageType.message); + + @protected + void error(String string) => messageCallback?.call(string, type: MessageType.error); + + @protected + Future runGit(List args) async => _runGit(args, _processRunner); + + /// Converts a given raw string of code units to a stream that yields those + /// code units. + /// + /// Uses to convert the stdout of a previous command into an input stream for + /// the next command. + @protected + Stream> codeUnitsAsStream(List input) async* { + yield input; + } + + @protected + Future applyPatch(List patches) async { + final ProcessPool patchPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('patch'), + ); + final List jobs = patches.map((String patch) { + return WorkerJob( + ['patch', '-p0'], + stdinRaw: codeUnitsAsStream(patch.codeUnits), + failOk: true, + ); + }).toList(); + final List completedJobs = await patchPool.runToCompletion(jobs); + if (patchPool.failedJobs != 0) { + error('${patchPool.failedJobs} patch${patchPool.failedJobs > 1 ? 'es' : ''} ' + 'failed to apply.'); + completedJobs + .where((WorkerJob job) => job.result.exitCode != 0) + .map((WorkerJob job) => job.result.output) + .forEach(message); + } + return patchPool.failedJobs == 0; + } + + /// Gets the list of files to operate on. + /// + /// If [allFiles] is true, then returns all git controlled files in the repo + /// of the given types. + /// + /// If [allFiles] is false, then only return those files of the given types + /// that have changed between the current working tree and the [baseGitRef]. + @protected + Future> getFileList(List types) async { + String output; + if (allFiles) { + output = await runGit([ + 'ls-files', + '--', + ...types, + ]); + } else { + output = await runGit([ + 'diff', + '-U0', + '--no-color', + '--diff-filter=d', + '--name-only', + baseGitRef, + '--', + ...types, + ]); + } + return output.split('\n').where((String line) => line.isNotEmpty).toList(); + } + + /// Generates a reporting function to supply to ProcessRunner to use instead + /// of the default reporting function. + @protected + ProcessPoolProgressReporter namedReport(String name) { + return (int total, int completed, int inProgress, int pending, int failed) { + final String percent = + total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3); + final String completedStr = completed.toString().padLeft(3); + final String totalStr = total.toString().padRight(3); + final String inProgressStr = inProgress.toString().padLeft(2); + final String pendingStr = pending.toString().padLeft(3); + final String failedStr = failed.toString().padLeft(3); + + stderr.write('$name Jobs: $percent% done, ' + '$completedStr/$totalStr completed, ' + '$inProgressStr in progress, ' + '$pendingStr pending, ' + '$failedStr failed.${' ' * 20}\r'); + }; + } + + /// Clears the last printed report line so garbage isn't left on the terminal. + @protected + void reportDone() { + stderr.write('\r${' ' * 100}\r'); + } +} + +/// Checks and formats C++/ObjC files using clang-format. +class ClangFormatChecker extends FormatChecker { + ClangFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ) { + /*late*/ String clangOs; + if (Platform.isLinux) { + clangOs = 'linux-x64'; + } else if (Platform.isMacOS) { + clangOs = 'mac-x64'; + } else { + throw FormattingException( + "Unknown operating system: don't know how to run clang-format here."); + } + clangFormat = File( + path.join( + srcDir.absolute.path, + 'buildtools', + clangOs, + 'clang', + 'bin', + 'clang-format', + ), + ); + } + + /*late*/ File clangFormat; + + @override + Future checkFormatting() async { + final List failures = await _getCFormatFailures(); + failures.map(stdout.writeln); + return failures.isEmpty; + } + + @override + Future fixFormatting() async { + message('Fixing C++/ObjC formatting...'); + final List failures = await _getCFormatFailures(fixing: true); + if (failures.isEmpty) { + return true; + } + return await applyPatch(failures); + } + + Future _getClangFormatVersion() async { + final ProcessRunnerResult result = + await _processRunner.runProcess([clangFormat.path, '--version']); + return result.stdout.trim(); + } + + Future> _getCFormatFailures({bool fixing = false}) async { + message('Checking C++/ObjC formatting...'); + const List clangFiletypes = [ + '*.c', + '*.cc', + '*.cxx', + '*.cpp', + '*.h', + '*.m', + '*.mm', + ]; + final List files = await getFileList(clangFiletypes); + if (files.isEmpty) { + message('No C++/ObjC files with changes, skipping C++/ObjC format check.'); + return []; + } + if (verbose) { + message('Using ${await _getClangFormatVersion()}'); + } + final List clangJobs = []; + for (String file in files) { + if (file.trim().isEmpty) { + continue; + } + clangJobs.add(WorkerJob([clangFormat.path, '--style=file', file.trim()])); + } + final ProcessPool clangPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('clang-format'), + ); + final Stream completedClangFormats = clangPool.startWorkers(clangJobs); + final List diffJobs = []; + await for (final WorkerJob completedJob in completedClangFormats) { + if (completedJob.result != null && completedJob.result.exitCode == 0) { + diffJobs.add( + WorkerJob(['diff', '-u', completedJob.command.last, '-'], + stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw), failOk: true), + ); + } + } + final ProcessPool diffPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('diff'), + ); + final List completedDiffs = await diffPool.runToCompletion(diffJobs); + final Iterable failed = completedDiffs.where((WorkerJob job) { + return job.result.exitCode != 0; + }); + reportDone(); + if (failed.isNotEmpty) { + final bool plural = failed.length > 1; + if (fixing) { + message('Fixing ${failed.length} C++/ObjC file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + } else { + error('Found ${failed.length} C++/ObjC file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + for (final WorkerJob job in failed) { + stdout.write(job.result.stdout); + } + } + } else { + message('Completed checking ${diffJobs.length} C++/ObjC files with no formatting problems.'); + } + return failed.map((WorkerJob job) { + return job.result.stdout; + }).toList(); + } +} + +/// Checks the format of Java files uing the Google Java format checker. +class JavaFormatChecker extends FormatChecker { + JavaFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ) { + googleJavaFormatJar = File( + path.absolute( + path.join( + srcDir.absolute.path, + 'third_party', + 'android_tools', + 'google-java-format', + 'google-java-format-1.7-all-deps.jar', + ), + ), + ); + } + + /*late*/ File googleJavaFormatJar; + + Future _getGoogleJavaFormatVersion() async { + final ProcessRunnerResult result = await _processRunner + .runProcess(['java', '-jar', googleJavaFormatJar.path, '--version']); + return result.stderr.trim(); + } + + @override + Future checkFormatting() async { + final List failures = await _getJavaFormatFailures(); + failures.map(stdout.writeln); + return failures.isEmpty; + } + + @override + Future fixFormatting() async { + message('Fixing Java formatting...'); + final List failures = await _getJavaFormatFailures(fixing: true); + if (failures.isEmpty) { + return true; + } + return await applyPatch(failures); + } + + Future _getJavaVersion() async { + final ProcessRunnerResult result = + await _processRunner.runProcess(['java', '-version']); + return result.stderr.trim().split('\n')[0]; + } + + Future> _getJavaFormatFailures({bool fixing = false}) async { + message('Checking Java formatting...'); + final List formatJobs = []; + final List files = await getFileList(['*.java']); + if (files.isEmpty) { + message('No Java files with changes, skipping Java format check.'); + return []; + } + String javaVersion = ''; + String javaFormatVersion = ''; + try { + javaVersion = await _getJavaVersion(); + } on ProcessRunnerException { + error('Cannot run Java, skipping Java file formatting!'); + return const []; + } + try { + javaFormatVersion = await _getGoogleJavaFormatVersion(); + } on ProcessRunnerException { + error('Cannot find google-java-format, skipping Java format check.'); + return const []; + } + if (verbose) { + message('Using $javaFormatVersion with Java $javaVersion'); + } + for (String file in files) { + if (file.trim().isEmpty) { + continue; + } + formatJobs.add( + WorkerJob( + ['java', '-jar', googleJavaFormatJar.path, file.trim()], + ), + ); + } + final ProcessPool formatPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('Java format'), + ); + final Stream completedClangFormats = formatPool.startWorkers(formatJobs); + final List diffJobs = []; + await for (final WorkerJob completedJob in completedClangFormats) { + if (completedJob.result != null && completedJob.result.exitCode == 0) { + diffJobs.add( + WorkerJob( + ['diff', '-u', completedJob.command.last, '-'], + stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw), + failOk: true, + ), + ); + } + } + final ProcessPool diffPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('diff'), + ); + final List completedDiffs = await diffPool.runToCompletion(diffJobs); + final Iterable failed = completedDiffs.where((WorkerJob job) { + return job.result.exitCode != 0; + }); + reportDone(); + if (failed.isNotEmpty) { + final bool plural = failed.length > 1; + if (fixing) { + error('Fixing ${failed.length} Java file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + } else { + error('Found ${failed.length} Java file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + for (final WorkerJob job in failed) { + stdout.write(job.result.stdout); + } + } + } else { + message('Completed checking ${diffJobs.length} Java files with no formatting problems.'); + } + return failed.map((WorkerJob job) { + return job.result.stdout; + }).toList(); + } +} + +/// Checks the format of any BUILD.gn files using the "gn format" command. +class GnFormatChecker extends FormatChecker { + GnFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ) { + gnBinary = File( + path.join( + repoDir.absolute.path, + 'third_party', + 'gn', + Platform.isWindows ? 'gn.exe' : 'gn', + ), + ); + } + + /*late*/ File gnBinary; + + @override + Future checkFormatting() async { + message('Checking GN formatting...'); + return (await _runGnCheck(fixing: false)) == 0; + } + + @override + Future fixFormatting() async { + message('Fixing GN formatting...'); + await _runGnCheck(fixing: true); + // The GN script shouldn't fail when fixing errors. + return true; + } + + Future _runGnCheck({@required bool fixing}) async { + final List filesToCheck = await getFileList(['*.gn', '*.gni']); + + final List cmd = [ + gnBinary.path, + 'format', + if (!fixing) '--dry-run', + ]; + final List jobs = []; + for (final String file in filesToCheck) { + jobs.add(WorkerJob([...cmd, file])); + } + final ProcessPool gnPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('gn format'), + ); + final List completedJobs = await gnPool.runToCompletion(jobs); + reportDone(); + final List incorrect = []; + for (final WorkerJob job in completedJobs) { + if (job.result.exitCode == 2) { + incorrect.add(' ${job.command.last}'); + } + if (job.result.exitCode == 1) { + // GN has exit code 1 if it had some problem formatting/checking the + // file. + throw FormattingException( + 'Unable to format ${job.command.last}:\n${job.result.output}', + ); + } + } + if (incorrect.isNotEmpty) { + final bool plural = incorrect.length > 1; + if (fixing) { + message('Fixed ${incorrect.length} GN file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + } else { + error('Found ${incorrect.length} GN file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly:'); + incorrect.forEach(stderr.writeln); + } + } else { + message('All GN files formatted correctly.'); + } + return incorrect.length; + } +} + +@immutable +class _GrepResult { + const _GrepResult(this.file, this.hits, this.lineNumbers); + final File file; + final List hits; + final List lineNumbers; +} + +/// Checks for trailing whitspace in Dart files. +class WhitespaceFormatChecker extends FormatChecker { + WhitespaceFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + + @override + Future checkFormatting() async { + final List failures = await _getWhitespaceFailures(); + return failures.isEmpty; + } + + static final RegExp trailingWsRegEx = RegExp(r'[ \t]+$', multiLine: true); + + @override + Future fixFormatting() async { + final List failures = await _getWhitespaceFailures(); + if (failures.isNotEmpty) { + for (File file in failures) { + stderr.writeln('Fixing $file'); + String contents = file.readAsStringSync(); + contents = contents.replaceAll(trailingWsRegEx, ''); + file.writeAsStringSync(contents); + } + } + return true; + } + + static Future<_GrepResult> _hasTrailingWhitespace(File file) async { + final List hits = []; + final List lineNumbers = []; + int lineNumber = 0; + for (final String line in file.readAsLinesSync()) { + if (trailingWsRegEx.hasMatch(line)) { + hits.add(line); + lineNumbers.add(lineNumber); + } + lineNumber++; + } + if (hits.isEmpty) { + return null; + } + return _GrepResult(file, hits, lineNumbers); + } + + Stream<_GrepResult> _whereHasTrailingWhitespace(Iterable files) async* { + final LoadBalancer pool = + await LoadBalancer.create(Platform.numberOfProcessors, IsolateRunner.spawn); + for (final File file in files) { + yield await pool.run<_GrepResult, File>(_hasTrailingWhitespace, file); + } + } + + Future> _getWhitespaceFailures() async { + final List files = await getFileList([ + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.dart', + '*.gn', + '*.gni', + '*.gradle', + '*.h', + '*.java', + '*.json', + '*.m', + '*.mm', + '*.py', + '*.sh', + '*.yaml', + ]); + if (files.isEmpty) { + message('No files that differ, skipping whitespace check.'); + return []; + } + message('Checking for trailing whitespace on ${files.length} source ' + 'file${files.length > 1 ? 's' : ''}...'); + + final ProcessPoolProgressReporter reporter = namedReport('whitespace'); + final List<_GrepResult> found = <_GrepResult>[]; + final int total = files.length; + int completed = 0; + int inProgress = Platform.numberOfProcessors; + int pending = total; + int failed = 0; + await for (final _GrepResult result in _whereHasTrailingWhitespace( + files.map( + (String file) => File( + path.join(repoDir.absolute.path, file), + ), + ), + )) { + if (result == null) { + completed++; + } else { + failed++; + found.add(result); + } + pending--; + inProgress = pending < Platform.numberOfProcessors ? pending : Platform.numberOfProcessors; + reporter(total, completed, inProgress, pending, failed); + } + reportDone(); + if (found.isNotEmpty) { + error('Whitespace check failed. The following files have trailing spaces:'); + for (final _GrepResult result in found) { + for (int i = 0; i < result.hits.length; ++i) { + message(' ${result.file.path}:${result.lineNumbers[i]}:${result.hits[i]}'); + } + } + } else { + message('No trailing whitespace found.'); + } + return found.map((_GrepResult result) => result.file).toList(); + } +} + +Future _getDiffBaseRevision(ProcessManager processManager, Directory repoDir) async { + final ProcessRunner processRunner = ProcessRunner( + defaultWorkingDirectory: repoDir, + processManager: processManager ?? const LocalProcessManager(), + ); + String upstream = 'upstream'; + final String upstreamUrl = await _runGit( + ['remote', 'get-url', upstream], + processRunner, + failOk: true, + ); + if (upstreamUrl.isEmpty) { + upstream = 'origin'; + } + await _runGit(['fetch', upstream, 'master'], processRunner); + String result = ''; + try { + // This is the preferred command to use, but developer checkouts often do + // not have a clear fork point, so we fall back to just the regular + // merge-base in that case. + result = await _runGit( + ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], + processRunner, + ); + } on ProcessRunnerException { + result = await _runGit(['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner); + } + return result.trim(); +} + +void _usage(ArgParser parser, {int exitCode = 1}) { + stderr.writeln('format.dart [--help] [--fix] [--all-files] ' + '[--check <${formatCheckNames().join('|')}>]'); + stderr.writeln(parser.usage); + exit(exitCode); +} + +bool verbose = false; + +Future main(List arguments) async { + final ArgParser parser = ArgParser(); + parser.addFlag('help', help: 'Print help.', abbr: 'h'); + parser.addFlag('fix', + abbr: 'f', + help: 'Instead of just checking for formatting errors, fix them in place.', + defaultsTo: false); + parser.addFlag('all-files', + abbr: 'a', + help: 'Instead of just checking for formatting errors in changed files, ' + 'check for them in all files.', + defaultsTo: false); + parser.addMultiOption('check', + abbr: 'c', + allowed: formatCheckNames(), + defaultsTo: formatCheckNames(), + help: 'Specifies which checks will be performed. Defaults to all checks. ' + 'May be specified more than once to perform multiple types of checks. ' + 'On Windows, only whitespace and gn checks are currently supported.'); + parser.addFlag('verbose', help: 'Print verbose output.', defaultsTo: verbose); + + ArgResults options; + try { + options = parser.parse(arguments); + } on FormatException catch (e) { + stderr.writeln('ERROR: $e'); + _usage(parser, exitCode: 0); + } + + verbose = options['verbose'] as bool; + + if (options['help'] as bool) { + _usage(parser, exitCode: 0); + } + + final File script = File.fromUri(Platform.script).absolute; + final Directory repoDir = script.parent.parent.parent; + final Directory srcDir = repoDir.parent; + if (verbose) { + stderr.writeln('Repo: $repoDir'); + stderr.writeln('Src: $srcDir'); + } + + void message(String message, {MessageType type = MessageType.message}) { + switch (type) { + case MessageType.message: + stderr.writeln(message); + break; + case MessageType.error: + stderr.writeln('ERROR: $message'); + break; + case MessageType.warning: + stderr.writeln('WARNING: $message'); + break; + } + } + + const ProcessManager processManager = LocalProcessManager(); + final String baseGitRef = await _getDiffBaseRevision(processManager, repoDir); + + bool result = true; + final List checks = options['check'] as List; + try { + for (final String checkName in checks) { + final FormatCheck check = nameToFormatCheck(checkName); + final String humanCheckName = formatCheckToName(check); + final FormatChecker checker = FormatChecker.ofType(check, + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: options['all-files'] as bool, + messageCallback: message); + bool stepResult; + if (options['fix'] as bool) { + message('Fixing any $humanCheckName format problems'); + stepResult = await checker.fixFormatting(); + if (!stepResult) { + message('Unable to apply $humanCheckName format fixes.'); + } + } else { + message('Performing $humanCheckName format check'); + stepResult = await checker.checkFormatting(); + if (!stepResult) { + message('Found $humanCheckName format problems.'); + } + } + result = result && stepResult; + } + } on FormattingException catch (e) { + message('ERROR: $e', type: MessageType.error); + } + + exit(result ? 0 : 1); +} diff --git a/ci/bin/lint.dart b/ci/bin/lint.dart index 658388f4e9773..04ff295acd5de 100644 --- a/ci/bin/lint.dart +++ b/ci/bin/lint.dart @@ -1,9 +1,13 @@ -/// Runs clang-tidy on files with changes. -/// -/// usage: -/// dart lint.dart [clang-tidy checks] -/// -/// User environment variable FLUTTER_LINT_ALL to run on all files. +// 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. + +// Runs clang-tidy on files with changes. +// +// usage: +// dart lint.dart [clang-tidy checks] +// +// User environment variable FLUTTER_LINT_ALL to run on all files. import 'dart:async' show Completer; import 'dart:convert' show jsonDecode, utf8, LineSplitter; @@ -241,7 +245,7 @@ void main(List arguments) async { final ProcessPool pool = ProcessPool(); await for (final WorkerJob job in pool.startWorkers(jobs)) { - if (job.result.stdout.isEmpty) { + if (job.result?.stdout.isEmpty ?? true) { continue; } print('❌ Failures for ${job.name}:'); diff --git a/ci/build.sh b/ci/build.sh index 6e63dd9c9fd62..5555fc4efd5e3 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -1,21 +1,50 @@ #!/bin/bash -set -ex +# +# 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. -PATH="$HOME/depot_tools:$PATH" -cd .. +set -e -PATH=$(pwd)/third_party/dart/tools/sdks/dart-sdk/bin:$PATH +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +FLUTTER_DIR="$SRC_DIR/flutter" + +set -x + +PATH="$SRC_DIR/third_party/dart/tools/sdks/dart-sdk/bin:$HOME/depot_tools:$PATH" + +cd "$SRC_DIR" # Build the dart UI files -flutter/tools/gn --unoptimized -ninja -C out/host_debug_unopt generate_dart_ui +"$FLUTTER_DIR/tools/gn" --unoptimized +ninja -C "$SRC_DIR/out/host_debug_unopt" generate_dart_ui # Analyze the dart UI -flutter/ci/analyze.sh -flutter/ci/licenses.sh +"$FLUTTER_DIR/ci/analyze.sh" +"$FLUTTER_DIR/ci/licenses.sh" # Check that dart libraries conform -cd flutter/web_sdk -pub get -cd .. -dart web_sdk/test/api_conform_test.dart +(cd "$FLUTTER_DIR/web_sdk"; pub get) +(cd "$FLUTTER_DIR"; dart "web_sdk/test/api_conform_test.dart") \ No newline at end of file diff --git a/ci/check_gn_format.py b/ci/check_gn_format.py index 2257a58984dc6..e69de29bb2d1d 100755 --- a/ci/check_gn_format.py +++ b/ci/check_gn_format.py @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -# 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 sys -import subprocess -import os -import argparse -import errno -import shutil - -def GetGNFiles(directory): - directory = os.path.abspath(directory) - gn_files = [] - assert os.path.exists(directory), "Directory must exist %s" % directory - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith(".gn") or file.endswith(".gni"): - gn_files.append(os.path.join(root, file)) - return gn_files - -def main(): - parser = argparse.ArgumentParser(); - - parser.add_argument('--gn-binary', dest='gn_binary', required=True, type=str) - parser.add_argument('--dry-run', dest='dry_run', default=False, action='store_true') - parser.add_argument('--root-directory', dest='root_directory', required=True, type=str) - - args = parser.parse_args() - - gn_binary = os.path.abspath(args.gn_binary) - assert os.path.exists(gn_binary), "GN Binary must exist %s" % gn_binary - - gn_command = [ gn_binary, 'format'] - - if args.dry_run: - gn_command.append('--dry-run') - - for gn_file in GetGNFiles(args.root_directory): - if subprocess.call(gn_command + [ gn_file ]) != 0: - print "ERROR: '%s' is incorrectly formatted." % os.path.relpath(gn_file, args.root_directory) - print "Format the same with 'gn format' using the 'gn' binary in third_party/gn/gn." - print "Or, run ./ci/check_gn_format.py without '--dry-run'" - return 1 - - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/ci/check_roll.sh b/ci/check_roll.sh index 296300d3eaef8..ffb2cca9728d1 100755 --- a/ci/check_roll.sh +++ b/ci/check_roll.sh @@ -1,4 +1,36 @@ #!/bin/bash +# +# 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. + +set -e + +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +FLUTTER_DIR="$(cd "$SCRIPT_DIR/.."; pwd -P)" + +cd "$FLUTTER_DIR" if git remote get-url upstream >/dev/null 2>&1; then UPSTREAM=upstream/master @@ -7,7 +39,7 @@ else fi; FLUTTER_VERSION="$(curl -s https://raw.githubusercontent.com/flutter/flutter/master/bin/internal/engine.version)" -BEHIND="$(git rev-list $FLUTTER_VERSION..$UPSTREAM --oneline | wc -l)" +BEHIND="$(git rev-list "$FLUTTER_VERSION".."$UPSTREAM" --oneline | wc -l)" MAX_BEHIND=16 # no more than 4 bisections to identify the issue if [[ $BEHIND -le $MAX_BEHIND ]]; then @@ -18,4 +50,3 @@ else echo " please roll engine into flutter first before merging more commits into engine." exit 1 fi - diff --git a/ci/dev/README.md b/ci/dev/README.md new file mode 100644 index 0000000000000..6f5701397bfe6 --- /dev/null +++ b/ci/dev/README.md @@ -0,0 +1,32 @@ +This directory contains resources that the Flutter team uses during +the development of engine. + +## Luci builder file +`try_builders.json` and `prod_builders.json` contains the +supported luci try/prod builders for engine. It follows format: +```json +{ + "builders":[ + { + "name":"yyy", + "repo":"engine", + "enabled":true + } + ] +} +``` +for `try_builders.json`, and follows format: +```json +{ + "builders":[ + { + "name":"yyy", + "repo":"engine" + } + ] +} +``` +for `prod_builders.json`. `try_builders.json` will be mainly used in +[`flutter/cocoon`](https://github.com/flutter/cocoon) to trigger/update pre-submit +engine luci tasks, whereas `prod_builders.json` will be mainly used in `flutter/cocoon` +to push luci task statuses to GitHub. \ No newline at end of file diff --git a/ci/dev/prod_builders.json b/ci/dev/prod_builders.json new file mode 100644 index 0000000000000..435046df1bcd8 --- /dev/null +++ b/ci/dev/prod_builders.json @@ -0,0 +1,68 @@ +{ + "builders":[ + { + "name":"Linux Android AOT Engine", + "repo":"engine" + }, + { + "name":"Linux Android Debug Engine", + "repo":"engine" + }, + { + "name":"Linux Host Engine", + "repo":"engine" + }, + { + "name":"Linux Fuchsia", + "repo":"engine" + }, + { + "name":"Linux Fuchsia FEMU", + "repo":"engine" + }, + { + "name":"Linux Web Engine", + "repo":"engine" + }, + { + "name":"Mac Android AOT Engine", + "repo":"engine" + }, + { + "name":"Mac Android Debug Engine", + "repo":"engine" + }, + { + "name":"Mac Host Engine", + "repo":"engine" + }, + { + "name":"Mac iOS Engine", + "repo":"engine" + }, + { + "name":"Mac iOS Engine Profile", + "repo":"engine" + }, + { + "name":"Mac iOS Engine Release", + "repo":"engine" + }, + { + "name":"Mac Web Engine", + "repo":"engine" + }, + { + "name":"Windows Android AOT Engine", + "repo":"engine" + }, + { + "name":"Windows Host Engine", + "repo":"engine" + }, + { + "name":"Windows Web Engine", + "repo":"engine" + } + ] +} diff --git a/ci/dev/try_builders.json b/ci/dev/try_builders.json new file mode 100644 index 0000000000000..b4d5bbb807f76 --- /dev/null +++ b/ci/dev/try_builders.json @@ -0,0 +1,74 @@ +{ + "builders":[ + { + "name":"Linux Android AOT Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Android Debug Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Android Scenarios", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Fuchsia", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Host Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Web Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Android AOT Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Android Debug Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Host Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac iOS Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Web Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Windows Android AOT Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Windows Host Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Windows Web Engine", + "repo":"engine", + "enabled": true + } + ] +} diff --git a/ci/firebase_testlab.sh b/ci/firebase_testlab.sh index 76540ebcb2a6e..45631d9bfd025 100755 --- a/ci/firebase_testlab.sh +++ b/ci/firebase_testlab.sh @@ -1,16 +1,26 @@ #!/bin/bash +# +# 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. set -e -if [[ ! -f $1 ]]; then - echo "File $1 not found." - exit -1 +APP="$1" +if [[ -z "$APP" ]]; then + echo "Application must be specified as the first argument to the script." + exit 255 fi -GIT_REVISION=${2:-$(git rev-parse HEAD)} -BUILD_ID=${3:-$CIRRUS_BUILD_ID} +if [[ ! -f "$APP" ]]; then + echo "File '$APP' not found." + exit 255 +fi + +GIT_REVISION="${2:-$(git rev-parse HEAD)}" +BUILD_ID="${3:-$CIRRUS_BUILD_ID}" -if [[ ! -z $GCLOUD_FIREBASE_TESTLAB_KEY ]]; then +if [[ -n $GCLOUD_FIREBASE_TESTLAB_KEY ]]; then # New contributors will not have permissions to run this test - they won't be # able to access the service account information. We should just mark the test # as passed - it will run fine on post submit, where it will still catch @@ -21,8 +31,8 @@ if [[ ! -z $GCLOUD_FIREBASE_TESTLAB_KEY ]]; then exit 0 fi - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json + echo "$GCLOUD_FIREBASE_TESTLAB_KEY" > "${HOME}/gcloud-service-key.json" + gcloud auth activate-service-account --key-file="${HOME}/gcloud-service-key.json" fi # Run the test. @@ -32,8 +42,8 @@ fi # See https://firebase.google.com/docs/test-lab/android/game-loop gcloud --project flutter-infra firebase test android run \ --type game-loop \ - --app $1 \ + --app "$APP" \ --timeout 2m \ --results-bucket=gs://flutter_firebase_testlab \ - --results-dir=engine_scenario_test/$GIT_REVISION/$BUILD_ID \ + --results-dir="engine_scenario_test/$GIT_REVISION/$BUILD_ID" \ --no-auto-google-login diff --git a/ci/format.bat b/ci/format.bat new file mode 100644 index 0000000000000..12a6bb8d7af89 --- /dev/null +++ b/ci/format.bat @@ -0,0 +1,32 @@ +@ECHO off +REM Copyright 2013 The Flutter Authors. All rights reserved. +REM Use of this source code is governed by a BSD-style license that can be +REM found in the LICENSE file. + +REM ---------------------------------- NOTE ---------------------------------- +REM +REM Please keep the logic in this file consistent with the logic in the +REM `format.sh` script in the same directory to ensure that it continues to +REM work across all platforms! +REM +REM -------------------------------------------------------------------------- + +SETLOCAL ENABLEDELAYEDEXPANSION + +FOR %%i IN ("%~dp0..\..") DO SET SRC_DIR=%%~fi + +REM Test if Git is available on the Host +where /q git || ECHO Error: Unable to find git in your PATH. && EXIT /B 1 + +SET repo_dir=%SRC_DIR%\flutter +SET ci_dir=%repo_dir%\flutter\ci +SET dart_sdk_path=%SRC_DIR%\third_party\dart\tools\sdks\dart-sdk +SET dart=%dart_sdk_path%\bin\dart.exe +SET pub=%dart_sdk_path%\bin\pub.bat + +cd "%ci_dir%" + +REM Do not use the CALL command in the next line to execute Dart. CALL causes +REM Windows to re-read the line from disk after the CALL command has finished +REM regardless of the ampersand chain. +"%pub%" get & "%dart%" --disable-dart-dev bin\format.dart %* & exit /B !ERRORLEVEL! diff --git a/ci/format.sh b/ci/format.sh index ff45d2ad8b4ba..6725635583f98 100755 --- a/ci/format.sh +++ b/ci/format.sh @@ -1,100 +1,41 @@ #!/bin/bash # -# Code formatting presubmit -# -# This presubmit script ensures that code under the src/flutter directory is -# formatted according to the Flutter engine style requirements. On failure, a -# diff is emitted that can be applied from within the src/flutter directory -# via: -# -# patch -p0 < diff.patch +# 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. set -e -echo "Checking formatting..." - -case "$(uname -s)" in - Darwin) - OS="mac-x64" - ;; - Linux) - OS="linux-x64" - ;; - *) - echo "Unknown operating system." - exit -1 - ;; -esac - -# Tools -CLANG_FORMAT="../buildtools/$OS/clang/bin/clang-format" -$CLANG_FORMAT --version - -# Compute the diffs. -CLANG_FILETYPES="*.c *.cc *.cpp *.h *.m *.mm" -DIFF_OPTS="-U0 --no-color --name-only" - -if git remote get-url upstream >/dev/null 2>&1; then - UPSTREAM=upstream -else - UPSTREAM=origin -fi; +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH -BASE_SHA="$(git fetch $UPSTREAM master > /dev/null 2>&1 && \ - (git merge-base --fork-point FETCH_HEAD HEAD || git merge-base FETCH_HEAD HEAD))" -# Disable glob matching otherwise a file in the current directory that matches -# $CLANG_FILETYPES will cause git to query for that exact file instead of doing -# a match. -set -f -CLANG_FILES_TO_CHECK="$(git ls-files $CLANG_FILETYPES)" -set +f -FAILED_CHECKS=0 -for f in $CLANG_FILES_TO_CHECK; do - set +e - CUR_DIFF="$(diff -u "$f" <("$CLANG_FORMAT" --style=file "$f"))" - set -e - if [[ ! -z "$CUR_DIFF" ]]; then - echo "$CUR_DIFF" - FAILED_CHECKS=$(($FAILED_CHECKS+1)) - fi -done - -GOOGLE_JAVA_FORMAT="../third_party/android_tools/google-java-format/google-java-format-1.7-all-deps.jar" -if [[ -f "$GOOGLE_JAVA_FORMAT" && -f "$(which java)" ]]; then - java -jar "$GOOGLE_JAVA_FORMAT" --version 2>&1 - JAVA_FILETYPES="*.java" - JAVA_FILES_TO_CHECK="$(git diff $DIFF_OPTS $BASE_SHA -- $JAVA_FILETYPES)" - for f in $JAVA_FILES_TO_CHECK; do - set +e - CUR_DIFF="$(diff -u "$f" <(java -jar "$GOOGLE_JAVA_FORMAT" "$f"))" - set -e - if [[ ! -z "$CUR_DIFF" ]]; then - echo "$CUR_DIFF" - FAILED_CHECKS=$(($FAILED_CHECKS+1)) - fi +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" done -else - echo "WARNING: Cannot find google-java-format, skipping Java file formatting!" -fi - -if [[ $FAILED_CHECKS -ne 0 ]]; then - echo "" - echo "ERROR: Some files are formatted incorrectly. To fix, run \`./ci/format.sh | patch -p0\` from the flutter/engine/src/flutter directory." - exit 1 -fi + echo "$file" +) -FILETYPES="*.dart" - -set +e -TRAILING_SPACES=$(git diff $DIFF_OPTS $BASE_SHA..HEAD -- $FILETYPES | xargs grep --line-number --with-filename '[[:blank:]]\+$') -set -e -if [[ ! -z "$TRAILING_SPACES" ]]; then - echo "$TRAILING_SPACES" - echo "" - echo "ERROR: Some files have trailing spaces. To fix, try something like \`find . -name "*.dart" -exec sed -i -e 's/\s\+$//' {} \;\`." - exit 1 -fi +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +DART_SDK_DIR="${SRC_DIR}/third_party/dart/tools/sdks/dart-sdk" +DART="${DART_SDK_DIR}/bin/dart" +PUB="${DART_SDK_DIR}/bin/pub" -# Check GN format consistency -./ci/check_gn_format.py --dry-run --root-directory . --gn-binary "third_party/gn/gn" +cd "$SCRIPT_DIR" +"$PUB" get && "$DART" \ + --disable-dart-dev \ + bin/format.dart \ + "$@" diff --git a/ci/licenses.sh b/ci/licenses.sh index 58b06ea8a179e..48843bf58d761 100755 --- a/ci/licenses.sh +++ b/ci/licenses.sh @@ -1,81 +1,140 @@ #!/bin/bash +# +# 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. + set -e shopt -s nullglob +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +DART_BIN="$SRC_DIR/third_party/dart/tools/sdks/dart-sdk/bin" +PATH="$DART_BIN:$PATH" + echo "Verifying license script is still happy..." -echo "Using pub from `which pub`, dart from `which dart`" +echo "Using pub from $(command -v pub), dart from $(command -v dart)" -exitStatus=0 +untracked_files="$(cd "$SRC_DIR/flutter"; git status --ignored --short | grep -E "^!" | awk "{print\$2}")" +untracked_count="$(echo "$untracked_files" | wc -l)" +if [[ $untracked_count -gt 0 ]]; then + echo "" + echo "WARNING: There are $untracked_count untracked/ignored files or directories in the flutter repository." + echo "False positives may occur." + echo "You can use 'git clean -dxf' in the flutter dir to clean out these files." + echo "BUT, be warned that this will recursively remove all these files and directories:" + echo "$untracked_files" + echo "" +fi dart --version -# These files trip up the script on Mac OS X. -find . -name ".DS_Store" -exec rm {} \; - -(cd flutter/tools/licenses; pub get; dart --enable-asserts lib/main.dart --src ../../.. --out ../../../out/license_script_output --golden ../../ci/licenses_golden) - -for f in out/license_script_output/licenses_*; do - if ! cmp -s flutter/ci/licenses_golden/$(basename $f) $f - then - echo "============================= ERROR =============================" - echo "License script got different results than expected for $f." - echo "Please rerun the licenses script locally to verify that it is" - echo "correctly catching any new licenses for anything you may have" - echo "changed, and then update this file:" - echo " flutter/sky/packages/sky_engine/LICENSE" - echo "For more information, see the script in:" - echo " https://github.com/flutter/engine/tree/master/tools/licenses" - echo "" - diff -U 6 flutter/ci/licenses_golden/$(basename $f) $f - echo "=================================================================" - echo "" - exitStatus=1 - fi -done - -echo "Verifying license tool signature..." -if ! cmp -s flutter/ci/licenses_golden/tool_signature out/license_script_output/tool_signature -then - echo "============================= ERROR =============================" - echo "The license tool signature has changed. This is expected when" - echo "there have been changes to the license tool itself. Licenses have" - echo "been re-computed for all components. If only the license script has" - echo "changed, no diffs are typically expected in the output of the" - echo "script. Verify the output, and if it looks correct, update the" - echo "license tool signature golden file:" - echo " ci/licenses_golden/tool_signature" - echo "For more information, see the script in:" - echo " https://github.com/flutter/engine/tree/master/tools/licenses" - echo "" - diff -U 6 flutter/ci/licenses_golden/tool_signature out/license_script_output/tool_signature - echo "=================================================================" - echo "" - exitStatus=1 -fi +# Collects the license information from the repo. +# Runs in a subshell. +function collect_licenses() ( + cd "$SRC_DIR/flutter/tools/licenses" + pub get + dart --enable-asserts lib/main.dart \ + --src ../../.. \ + --out ../../../out/license_script_output \ + --golden ../../ci/licenses_golden +) -echo "Checking license count in licenses_flutter..." -actualLicenseCount=`tail -n 1 flutter/ci/licenses_golden/licenses_flutter | tr -dc '0-9'` -expectedLicenseCount=2 # When changing this number: Update the error message below as well describing all expected license types. - -if [ "$actualLicenseCount" -ne "$expectedLicenseCount" ] -then - echo "=============================== ERROR ===============================" - echo "The total license count in flutter/ci/licenses_golden/licenses_flutter" - echo "changed from $expectedLicenseCount to $actualLicenseCount." - echo "It's very likely that this is an unintentional change. Please" - echo "double-check that all newly added files have a BSD-style license" - echo "header with the following copyright:" - echo " Copyright 2013 The Flutter Authors. All rights reserved." - echo "Files in 'third_party/txt' may have an Apache license header instead." - echo "If you're absolutely sure that the change in license count is" - echo "intentional, update 'flutter/ci/licenses.sh' with the new count." - echo "=================================================================" - echo "" - exitStatus=1 -fi +# Verifies the licenses in the repo. +# Runs in a subshell. +function verify_licenses() ( + local exitStatus=0 + cd "$SRC_DIR" -if [ "$exitStatus" -eq "0" ] -then - echo "Licenses are as expected." -fi -exit $exitStatus + # These files trip up the script on Mac OS X. + find . -name ".DS_Store" -exec rm {} \; + + collect_licenses + + for f in out/license_script_output/licenses_*; do + if ! cmp -s "flutter/ci/licenses_golden/$(basename "$f")" "$f"; then + echo "============================= ERROR =============================" + echo "License script got different results than expected for $f." + echo "Please rerun the licenses script locally to verify that it is" + echo "correctly catching any new licenses for anything you may have" + echo "changed, and then update this file:" + echo " flutter/sky/packages/sky_engine/LICENSE" + echo "For more information, see the script in:" + echo " https://github.com/flutter/engine/tree/master/tools/licenses" + echo "" + diff -U 6 "flutter/ci/licenses_golden/$(basename "$f")" "$f" + echo "=================================================================" + echo "" + exitStatus=1 + fi + done + + echo "Verifying license tool signature..." + if ! cmp -s "flutter/ci/licenses_golden/tool_signature" "out/license_script_output/tool_signature"; then + echo "============================= ERROR =============================" + echo "The license tool signature has changed. This is expected when" + echo "there have been changes to the license tool itself. Licenses have" + echo "been re-computed for all components. If only the license script has" + echo "changed, no diffs are typically expected in the output of the" + echo "script. Verify the output, and if it looks correct, update the" + echo "license tool signature golden file:" + echo " ci/licenses_golden/tool_signature" + echo "For more information, see the script in:" + echo " https://github.com/flutter/engine/tree/master/tools/licenses" + echo "" + diff -U 6 "flutter/ci/licenses_golden/tool_signature" "out/license_script_output/tool_signature" + echo "=================================================================" + echo "" + exitStatus=1 + fi + + echo "Checking license count in licenses_flutter..." + + local actualLicenseCount + actualLicenseCount="$(tail -n 1 flutter/ci/licenses_golden/licenses_flutter | tr -dc '0-9')" + local expectedLicenseCount=2 # When changing this number: Update the error message below as well describing all expected license types. + + if [[ $actualLicenseCount -ne $expectedLicenseCount ]]; then + echo "=============================== ERROR ===============================" + echo "The total license count in flutter/ci/licenses_golden/licenses_flutter" + echo "changed from $expectedLicenseCount to $actualLicenseCount." + echo "It's very likely that this is an unintentional change. Please" + echo "double-check that all newly added files have a BSD-style license" + echo "header with the following copyright:" + echo " Copyright 2013 The Flutter Authors. All rights reserved." + echo "Files in 'third_party/txt' may have an Apache license header instead." + echo "If you're absolutely sure that the change in license count is" + echo "intentional, update 'flutter/ci/licenses.sh' with the new count." + echo "=================================================================" + echo "" + exitStatus=1 + fi + + if [[ $exitStatus -eq 0 ]]; then + echo "Licenses are as expected." + fi + return $exitStatus +) + +verify_licenses \ No newline at end of file diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 4cd5031ebf219..99124c9effdbb 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -399,6 +399,9 @@ FILE: ../../../flutter/lib/ui/ui_benchmarks.cc FILE: ../../../flutter/lib/ui/ui_dart_state.cc FILE: ../../../flutter/lib/ui/ui_dart_state.h FILE: ../../../flutter/lib/ui/window.dart +FILE: ../../../flutter/lib/ui/window/platform_configuration.cc +FILE: ../../../flutter/lib/ui/window/platform_configuration.h +FILE: ../../../flutter/lib/ui/window/platform_configuration_unittests.cc FILE: ../../../flutter/lib/ui/window/platform_message.cc FILE: ../../../flutter/lib/ui/window/platform_message.h FILE: ../../../flutter/lib/ui/window/platform_message_response.cc @@ -416,7 +419,6 @@ FILE: ../../../flutter/lib/ui/window/viewport_metrics.cc FILE: ../../../flutter/lib/ui/window/viewport_metrics.h FILE: ../../../flutter/lib/ui/window/window.cc FILE: ../../../flutter/lib/ui/window/window.h -FILE: ../../../flutter/lib/web_ui/lib/assets/houdini_painter.js FILE: ../../../flutter/lib/web_ui/lib/src/engine.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/assets.dart @@ -424,42 +426,69 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_location.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvas_pool.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/vertices.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/viewport_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/clipboard.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/color_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/color_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/embedded_views.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/fonts.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/image.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/image_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/initialization.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_tree.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/painting.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path_metrics.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/platform_message.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/raster_cache.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/rasterizer.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/surface.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/text.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/util.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/vertices.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/viewport_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/history.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/houdini_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/clip.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/image_filter.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 +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/conic.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/cubic.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_metrics.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_ref.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_to_svg.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_utils.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_windings.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/tangent.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/picture.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/platform_view.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/recording_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shader.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/keyboard.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart @@ -486,34 +515,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/buffers.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/message_codec.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/message_codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/serialization.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/shader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/shadow.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/clip.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/debug_canvas_reuse_overlay.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/image_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/offset.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/opacity.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/painting.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/conic.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/cubic.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_ref.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_utils.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_windings.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/tangent.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/picture.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/platform_view.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/recording_canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/render_vertices.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene_builder.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/surface.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/surface_stats.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/transform.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/test_embedding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/font_collection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_break_properties.dart @@ -575,6 +577,8 @@ FILE: ../../../flutter/runtime/dart_vm_unittests.cc FILE: ../../../flutter/runtime/embedder_resources.cc FILE: ../../../flutter/runtime/embedder_resources.h FILE: ../../../flutter/runtime/fixtures/runtime_test.dart +FILE: ../../../flutter/runtime/platform_data.cc +FILE: ../../../flutter/runtime/platform_data.h FILE: ../../../flutter/runtime/ptrace_ios.cc FILE: ../../../flutter/runtime/ptrace_ios.h FILE: ../../../flutter/runtime/runtime_controller.cc @@ -587,8 +591,6 @@ FILE: ../../../flutter/runtime/skia_concurrent_executor.cc FILE: ../../../flutter/runtime/skia_concurrent_executor.h FILE: ../../../flutter/runtime/test_font_data.cc FILE: ../../../flutter/runtime/test_font_data.h -FILE: ../../../flutter/runtime/window_data.cc -FILE: ../../../flutter/runtime/window_data.h FILE: ../../../flutter/shell/common/animator.cc FILE: ../../../flutter/shell/common/animator.h FILE: ../../../flutter/shell/common/animator_unittests.cc @@ -597,6 +599,7 @@ FILE: ../../../flutter/shell/common/canvas_spy.h FILE: ../../../flutter/shell/common/canvas_spy_unittests.cc FILE: ../../../flutter/shell/common/engine.cc FILE: ../../../flutter/shell/common/engine.h +FILE: ../../../flutter/shell/common/engine_unittests.cc FILE: ../../../flutter/shell/common/fixtures/shell_test.dart FILE: ../../../flutter/shell/common/fixtures/shelltest_screenshot.png FILE: ../../../flutter/shell/common/input_events_unittests.cc @@ -726,6 +729,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -831,12 +836,15 @@ FILE: ../../../flutter/shell/platform/android/surface/android_surface_mock.h FILE: ../../../flutter/shell/platform/android/vsync_waiter_android.cc FILE: ../../../flutter/shell/platform/android/vsync_waiter_android.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/core_implementations.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/engine_method_result.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/event_channel_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/basic_message_channel.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/engine_method_result.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h @@ -851,6 +859,7 @@ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registry.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/method_call_unittests.cc @@ -859,7 +868,6 @@ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/method_result_fu FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_codec.cc -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/incoming_message_dispatcher.cc @@ -914,11 +922,13 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryM FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache_Internal.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm @@ -926,6 +936,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlay FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -1321,17 +1332,24 @@ FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/flutter_linux.h FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.cc FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc +FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine.cc +FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc FILE: ../../../flutter/shell/platform/windows/cursor_handler.cc FILE: ../../../flutter/shell/platform/windows/cursor_handler.h +FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.cc +FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.h FILE: ../../../flutter/shell/platform/windows/flutter_windows.cc +FILE: ../../../flutter/shell/platform/windows/flutter_windows_engine.cc +FILE: ../../../flutter/shell/platform/windows/flutter_windows_engine.h FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.cc FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.h FILE: ../../../flutter/shell/platform/windows/key_event_handler.cc @@ -1341,6 +1359,9 @@ FILE: ../../../flutter/shell/platform/windows/public/flutter_windows.h FILE: ../../../flutter/shell/platform/windows/string_conversion.cc FILE: ../../../flutter/shell/platform/windows/string_conversion.h FILE: ../../../flutter/shell/platform/windows/string_conversion_unittests.cc +FILE: ../../../flutter/shell/platform/windows/system_utils.h +FILE: ../../../flutter/shell/platform/windows/system_utils_unittests.cc +FILE: ../../../flutter/shell/platform/windows/system_utils_win32.cc FILE: ../../../flutter/shell/platform/windows/text_input_plugin.cc FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h FILE: ../../../flutter/shell/platform/windows/win32_dpi_utils.cc @@ -1355,6 +1376,9 @@ FILE: ../../../flutter/shell/platform/windows/win32_task_runner.cc FILE: ../../../flutter/shell/platform/windows/win32_task_runner.h FILE: ../../../flutter/shell/platform/windows/win32_window.cc FILE: ../../../flutter/shell/platform/windows/win32_window.h +FILE: ../../../flutter/shell/platform/windows/win32_window_proc_delegate_manager.cc +FILE: ../../../flutter/shell/platform/windows/win32_window_proc_delegate_manager.h +FILE: ../../../flutter/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc FILE: ../../../flutter/shell/platform/windows/win32_window_unittests.cc FILE: ../../../flutter/shell/platform/windows/window_binding_handler.h FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h diff --git a/ci/licenses_golden/licenses_fuchsia b/ci/licenses_golden/licenses_fuchsia index 454747e3fd392..c851dcd928364 100644 --- a/ci/licenses_golden/licenses_fuchsia +++ b/ci/licenses_golden/licenses_fuchsia @@ -1,4 +1,4 @@ -Signature: 9e13cc16b79c2ae5720973f66c12df77 +Signature: cca1700ca777f682864d7baed336e5bc UNUSED LICENSES: @@ -471,6 +471,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.ethernet/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.images/meta.json @@ -503,6 +504,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.testing/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json @@ -1315,6 +1317,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/goldfish_pipe.fi FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/light.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/admin.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/hwinfo.fidl @@ -1385,6 +1388,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/options.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/server.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json @@ -2129,6 +2133,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.ethernet/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.images/meta.json @@ -2161,6 +2166,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.testing/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json @@ -2595,9 +2601,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/module_manifest.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/puppet_master.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_command.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_options.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_visibility_state.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/client.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/connectivity.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/net.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.process/launcher.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.process/resolver.fidl @@ -2892,12 +2896,11 @@ FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/limits.h FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/namespace.h FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/private.h FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/unsafe.h -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/decoding.cc +FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/decoding_and_validating.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/encoding.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/formatting.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/coding.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/internal.h -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/validating.cc FILE: ../../../fuchsia/sdk/linux/pkg/fit/include/lib/fit/function.h FILE: ../../../fuchsia/sdk/linux/pkg/fit/include/lib/fit/function_internal.h FILE: ../../../fuchsia/sdk/linux/pkg/memfs/include/lib/memfs/memfs.h @@ -3057,9 +3060,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.math/math.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.playback/problem.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.playback/seeking_reader.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/audio.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/focus.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/module_context.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/module_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/session_shell.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_info.fidl @@ -3150,18 +3151,24 @@ FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/noop_view_p FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/view_provider.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/module_interceptor.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/module_with_view_provider_impl.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/src/child_view_render_box_2.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/component.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/device.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/diagnostics.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/feedback_data_provider.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/power.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/repository_manager.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/tiles.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/time.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/trace_processing/metrics/gpu_metrics.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/trace_processing/metrics/total_trace_wall_time.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/update.dart FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.gesture/gesture_listener.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.a2dp/audio_mode.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.le/connection_options.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.sys/configuration.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.sys/pairing_options.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.sys/security_mode.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/device.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/device_watcher.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/stream.fidl @@ -3177,6 +3184,11 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory.wlan/iovar.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/crash_register.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/data_register.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/last_reboot_info.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/device.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/frames.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/instance.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/mac.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/session.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.report/consumer_control.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.report/device_ids.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input/keys.fidl @@ -3190,11 +3202,11 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.drm/properties.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.drm/types.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.target/target_discovery.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/activity_reporter.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/audio_errors.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/profile_provider.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.memorypressure/memorypressure.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.session/launcher.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/session_restart_controller.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/interfaces.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/routes.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/socket.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.posix/error.fidl @@ -3208,6 +3220,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.input3/pointer.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.pointerinjector/config.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.pointerinjector/device.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.pointerinjector/event.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.policy/display_backlight.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/view_ref.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/view_ref_focused.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/view_ref_installed.fidl diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index 6099a770e54ab..9b18c12e1d4b7 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: c7ee484f0bf49aed48999011c8ec793c +Signature: 2590d5f2ed193cd53addb25bee19db33 UNUSED LICENSES: @@ -970,6 +970,11 @@ FILE: ../../../third_party/skia/bench/skpbench.json FILE: ../../../third_party/skia/build/fuchsia/skqp/skqp.cmx FILE: ../../../third_party/skia/build/fuchsia/skqp/test_manifest.json FILE: ../../../third_party/skia/demos.skia.org/demos/hello_world/index.html +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/garbage.svg +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/index.html +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/main.js +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/shared.js +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/worker.js FILE: ../../../third_party/skia/demos.skia.org/demos/web_worker/index.html FILE: ../../../third_party/skia/demos.skia.org/demos/web_worker/main.js FILE: ../../../third_party/skia/demos.skia.org/demos/web_worker/shared.js @@ -1452,15 +1457,21 @@ FILE: ../../../third_party/skia/specs/web-img-decode/proposed/index.html FILE: ../../../third_party/skia/src/core/SkOrderedReadBuffer.h FILE: ../../../third_party/skia/src/ports/SkTLS_pthread.cpp FILE: ../../../third_party/skia/src/ports/SkTLS_win.cpp +FILE: ../../../third_party/skia/src/sksl/generated/sksl_fp.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_frag.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_geom.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_gpu.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_interp.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_pipeline.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_vert.dehydrated.sksl FILE: ../../../third_party/skia/src/sksl/lex/sksl.lex -FILE: ../../../third_party/skia/src/sksl/sksl_blend.inc -FILE: ../../../third_party/skia/src/sksl/sksl_fp.inc -FILE: ../../../third_party/skia/src/sksl/sksl_frag.inc -FILE: ../../../third_party/skia/src/sksl/sksl_geom.inc -FILE: ../../../third_party/skia/src/sksl/sksl_gpu.inc -FILE: ../../../third_party/skia/src/sksl/sksl_interp.inc -FILE: ../../../third_party/skia/src/sksl/sksl_pipeline.inc -FILE: ../../../third_party/skia/src/sksl/sksl_vert.inc +FILE: ../../../third_party/skia/src/sksl/sksl_fp_raw.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_frag.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_geom.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_gpu.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_interp.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_pipeline.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_vert.sksl ---------------------------------------------------------------------------------------------------- Copyright (c) 2011 Google Inc. All rights reserved. @@ -3325,7 +3336,6 @@ FILE: ../../../third_party/skia/src/core/SkCanvasPriv.cpp FILE: ../../../third_party/skia/src/core/SkColorSpaceXformSteps.cpp FILE: ../../../third_party/skia/src/core/SkColorSpaceXformSteps.h FILE: ../../../third_party/skia/src/core/SkContourMeasure.cpp -FILE: ../../../third_party/skia/src/core/SkCoverageModePriv.h FILE: ../../../third_party/skia/src/core/SkCubicMap.cpp FILE: ../../../third_party/skia/src/core/SkCubicSolver.h FILE: ../../../third_party/skia/src/core/SkDeferredDisplayList.cpp @@ -3421,7 +3431,6 @@ FILE: ../../../third_party/skia/src/gpu/gradients/GrLinearGradientLayout.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrRadialGradientLayout.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrSingleIntervalGradientColorizer.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrSweepGradientLayout.fp -FILE: ../../../third_party/skia/src/gpu/gradients/GrTextureGradientColorizer.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrTiledGradientEffect.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrTwoPointConicalGradientLayout.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrUnrolledBinaryGradientColorizer.fp @@ -3437,8 +3446,6 @@ FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSingleIntervalGrad FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSingleIntervalGradientColorizer.h FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSweepGradientLayout.cpp FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSweepGradientLayout.h -FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTextureGradientColorizer.cpp -FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTextureGradientColorizer.h FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTiledGradientEffect.cpp FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTiledGradientEffect.h FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTwoPointConicalGradientLayout.cpp @@ -3831,7 +3838,6 @@ FILE: ../../../third_party/skia/src/sksl/lex/RegexNode.cpp FILE: ../../../third_party/skia/src/sksl/lex/RegexNode.h FILE: ../../../third_party/skia/src/sksl/lex/RegexParser.cpp FILE: ../../../third_party/skia/src/sksl/lex/RegexParser.h -FILE: ../../../third_party/skia/src/sksl/sksl_enums.inc FILE: ../../../third_party/skia/src/utils/SkFloatToDecimal.cpp FILE: ../../../third_party/skia/src/utils/SkFloatToDecimal.h FILE: ../../../third_party/skia/src/utils/SkJSONWriter.cpp @@ -3892,6 +3898,7 @@ FILE: ../../../third_party/skia/include/gpu/GrBackendSurfaceMutableState.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DBackendContext.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypes.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypesMinimal.h +FILE: ../../../third_party/skia/include/ports/SkImageGeneratorNDK.h FILE: ../../../third_party/skia/include/private/GrD3DTypesPriv.h FILE: ../../../third_party/skia/include/private/SkIDChangeListener.h FILE: ../../../third_party/skia/include/private/SkSLSampleUsage.h @@ -3901,6 +3908,7 @@ FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/SIMD/simd_float_cap FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/SIMD/simd_int_capabilities.cpp FILE: ../../../third_party/skia/src/core/SkIDChangeListener.cpp FILE: ../../../third_party/skia/src/core/SkMatrixProvider.h +FILE: ../../../third_party/skia/src/core/SkRuntimeEffectPriv.h FILE: ../../../third_party/skia/src/core/SkVM_fwd.h FILE: ../../../third_party/skia/src/gpu/GrBackendSemaphore.cpp FILE: ../../../third_party/skia/src/gpu/GrBackendSurfaceMutableStateImpl.h @@ -3917,6 +3925,7 @@ FILE: ../../../third_party/skia/src/gpu/GrStencilMaskHelper.h FILE: ../../../third_party/skia/src/gpu/GrUniformDataManager.cpp FILE: ../../../third_party/skia/src/gpu/GrUniformDataManager.h FILE: ../../../third_party/skia/src/gpu/GrUnrefDDLTask.h +FILE: ../../../third_party/skia/src/gpu/GrUtil.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DCaps.cpp @@ -3967,11 +3976,18 @@ FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDeviceSpaceEffect.cp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDeviceSpaceEffect.h FILE: ../../../third_party/skia/src/gpu/geometry/GrShape.cpp FILE: ../../../third_party/skia/src/gpu/geometry/GrShape.h +FILE: ../../../third_party/skia/src/gpu/gl/webgl/GrGLMakeNativeInterface_webgl.cpp FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLUniformHandler.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkManagedResource.h FILE: ../../../third_party/skia/src/image/SkRescaleAndReadPixels.cpp FILE: ../../../third_party/skia/src/image/SkRescaleAndReadPixels.h +FILE: ../../../third_party/skia/src/ports/SkImageEncoder_NDK.cpp +FILE: ../../../third_party/skia/src/ports/SkImageGeneratorNDK.cpp +FILE: ../../../third_party/skia/src/ports/SkNDKConversions.cpp +FILE: ../../../third_party/skia/src/ports/SkNDKConversions.h FILE: ../../../third_party/skia/src/sksl/SkSLAnalysis.h +FILE: ../../../third_party/skia/src/sksl/SkSLDehydrator.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLDehydrator.h FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.cpp FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.h FILE: ../../../third_party/skia/src/sksl/SkSLSampleUsage.cpp @@ -4008,6 +4024,171 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +LIBRARY: skia +ORIGIN: ../../../third_party/skia/bench/GlyphQuadFillBench.cpp + ../../../third_party/skia/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../third_party/skia/bench/GlyphQuadFillBench.cpp +FILE: ../../../third_party/skia/bench/TessellateBench.cpp +FILE: ../../../third_party/skia/experimental/skrive/include/SkRive.h +FILE: ../../../third_party/skia/experimental/skrive/src/Artboard.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Color.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Component.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Drawable.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Ellipse.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Node.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Paint.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Rectangle.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Shape.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/SkRive.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/BinaryReader.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/JsonReader.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.h +FILE: ../../../third_party/skia/gm/3d.cpp +FILE: ../../../third_party/skia/gm/bc1_transparency.cpp +FILE: ../../../third_party/skia/gm/bicubic.cpp +FILE: ../../../third_party/skia/gm/compressed_textures.cpp +FILE: ../../../third_party/skia/gm/crbug_1073670.cpp +FILE: ../../../third_party/skia/gm/crbug_1086705.cpp +FILE: ../../../third_party/skia/gm/crbug_1113794.cpp +FILE: ../../../third_party/skia/gm/exoticformats.cpp +FILE: ../../../third_party/skia/gm/skbug_9819.cpp +FILE: ../../../third_party/skia/gm/strokerect_anisotropic.cpp +FILE: ../../../third_party/skia/gm/verifiers/gmverifier.cpp +FILE: ../../../third_party/skia/gm/verifiers/gmverifier.h +FILE: ../../../third_party/skia/gm/widebuttcaps.cpp +FILE: ../../../third_party/skia/include/core/SkM44.h +FILE: ../../../third_party/skia/include/effects/SkStrokeAndFillPathEffect.h +FILE: ../../../third_party/skia/include/gpu/GrDirectContext.h +FILE: ../../../third_party/skia/include/private/SkOpts_spi.h +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer.cpp +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer.h +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer_mac.mm +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer_none.cpp +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer_sfml.cpp +FILE: ../../../third_party/skia/modules/skottie/include/ExternalLayer.h +FILE: ../../../third_party/skia/modules/skottie/src/Adapter.h +FILE: ../../../third_party/skia/modules/skottie/src/Camera.cpp +FILE: ../../../third_party/skia/modules/skottie/src/Camera.h +FILE: ../../../third_party/skia/modules/skottie/src/Path.cpp +FILE: ../../../third_party/skia/modules/skottie/src/Transform.cpp +FILE: ../../../third_party/skia/modules/skottie/src/Transform.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/ScalarKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/ShapeKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/TextKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/Vec2KeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.h +FILE: ../../../third_party/skia/modules/skottie/src/effects/BrightnessContrastEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/CornerPinEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/GlowStyles.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/ShadowStyles.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/AudioLayer.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Ellipse.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/FillStroke.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Gradient.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/MergePaths.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/OffsetPaths.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Polystar.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/PuckerBloat.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Rectangle.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Repeater.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/RoundCorners.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/ShapeLayer.h +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/TrimPaths.cpp +FILE: ../../../third_party/skia/modules/skparagraph/gm/simple_gm.cpp +FILE: ../../../third_party/skia/modules/sksg/include/SkSGGeometryEffect.h +FILE: ../../../third_party/skia/modules/sksg/src/SkSGGeometryEffect.cpp +FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_coretext.cpp +FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode.h +FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode_icu.cpp +FILE: ../../../third_party/skia/samplecode/Sample3D.cpp +FILE: ../../../third_party/skia/samplecode/SampleAudio.cpp +FILE: ../../../third_party/skia/samplecode/SampleFitCubicToCircle.cpp +FILE: ../../../third_party/skia/samplecode/SampleSimpleStroker.cpp +FILE: ../../../third_party/skia/src/core/SkColorFilterPriv.h +FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.cpp +FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.h +FILE: ../../../third_party/skia/src/core/SkM44.cpp +FILE: ../../../third_party/skia/src/core/SkMarkerStack.cpp +FILE: ../../../third_party/skia/src/core/SkMarkerStack.h +FILE: ../../../third_party/skia/src/core/SkPathView.h +FILE: ../../../third_party/skia/src/core/SkVerticesPriv.h +FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.cpp +FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.h +FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.h +FILE: ../../../third_party/skia/src/gpu/GrHashMapWithCache.h +FILE: ../../../third_party/skia/src/gpu/GrRecordingContextPriv.cpp +FILE: ../../../third_party/skia/src/gpu/GrSTArenaList.h +FILE: ../../../third_party/skia/src/gpu/ccpr/GrAutoMapVertexBuffer.h +FILE: ../../../third_party/skia/src/gpu/effects/GrArithmeticProcessor.fp +FILE: ../../../third_party/skia/src/gpu/effects/GrDitherEffect.fp +FILE: ../../../third_party/skia/src/gpu/effects/GrHighContrastFilterEffect.fp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.cpp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.h +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.cpp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.h +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.cpp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.h +FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathAtlasMgr.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathAtlasMgr.h +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathShapeData.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathShapeData.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrMidpointContourParser.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrResolveLevelCounter.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrVectorXform.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrWangsFormula.h +FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.cpp +FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.h +FILE: ../../../third_party/skia/src/opts/SkOpts_skx.cpp +FILE: ../../../third_party/skia/src/ports/SkScalerContext_mac_ct.h +FILE: ../../../third_party/skia/src/ports/SkTypeface_mac_ct.h +FILE: ../../../third_party/skia/src/utils/mac/SkCGBase.h +FILE: ../../../third_party/skia/src/utils/mac/SkCGGeometry.h +FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.cpp +FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.h +---------------------------------------------------------------------------------------------------- +Copyright 2020 Google Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +==================================================================================================== + ==================================================================================================== LIBRARY: skia ORIGIN: ../../../third_party/skia/bench/ReadPixBench.cpp + ../../../third_party/skia/LICENSE @@ -4242,9 +4423,9 @@ FILE: ../../../third_party/skia/docs/examples/Canvas_drawVertices.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_drawVertices_2.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_empty_constructor.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getBaseLayerSize.cpp +FILE: ../../../third_party/skia/docs/examples/Canvas_getContext.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getDeviceClipBounds.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getDeviceClipBounds_2.cpp -FILE: ../../../third_party/skia/docs/examples/Canvas_getGrContext.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getLocalClipBounds.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getLocalClipBounds_2.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getProps.cpp @@ -5163,10 +5344,10 @@ FILE: ../../../third_party/skia/src/gpu/ccpr/GrStencilAtlasOp.h FILE: ../../../third_party/skia/src/gpu/effects/GrComposeLerpEffect.fp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrComposeLerpEffect.cpp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrComposeLerpEffect.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathTessellateOp.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathTessellateOp.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrStencilPathShader.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrStencilPathShader.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellatePathOp.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellatePathOp.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellationPathRenderer.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellationPathRenderer.h FILE: ../../../third_party/skia/src/pdf/SkPDFGraphicStackState.cpp @@ -5206,156 +5387,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== -==================================================================================================== -LIBRARY: skia -ORIGIN: ../../../third_party/skia/bench/TessellatePathBench.cpp + ../../../third_party/skia/LICENSE -TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/bench/TessellatePathBench.cpp -FILE: ../../../third_party/skia/experimental/skrive/include/SkRive.h -FILE: ../../../third_party/skia/experimental/skrive/src/Artboard.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Color.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Component.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Drawable.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Ellipse.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Node.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Paint.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Rectangle.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Shape.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/SkRive.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/BinaryReader.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/JsonReader.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.h -FILE: ../../../third_party/skia/gm/3d.cpp -FILE: ../../../third_party/skia/gm/bc1_transparency.cpp -FILE: ../../../third_party/skia/gm/bicubic.cpp -FILE: ../../../third_party/skia/gm/compressed_textures.cpp -FILE: ../../../third_party/skia/gm/crbug_1073670.cpp -FILE: ../../../third_party/skia/gm/exoticformats.cpp -FILE: ../../../third_party/skia/gm/skbug_9819.cpp -FILE: ../../../third_party/skia/gm/strokerect_anisotropic.cpp -FILE: ../../../third_party/skia/gm/verifiers/gmverifier.cpp -FILE: ../../../third_party/skia/gm/verifiers/gmverifier.h -FILE: ../../../third_party/skia/gm/widebuttcaps.cpp -FILE: ../../../third_party/skia/include/core/SkM44.h -FILE: ../../../third_party/skia/include/effects/SkStrokeAndFillPathEffect.h -FILE: ../../../third_party/skia/include/gpu/GrDirectContext.h -FILE: ../../../third_party/skia/include/private/SkOpts_spi.h -FILE: ../../../third_party/skia/modules/skottie/include/ExternalLayer.h -FILE: ../../../third_party/skia/modules/skottie/src/Adapter.h -FILE: ../../../third_party/skia/modules/skottie/src/Camera.cpp -FILE: ../../../third_party/skia/modules/skottie/src/Camera.h -FILE: ../../../third_party/skia/modules/skottie/src/Path.cpp -FILE: ../../../third_party/skia/modules/skottie/src/Transform.cpp -FILE: ../../../third_party/skia/modules/skottie/src/Transform.h -FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.h -FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.h -FILE: ../../../third_party/skia/modules/skottie/src/animator/ScalarKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/ShapeKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/TextKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/Vec2KeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.h -FILE: ../../../third_party/skia/modules/skottie/src/effects/BrightnessContrastEffect.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/CornerPinEffect.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/GlowStyles.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/ShadowStyles.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Ellipse.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/FillStroke.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Gradient.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/MergePaths.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/OffsetPaths.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Polystar.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/PuckerBloat.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Rectangle.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Repeater.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/RoundCorners.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/ShapeLayer.h -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/TrimPaths.cpp -FILE: ../../../third_party/skia/modules/skparagraph/gm/simple_gm.cpp -FILE: ../../../third_party/skia/modules/sksg/include/SkSGGeometryEffect.h -FILE: ../../../third_party/skia/modules/sksg/src/SkSGGeometryEffect.cpp -FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_coretext.cpp -FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode.h -FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode_icu.cpp -FILE: ../../../third_party/skia/samplecode/Sample3D.cpp -FILE: ../../../third_party/skia/samplecode/SampleFitCubicToCircle.cpp -FILE: ../../../third_party/skia/samplecode/SampleSimpleStroker.cpp -FILE: ../../../third_party/skia/src/core/SkColorFilterPriv.h -FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.cpp -FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.h -FILE: ../../../third_party/skia/src/core/SkM44.cpp -FILE: ../../../third_party/skia/src/core/SkMarkerStack.cpp -FILE: ../../../third_party/skia/src/core/SkMarkerStack.h -FILE: ../../../third_party/skia/src/core/SkVerticesPriv.h -FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.cpp -FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.h -FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.h -FILE: ../../../third_party/skia/src/gpu/GrHashMapWithCache.h -FILE: ../../../third_party/skia/src/gpu/GrRecordingContextPriv.cpp -FILE: ../../../third_party/skia/src/gpu/GrSTArenaList.h -FILE: ../../../third_party/skia/src/gpu/ccpr/GrAutoMapVertexBuffer.h -FILE: ../../../third_party/skia/src/gpu/effects/GrArithmeticProcessor.fp -FILE: ../../../third_party/skia/src/gpu/effects/GrDitherEffect.fp -FILE: ../../../third_party/skia/src/gpu/effects/GrHighContrastFilterEffect.fp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.cpp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.h -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.cpp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.h -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.cpp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.h -FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.cpp -FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrMidpointContourParser.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrResolveLevelCounter.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrVectorXform.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrWangsFormula.h -FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.cpp -FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.h -FILE: ../../../third_party/skia/src/opts/SkOpts_skx.cpp -FILE: ../../../third_party/skia/src/ports/SkScalerContext_mac_ct.h -FILE: ../../../third_party/skia/src/ports/SkTypeface_mac_ct.h -FILE: ../../../third_party/skia/src/utils/mac/SkCGBase.h -FILE: ../../../third_party/skia/src/utils/mac/SkCGGeometry.h -FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.cpp -FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.h ----------------------------------------------------------------------------------------------------- -Copyright 2020 Google Inc. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - - * Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -==================================================================================================== - ==================================================================================================== LIBRARY: skia ORIGIN: ../../../third_party/skia/docs/examples/50_percent_gray.cpp + ../../../third_party/skia/LICENSE @@ -5534,12 +5565,14 @@ FILE: ../../../third_party/skia/src/gpu/GrFinishCallbacks.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrFillPathShader.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrFillPathShader.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathShader.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeOp.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeOp.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeShader.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeShader.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateOp.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateOp.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateShader.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateShader.h FILE: ../../../third_party/skia/src/opts/SkVM_opts.h FILE: ../../../third_party/skia/src/sksl/SkSLAnalysis.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLRehydrator.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLRehydrator.h ---------------------------------------------------------------------------------------------------- Copyright 2020 Google LLC. @@ -5655,8 +5688,11 @@ LIBRARY: skia ORIGIN: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPICreateDDL.cpp + ../../../third_party/skia/LICENSE TYPE: LicenseType.bsd FILE: ../../../third_party/skia/fuzz/FuzzCreateDDL.cpp +FILE: ../../../third_party/skia/fuzz/FuzzPath.cpp +FILE: ../../../third_party/skia/fuzz/FuzzRRect.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPICreateDDL.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPISVGCanvas.cpp +FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKP.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSVG.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSkRuntimeEffect.cpp ---------------------------------------------------------------------------------------------------- @@ -5749,10 +5785,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== LIBRARY: skia -ORIGIN: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPISkDescriptor.cpp + ../../../third_party/skia/LICENSE +ORIGIN: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2GLSL.cpp + ../../../third_party/skia/LICENSE TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/fuzz/FuzzSkDescriptor.cpp -FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPISkDescriptor.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2GLSL.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2Metal.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2Pipeline.cpp diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index b3d1975a1fe3f..672a13f903388 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: 05b5f53049ea4c29e6228cf204ac94bf +Signature: 9f3361f4c0d2a3218f0a27ac140fb4c0 UNUSED LICENSES: @@ -67,6 +67,33 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +ORIGIN: ../../../third_party/harfbuzz/src/ms-use/COPYING +TYPE: LicenseType.mit +---------------------------------------------------------------------------------------------------- +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE +==================================================================================================== + ==================================================================================================== ORIGIN: ../../../third_party/pkg/when/LICENSE TYPE: LicenseType.bsd @@ -8027,6 +8054,18 @@ FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/netext_3_10k.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/rutext_2_10k.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/sktext_10k.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/zhtext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/datext_latin1_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/entext_ascii_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/netext_3_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/rutext_2_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/sktext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/zhtext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/datext_latin1_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/entext_ascii_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/netext_3_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/rutext_2_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/sktext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/zhtext_10k.dart FILE: ../../../third_party/dart/client/idea/.idea/.name FILE: ../../../third_party/dart/client/idea/.idea/inspectionProfiles/Project_Default.xml FILE: ../../../third_party/dart/client/idea/.idea/vcs.xml @@ -8156,8 +8195,6 @@ FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart/SoundSplayTr FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart/sound_splay_tree.dart FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart2/SoundSplayTreeSieve.dart FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart2/sound_splay_tree.dart -FILE: ../../../third_party/dart/runtime/bin/abi_version.h -FILE: ../../../third_party/dart/runtime/bin/abi_version_in.cc FILE: ../../../third_party/dart/runtime/bin/elf_loader.cc FILE: ../../../third_party/dart/runtime/bin/elf_loader.h FILE: ../../../third_party/dart/runtime/bin/entrypoints_verification_test_extension.cc @@ -8356,13 +8393,15 @@ FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart/TypedDataDupl FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart2/TypedDataDuplicate.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/Utf8Decode.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/Utf8Decode.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/Utf8Encode.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/Utf8Encode.dart FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.cc FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.h FILE: ../../../third_party/dart/runtime/bin/exe_utils.cc FILE: ../../../third_party/dart/runtime/bin/exe_utils.h FILE: ../../../third_party/dart/runtime/bin/platform_macos.h FILE: ../../../third_party/dart/runtime/bin/platform_macos_test.cc -FILE: ../../../third_party/dart/runtime/include/dart_api_dl.cc +FILE: ../../../third_party/dart/runtime/include/dart_api_dl.c FILE: ../../../third_party/dart/runtime/include/dart_api_dl.h FILE: ../../../third_party/dart/runtime/include/dart_version.h FILE: ../../../third_party/dart/runtime/include/internal/dart_api_dl_impl.h @@ -8943,7 +8982,6 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/cli.dart FILE: ../../../third_party/dart/runtime/observatory/lib/debugger.dart FILE: ../../../third_party/dart/runtime/observatory/lib/sample_profile.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/allocation_profile/allocation_profile.dart -FILE: ../../../third_party/dart/runtime/observatory/lib/src/app/analytics.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/cli/command.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/debugger/debugger.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/debugger/debugger_location.dart @@ -12059,20 +12097,21 @@ LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/COPYING TYPE: LicenseType.unknown FILE: ../../../third_party/harfbuzz/.circleci/config.yml +FILE: ../../../third_party/harfbuzz/.codecov.yml FILE: ../../../third_party/harfbuzz/.editorconfig FILE: ../../../third_party/harfbuzz/THANKS FILE: ../../../third_party/harfbuzz/TODO -FILE: ../../../third_party/harfbuzz/appveyor.yml -FILE: ../../../third_party/harfbuzz/azure-pipelines.yml FILE: ../../../third_party/harfbuzz/docs/HarfBuzz.png FILE: ../../../third_party/harfbuzz/docs/HarfBuzz.svg FILE: ../../../third_party/harfbuzz/docs/harfbuzz-docs.xml +FILE: ../../../third_party/harfbuzz/docs/meson.build FILE: ../../../third_party/harfbuzz/docs/usermanual-buffers-language-script-and-direction.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-clusters.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-fonts-and-faces.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-getting-started.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-glyph-information.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-install-harfbuzz.xml +FILE: ../../../third_party/harfbuzz/docs/usermanual-integration.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-object-model.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-opentype-features.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-shaping-concepts.xml @@ -12080,6 +12119,18 @@ FILE: ../../../third_party/harfbuzz/docs/usermanual-utilities.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-what-is-harfbuzz.xml FILE: ../../../third_party/harfbuzz/docs/version.xml.in FILE: ../../../third_party/harfbuzz/harfbuzz.doap +FILE: ../../../third_party/harfbuzz/meson-cc-tests/intel-atomic-primitives-test.c +FILE: ../../../third_party/harfbuzz/meson-cc-tests/solaris-atomic-operations.c +FILE: ../../../third_party/harfbuzz/meson.build +FILE: ../../../third_party/harfbuzz/perf/fonts/Amiri-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/fonts/NotoNastaliqUrdu-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/fonts/NotoSansDevanagari-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/fonts/Roboto-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/meson.build +FILE: ../../../third_party/harfbuzz/perf/perf-draw.hh +FILE: ../../../third_party/harfbuzz/perf/perf-extents.hh +FILE: ../../../third_party/harfbuzz/perf/perf-shaping.hh +FILE: ../../../third_party/harfbuzz/perf/perf.cc FILE: ../../../third_party/harfbuzz/src/Makefile.sources FILE: ../../../third_party/harfbuzz/src/harfbuzz-config.cmake.in FILE: ../../../third_party/harfbuzz/src/harfbuzz-gobject.pc.in @@ -12087,6 +12138,7 @@ FILE: ../../../third_party/harfbuzz/src/harfbuzz-icu.pc.in FILE: ../../../third_party/harfbuzz/src/harfbuzz-subset.pc.in FILE: ../../../third_party/harfbuzz/src/harfbuzz.cc FILE: ../../../third_party/harfbuzz/src/harfbuzz.pc.in +FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-arabic-joining-list.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-arabic-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-indic-table.cc FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-use-table.cc @@ -12094,13 +12146,28 @@ FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-vowel-constraints.cc FILE: ../../../third_party/harfbuzz/src/hb-ot-tag-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ucd-table.hh FILE: ../../../third_party/harfbuzz/src/hb-unicode-emoji-table.hh +FILE: ../../../third_party/harfbuzz/src/meson.build +FILE: ../../../third_party/harfbuzz/src/update-unicode-tables.make +FILE: ../../../third_party/harfbuzz/subprojects/cairo.wrap +FILE: ../../../third_party/harfbuzz/subprojects/expat.wrap +FILE: ../../../third_party/harfbuzz/subprojects/fontconfig.wrap +FILE: ../../../third_party/harfbuzz/subprojects/freetype2.wrap +FILE: ../../../third_party/harfbuzz/subprojects/glib.wrap +FILE: ../../../third_party/harfbuzz/subprojects/google-benchmark.wrap +FILE: ../../../third_party/harfbuzz/subprojects/libffi.wrap +FILE: ../../../third_party/harfbuzz/subprojects/libpng.wrap +FILE: ../../../third_party/harfbuzz/subprojects/pixman.wrap +FILE: ../../../third_party/harfbuzz/subprojects/proxy-libintl.wrap +FILE: ../../../third_party/harfbuzz/subprojects/ttf-parser.wrap +FILE: ../../../third_party/harfbuzz/subprojects/zlib.wrap ---------------------------------------------------------------------------------------------------- HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. For parts of HarfBuzz that are licensed under different licenses see individual files names COPYING in subdirectories where applicable. -Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019 Google, Inc. -Copyright © 2019 Facebook, Inc. +Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020 Google, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. Copyright © 2012 Mozilla Foundation Copyright © 2011 Codethink Limited Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) @@ -12195,19 +12262,45 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/hb-aat-fdsc-table.hh +ORIGIN: ../../../third_party/harfbuzz/src/failing-alloc.c +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/failing-alloc.c +FILE: ../../../third_party/harfbuzz/src/hb-draw.hh +---------------------------------------------------------------------------------------------------- +Copyright © 2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-aat-layout-ankr-table.hh TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/hb-aat-fdsc-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-ankr-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-bsln-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-feat-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-just-table.hh -FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-lcar-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout.h FILE: ../../../third_party/harfbuzz/src/hb-aat-ltag-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat.h -FILE: ../../../third_party/harfbuzz/src/hb-ot-color-colr-table.hh -FILE: ../../../third_party/harfbuzz/src/hb-ot-color-sbix-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-color-svg-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-gasp-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-metrics.h @@ -12324,6 +12417,8 @@ FILE: ../../../third_party/harfbuzz/src/hb-number.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-meta-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-meta.cc FILE: ../../../third_party/harfbuzz/src/hb-ot-meta.h +FILE: ../../../third_party/harfbuzz/src/hb-style.cc +FILE: ../../../third_party/harfbuzz/src/hb-style.h FILE: ../../../third_party/harfbuzz/src/test-number.cc FILE: ../../../third_party/harfbuzz/src/test-ot-meta.cc ---------------------------------------------------------------------------------------------------- @@ -12605,7 +12700,6 @@ FILE: ../../../third_party/harfbuzz/src/hb-buffer-deserialize-json.rl FILE: ../../../third_party/harfbuzz/src/hb-buffer-deserialize-text.hh FILE: ../../../third_party/harfbuzz/src/hb-buffer-deserialize-text.rl FILE: ../../../third_party/harfbuzz/src/hb-deprecated.h -FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.h.tmpl FILE: ../../../third_party/harfbuzz/src/hb-ot-layout-jstf-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-hangul.cc ---------------------------------------------------------------------------------------------------- @@ -12742,7 +12836,6 @@ FILE: ../../../third_party/harfbuzz/src/hb-shaper-impl.hh FILE: ../../../third_party/harfbuzz/src/hb-shaper-list.hh FILE: ../../../third_party/harfbuzz/src/hb-shaper.cc FILE: ../../../third_party/harfbuzz/src/hb-shaper.hh -FILE: ../../../third_party/harfbuzz/src/hb-warning.cc ---------------------------------------------------------------------------------------------------- Copyright © 2012 Google, Inc. @@ -13026,6 +13119,36 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-draw.cc +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-draw.cc +FILE: ../../../third_party/harfbuzz/src/hb-draw.h +---------------------------------------------------------------------------------------------------- +Copyright © 2019-2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-face.cc @@ -13099,10 +13222,7 @@ LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-fallback-shape.cc TYPE: LicenseType.unknown FILE: ../../../third_party/harfbuzz/src/hb-fallback-shape.cc -FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.cc.tmpl FILE: ../../../third_party/harfbuzz/src/hb-gobject-structs.cc -FILE: ../../../third_party/harfbuzz/src/hb-gobject-structs.h -FILE: ../../../third_party/harfbuzz/src/hb-gobject.h FILE: ../../../third_party/harfbuzz/src/hb-uniscribe.h FILE: ../../../third_party/harfbuzz/src/hb-version.h FILE: ../../../third_party/harfbuzz/src/hb-version.h.in @@ -13191,6 +13311,66 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-gobject-enums.cc.tmpl +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.cc.tmpl +FILE: ../../../third_party/harfbuzz/src/hb-gobject-structs.h +FILE: ../../../third_party/harfbuzz/src/hb-gobject.h +---------------------------------------------------------------------------------------------------- +Copyright (C) 2011 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-gobject-enums.h.tmpl +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.h.tmpl +---------------------------------------------------------------------------------------------------- +Copyright (C) 2013 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-graphite2.cc @@ -13373,6 +13553,37 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-cff1-std-str.hh +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-ot-cff1-std-str.hh +FILE: ../../../third_party/harfbuzz/src/test-bimap.cc +FILE: ../../../third_party/harfbuzz/src/test-ot-glyphname.cc +---------------------------------------------------------------------------------------------------- +Copyright © 2019 Adobe, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-cmap-table.hh @@ -13434,6 +13645,37 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-color-colr-table.hh +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-ot-color-colr-table.hh +FILE: ../../../third_party/harfbuzz/src/hb-ot-color-sbix-table.hh +---------------------------------------------------------------------------------------------------- +Copyright © 2018 Ebrahim Byagowi +Copyright © 2020 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-color-cpal-table.hh @@ -13879,7 +14121,6 @@ LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-layout.h TYPE: LicenseType.unknown FILE: ../../../third_party/harfbuzz/src/hb-ot-layout.h -FILE: ../../../third_party/harfbuzz/src/main.cc ---------------------------------------------------------------------------------------------------- Copyright © 2007,2008,2009 Red Hat, Inc. @@ -14529,11 +14770,13 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-bimap.cc +ORIGIN: ../../../third_party/harfbuzz/src/main.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-bimap.cc +FILE: ../../../third_party/harfbuzz/src/main.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2019 Adobe, Inc. +Copyright © 2007,2008,2009 Red Hat, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2018 Khaled Hosny This is part of HarfBuzz, a text shaping library. @@ -14558,11 +14801,11 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc +ORIGIN: ../../../third_party/harfbuzz/src/test-array.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc +FILE: ../../../third_party/harfbuzz/src/test-array.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2010,2011,2013 Google, Inc. +Copyright © 2020 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -14587,13 +14830,11 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc +ORIGIN: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc -FILE: ../../../third_party/harfbuzz/src/test-gsub-would-substitute.cc -FILE: ../../../third_party/harfbuzz/src/test.cc +FILE: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2010,2011 Google, Inc. +Copyright © 2010,2011,2013 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -14618,12 +14859,13 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-ot-color.cc +ORIGIN: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-ot-color.cc +FILE: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc +FILE: ../../../third_party/harfbuzz/src/test-gsub-would-substitute.cc +FILE: ../../../third_party/harfbuzz/src/test.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2018 Ebrahim Byagowi -Copyright © 2018 Khaled Hosny +Copyright © 2010,2011 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -21819,7 +22061,7 @@ FILE: ../../../third_party/dart/third_party/wasmer/wasmer.rs ---------------------------------------------------------------------------------------------------- MIT License -Copyright (c) 2019 Wasmer, Inc. and its affiliates. +Copyright (c) 2019-present Wasmer, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22856,4 +23098,4 @@ freely, subject to the following restrictions: misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ==================================================================================================== -Total license count: 360 +Total license count: 367 diff --git a/ci/lint.sh b/ci/lint.sh index c3292d8e369fe..e2698f255f7b5 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -1,4 +1,8 @@ #!/bin/bash +# +# 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. set -e @@ -9,15 +13,6 @@ unset CDPATH # link at a time, and then cds into the link destination and find out where it # ends up. # -# The returned filesystem path must be a format usable by Dart's URI parser, -# since the Dart command line tool treats its argument as a file URI, not a -# filename. For instance, multiple consecutive slashes should be reduced to a -# single slash, since double-slashes indicate a URI "authority", and these are -# supposed to be filenames. There is an edge case where this will return -# multiple slashes: when the input resolves to the root directory. However, if -# that were the case, we wouldn't be running this shell, so we don't do anything -# about it. -# # The function is enclosed in a subshell to avoid changing the working directory # of the caller. function follow_links() ( @@ -31,17 +26,21 @@ function follow_links() ( done echo "$file" ) -PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")" -CI_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)" -SRC_DIR="$(cd "$CI_DIR/../.."; pwd -P)" + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +DART_BIN="${SRC_DIR}/third_party/dart/tools/sdks/dart-sdk/bin" +DART="${DART_BIN}/dart" +PUB="${DART_BIN}/pub" COMPILE_COMMANDS="$SRC_DIR/out/compile_commands.json" if [ ! -f "$COMPILE_COMMANDS" ]; then - (cd $SRC_DIR; ./flutter/tools/gn) + (cd "$SRC_DIR"; ./flutter/tools/gn) fi -cd "$CI_DIR" -pub get && dart \ +cd "$SCRIPT_DIR" +"$PUB" get && "$DART" \ + --disable-dart-dev \ bin/lint.dart \ --compile-commands="$COMPILE_COMMANDS" \ --repo="$SRC_DIR/flutter" \ diff --git a/ci/pubspec.yaml b/ci/pubspec.yaml index d345cab6e78f1..eba8dd49bfbbf 100644 --- a/ci/pubspec.yaml +++ b/ci/pubspec.yaml @@ -1,9 +1,14 @@ +# 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. + name: ci_scripts dependencies: args: ^1.6.0 path: ^1.7.0 - process_runner: ^2.0.3 + isolate: ^2.0.3 + process_runner: ^3.0.0 environment: sdk: '>=2.8.0 <3.0.0' diff --git a/ci/test.sh b/ci/test.sh deleted file mode 100755 index c0ea5babca926..0000000000000 --- a/ci/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -cd frontend_server -dart test/server_test.dart diff --git a/common/config.gni b/common/config.gni index c0727a6f8b6f4..f7b122a3f480e 100644 --- a/common/config.gni +++ b/common/config.gni @@ -16,6 +16,9 @@ declare_args() { # Whether to use the Skia text shaper module flutter_enable_skshaper = false + + # Whether to use the legacy embedder when building for Fuchsia. + flutter_enable_legacy_fuchsia_embedder = true } # feature_defines_list --------------------------------------------------------- @@ -56,119 +59,3 @@ if (is_ios || is_mac) { ] flutter_cflags_objcc = flutter_cflags_objc } - -# This template creates a `source_set` in both standard and "fuchsia_legacy" -# configurations. -# -# The "fuchsia_legacy" configuration includes old, non-embedder API sources and -# defines the LEGACY_FUCHSIA_EMBEDDER symbol. This template and the config -# are both transitional and will be removed after the embedder API transition -# is complete. -# TODO(fxb/54041): Remove when no longer neccesary. -# -# `sources`, `defines`, `public_configs`, `configs`, `public_deps`, `deps` work -# as they do in a normal `source_set`. -# -# `legacy_deps` is the list of dependencies which should be mutated by -# appending '_fuchsia_legacy' when creating the 2 `source_set`'s. The template adds -# `legacy_deps` to `public_deps`, whether it mutates them or not. -template("source_set_maybe_fuchsia_legacy") { - public_deps_non_legacy = [] - deps_non_legacy = [] - if (defined(invoker.public_deps)) { - public_deps_non_legacy += invoker.public_deps - } - if (defined(invoker.deps)) { - deps_non_legacy += invoker.deps - } - if (defined(invoker.public_deps_legacy_and_next)) { - foreach(legacy_dep, invoker.public_deps_legacy_and_next) { - public_deps_non_legacy += [ legacy_dep ] - } - } - if (defined(invoker.deps_legacy_and_next)) { - foreach(legacy_dep, invoker.deps_legacy_and_next) { - deps_non_legacy += [ legacy_dep ] - } - } - - source_set(target_name) { - forward_variables_from(invoker, - [ - "testonly", - "sources", - "defines", - "public_configs", - "configs", - ]) - public_deps = public_deps_non_legacy - deps = deps_non_legacy - } - - if (is_fuchsia) { - legagcy_suffix = "_fuchsia_legacy" - - sources_legacy = [] - if (defined(invoker.sources_legacy)) { - sources_legacy += invoker.sources_legacy - } - if (defined(invoker.sources)) { - sources_legacy += invoker.sources - } - - public_configs_legacy = [ "//flutter:fuchsia_legacy" ] - if (defined(invoker.public_configs)) { - public_configs_legacy += invoker.public_configs - } - - public_deps_legacy = [] - deps_legacy = [] - if (defined(invoker.public_deps)) { - public_deps_legacy += invoker.public_deps - } - if (defined(invoker.deps)) { - deps_legacy += invoker.deps - } - if (defined(invoker.public_deps_legacy)) { - public_deps_legacy += invoker.public_deps_legacy - } - if (defined(invoker.deps_legacy)) { - deps_legacy += invoker.deps_legacy - } - if (defined(invoker.public_deps_legacy_and_next)) { - foreach(legacy_dep, invoker.public_deps_legacy_and_next) { - public_deps_legacy += [ legacy_dep + legagcy_suffix ] - } - } - if (defined(invoker.deps_legacy_and_next)) { - foreach(legacy_dep, invoker.deps_legacy_and_next) { - deps_legacy += [ legacy_dep + legagcy_suffix ] - } - } - - source_set(target_name + legagcy_suffix) { - forward_variables_from(invoker, - [ - "testonly", - "defines", - "configs", - ]) - sources = sources_legacy - - public_configs = public_configs_legacy - - public_deps = public_deps_legacy - deps = deps_legacy - } - } else { - if (defined(invoker.sources_legacy)) { - not_needed(invoker, [ "sources_legacy" ]) - } - if (defined(invoker.public_deps_legacy)) { - not_needed(invoker, [ "public_deps_legacy" ]) - } - if (defined(invoker.deps_legacy)) { - not_needed(invoker, [ "deps_legacy" ]) - } - } -} diff --git a/common/settings.h b/common/settings.h index 15166b8d38213..ae52e4a7342ba 100644 --- a/common/settings.h +++ b/common/settings.h @@ -22,10 +22,17 @@ namespace flutter { class FrameTiming { public: - enum Phase { kBuildStart, kBuildFinish, kRasterStart, kRasterFinish, kCount }; - - static constexpr Phase kPhases[kCount] = {kBuildStart, kBuildFinish, - kRasterStart, kRasterFinish}; + enum Phase { + kVsyncStart, + kBuildStart, + kBuildFinish, + kRasterStart, + kRasterFinish, + kCount + }; + + static constexpr Phase kPhases[kCount] = { + kVsyncStart, kBuildStart, kBuildFinish, kRasterStart, kRasterFinish}; fml::TimePoint Get(Phase phase) const { return data_[phase]; } fml::TimePoint Set(Phase phase, fml::TimePoint value) { @@ -102,8 +109,10 @@ struct Settings { bool enable_dart_profiling = false; bool disable_dart_asserts = false; - // Used to signal the embedder whether HTTP connections are disabled. - bool disable_http = false; + // Whether embedder only allows secure connections. + bool may_insecurely_connect_to_all_domains = true; + // JSON-formatted domain network policy. + std::string domain_network_policy; // Used as the script URI in debug messages. Does not affect how the Dart code // is executed. diff --git a/flow/BUILD.gn b/flow/BUILD.gn index 89647c7189784..63f1fe529108b 100644 --- a/flow/BUILD.gn +++ b/flow/BUILD.gn @@ -6,7 +6,7 @@ import("//build/fuchsia/sdk.gni") import("//flutter/common/config.gni") import("//flutter/testing/testing.gni") -source_set_maybe_fuchsia_legacy("flow") { +source_set("flow") { sources = [ "compositor_context.cc", "compositor_context.h", @@ -78,20 +78,23 @@ source_set_maybe_fuchsia_legacy("flow") { "//third_party/skia", ] - sources_legacy = [ - "layers/child_scene_layer.cc", - "layers/child_scene_layer.h", - "scene_update_context.cc", - "scene_update_context.h", - "view_holder.cc", - "view_holder.h", - ] + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { + sources += [ + "layers/child_scene_layer.cc", + "layers/child_scene_layer.h", + "scene_update_context.cc", + "scene_update_context.h", + "view_holder.cc", + "view_holder.h", + ] - public_deps_legacy = [ - "$fuchsia_sdk_root/fidl:fuchsia.ui.app", - "$fuchsia_sdk_root/fidl:fuchsia.ui.gfx", - "$fuchsia_sdk_root/pkg:scenic_cpp", - ] + public_deps = [ + "$fuchsia_sdk_root/fidl:fuchsia.ui.app", + "$fuchsia_sdk_root/fidl:fuchsia.ui.gfx", + "$fuchsia_sdk_root/fidl:fuchsia.ui.views", + "$fuchsia_sdk_root/pkg:scenic_cpp", + ] + } } if (enable_unittests) { @@ -99,7 +102,7 @@ if (enable_unittests) { fixtures = [] } - source_set_maybe_fuchsia_legacy("flow_testing") { + source_set("flow_testing") { testonly = true sources = [ @@ -121,10 +124,10 @@ if (enable_unittests) { "//third_party/googletest:gtest", ] - deps_legacy_and_next = [ ":flow" ] + deps = [ ":flow" ] } - source_set_maybe_fuchsia_legacy("flow_unittests_common") { + executable("flow_unittests") { testonly = true sources = [ @@ -160,7 +163,9 @@ if (enable_unittests) { ] deps = [ + ":flow", ":flow_fixtures", + ":flow_testing", "//flutter/fml", "//flutter/testing:skia", "//flutter/testing:testing_lib", @@ -169,32 +174,10 @@ if (enable_unittests) { "//third_party/skia", ] - sources_legacy = [ "layers/fuchsia_layer_unittests.cc" ] - - deps_legacy = [ "//build/fuchsia/pkg:sys_cpp_testing" ] - - deps_legacy_and_next = [ - ":flow", - ":flow_testing", - ] - } - - if (is_fuchsia) { - executable("flow_unittests") { - testonly = true - - deps = [ ":flow_unittests_common_fuchsia_legacy" ] - } - executable("flow_unittests_next") { - testonly = true - - deps = [ ":flow_unittests_common" ] - } - } else { - executable("flow_unittests") { - testonly = true + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { + sources += [ "layers/fuchsia_layer_unittests.cc" ] - deps = [ ":flow_unittests_common" ] + deps += [ "//build/fuchsia/pkg:sys_cpp_testing" ] } } } diff --git a/flow/compositor_context.cc b/flow/compositor_context.cc index ddf9f1c698893..01e23e057196c 100644 --- a/flow/compositor_context.cc +++ b/flow/compositor_context.cc @@ -9,8 +9,10 @@ namespace flutter { -CompositorContext::CompositorContext(fml::Milliseconds frame_budget) - : raster_time_(frame_budget), ui_time_(frame_budget) {} +CompositorContext::CompositorContext(Delegate& delegate) + : delegate_(delegate), + raster_time_(delegate.GetFrameBudget()), + ui_time_(delegate.GetFrameBudget()) {} CompositorContext::~CompositorContext() = default; @@ -23,8 +25,11 @@ void CompositorContext::BeginFrame(ScopedFrame& frame, } void CompositorContext::EndFrame(ScopedFrame& frame, - bool enable_instrumentation) { - raster_cache_.SweepAfterFrame(); + bool enable_instrumentation, + size_t freed_hint) { + freed_hint += raster_cache_.SweepAfterFrame(); + delegate_.OnCompositorEndFrame(freed_hint); + if (enable_instrumentation) { raster_time_.Stop(); } @@ -64,7 +69,7 @@ CompositorContext::ScopedFrame::ScopedFrame( } CompositorContext::ScopedFrame::~ScopedFrame() { - context_.EndFrame(*this, instrumentation_enabled_); + context_.EndFrame(*this, instrumentation_enabled_, uncached_external_size_); } RasterStatus CompositorContext::ScopedFrame::Raster( diff --git a/flow/compositor_context.h b/flow/compositor_context.h index 47992abda6028..b17dc907420da 100644 --- a/flow/compositor_context.h +++ b/flow/compositor_context.h @@ -37,6 +37,18 @@ enum class RasterStatus { class CompositorContext { public: + class Delegate { + public: + /// Called at the end of a frame with approximately how many bytes mightbe + /// freed if a GC ran now. + /// + /// This method is called from the raster task runner. + virtual void OnCompositorEndFrame(size_t freed_hint) = 0; + + /// Time limit for a smooth frame. See `Engine::GetDisplayRefreshRate`. + virtual fml::Milliseconds GetFrameBudget() = 0; + }; + class ScopedFrame { public: ScopedFrame(CompositorContext& context, @@ -67,6 +79,8 @@ class CompositorContext { virtual RasterStatus Raster(LayerTree& layer_tree, bool ignore_raster_cache); + void add_external_size(size_t size) { uncached_external_size_ += size; } + private: CompositorContext& context_; GrDirectContext* gr_context_; @@ -76,11 +90,12 @@ class CompositorContext { const bool instrumentation_enabled_; const bool surface_supports_readback_; fml::RefPtr raster_thread_merger_; + size_t uncached_external_size_ = 0; FML_DISALLOW_COPY_AND_ASSIGN(ScopedFrame); }; - CompositorContext(fml::Milliseconds frame_budget = fml::kDefaultFrameBudget); + explicit CompositorContext(Delegate& delegate); virtual ~CompositorContext(); @@ -108,6 +123,7 @@ class CompositorContext { Stopwatch& ui_time() { return ui_time_; } private: + Delegate& delegate_; RasterCache raster_cache_; TextureRegistry texture_registry_; Counter frame_count_; @@ -116,7 +132,9 @@ class CompositorContext { void BeginFrame(ScopedFrame& frame, bool enable_instrumentation); - void EndFrame(ScopedFrame& frame, bool enable_instrumentation); + void EndFrame(ScopedFrame& frame, + bool enable_instrumentation, + size_t freed_hint); FML_DISALLOW_COPY_AND_ASSIGN(CompositorContext); }; diff --git a/flow/embedded_views.cc b/flow/embedded_views.cc index 07a484999ff51..9441c8dc9470c 100644 --- a/flow/embedded_views.cc +++ b/flow/embedded_views.cc @@ -60,4 +60,8 @@ const std::vector>::const_iterator MutatorsStack::End() return vector_.end(); }; +bool ExternalViewEmbedder::SupportsDynamicThreadMerging() { + return false; +} + } // namespace flutter diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 455eb91511801..cbfde228786a3 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -266,6 +266,10 @@ class ExternalViewEmbedder { // sets the stage for the next pre-roll. virtual void CancelFrame() = 0; + // Indicates the begining of a frame. + // + // The `raster_thread_merger` will be null if |SupportsDynamicThreadMerging| + // returns false. virtual void BeginFrame( SkISize frame_size, GrDirectContext* context, @@ -306,10 +310,20 @@ class ExternalViewEmbedder { // A new frame on the platform thread starts immediately. If the GPU thread // still has some task running, there could be two frames being rendered // concurrently, which causes undefined behaviors. + // + // The `raster_thread_merger` will be null if |SupportsDynamicThreadMerging| + // returns false. virtual void EndFrame( bool should_resubmit_frame, fml::RefPtr raster_thread_merger) {} + // Whether the embedder should support dynamic thread merging. + // + // Returning `true` results a |RasterThreadMerger| instance to be created. + // * See also |BegineFrame| and |EndFrame| for getting the + // |RasterThreadMerger| instance. + virtual bool SupportsDynamicThreadMerging(); + FML_DISALLOW_COPY_AND_ASSIGN(ExternalViewEmbedder); }; // ExternalViewEmbedder diff --git a/flow/instrumentation.cc b/flow/instrumentation.cc index ea85d06e1cce9..541b8635b6aa7 100644 --- a/flow/instrumentation.cc +++ b/flow/instrumentation.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/instrumentation.h" @@ -52,16 +51,18 @@ double Stopwatch::UnitFrameInterval(double raster_time_ms) const { double Stopwatch::UnitHeight(double raster_time_ms, double max_unit_interval) const { double unitHeight = UnitFrameInterval(raster_time_ms) / max_unit_interval; - if (unitHeight > 1.0) + if (unitHeight > 1.0) { unitHeight = 1.0; + } return unitHeight; } fml::TimeDelta Stopwatch::MaxDelta() const { fml::TimeDelta max_delta; for (size_t i = 0; i < kMaxSamples; i++) { - if (laps_[i] > max_delta) + if (laps_[i] > max_delta) { max_delta = laps_[i]; + } } return max_delta; } @@ -135,7 +136,7 @@ void Stopwatch::InitVisualizeSurface(const SkRect& rect) const { cache_canvas->drawPath(path, paint); } -void Stopwatch::Visualize(SkCanvas& canvas, const SkRect& rect) const { +void Stopwatch::Visualize(SkCanvas* canvas, const SkRect& rect) const { // Initialize visualize cache if it has not yet been initialized. InitVisualizeSurface(rect); @@ -191,8 +192,9 @@ void Stopwatch::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Limit the number of markers displayed. After a certain point, the graph // becomes crowded - if (frame_marker_count > kMaxFrameMarkers) + if (frame_marker_count > kMaxFrameMarkers) { frame_marker_count = 1; + } for (size_t frame_index = 0; frame_index < frame_marker_count; frame_index++) { @@ -224,7 +226,7 @@ void Stopwatch::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Draw the cached surface onto the output canvas. paint.reset(); - visualize_cache_surface_->draw(&canvas, rect.x(), rect.y(), &paint); + visualize_cache_surface_->draw(canvas, rect.x(), rect.y(), &paint); } CounterValues::CounterValues() : current_sample_(kMaxSamples - 1) { @@ -238,7 +240,7 @@ void CounterValues::Add(int64_t value) { values_[current_sample_] = value; } -void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { +void CounterValues::Visualize(SkCanvas* canvas, const SkRect& rect) const { size_t max_bytes = GetMaxValue(); if (max_bytes == 0) { @@ -252,7 +254,7 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Paint the background. paint.setColor(0x99FFFFFF); - canvas.drawRect(rect, paint); + canvas->drawRect(rect, paint); // Establish the graph position. const SkScalar x = rect.x(); @@ -268,10 +270,12 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { for (size_t i = 0; i < kMaxSamples; ++i) { int64_t current_bytes = values_[i]; - double ratio = - (double)(current_bytes - min_bytes) / (max_bytes - min_bytes); - path.lineTo(x + (((double)(i) / (double)kMaxSamples) * width), - y + ((1.0 - ratio) * height)); + double ratio = static_cast(current_bytes - min_bytes) / + static_cast(max_bytes - min_bytes); + path.lineTo( + x + ((static_cast(i) / static_cast(kMaxSamples)) * + width), + y + ((1.0 - ratio) * height)); } path.rLineTo(100, 0); @@ -280,7 +284,7 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Draw the graph. paint.setColor(0xAA0000FF); - canvas.drawPath(path, paint); + canvas->drawPath(path, paint); // Paint the vertical marker for the current frame. const double sample_unit_width = (1.0 / kMaxSamples); @@ -294,7 +298,7 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { const auto marker_rect = SkRect::MakeLTRB( sample_x, y, sample_x + width * sample_unit_width + sample_margin_width * 2, bottom); - canvas.drawRect(marker_rect, paint); + canvas->drawRect(marker_rect, paint); } int64_t CounterValues::GetCurrentValue() const { diff --git a/flow/instrumentation.h b/flow/instrumentation.h index a4697830cf972..49ca887844cec 100644 --- a/flow/instrumentation.h +++ b/flow/instrumentation.h @@ -30,7 +30,7 @@ class Stopwatch { void InitVisualizeSurface(const SkRect& rect) const; - void Visualize(SkCanvas& canvas, const SkRect& rect) const; + void Visualize(SkCanvas* canvas, const SkRect& rect) const; void Start(); @@ -81,7 +81,7 @@ class CounterValues { void Add(int64_t value); - void Visualize(SkCanvas& canvas, const SkRect& rect) const; + void Visualize(SkCanvas* canvas, const SkRect& rect) const; int64_t GetCurrentValue() const; diff --git a/flow/layers/child_scene_layer.cc b/flow/layers/child_scene_layer.cc index 795946e0855c9..2a51590ff785c 100644 --- a/flow/layers/child_scene_layer.cc +++ b/flow/layers/child_scene_layer.cc @@ -4,8 +4,6 @@ #include "flutter/flow/layers/child_scene_layer.h" -#include "flutter/flow/view_holder.h" - namespace flutter { ChildSceneLayer::ChildSceneLayer(zx_koid_t layer_id, @@ -19,11 +17,9 @@ ChildSceneLayer::ChildSceneLayer(zx_koid_t layer_id, void ChildSceneLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "ChildSceneLayer::Preroll"); - set_needs_system_composite(true); - - CheckForChildLayerBelow(context); context->child_scene_layer_exists_below = true; + CheckForChildLayerBelow(context); // An alpha "hole punch" is required if the frame behind us is not opaque. if (!context->is_opaque) { @@ -49,15 +45,7 @@ void ChildSceneLayer::Paint(PaintContext& context) const { void ChildSceneLayer::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "ChildSceneLayer::UpdateScene"); FML_DCHECK(needs_system_composite()); - - Layer::UpdateScene(context); - - auto* view_holder = ViewHolder::FromId(layer_id_); - FML_DCHECK(view_holder); - - view_holder->UpdateScene(context, offset_, size_, - SkScalarRoundToInt(context.alphaf() * 255), - hit_testable_); + context.UpdateView(layer_id_, offset_, size_, hit_testable_); } } // namespace flutter diff --git a/flow/layers/container_layer.cc b/flow/layers/container_layer.cc index d8bf8ed13a1b4..825826b70835f 100644 --- a/flow/layers/container_layer.cc +++ b/flow/layers/container_layer.cc @@ -4,6 +4,8 @@ #include "flutter/flow/layers/container_layer.h" +#include + namespace flutter { ContainerLayer::ContainerLayer() {} @@ -30,6 +32,9 @@ void ContainerLayer::PrerollChildren(PrerollContext* context, const SkMatrix& child_matrix, SkRect* child_paint_bounds) { #if defined(LEGACY_FUCHSIA_EMBEDDER) + // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), + // Layers that appear above the embedded content will be turned into their own + // Scenic layers. child_layer_exists_below_ = context->child_scene_layer_exists_below; context->child_scene_layer_exists_below = false; #endif @@ -98,63 +103,20 @@ void ContainerLayer::UpdateScene(SceneUpdateContext& context) { } void ContainerLayer::UpdateSceneChildren(SceneUpdateContext& context) { - auto update_scene_layers = [&] { - // Paint all of the layers which need to be drawn into the container. - // These may be flattened down to a containing Scenic Frame. - for (auto& layer : layers_) { - if (layer->needs_system_composite()) { - layer->UpdateScene(context); - } - } - }; - FML_DCHECK(needs_system_composite()); - // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), - // PhysicalShapeLayers that appear above the embedded content will be turned - // into their own Scenic layers. + std::optional frame; if (child_layer_exists_below_) { - float global_scenic_elevation = - context.GetGlobalElevationForNextScenicLayer(); - float local_scenic_elevation = - global_scenic_elevation - context.scenic_elevation(); - float z_translation = -local_scenic_elevation; - - // Retained rendering: speedup by reusing a retained entity node if - // possible. When an entity node is reused, no paint layer is added to the - // frame so we won't call PhysicalShapeLayer::Paint. - LayerRasterCacheKey key(unique_id(), context.Matrix()); - if (context.HasRetainedNode(key)) { - TRACE_EVENT_INSTANT0("flutter", "retained layer cache hit"); - scenic::EntityNode* retained_node = context.GetRetainedNode(key); - FML_DCHECK(context.top_entity()); - FML_DCHECK(retained_node->session() == context.session()); - - // Re-adjust the elevation. - retained_node->SetTranslation(0.f, 0.f, z_translation); - - context.top_entity()->entity_node().AddChild(*retained_node); - return; - } - - TRACE_EVENT_INSTANT0("flutter", "cache miss, creating"); - // If we can't find an existing retained surface, create one. - SceneUpdateContext::Frame frame( + frame.emplace( context, SkRRect::MakeRect(paint_bounds()), SK_ColorTRANSPARENT, - SkScalarRoundToInt(context.alphaf() * 255), - "flutter::PhysicalShapeLayer", z_translation, this); - - frame.AddPaintLayer(this); - - // Node: UpdateSceneChildren needs to be called here so that |frame| is - // still in scope (and therefore alive) while UpdateSceneChildren is being - // called. - float scenic_elevation = context.scenic_elevation(); - context.set_scenic_elevation(scenic_elevation + local_scenic_elevation); - update_scene_layers(); - context.set_scenic_elevation(scenic_elevation); - } else { - update_scene_layers(); + SkScalarRoundToInt(context.alphaf() * 255), "flutter::ContainerLayer"); + frame->AddPaintLayer(this); + } + + for (auto& layer : layers_) { + if (layer->needs_system_composite()) { + layer->UpdateScene(context); + } } } diff --git a/flow/layers/fuchsia_layer_unittests.cc b/flow/layers/fuchsia_layer_unittests.cc index b1e7d2be05500..fcc17c0ad06e4 100644 --- a/flow/layers/fuchsia_layer_unittests.cc +++ b/flow/layers/fuchsia_layer_unittests.cc @@ -238,57 +238,17 @@ class MockSession : public fuchsia::ui::scenic::testing::Session_TestBase { fuchsia::ui::scenic::SessionListenerPtr listener_; }; -class MockSurfaceProducerSurface - : public SceneUpdateContext::SurfaceProducerSurface { +class MockSessionWrapper : public flutter::SessionWrapper { public: - MockSurfaceProducerSurface(scenic::Session* session, const SkISize& size) - : image_(session, 0, 0, {}), size_(size) {} + MockSessionWrapper(fuchsia::ui::scenic::SessionPtr session_ptr) + : session_(std::move(session_ptr)) {} + ~MockSessionWrapper() override = default; - size_t AdvanceAndGetAge() override { return 0; } - - bool FlushSessionAcquireAndReleaseEvents() override { return false; } - - bool IsValid() const override { return false; } - - SkISize GetSize() const override { return size_; } - - void SignalWritesFinished( - const std::function& on_writes_committed) override {} - - scenic::Image* GetImage() override { return &image_; }; - - sk_sp GetSkiaSurface() const override { return nullptr; }; + scenic::Session* get() override { return &session_; } + void Present() override { session_.Flush(); } private: - scenic::Image image_; - SkISize size_; -}; - -class MockSurfaceProducer : public SceneUpdateContext::SurfaceProducer { - public: - MockSurfaceProducer(scenic::Session* session) : session_(session) {} - std::unique_ptr ProduceSurface( - const SkISize& size, - const LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) override { - return std::make_unique(session_, size); - } - - // Query a retained entity node (owned by a retained surface) for retained - // rendering. - bool HasRetainedNode(const LayerRasterCacheKey& key) const override { - return false; - } - - scenic::EntityNode* GetRetainedNode(const LayerRasterCacheKey& key) override { - return nullptr; - } - - void SubmitSurface(std::unique_ptr - surface) override {} - - private: - scenic::Session* session_; + scenic::Session session_; }; struct TestContext { @@ -297,12 +257,11 @@ struct TestContext { fml::RefPtr task_runner; // Session. - MockSession mock_session; fidl::InterfaceRequest listener_request; - std::unique_ptr session; + MockSession mock_session; + std::unique_ptr mock_session_wrapper; // SceneUpdateContext. - std::unique_ptr mock_surface_producer; std::unique_ptr scene_update_context; // PrerollContext. @@ -324,15 +283,13 @@ std::unique_ptr InitTest() { fuchsia::ui::scenic::SessionListenerPtr listener; context->listener_request = listener.NewRequest(); context->mock_session.Bind(session_ptr.NewRequest(), std::move(listener)); - context->session = std::make_unique(std::move(session_ptr)); + context->mock_session_wrapper = + std::make_unique(std::move(session_ptr)); // Init SceneUpdateContext. - context->mock_surface_producer = - std::make_unique(context->session.get()); context->scene_update_context = std::make_unique( - context->session.get(), context->mock_surface_producer.get()); - context->scene_update_context->set_metrics( - fidl::MakeOptional(fuchsia::ui::gfx::Metrics{1.f, 1.f, 1.f})); + "fuchsia_layer_unittest", fuchsia::ui::views::ViewToken(), + scenic::ViewRefPair::New(), *(context->mock_session_wrapper)); // Init PrerollContext. context->preroll_context = std::unique_ptr(new PrerollContext{ @@ -348,7 +305,6 @@ std::unique_ptr InitTest() { context->unused_texture_registry, // texture registry (not // supported) false, // checkerboard_offscreen_layers - 100.f, // maximum depth allowed for rendering 1.f // ratio between logical and physical }); @@ -602,7 +558,7 @@ TEST_F(FuchsiaLayerTest, DISABLED_PhysicalShapeLayersAndChildSceneLayers) { // against the list above. root->UpdateScene(*(test_context->scene_update_context)); - test_context->session->Flush(); + test_context->mock_session_wrapper->Present(); // Run loop until idle, so that the Session receives and processes // its method calls. @@ -784,7 +740,7 @@ TEST_F(FuchsiaLayerTest, DISABLED_OpacityAndTransformLayer) { // commands against the list above. root->UpdateScene(*(test_context->scene_update_context)); - test_context->session->Flush(); + test_context->mock_session_wrapper->Present(); // Run loop until idle, so that the Session receives and processes // its method calls. diff --git a/flow/layers/layer.cc b/flow/layers/layer.cc index 97da04f7f54c8..490f123ec5f6f 100644 --- a/flow/layers/layer.cc +++ b/flow/layers/layer.cc @@ -9,10 +9,11 @@ namespace flutter { -Layer::Layer() +Layer::Layer(size_t external_size) : paint_bounds_(SkRect::MakeEmpty()), unique_id_(NextUniqueID()), - needs_system_composite_(false) {} + needs_system_composite_(false), + external_size_(external_size) {} Layer::~Layer() = default; @@ -58,6 +59,9 @@ Layer::AutoPrerollSaveLayerState::~AutoPrerollSaveLayerState() { #if defined(LEGACY_FUCHSIA_EMBEDDER) void Layer::CheckForChildLayerBelow(PrerollContext* context) { + // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), + // PhysicalShapeLayers that appear above the embedded content will be turned + // into their own Scenic layers. child_layer_exists_below_ = context->child_scene_layer_exists_below; if (child_layer_exists_below_) { set_needs_system_composite(true); @@ -65,42 +69,14 @@ void Layer::CheckForChildLayerBelow(PrerollContext* context) { } void Layer::UpdateScene(SceneUpdateContext& context) { - // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), - // PhysicalShapeLayers that appear above the embedded content will be turned - // into their own Scenic layers. - if (child_layer_exists_below_) { - float global_scenic_elevation = - context.GetGlobalElevationForNextScenicLayer(); - float local_scenic_elevation = - global_scenic_elevation - context.scenic_elevation(); - float z_translation = -local_scenic_elevation; - - // Retained rendering: speedup by reusing a retained entity node if - // possible. When an entity node is reused, no paint layer is added to the - // frame so we won't call PhysicalShapeLayer::Paint. - LayerRasterCacheKey key(unique_id(), context.Matrix()); - if (context.HasRetainedNode(key)) { - TRACE_EVENT_INSTANT0("flutter", "retained layer cache hit"); - scenic::EntityNode* retained_node = context.GetRetainedNode(key); - FML_DCHECK(context.top_entity()); - FML_DCHECK(retained_node->session() == context.session()); - - // Re-adjust the elevation. - retained_node->SetTranslation(0.f, 0.f, z_translation); - - context.top_entity()->entity_node().AddChild(*retained_node); - return; - } - - TRACE_EVENT_INSTANT0("flutter", "cache miss, creating"); - // If we can't find an existing retained surface, create one. - SceneUpdateContext::Frame frame( - context, SkRRect::MakeRect(paint_bounds()), SK_ColorTRANSPARENT, - SkScalarRoundToInt(context.alphaf() * 255), - "flutter::PhysicalShapeLayer", z_translation, this); - - frame.AddPaintLayer(this); - } + FML_DCHECK(needs_system_composite()); + FML_DCHECK(child_layer_exists_below_); + + SceneUpdateContext::Frame frame( + context, SkRRect::MakeRect(paint_bounds()), SK_ColorTRANSPARENT, + SkScalarRoundToInt(context.alphaf() * 255), "flutter::Layer"); + + frame.AddPaintLayer(this); } #endif diff --git a/flow/layers/layer.h b/flow/layers/layer.h index b22a322a73422..5e75b3937ca18 100644 --- a/flow/layers/layer.h +++ b/flow/layers/layer.h @@ -56,14 +56,10 @@ struct PrerollContext { const Stopwatch& ui_time; TextureRegistry& texture_registry; const bool checkerboard_offscreen_layers; - - // These allow us to make use of the scene metrics during Preroll. - float frame_physical_depth; - float frame_device_pixel_ratio; + const float frame_device_pixel_ratio; // These allow us to track properties like elevation, opacity, and the // prescence of a platform view during Preroll. - float total_elevation = 0.0f; bool has_platform_view = false; bool is_opaque = true; #if defined(LEGACY_FUCHSIA_EMBEDDER) @@ -71,13 +67,14 @@ struct PrerollContext { // Informs whether a layer needs to be system composited. bool child_scene_layer_exists_below = false; #endif + size_t uncached_external_size = 0; }; // Represents a single composited layer. Created on the UI thread but then // subquently used on the Rasterizer thread. class Layer { public: - Layer(); + Layer(size_t external_size = 0); virtual ~Layer(); virtual void Preroll(PrerollContext* context, const SkMatrix& matrix); @@ -128,10 +125,7 @@ class Layer { TextureRegistry& texture_registry; const RasterCache* raster_cache; const bool checkerboard_offscreen_layers; - - // These allow us to make use of the scene metrics during Paint. - float frame_physical_depth; - float frame_device_pixel_ratio; + const float frame_device_pixel_ratio; }; // Calls SkCanvas::saveLayer and restores the layer upon destruction. Also @@ -185,6 +179,8 @@ class Layer { uint64_t unique_id() const { return unique_id_; } + size_t external_size() const { return external_size_; } + protected: #if defined(LEGACY_FUCHSIA_EMBEDDER) bool child_layer_exists_below_ = false; @@ -194,6 +190,7 @@ class Layer { SkRect paint_bounds_; uint64_t unique_id_; bool needs_system_composite_; + size_t external_size_ = 0; static uint64_t NextUniqueID(); diff --git a/flow/layers/layer_tree.cc b/flow/layers/layer_tree.cc index c8778630fc1b4..160c8c50e1024 100644 --- a/flow/layers/layer_tree.cc +++ b/flow/layers/layer_tree.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/layers/layer_tree.h" @@ -12,18 +11,19 @@ namespace flutter { -LayerTree::LayerTree(const SkISize& frame_size, - float frame_physical_depth, - float frame_device_pixel_ratio) +LayerTree::LayerTree(const SkISize& frame_size, float device_pixel_ratio) : frame_size_(frame_size), - frame_physical_depth_(frame_physical_depth), - frame_device_pixel_ratio_(frame_device_pixel_ratio), + device_pixel_ratio_(device_pixel_ratio), rasterizer_tracing_threshold_(0), checkerboard_raster_cache_images_(false), - checkerboard_offscreen_layers_(false) {} + checkerboard_offscreen_layers_(false) { + FML_CHECK(device_pixel_ratio_ != 0.0f); +} -void LayerTree::RecordBuildTime(fml::TimePoint build_start, +void LayerTree::RecordBuildTime(fml::TimePoint vsync_start, + fml::TimePoint build_start, fml::TimePoint target_time) { + vsync_start_ = vsync_start; build_start_ = build_start; target_time_ = target_time; build_finish_ = fml::TimePoint::Now(); @@ -55,32 +55,22 @@ bool LayerTree::Preroll(CompositorContext::ScopedFrame& frame, frame.context().ui_time(), frame.context().texture_registry(), checkerboard_offscreen_layers_, - frame_physical_depth_, - frame_device_pixel_ratio_}; + device_pixel_ratio_}; root_layer_->Preroll(&context, frame.root_surface_transformation()); + frame.add_external_size(context.uncached_external_size); return context.surface_needs_readback; } #if defined(LEGACY_FUCHSIA_EMBEDDER) -void LayerTree::UpdateScene(SceneUpdateContext& context, - scenic::ContainerNode& container) { +void LayerTree::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "LayerTree::UpdateScene"); - // Ensure the context is aware of the view metrics. - context.set_dimensions(frame_size_, frame_physical_depth_, - frame_device_pixel_ratio_); - - const auto& metrics = context.metrics(); - FML_DCHECK(metrics->scale_x > 0.0f); - FML_DCHECK(metrics->scale_y > 0.0f); - FML_DCHECK(metrics->scale_z > 0.0f); + // Reset for a new Scene. + context.Reset(); - SceneUpdateContext::Transform transform(context, // context - 1.0f / metrics->scale_x, // X - 1.0f / metrics->scale_y, // Y - 1.0f / metrics->scale_z // Z - ); + const float inv_dpr = 1.0f / device_pixel_ratio_; + SceneUpdateContext::Transform transform(context, inv_dpr, inv_dpr, 1.0f); SceneUpdateContext::Frame frame( context, @@ -93,7 +83,7 @@ void LayerTree::UpdateScene(SceneUpdateContext& context, if (root_layer_->needs_painting()) { frame.AddPaintLayer(root_layer_.get()); } - container.AddChild(transform.entity_node()); + context.root_node().AddChild(transform.entity_node()); } #endif @@ -117,7 +107,7 @@ void LayerTree::Paint(CompositorContext::ScopedFrame& frame, } Layer::PaintContext context = { - (SkCanvas*)&internal_nodes_canvas, + static_cast(&internal_nodes_canvas), frame.canvas(), frame.gr_context(), frame.view_embedder(), @@ -126,11 +116,11 @@ void LayerTree::Paint(CompositorContext::ScopedFrame& frame, frame.context().texture_registry(), ignore_raster_cache ? nullptr : &frame.context().raster_cache(), checkerboard_offscreen_layers_, - frame_physical_depth_, - frame_device_pixel_ratio_}; + device_pixel_ratio_}; - if (root_layer_->needs_painting()) + if (root_layer_->needs_painting()) { root_layer_->Paint(context); + } } sk_sp LayerTree::Flatten(const SkRect& bounds) { @@ -151,19 +141,18 @@ sk_sp LayerTree::Flatten(const SkRect& bounds) { root_surface_transformation.reset(); PrerollContext preroll_context{ - nullptr, // raster_cache (don't consult the cache) - nullptr, // gr_context (used for the raster cache) - nullptr, // external view embedder - unused_stack, // mutator stack - nullptr, // SkColorSpace* dst_color_space - kGiantRect, // SkRect cull_rect - false, // layer reads from surface - unused_stopwatch, // frame time (dont care) - unused_stopwatch, // engine time (dont care) - unused_texture_registry, // texture registry (not supported) - false, // checkerboard_offscreen_layers - frame_physical_depth_, // maximum depth allowed for rendering - frame_device_pixel_ratio_ // ratio between logical and physical + nullptr, // raster_cache (don't consult the cache) + nullptr, // gr_context (used for the raster cache) + nullptr, // external view embedder + unused_stack, // mutator stack + nullptr, // SkColorSpace* dst_color_space + kGiantRect, // SkRect cull_rect + false, // layer reads from surface + unused_stopwatch, // frame time (dont care) + unused_stopwatch, // engine time (dont care) + unused_texture_registry, // texture registry (not supported) + false, // checkerboard_offscreen_layers + device_pixel_ratio_ // ratio between logical and physical }; SkISize canvas_size = canvas->getBaseLayerSize(); @@ -171,17 +160,16 @@ sk_sp LayerTree::Flatten(const SkRect& bounds) { internal_nodes_canvas.addCanvas(canvas); Layer::PaintContext paint_context = { - (SkCanvas*)&internal_nodes_canvas, + static_cast(&internal_nodes_canvas), canvas, // canvas nullptr, nullptr, - unused_stopwatch, // frame time (dont care) - unused_stopwatch, // engine time (dont care) - unused_texture_registry, // texture registry (not supported) - nullptr, // raster cache - false, // checkerboard offscreen layers - frame_physical_depth_, // maximum depth allowed for rendering - frame_device_pixel_ratio_ // ratio between logical and physical + unused_stopwatch, // frame time (dont care) + unused_stopwatch, // engine time (dont care) + unused_texture_registry, // texture registry (not supported) + nullptr, // raster cache + false, // checkerboard offscreen layers + device_pixel_ratio_ // ratio between logical and physical }; // Even if we don't have a root layer, we still need to create an empty diff --git a/flow/layers/layer_tree.h b/flow/layers/layer_tree.h index 733284afe65db..81423c353d458 100644 --- a/flow/layers/layer_tree.h +++ b/flow/layers/layer_tree.h @@ -20,9 +20,7 @@ namespace flutter { class LayerTree { public: - LayerTree(const SkISize& frame_size, - float frame_physical_depth, - float frame_device_pixel_ratio); + LayerTree(const SkISize& frame_size, float device_pixel_ratio); // Perform a preroll pass on the tree and return information about // the tree that affects rendering this frame. @@ -35,8 +33,7 @@ class LayerTree { bool ignore_raster_cache = false); #if defined(LEGACY_FUCHSIA_EMBEDDER) - void UpdateScene(SceneUpdateContext& context, - scenic::ContainerNode& container); + void UpdateScene(SceneUpdateContext& context); #endif void Paint(CompositorContext::ScopedFrame& frame, @@ -51,10 +48,13 @@ class LayerTree { } const SkISize& frame_size() const { return frame_size_; } - float frame_physical_depth() const { return frame_physical_depth_; } - float frame_device_pixel_ratio() const { return frame_device_pixel_ratio_; } + float device_pixel_ratio() const { return device_pixel_ratio_; } - void RecordBuildTime(fml::TimePoint build_start, fml::TimePoint target_time); + void RecordBuildTime(fml::TimePoint vsync_start, + fml::TimePoint build_start, + fml::TimePoint target_time); + fml::TimePoint vsync_start() const { return vsync_start_; } + fml::TimeDelta vsync_overhead() const { return build_start_ - vsync_start_; } fml::TimePoint build_start() const { return build_start_; } fml::TimePoint build_finish() const { return build_finish_; } fml::TimeDelta build_time() const { return build_finish_ - build_start_; } @@ -79,16 +79,14 @@ class LayerTree { checkerboard_offscreen_layers_ = checkerboard; } - double device_pixel_ratio() const { return frame_device_pixel_ratio_; } - private: std::shared_ptr root_layer_; + fml::TimePoint vsync_start_; fml::TimePoint build_start_; fml::TimePoint build_finish_; fml::TimePoint target_time_; SkISize frame_size_ = SkISize::MakeEmpty(); // Physical pixels. - float frame_physical_depth_; - float frame_device_pixel_ratio_ = 1.0f; // Logical / Physical pixels ratio. + const float device_pixel_ratio_; // Logical / Physical pixels ratio. uint32_t rasterizer_tracing_threshold_; bool checkerboard_raster_cache_images_; bool checkerboard_offscreen_layers_; diff --git a/flow/layers/layer_tree_unittests.cc b/flow/layers/layer_tree_unittests.cc index 1215b72f78726..99231f8254edd 100644 --- a/flow/layers/layer_tree_unittests.cc +++ b/flow/layers/layer_tree_unittests.cc @@ -15,11 +15,11 @@ namespace flutter { namespace testing { -class LayerTreeTest : public CanvasTest { +class LayerTreeTest : public CanvasTest, public CompositorContext::Delegate { public: LayerTreeTest() - : layer_tree_(SkISize::Make(64, 64), 100.0f, 1.0f), - compositor_context_(fml::kDefaultFrameBudget), + : layer_tree_(SkISize::Make(64, 64), 1.0f), + compositor_context_(*this), root_transform_(SkMatrix::Translate(1.0f, 1.0f)), scoped_frame_(compositor_context_.AcquireFrame(nullptr, &mock_canvas(), @@ -33,11 +33,24 @@ class LayerTreeTest : public CanvasTest { CompositorContext::ScopedFrame& frame() { return *scoped_frame_.get(); } const SkMatrix& root_transform() { return root_transform_; } + // |CompositorContext::Delegate| + void OnCompositorEndFrame(size_t freed_hint) override { + last_freed_hint_ = freed_hint; + } + + // |CompositorContext::Delegate| + fml::Milliseconds GetFrameBudget() override { + return fml::kDefaultFrameBudget; + } + + size_t last_freed_hint() { return last_freed_hint_; } + private: LayerTree layer_tree_; CompositorContext compositor_context_; SkMatrix root_transform_; std::unique_ptr scoped_frame_; + size_t last_freed_hint_ = 0; }; TEST_F(LayerTreeTest, PaintingEmptyLayerDies) { diff --git a/flow/layers/opacity_layer.h b/flow/layers/opacity_layer.h index 73e508f854bc4..ed5f0283ad356 100644 --- a/flow/layers/opacity_layer.h +++ b/flow/layers/opacity_layer.h @@ -38,7 +38,6 @@ class OpacityLayer : public MergedContainerLayer { private: SkAlpha alpha_; SkPoint offset_; - SkRRect frameRRect_; FML_DISALLOW_COPY_AND_ASSIGN(OpacityLayer); }; diff --git a/flow/layers/performance_overlay_layer.cc b/flow/layers/performance_overlay_layer.cc index 3aac5a54d9a72..05ade5e21af73 100644 --- a/flow/layers/performance_overlay_layer.cc +++ b/flow/layers/performance_overlay_layer.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include #include @@ -14,7 +13,7 @@ namespace flutter { namespace { -void VisualizeStopWatch(SkCanvas& canvas, +void VisualizeStopWatch(SkCanvas* canvas, const Stopwatch& stopwatch, SkScalar x, SkScalar y, @@ -37,7 +36,7 @@ void VisualizeStopWatch(SkCanvas& canvas, stopwatch, label_prefix, font_path); SkPaint paint; paint.setColor(SK_ColorGRAY); - canvas.drawTextBlob(text, x + label_x, y + height + label_y, paint); + canvas->drawTextBlob(text, x + label_x, y + height + label_y, paint); } } @@ -77,8 +76,9 @@ PerformanceOverlayLayer::PerformanceOverlayLayer(uint64_t options, void PerformanceOverlayLayer::Paint(PaintContext& context) const { const int padding = 8; - if (!options_) + if (!options_) { return; + } TRACE_EVENT0("flutter", "PerformanceOverlayLayer::Paint"); SkScalar x = paint_bounds().x() + padding; @@ -88,11 +88,11 @@ void PerformanceOverlayLayer::Paint(PaintContext& context) const { SkAutoCanvasRestore save(context.leaf_nodes_canvas, true); VisualizeStopWatch( - *context.leaf_nodes_canvas, context.raster_time, x, y, width, + context.leaf_nodes_canvas, context.raster_time, x, y, width, height - padding, options_ & kVisualizeRasterizerStatistics, options_ & kDisplayRasterizerStatistics, "Raster", font_path_); - VisualizeStopWatch(*context.leaf_nodes_canvas, context.ui_time, x, y + height, + VisualizeStopWatch(context.leaf_nodes_canvas, context.ui_time, x, y + height, width, height - padding, options_ & kVisualizeEngineStatistics, options_ & kDisplayEngineStatistics, "UI", font_path_); diff --git a/flow/layers/physical_shape_layer.cc b/flow/layers/physical_shape_layer.cc index 4f87fb23605a0..7ba2b7cb734ea 100644 --- a/flow/layers/physical_shape_layer.cc +++ b/flow/layers/physical_shape_layer.cc @@ -21,28 +21,7 @@ PhysicalShapeLayer::PhysicalShapeLayer(SkColor color, shadow_color_(shadow_color), elevation_(elevation), path_(path), - isRect_(false), - clip_behavior_(clip_behavior) { - SkRect rect; - if (path.isRect(&rect)) { - isRect_ = true; - frameRRect_ = SkRRect::MakeRect(rect); - } else if (path.isRRect(&frameRRect_)) { - isRect_ = frameRRect_.isRect(); - } else if (path.isOval(&rect)) { - // isRRect returns false for ovals, so we need to explicitly check isOval - // as well. - frameRRect_ = SkRRect::MakeOval(rect); - } else { - // Scenic currently doesn't provide an easy way to create shapes from - // arbitrary paths. - // For shapes that cannot be represented as a rounded rectangle we - // default to use the bounding rectangle. - // TODO(amirh): fix this once we have a way to create a Scenic shape from - // an SkPath. - frameRRect_ = SkRRect::MakeRect(path.getBounds()); - } -} + clip_behavior_(clip_behavior) {} void PhysicalShapeLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { @@ -50,14 +29,9 @@ void PhysicalShapeLayer::Preroll(PrerollContext* context, Layer::AutoPrerollSaveLayerState save = Layer::AutoPrerollSaveLayerState::Create(context, UsesSaveLayer()); - context->total_elevation += elevation_; - total_elevation_ = context->total_elevation; - SkRect child_paint_bounds; PrerollChildren(context, matrix, &child_paint_bounds); - context->total_elevation -= elevation_; - if (elevation_ == 0) { set_paint_bounds(path_.getBounds()); } else { diff --git a/flow/layers/physical_shape_layer.h b/flow/layers/physical_shape_layer.h index 2c04368e9a81e..ce49af1a003ae 100644 --- a/flow/layers/physical_shape_layer.h +++ b/flow/layers/physical_shape_layer.h @@ -35,16 +35,13 @@ class PhysicalShapeLayer : public ContainerLayer { return clip_behavior_ == Clip::antiAliasWithSaveLayer; } - float total_elevation() const { return total_elevation_; } + float elevation() const { return elevation_; } private: SkColor color_; SkColor shadow_color_; float elevation_ = 0.0f; - float total_elevation_ = 0.0f; SkPath path_; - bool isRect_; - SkRRect frameRRect_; Clip clip_behavior_; }; diff --git a/flow/layers/physical_shape_layer_unittests.cc b/flow/layers/physical_shape_layer_unittests.cc index 7ad0b4e5eddcb..bb5d0acfad757 100644 --- a/flow/layers/physical_shape_layer_unittests.cc +++ b/flow/layers/physical_shape_layer_unittests.cc @@ -131,7 +131,7 @@ TEST_F(PhysicalShapeLayerTest, ElevationSimple) { initial_elevation, 1.0f)); EXPECT_TRUE(layer->needs_painting()); EXPECT_FALSE(layer->needs_system_composite()); - EXPECT_EQ(layer->total_elevation(), initial_elevation); + EXPECT_EQ(layer->elevation(), initial_elevation); // The Fuchsia system compositor handles all elevated PhysicalShapeLayers and // their shadows , so we do not use the direct |Paint()| path there. @@ -162,7 +162,6 @@ TEST_F(PhysicalShapeLayerTest, ElevationComplex) { // | // layers[1] + 2.0f = 3.0f constexpr float initial_elevations[4] = {1.0f, 2.0f, 3.0f, 4.0f}; - constexpr float total_elevations[4] = {1.0f, 3.0f, 4.0f, 8.0f}; SkPath layer_path; layer_path.addRect(0, 0, 80, 80).close(); @@ -187,7 +186,6 @@ TEST_F(PhysicalShapeLayerTest, ElevationComplex) { 1.0f /* pixel_ratio */))); EXPECT_TRUE(layers[i]->needs_painting()); EXPECT_FALSE(layers[i]->needs_system_composite()); - EXPECT_EQ(layers[i]->total_elevation(), total_elevations[i]); } // The Fuchsia system compositor handles all elevated PhysicalShapeLayers and diff --git a/flow/layers/picture_layer.cc b/flow/layers/picture_layer.cc index d5d6a34b573e8..067a59d782398 100644 --- a/flow/layers/picture_layer.cc +++ b/flow/layers/picture_layer.cc @@ -11,8 +11,10 @@ namespace flutter { PictureLayer::PictureLayer(const SkPoint& offset, SkiaGPUObject picture, bool is_complex, - bool will_change) - : offset_(offset), + bool will_change, + size_t external_size) + : Layer(external_size), + offset_(offset), picture_(std::move(picture)), is_complex_(is_complex), will_change_(will_change) {} @@ -26,6 +28,7 @@ void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { SkPicture* sk_picture = picture(); + bool cached = false; if (auto* cache = context->raster_cache) { TRACE_EVENT0("flutter", "PictureLayer::RasterCache (Preroll)"); @@ -34,8 +37,13 @@ void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { #ifndef SUPPORT_FRACTIONAL_TRANSLATION ctm = RasterCache::GetIntegralTransCTM(ctm); #endif - cache->Prepare(context->gr_context, sk_picture, ctm, - context->dst_color_space, is_complex_, will_change_); + cached = cache->Prepare(context->gr_context, sk_picture, ctm, + context->dst_color_space, is_complex_, will_change_, + external_size()); + } + + if (!cached) { + context->uncached_external_size += external_size(); } SkRect bounds = sk_picture->cullRect().makeOffset(offset_.x(), offset_.y()); diff --git a/flow/layers/picture_layer.h b/flow/layers/picture_layer.h index e733e7455ca6c..c86361a9aaaae 100644 --- a/flow/layers/picture_layer.h +++ b/flow/layers/picture_layer.h @@ -18,7 +18,8 @@ class PictureLayer : public Layer { PictureLayer(const SkPoint& offset, SkiaGPUObject picture, bool is_complex, - bool will_change); + bool will_change, + size_t external_size); SkPicture* picture() const { return picture_.get().get(); } diff --git a/flow/layers/picture_layer_unittests.cc b/flow/layers/picture_layer_unittests.cc index dc9e6080c1508..b7bfce854ed82 100644 --- a/flow/layers/picture_layer_unittests.cc +++ b/flow/layers/picture_layer_unittests.cc @@ -24,7 +24,7 @@ using PictureLayerTest = SkiaGPUObjectLayerTest; TEST_F(PictureLayerTest, PaintBeforePrerollInvalidPictureDies) { const SkPoint layer_offset = SkPoint::Make(0.0f, 0.0f); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(), false, false); + layer_offset, SkiaGPUObject(), false, false, 0); EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), "picture_\\.get\\(\\)"); @@ -35,7 +35,8 @@ TEST_F(PictureLayerTest, PaintBeforePreollDies) { const SkRect picture_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f); auto mock_picture = SkPicture::MakePlaceholder(picture_bounds); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false); + layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false, + 0); EXPECT_EQ(layer->paint_bounds(), SkRect::MakeEmpty()); EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), @@ -47,7 +48,8 @@ TEST_F(PictureLayerTest, PaintingEmptyLayerDies) { const SkRect picture_bounds = SkRect::MakeEmpty(); auto mock_picture = SkPicture::MakePlaceholder(picture_bounds); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false); + layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false, + 0); layer->Preroll(preroll_context(), SkMatrix()); EXPECT_EQ(layer->paint_bounds(), SkRect::MakeEmpty()); @@ -62,7 +64,7 @@ TEST_F(PictureLayerTest, PaintingEmptyLayerDies) { TEST_F(PictureLayerTest, InvalidPictureDies) { const SkPoint layer_offset = SkPoint::Make(0.0f, 0.0f); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(), false, false); + layer_offset, SkiaGPUObject(), false, false, 0); // Crashes reading a nullptr. EXPECT_DEATH_IF_SUPPORTED(layer->Preroll(preroll_context(), SkMatrix()), ""); @@ -75,7 +77,10 @@ TEST_F(PictureLayerTest, SimplePicture) { const SkRect picture_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f); auto mock_picture = SkPicture::MakePlaceholder(picture_bounds); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false); + layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false, + 1000); + + EXPECT_EQ(layer->external_size(), 1000ul); layer->Preroll(preroll_context(), SkMatrix()); EXPECT_EQ(layer->paint_bounds(), diff --git a/flow/layers/platform_view_layer.cc b/flow/layers/platform_view_layer.cc index 0bd6ee7f6dfab..80514b5213e18 100644 --- a/flow/layers/platform_view_layer.cc +++ b/flow/layers/platform_view_layer.cc @@ -48,7 +48,9 @@ void PlatformViewLayer::Paint(PaintContext& context) const { #if defined(LEGACY_FUCHSIA_EMBEDDER) void PlatformViewLayer::UpdateScene(SceneUpdateContext& context) { - context.UpdateScene(view_id_, offset_, size_); + TRACE_EVENT0("flutter", "PlatformViewLayer::UpdateScene"); + FML_DCHECK(needs_system_composite()); + context.UpdateView(view_id_, offset_, size_); } #endif diff --git a/flow/layers/transform_layer.cc b/flow/layers/transform_layer.cc index d01c21950e498..8fe5dd32e1e85 100644 --- a/flow/layers/transform_layer.cc +++ b/flow/layers/transform_layer.cc @@ -4,6 +4,8 @@ #include "flutter/flow/layers/transform_layer.h" +#include + namespace flutter { TransformLayer::TransformLayer(const SkMatrix& transform) @@ -56,12 +58,12 @@ void TransformLayer::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "TransformLayer::UpdateScene"); FML_DCHECK(needs_system_composite()); + std::optional transform; if (!transform_.isIdentity()) { - SceneUpdateContext::Transform transform(context, transform_); - UpdateSceneChildren(context); - } else { - UpdateSceneChildren(context); + transform.emplace(context, transform_); } + + UpdateSceneChildren(context); } #endif diff --git a/flow/matrix_decomposition.cc b/flow/matrix_decomposition.cc index 3d3cb9e969cd9..2207fa35fb878 100644 --- a/flow/matrix_decomposition.cc +++ b/flow/matrix_decomposition.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/matrix_decomposition.h" @@ -18,12 +17,12 @@ MatrixDecomposition::MatrixDecomposition(const SkMatrix& matrix) : MatrixDecomposition(SkM44{matrix}) {} // Use custom normalize to avoid skia precision loss/normalize() privatization. -static inline void SkV3Normalize(SkV3& v) { - double mag = sqrt(v.x * v.x + v.y * v.y + v.z * v.z); +static inline void SkV3Normalize(SkV3* v) { + double mag = sqrt(v->x * v->x + v->y * v->y + v->z * v->z); double scale = 1.0 / mag; - v.x *= scale; - v.y *= scale; - v.z *= scale; + v->x *= scale; + v->y *= scale; + v->z *= scale; } MatrixDecomposition::MatrixDecomposition(SkM44 matrix) : valid_(false) { @@ -71,14 +70,14 @@ MatrixDecomposition::MatrixDecomposition(SkM44 matrix) : valid_(false) { scale_.x = row[0].length(); - SkV3Normalize(row[0]); + SkV3Normalize(&row[0]); shear_.x = row[0].dot(row[1]); row[1] = SkV3Combine(row[1], 1.0, row[0], -shear_.x); scale_.y = row[1].length(); - SkV3Normalize(row[1]); + SkV3Normalize(&row[1]); shear_.x /= scale_.y; @@ -89,7 +88,7 @@ MatrixDecomposition::MatrixDecomposition(SkM44 matrix) : valid_(false) { scale_.z = row[2].length(); - SkV3Normalize(row[2]); + SkV3Normalize(&row[2]); shear_.y /= scale_.z; shear_.z /= scale_.z; diff --git a/flow/matrix_decomposition_unittests.cc b/flow/matrix_decomposition_unittests.cc index f3c6a46c1f985..dc95033e43898 100644 --- a/flow/matrix_decomposition_unittests.cc +++ b/flow/matrix_decomposition_unittests.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/fml/build_config.h" @@ -98,7 +97,8 @@ TEST(MatrixDecomposition, Combination) { TEST(MatrixDecomposition, ScaleFloatError) { constexpr float scale_increment = 0.00001f; - for (float scale = 0.0001f; scale < 2.0f; scale += scale_increment) { + float scale = 0.0001f; + while (scale < 2.0f) { SkM44 matrix; matrix.setScale(scale, scale, 1.0f); @@ -111,11 +111,12 @@ TEST(MatrixDecomposition, ScaleFloatError) { ASSERT_FLOAT_EQ(0, decomposition3.rotation().x); ASSERT_FLOAT_EQ(0, decomposition3.rotation().y); ASSERT_FLOAT_EQ(0, decomposition3.rotation().z); + scale += scale_increment; } SkM44 matrix; - const auto scale = 1.7734375f; - matrix.setScale(scale, scale, 1.f); + const auto scale1 = 1.7734375f; + matrix.setScale(scale1, scale1, 1.f); // Bug upper bound (empirical) const auto scale2 = 1.773437559603f; @@ -136,8 +137,8 @@ TEST(MatrixDecomposition, ScaleFloatError) { flutter::MatrixDecomposition decomposition3(matrix3); ASSERT_TRUE(decomposition3.IsValid()); - ASSERT_FLOAT_EQ(scale, decomposition.scale().x); - ASSERT_FLOAT_EQ(scale, decomposition.scale().y); + ASSERT_FLOAT_EQ(scale1, decomposition.scale().x); + ASSERT_FLOAT_EQ(scale1, decomposition.scale().y); ASSERT_FLOAT_EQ(1.f, decomposition.scale().z); ASSERT_FLOAT_EQ(0, decomposition.rotation().x); ASSERT_FLOAT_EQ(0, decomposition.rotation().y); diff --git a/flow/paint_utils.cc b/flow/paint_utils.cc index b19cd02cfe216..38fc17979a0c3 100644 --- a/flow/paint_utils.cc +++ b/flow/paint_utils.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/paint_utils.h" diff --git a/flow/raster_cache.cc b/flow/raster_cache.cc index c9fcad4a34b49..f077d215a28d4 100644 --- a/flow/raster_cache.cc +++ b/flow/raster_cache.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/raster_cache.h" @@ -142,6 +141,7 @@ void RasterCache::Prepare(PrerollContext* context, Entry& entry = layer_cache_[cache_key]; entry.access_count++; entry.used_this_frame = true; + entry.external_size = layer->external_size(); if (!entry.image) { entry.image = RasterizeLayer(context, layer, ctm, checkerboard_images_); } @@ -160,16 +160,16 @@ std::unique_ptr RasterCache::RasterizeLayer( canvas_size.height()); internal_nodes_canvas.addCanvas(canvas); Layer::PaintContext paintContext = { - (SkCanvas*)&internal_nodes_canvas, // internal_nodes_canvas - canvas, // leaf_nodes_canvas - context->gr_context, // gr_context - nullptr, // view_embedder + /* internal_nodes_canvas= */ static_cast( + &internal_nodes_canvas), + /* leaf_nodes_canvas= */ canvas, + /* gr_context= */ context->gr_context, + /* view_embedder= */ nullptr, context->raster_time, context->ui_time, context->texture_registry, context->has_platform_view ? nullptr : context->raster_cache, context->checkerboard_offscreen_layers, - context->frame_physical_depth, context->frame_device_pixel_ratio}; if (layer->needs_painting()) { layer->Paint(paintContext); @@ -182,7 +182,8 @@ bool RasterCache::Prepare(GrDirectContext* context, const SkMatrix& transformation_matrix, SkColorSpace* dst_color_space, bool is_complex, - bool will_change) { + bool will_change, + size_t external_size) { // Disabling caching when access_threshold is zero is historic behavior. if (access_threshold_ == 0) { return false; @@ -208,6 +209,7 @@ bool RasterCache::Prepare(GrDirectContext* context, // Creates an entry, if not present prior. Entry& entry = picture_cache_[cache_key]; + entry.external_size = external_size; if (entry.access_count < access_threshold_) { // Frame threshold has not yet been reached. return false; @@ -233,7 +235,7 @@ bool RasterCache::Draw(const SkPicture& picture, SkCanvas& canvas) const { entry.used_this_frame = true; if (entry.image) { - entry.image->draw(canvas); + entry.image->draw(canvas, nullptr); return true; } @@ -261,11 +263,12 @@ bool RasterCache::Draw(const Layer* layer, return false; } -void RasterCache::SweepAfterFrame() { - SweepOneCacheAfterFrame(picture_cache_); - SweepOneCacheAfterFrame(layer_cache_); +size_t RasterCache::SweepAfterFrame() { + size_t removed_size = SweepOneCacheAfterFrame(picture_cache_); + removed_size += SweepOneCacheAfterFrame(layer_cache_); picture_cached_this_frame_ = 0; TraceStatsToTimeline(); + return removed_size; } void RasterCache::Clear() { @@ -299,35 +302,34 @@ void RasterCache::SetCheckboardCacheImages(bool checkerboard) { void RasterCache::TraceStatsToTimeline() const { #if !FLUTTER_RELEASE + constexpr double kMegaBytes = (1 << 20); + FML_TRACE_COUNTER("flutter", "RasterCache", reinterpret_cast(this), + "LayerCount", layer_cache_.size(), "LayerMBytes", + EstimateLayerCacheByteSize() / kMegaBytes, "PictureCount", + picture_cache_.size(), "PictureMBytes", + EstimatePictureCacheByteSize() / kMegaBytes); - size_t layer_cache_count = 0; - size_t layer_cache_bytes = 0; - size_t picture_cache_count = 0; - size_t picture_cache_bytes = 0; +#endif // !FLUTTER_RELEASE +} +size_t RasterCache::EstimateLayerCacheByteSize() const { + size_t layer_cache_bytes = 0; for (const auto& item : layer_cache_) { - layer_cache_count++; if (item.second.image) { layer_cache_bytes += item.second.image->image_bytes(); } } + return layer_cache_bytes; +} +size_t RasterCache::EstimatePictureCacheByteSize() const { + size_t picture_cache_bytes = 0; for (const auto& item : picture_cache_) { - picture_cache_count++; if (item.second.image) { picture_cache_bytes += item.second.image->image_bytes(); } } - - FML_TRACE_COUNTER("flutter", "RasterCache", - reinterpret_cast(this), // - "LayerCount", layer_cache_count, // - "LayerMBytes", layer_cache_bytes * 1e-6, // - "PictureCount", picture_cache_count, // - "PictureMBytes", picture_cache_bytes * 1e-6 // - ); - -#endif // !FLUTTER_RELEASE + return picture_cache_bytes; } } // namespace flutter diff --git a/flow/raster_cache.h b/flow/raster_cache.h index d71b4e2ff9aed..901757974abc1 100644 --- a/flow/raster_cache.h +++ b/flow/raster_cache.h @@ -22,16 +22,14 @@ class RasterCacheResult { virtual ~RasterCacheResult() = default; - virtual void draw(SkCanvas& canvas, const SkPaint* paint = nullptr) const; + virtual void draw(SkCanvas& canvas, const SkPaint* paint) const; virtual SkISize image_dimensions() const { return image_ ? image_->dimensions() : SkISize::Make(0, 0); }; virtual int64_t image_bytes() const { - return image_ ? image_->dimensions().area() * - image_->imageInfo().bytesPerPixel() - : 0; + return image_ ? image_->imageInfo().computeMinByteSize() : 0; }; private: @@ -60,7 +58,7 @@ class RasterCache { * to be stored in the cache. * * @param picture the SkPicture object to be cached. - * @param context the GrContext used for rendering. + * @param context the GrDirectContext used for rendering. * @param ctm the transformation matrix used for rendering. * @param dst_color_space the destination color space that the cached * rendering will be drawn into @@ -139,7 +137,8 @@ class RasterCache { const SkMatrix& transformation_matrix, SkColorSpace* dst_color_space, bool is_complex, - bool will_change); + bool will_change, + size_t external_size = 0); void Prepare(PrerollContext* context, Layer* layer, const SkMatrix& ctm); @@ -158,7 +157,8 @@ class RasterCache { SkCanvas& canvas, SkPaint* paint = nullptr) const; - void SweepAfterFrame(); + /// Returns the amount of external bytes freed by the sweep. + size_t SweepAfterFrame(); void Clear(); @@ -170,21 +170,44 @@ class RasterCache { size_t GetPictureCachedEntriesCount() const; + /** + * @brief Estimate how much memory is used by picture raster cache entries in + * bytes. + * + * Only SkImage's memory usage is counted as other objects are often much + * smaller compared to SkImage. SkImageInfo::computeMinByteSize is used to + * estimate the SkImage memory usage. + */ + size_t EstimatePictureCacheByteSize() const; + + /** + * @brief Estimate how much memory is used by layer raster cache entries in + * bytes. + * + * Only SkImage's memory usage is counted as other objects are often much + * smaller compared to SkImage. SkImageInfo::computeMinByteSize is used to + * estimate the SkImage memory usage. + */ + size_t EstimateLayerCacheByteSize() const; + private: struct Entry { bool used_this_frame = false; size_t access_count = 0; + size_t external_size = 0; std::unique_ptr image; }; template - static void SweepOneCacheAfterFrame(Cache& cache) { + static size_t SweepOneCacheAfterFrame(Cache& cache) { std::vector dead; + size_t removed_size = 0; for (auto it = cache.begin(); it != cache.end(); ++it) { Entry& entry = it->second; if (!entry.used_this_frame) { dead.push_back(it); + removed_size += entry.external_size; } entry.used_this_frame = false; } @@ -192,6 +215,7 @@ class RasterCache { for (auto it : dead) { cache.erase(it); } + return removed_size; } const size_t access_threshold_; diff --git a/flow/rtree_unittests.cc b/flow/rtree_unittests.cc index d5c8466c1ca7d..1a1498efc8af3 100644 --- a/flow/rtree_unittests.cc +++ b/flow/rtree_unittests.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "rtree.h" @@ -12,7 +11,7 @@ namespace flutter { namespace testing { -TEST(RTree, searchNonOverlappingDrawnRects_NoIntersection) { +TEST(RTree, searchNonOverlappingDrawnRectsNoIntersection) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -32,7 +31,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_NoIntersection) { ASSERT_TRUE(hits.empty()); } -TEST(RTree, searchNonOverlappingDrawnRects_SingleRectIntersection) { +TEST(RTree, searchNonOverlappingDrawnRectsSingleRectIntersection) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -54,7 +53,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_SingleRectIntersection) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(120, 120, 160, 160)); } -TEST(RTree, searchNonOverlappingDrawnRects_IgnoresNonDrawingRecords) { +TEST(RTree, searchNonOverlappingDrawnRectsIgnoresNonDrawingRecords) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -82,7 +81,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_IgnoresNonDrawingRecords) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(120, 120, 180, 180)); } -TEST(RTree, searchNonOverlappingDrawnRects_MultipleRectIntersection) { +TEST(RTree, searchNonOverlappingDrawnRectsMultipleRectIntersection) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -113,7 +112,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_MultipleRectIntersection) { ASSERT_EQ(*std::next(hits.begin(), 1), SkRect::MakeLTRB(300, 100, 400, 200)); } -TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase1) { +TEST(RTree, searchNonOverlappingDrawnRectsJoinRectsWhenIntersectedCase1) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -147,7 +146,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase1) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(100, 100, 175, 175)); } -TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase2) { +TEST(RTree, searchNonOverlappingDrawnRectsJoinRectsWhenIntersectedCase2) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -188,7 +187,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase2) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(50, 50, 500, 250)); } -TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase3) { +TEST(RTree, searchNonOverlappingDrawnRectsJoinRectsWhenIntersectedCase3) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = diff --git a/flow/scene_update_context.cc b/flow/scene_update_context.cc index b0628edf0c417..3edd9c4d9fe26 100644 --- a/flow/scene_update_context.cc +++ b/flow/scene_update_context.cc @@ -4,6 +4,7 @@ #include "flutter/flow/scene_update_context.h" +#include #include #include "flutter/flow/layers/layer.h" @@ -13,10 +14,10 @@ #include "include/core/SkColor.h" namespace flutter { +namespace { -// Helper function to generate clip planes for a scenic::EntityNode. -static void SetEntityNodeClipPlanes(scenic::EntityNode& entity_node, - const SkRect& bounds) { +void SetEntityNodeClipPlanes(scenic::EntityNode& entity_node, + const SkRect& bounds) { const float top = bounds.top(); const float bottom = bounds.bottom(); const float left = bounds.left(); @@ -53,32 +54,78 @@ static void SetEntityNodeClipPlanes(scenic::EntityNode& entity_node, entity_node.SetClipPlanes(std::move(clip_planes)); } -SceneUpdateContext::SceneUpdateContext(scenic::Session* session, - SurfaceProducer* surface_producer) - : session_(session), surface_producer_(surface_producer) { - FML_DCHECK(surface_producer_ != nullptr); +void SetMaterialColor(scenic::Material& material, + SkColor color, + SkAlpha opacity) { + const SkAlpha color_alpha = static_cast( + ((float)SkColorGetA(color) * (float)opacity) / 255.0f); + material.SetColor(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color), + color_alpha); +} + +} // namespace + +SceneUpdateContext::SceneUpdateContext(std::string debug_label, + fuchsia::ui::views::ViewToken view_token, + scenic::ViewRefPair view_ref_pair, + SessionWrapper& session) + : session_(session), + root_view_(session_.get(), + std::move(view_token), + std::move(view_ref_pair.control_ref), + std::move(view_ref_pair.view_ref), + debug_label), + root_node_(session_.get()) { + root_view_.AddChild(root_node_); + root_node_.SetEventMask(fuchsia::ui::gfx::kMetricsEventMask); + + session_.Present(); +} + +std::vector SceneUpdateContext::GetPaintTasks() { + std::vector frame_paint_tasks = std::move(paint_tasks_); + + paint_tasks_.clear(); + + return frame_paint_tasks; +} + +void SceneUpdateContext::EnableWireframe(bool enable) { + session_.get()->Enqueue( + scenic::NewSetEnableDebugViewBoundsCmd(root_view_.id(), enable)); +} + +void SceneUpdateContext::Reset() { + paint_tasks_.clear(); + top_entity_ = nullptr; + top_scale_x_ = 1.f; + top_scale_y_ = 1.f; + top_elevation_ = 0.f; + next_elevation_ = 0.f; + alpha_ = 1.f; + + // We are going to be sending down a fresh node hierarchy every frame. So just + // enqueue a detach op on the imported root node. + session_.get()->Enqueue(scenic::NewDetachChildrenCmd(root_node_.id())); } -void SceneUpdateContext::CreateFrame(scenic::EntityNode entity_node, +void SceneUpdateContext::CreateFrame(scenic::EntityNode& entity_node, const SkRRect& rrect, SkColor color, SkAlpha opacity, const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer) { - FML_DCHECK(!rrect.isEmpty()); + std::vector paint_layers) { + // We don't need a shape if the frame is zero size. + if (rrect.isEmpty()) + return; // Frames always clip their children. SkRect shape_bounds = rrect.getBounds(); SetEntityNodeClipPlanes(entity_node, shape_bounds); - // and possibly for its texture. // TODO(SCN-137): Need to be able to express the radii as vectors. - scenic::ShapeNode shape_node(session()); - scenic::Rectangle shape(session_, // session - rrect.width(), // width - rrect.height() // height - ); + scenic::ShapeNode shape_node(session_.get()); + scenic::Rectangle shape(session_.get(), rrect.width(), rrect.height()); shape_node.SetShape(shape); shape_node.SetTranslation(shape_bounds.width() * 0.5f + shape_bounds.left(), shape_bounds.height() * 0.5f + shape_bounds.top(), @@ -88,7 +135,7 @@ void SceneUpdateContext::CreateFrame(scenic::EntityNode entity_node, if (paint_bounds.isEmpty() || !paint_bounds.intersects(shape_bounds)) paint_layers.clear(); - scenic::Material material(session()); + scenic::Material material(session_.get()); shape_node.SetMaterial(material); entity_node.AddChild(shape_node); @@ -96,147 +143,48 @@ void SceneUpdateContext::CreateFrame(scenic::EntityNode entity_node, if (paint_layers.empty()) { SetMaterialColor(material, color, opacity); } else { - // Apply current metrics and transformation scale factors. - const float scale_x = ScaleX(); - const float scale_y = ScaleY(); - - // Apply a texture to the whole shape. - SetMaterialTextureAndColor(material, color, opacity, scale_x, scale_y, - shape_bounds, std::move(paint_layers), layer, - std::move(entity_node)); - } -} - -void SceneUpdateContext::SetMaterialTextureAndColor( - scenic::Material& material, - SkColor color, - SkAlpha opacity, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node) { - scenic::Image* image = GenerateImageIfNeeded( - color, scale_x, scale_y, paint_bounds, std::move(paint_layers), layer, - std::move(entity_node)); - - if (image != nullptr) { // The final shape's color is material_color * texture_color. The passed in // material color was already used as a background when generating the // texture, so set the model color to |SK_ColorWHITE| in order to allow // using the texture's color unmodified. SetMaterialColor(material, SK_ColorWHITE, opacity); - material.SetTexture(*image); - } else { - // No texture was needed, so apply a solid color to the whole shape. - SetMaterialColor(material, color, opacity); - } -} -void SceneUpdateContext::SetMaterialColor(scenic::Material& material, - SkColor color, - SkAlpha opacity) { - const SkAlpha color_alpha = static_cast( - ((float)SkColorGetA(color) * (float)opacity) / 255.0f); - material.SetColor(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color), - color_alpha); -} - -scenic::Image* SceneUpdateContext::GenerateImageIfNeeded( - SkColor color, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node) { - // Bail if there's nothing to paint. - if (paint_layers.empty()) - return nullptr; - - // Bail if the physical bounds are empty after rounding. - SkISize physical_size = SkISize::Make(paint_bounds.width() * scale_x, - paint_bounds.height() * scale_y); - if (physical_size.isEmpty()) - return nullptr; - - // Acquire a surface from the surface producer and register the paint tasks. - std::unique_ptr surface = - surface_producer_->ProduceSurface( - physical_size, - LayerRasterCacheKey( - // Root frame has a nullptr layer - layer ? layer->unique_id() : 0, Matrix()), - std::make_unique(std::move(entity_node))); - - if (!surface) { - FML_LOG(ERROR) << "Could not acquire a surface from the surface producer " - "of size: " - << physical_size.width() << "x" << physical_size.height(); - return nullptr; - } - - auto image = surface->GetImage(); - - // Enqueue the paint task. - paint_tasks_.push_back({.surface = std::move(surface), - .left = paint_bounds.left(), - .top = paint_bounds.top(), - .scale_x = scale_x, - .scale_y = scale_y, - .background_color = color, - .layers = std::move(paint_layers)}); - return image; -} - -std::vector< - std::unique_ptr> -SceneUpdateContext::ExecutePaintTasks(CompositorContext::ScopedFrame& frame) { - TRACE_EVENT0("flutter", "SceneUpdateContext::ExecutePaintTasks"); - std::vector> surfaces_to_submit; - for (auto& task : paint_tasks_) { - FML_DCHECK(task.surface); - SkCanvas* canvas = task.surface->GetSkiaSurface()->getCanvas(); - Layer::PaintContext context = {canvas, - canvas, - frame.gr_context(), - nullptr, - frame.context().raster_time(), - frame.context().ui_time(), - frame.context().texture_registry(), - &frame.context().raster_cache(), - false, - frame_physical_depth_, - frame_device_pixel_ratio_}; - canvas->restoreToCount(1); - canvas->save(); - canvas->clear(task.background_color); - canvas->scale(task.scale_x, task.scale_y); - canvas->translate(-task.left, -task.top); - for (Layer* layer : task.layers) { - layer->Paint(context); - } - surfaces_to_submit.emplace_back(std::move(task.surface)); + // Enqueue a paint task for these layers, to apply a texture to the whole + // shape. + // + // The task uses the |shape_bounds| as its rendering bounds instead of the + // |paint_bounds|. If the paint_bounds is large than the shape_bounds it + // will be clipped. + paint_tasks_.emplace_back(PaintTask{.paint_bounds = shape_bounds, + .scale_x = top_scale_x_, + .scale_y = top_scale_y_, + .background_color = color, + .material = std::move(material), + .layers = std::move(paint_layers)}); } - paint_tasks_.clear(); - alpha_ = 1.f; - topmost_global_scenic_elevation_ = kScenicZElevationBetweenLayers; - scenic_elevation_ = 0.f; - return surfaces_to_submit; } -void SceneUpdateContext::UpdateScene(int64_t view_id, - const SkPoint& offset, - const SkSize& size) { +void SceneUpdateContext::UpdateView(int64_t view_id, + const SkPoint& offset, + const SkSize& size, + std::optional override_hit_testable) { auto* view_holder = ViewHolder::FromId(view_id); FML_DCHECK(view_holder); - view_holder->SetProperties(size.width(), size.height(), 0, 0, 0, 0, - view_holder->focusable()); - view_holder->UpdateScene(*this, offset, size, - SkScalarRoundToInt(alphaf() * 255), - view_holder->hit_testable()); + if (size.width() > 0.f && size.height() > 0.f) { + view_holder->SetProperties(size.width(), size.height(), 0, 0, 0, 0, + view_holder->focusable()); + } + + bool hit_testable = override_hit_testable.has_value() + ? *override_hit_testable + : view_holder->hit_testable(); + view_holder->UpdateScene(session_.get(), top_entity_->embedder_node(), offset, + size, SkScalarRoundToInt(alphaf() * 255), + hit_testable); + + // Assume embedded views are 10 "layers" wide. + next_elevation_ += 10 * kScenicZElevationBetweenLayers; } void SceneUpdateContext::CreateView(int64_t view_id, @@ -253,6 +201,16 @@ void SceneUpdateContext::CreateView(int64_t view_id, view_holder->set_focusable(focusable); } +void SceneUpdateContext::UpdateView(int64_t view_id, + bool hit_testable, + bool focusable) { + auto* view_holder = ViewHolder::FromId(view_id); + FML_DCHECK(view_holder); + + view_holder->set_hit_testable(hit_testable); + view_holder->set_focusable(focusable); +} + void SceneUpdateContext::DestroyView(int64_t view_id) { ViewHolder::Destroy(view_id); } @@ -260,13 +218,17 @@ void SceneUpdateContext::DestroyView(int64_t view_id) { SceneUpdateContext::Entity::Entity(SceneUpdateContext& context) : context_(context), previous_entity_(context.top_entity_), - entity_node_(context.session()) { - if (previous_entity_) - previous_entity_->embedder_node().AddChild(entity_node_); + entity_node_(context.session_.get()) { context.top_entity_ = this; } SceneUpdateContext::Entity::~Entity() { + if (previous_entity_) { + previous_entity_->embedder_node().AddChild(entity_node_); + } else { + context_.root_node_.AddChild(entity_node_); + } + FML_DCHECK(context_.top_entity_ == this); context_.top_entity_ = previous_entity_; } @@ -329,19 +291,25 @@ SceneUpdateContext::Frame::Frame(SceneUpdateContext& context, const SkRRect& rrect, SkColor color, SkAlpha opacity, - std::string label, - float z_translation, - Layer* layer) + std::string label) : Entity(context), + previous_elevation_(context.top_elevation_), rrect_(rrect), color_(color), opacity_(opacity), - opacity_node_(context.session()), - paint_bounds_(SkRect::MakeEmpty()), - layer_(layer) { + opacity_node_(context.session_.get()), + paint_bounds_(SkRect::MakeEmpty()) { + // Increment elevation trackers before calculating any local elevation. + // |UpdateView| can modify context.next_elevation_, which is why it is + // neccesary to track this addtional state. + context.top_elevation_ += kScenicZElevationBetweenLayers; + context.next_elevation_ += kScenicZElevationBetweenLayers; + + float local_elevation = context.next_elevation_ - previous_elevation_; + entity_node().SetTranslation(0.f, 0.f, -local_elevation); entity_node().SetLabel(label); - entity_node().SetTranslation(0.f, 0.f, z_translation); entity_node().AddChild(opacity_node_); + // Scenic currently lacks an API to enable rendering of alpha channel; alpha // channels are only rendered if there is a OpacityNode higher in the tree // with opacity != 1. For now, clamp to a infinitesimally smaller value than @@ -350,20 +318,11 @@ SceneUpdateContext::Frame::Frame(SceneUpdateContext& context, } SceneUpdateContext::Frame::~Frame() { - // We don't need a shape if the frame is zero size. - if (rrect_.isEmpty()) - return; - - // isEmpty should account for this, but we are adding these experimental - // checks to validate if this is the root cause for b/144933519. - if (std::isnan(rrect_.width()) || std::isnan(rrect_.height())) { - FML_LOG(ERROR) << "Invalid RoundedRectangle"; - return; - } + context().top_elevation_ = previous_elevation_; // Add a part which represents the frame's geometry for clipping purposes - context().CreateFrame(std::move(entity_node()), rrect_, color_, opacity_, - paint_bounds_, std::move(paint_layers_), layer_); + context().CreateFrame(entity_node(), rrect_, color_, opacity_, paint_bounds_, + std::move(paint_layers_)); } void SceneUpdateContext::Frame::AddPaintLayer(Layer* layer) { diff --git a/flow/scene_update_context.h b/flow/scene_update_context.h index 3b46fb2247f3c..f15c59fa7f6ea 100644 --- a/flow/scene_update_context.h +++ b/flow/scene_update_context.h @@ -5,18 +5,19 @@ #ifndef FLUTTER_FLOW_SCENE_UPDATE_CONTEXT_H_ #define FLUTTER_FLOW_SCENE_UPDATE_CONTEXT_H_ +#include +#include +#include +#include + #include #include #include #include -#include "flutter/flow/compositor_context.h" #include "flutter/flow/embedded_views.h" -#include "flutter/flow/raster_cache_key.h" -#include "flutter/fml/compiler_specific.h" #include "flutter/fml/logging.h" #include "flutter/fml/macros.h" -#include "lib/ui/scenic/cpp/resources.h" #include "third_party/skia/include/core/SkRect.h" #include "third_party/skia/include/core/SkSurface.h" @@ -33,50 +34,16 @@ constexpr float kOneMinusEpsilon = 1 - FLT_EPSILON; // How much layers are separated in Scenic z elevation. constexpr float kScenicZElevationBetweenLayers = 10.f; -class SceneUpdateContext : public flutter::ExternalViewEmbedder { +class SessionWrapper { public: - class SurfaceProducerSurface { - public: - virtual ~SurfaceProducerSurface() = default; - - virtual size_t AdvanceAndGetAge() = 0; - - virtual bool FlushSessionAcquireAndReleaseEvents() = 0; - - virtual bool IsValid() const = 0; + virtual ~SessionWrapper() {} - virtual SkISize GetSize() const = 0; - - virtual void SignalWritesFinished( - const std::function& on_writes_committed) = 0; - - virtual scenic::Image* GetImage() = 0; - - virtual sk_sp GetSkiaSurface() const = 0; - }; - - class SurfaceProducer { - public: - virtual ~SurfaceProducer() = default; - - // The produced surface owns the entity_node and has a layer_key for - // retained rendering. The surface will only be retained if the layer_key - // has a non-null layer pointer (layer_key.id()). - virtual std::unique_ptr ProduceSurface( - const SkISize& size, - const LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) = 0; - - // Query a retained entity node (owned by a retained surface) for retained - // rendering. - virtual bool HasRetainedNode(const LayerRasterCacheKey& key) const = 0; - virtual scenic::EntityNode* GetRetainedNode( - const LayerRasterCacheKey& key) = 0; - - virtual void SubmitSurface( - std::unique_ptr surface) = 0; - }; + virtual scenic::Session* get() = 0; + virtual void Present() = 0; +}; +class SceneUpdateContext : public flutter::ExternalViewEmbedder { + public: class Entity { public: Entity(SceneUpdateContext& context); @@ -116,9 +83,7 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { const SkRRect& rrect, SkColor color, SkAlpha opacity, - std::string label, - float z_translation = 0.0f, - Layer* layer = nullptr); + std::string label); virtual ~Frame(); scenic::ContainerNode& embedder_node() override { return opacity_node_; } @@ -126,6 +91,8 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { void AddPaintLayer(Layer* layer); private: + const float previous_elevation_; + const SkRRect rrect_; SkColor const color_; SkAlpha const opacity_; @@ -133,7 +100,6 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { scenic::OpacityNodeHACK opacity_node_; std::vector paint_layers_; SkRect paint_bounds_; - Layer* layer_; }; class Clip : public Entity { @@ -142,68 +108,35 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { ~Clip() = default; }; - SceneUpdateContext(scenic::Session* session, - SurfaceProducer* surface_producer); - ~SceneUpdateContext() = default; - - scenic::Session* session() { return session_; } + struct PaintTask { + SkRect paint_bounds; + SkScalar scale_x; + SkScalar scale_y; + SkColor background_color; + scenic::Material material; + std::vector layers; + }; - Entity* top_entity() { return top_entity_; } + SceneUpdateContext(std::string debug_label, + fuchsia::ui::views::ViewToken view_token, + scenic::ViewRefPair view_ref_pair, + SessionWrapper& session); + ~SceneUpdateContext() = default; - bool has_metrics() const { return !!metrics_; } - void set_metrics(fuchsia::ui::gfx::MetricsPtr metrics) { - metrics_ = std::move(metrics); - } - const fuchsia::ui::gfx::MetricsPtr& metrics() const { return metrics_; } - - void set_dimensions(const SkISize& frame_physical_size, - float frame_physical_depth, - float frame_device_pixel_ratio) { - frame_physical_size_ = frame_physical_size; - frame_physical_depth_ = frame_physical_depth; - frame_device_pixel_ratio_ = frame_device_pixel_ratio; - } - const SkISize& frame_size() const { return frame_physical_size_; } - float frame_physical_depth() const { return frame_physical_depth_; } - float frame_device_pixel_ratio() const { return frame_device_pixel_ratio_; } - - // TODO(chinmaygarde): This method must submit the surfaces as soon as paint - // tasks are done. However, given that there is no support currently for - // Vulkan semaphores, we need to submit all the surfaces after an explicit - // CPU wait. Once Vulkan semaphores are available, this method must return - // void and the implementation must submit surfaces on its own as soon as the - // specific canvas operations are done. - [[nodiscard]] std::vector> - ExecutePaintTasks(CompositorContext::ScopedFrame& frame); - - float ScaleX() const { return metrics_->scale_x * top_scale_x_; } - float ScaleY() const { return metrics_->scale_y * top_scale_y_; } - - // The transformation matrix of the current context. It's used to construct - // the LayerRasterCacheKey for a given layer. - SkMatrix Matrix() const { return SkMatrix::MakeScale(ScaleX(), ScaleY()); } - - bool HasRetainedNode(const LayerRasterCacheKey& key) const { - return surface_producer_->HasRetainedNode(key); - } - scenic::EntityNode* GetRetainedNode(const LayerRasterCacheKey& key) { - return surface_producer_->GetRetainedNode(key); - } + scenic::ContainerNode& root_node() { return root_node_; } // The cumulative alpha value based on all the parent OpacityLayers. void set_alphaf(float alpha) { alpha_ = alpha; } float alphaf() { return alpha_; } - // The global scenic elevation at a given point in the traversal. - float scenic_elevation() { return scenic_elevation_; } + // Returns all `PaintTask`s generated for the current frame. + std::vector GetPaintTasks(); - void set_scenic_elevation(float elevation) { scenic_elevation_ = elevation; } + // Enable/disable wireframe rendering around the root view bounds. + void EnableWireframe(bool enable); - float GetGlobalElevationForNextScenicLayer() { - float elevation = topmost_global_scenic_elevation_; - topmost_global_scenic_elevation_ += kScenicZElevationBetweenLayers; - return elevation; - } + // Reset state for a new frame. + void Reset(); // |ExternalViewEmbedder| SkCanvas* GetRootCanvas() override { return nullptr; } @@ -234,73 +167,35 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { } void CreateView(int64_t view_id, bool hit_testable, bool focusable); - + void UpdateView(int64_t view_id, bool hit_testable, bool focusable); void DestroyView(int64_t view_id); - - void UpdateScene(int64_t view_id, const SkPoint& offset, const SkSize& size); + void UpdateView(int64_t view_id, + const SkPoint& offset, + const SkSize& size, + std::optional override_hit_testable = std::nullopt); private: - struct PaintTask { - std::unique_ptr surface; - SkScalar left; - SkScalar top; - SkScalar scale_x; - SkScalar scale_y; - SkColor background_color; - std::vector layers; - }; - - // Setup the entity_node as a frame that materialize all the paint_layers. In - // most cases, this creates a VulkanSurface (SurfaceProducerSurface) by - // calling SetShapeTextureOrColor and GenerageImageIfNeeded. Such surface will - // own the associated entity_node. If the layer pointer isn't nullptr, the - // surface (and thus the entity_node) will be retained for that layer to - // improve the performance. - void CreateFrame(scenic::EntityNode entity_node, + void CreateFrame(scenic::EntityNode& entity_node, const SkRRect& rrect, SkColor color, SkAlpha opacity, const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer); - void SetMaterialTextureAndColor(scenic::Material& material, - SkColor color, - SkAlpha opacity, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node); - void SetMaterialColor(scenic::Material& material, - SkColor color, - SkAlpha opacity); - scenic::Image* GenerateImageIfNeeded(SkColor color, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node); + std::vector paint_layers); - Entity* top_entity_ = nullptr; - float top_scale_x_ = 1.f; - float top_scale_y_ = 1.f; + SessionWrapper& session_; - scenic::Session* const session_; - SurfaceProducer* const surface_producer_; + scenic::View root_view_; + scenic::EntityNode root_node_; - fuchsia::ui::gfx::MetricsPtr metrics_; - SkISize frame_physical_size_; - float frame_physical_depth_ = 0.0f; - float frame_device_pixel_ratio_ = - 1.0f; // Ratio between logical and physical pixels. + std::vector paint_tasks_; - float alpha_ = 1.0f; - float scenic_elevation_ = 0.f; - float topmost_global_scenic_elevation_ = kScenicZElevationBetweenLayers; + Entity* top_entity_ = nullptr; + float top_scale_x_ = 1.f; + float top_scale_y_ = 1.f; + float top_elevation_ = 0.f; - std::vector paint_tasks_; + float next_elevation_ = 0.f; + float alpha_ = 1.f; FML_DISALLOW_COPY_AND_ASSIGN(SceneUpdateContext); }; diff --git a/flow/skia_gpu_object.cc b/flow/skia_gpu_object.cc index aadb4e3b72f71..7415916d77954 100644 --- a/flow/skia_gpu_object.cc +++ b/flow/skia_gpu_object.cc @@ -11,7 +11,7 @@ namespace flutter { SkiaUnrefQueue::SkiaUnrefQueue(fml::RefPtr task_runner, fml::TimeDelta delay, - fml::WeakPtr context) + fml::WeakPtr context) : task_runner_(std::move(task_runner)), drain_delay_(delay), drain_pending_(false), diff --git a/flow/skia_gpu_object.h b/flow/skia_gpu_object.h index ef7cb596f1a36..662d560fc9eff 100644 --- a/flow/skia_gpu_object.h +++ b/flow/skia_gpu_object.h @@ -35,14 +35,14 @@ class SkiaUnrefQueue : public fml::RefCountedThreadSafe { std::mutex mutex_; std::deque objects_; bool drain_pending_; - fml::WeakPtr context_; + fml::WeakPtr context_; // The `GrDirectContext* context` is only used for signaling Skia to // performDeferredCleanup. It can be nullptr when such signaling is not needed // (e.g., in unit tests). SkiaUnrefQueue(fml::RefPtr task_runner, fml::TimeDelta delay, - fml::WeakPtr context = {}); + fml::WeakPtr context = {}); ~SkiaUnrefQueue(); diff --git a/flow/testing/layer_test.h b/flow/testing/layer_test.h index c63057fca1942..d2df8b404ca9b 100644 --- a/flow/testing/layer_test.h +++ b/flow/testing/layer_test.h @@ -47,11 +47,9 @@ class LayerTestBase : public CanvasTestBase { kGiantRect, /* cull_rect */ false, /* layer reads from surface */ raster_time_, ui_time_, texture_registry_, - false, /* checkerboard_offscreen_layers */ - 100.0f, /* frame_physical_depth */ - 1.0f, /* frame_device_pixel_ratio */ - 0.0f, /* total_elevation */ - false, /* has_platform_view */ + false, /* checkerboard_offscreen_layers */ + 1.0f, /* frame_device_pixel_ratio */ + false, /* has_platform_view */ }), paint_context_({ TestT::mock_canvas().internal_canvas(), /* internal_nodes_canvas */ @@ -61,7 +59,6 @@ class LayerTestBase : public CanvasTestBase { raster_time_, ui_time_, texture_registry_, nullptr, /* raster_cache */ false, /* checkerboard_offscreen_layers */ - 100.0f, /* frame_physical_depth */ 1.0f, /* frame_device_pixel_ratio */ }) { use_null_raster_cache(); diff --git a/flow/testing/mock_layer.cc b/flow/testing/mock_layer.cc index 5fe1b98088af1..b32bdb23abc09 100644 --- a/flow/testing/mock_layer.cc +++ b/flow/testing/mock_layer.cc @@ -22,7 +22,6 @@ void MockLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { parent_mutators_ = context->mutators_stack; parent_matrix_ = matrix; parent_cull_rect_ = context->cull_rect; - parent_elevation_ = context->total_elevation; parent_has_platform_view_ = context->has_platform_view; context->has_platform_view = fake_has_platform_view_; diff --git a/flow/testing/mock_layer.h b/flow/testing/mock_layer.h index 835c3ee9621cc..b92583f581209 100644 --- a/flow/testing/mock_layer.h +++ b/flow/testing/mock_layer.h @@ -28,7 +28,6 @@ class MockLayer : public Layer { const MutatorsStack& parent_mutators() { return parent_mutators_; } const SkMatrix& parent_matrix() { return parent_matrix_; } const SkRect& parent_cull_rect() { return parent_cull_rect_; } - float parent_elevation() { return parent_elevation_; } bool parent_has_platform_view() { return parent_has_platform_view_; } private: @@ -37,7 +36,6 @@ class MockLayer : public Layer { SkRect parent_cull_rect_ = SkRect::MakeEmpty(); SkPath fake_paint_path_; SkPaint fake_paint_; - float parent_elevation_ = 0; bool parent_has_platform_view_ = false; bool fake_has_platform_view_ = false; bool fake_needs_system_composite_ = false; diff --git a/flow/testing/mock_layer_unittests.cc b/flow/testing/mock_layer_unittests.cc index 0e6e37978c2e1..ebb837ca8b8dd 100644 --- a/flow/testing/mock_layer_unittests.cc +++ b/flow/testing/mock_layer_unittests.cc @@ -39,13 +39,11 @@ TEST_F(MockLayerTest, SimpleParams) { const SkMatrix start_matrix = SkMatrix::Translate(1.0f, 2.0f); const SkMatrix scale_matrix = SkMatrix::Scale(0.5f, 0.5f); const SkRect cull_rect = SkRect::MakeWH(5.0f, 5.0f); - const float parent_elevation = 5.0f; const bool parent_has_platform_view = true; auto layer = std::make_shared(path, paint); preroll_context()->mutators_stack.PushTransform(scale_matrix); preroll_context()->cull_rect = cull_rect; - preroll_context()->total_elevation = parent_elevation; preroll_context()->has_platform_view = parent_has_platform_view; layer->Preroll(preroll_context(), start_matrix); EXPECT_EQ(preroll_context()->has_platform_view, false); @@ -55,7 +53,6 @@ TEST_F(MockLayerTest, SimpleParams) { EXPECT_EQ(layer->parent_mutators(), std::vector{Mutator(scale_matrix)}); EXPECT_EQ(layer->parent_matrix(), start_matrix); EXPECT_EQ(layer->parent_cull_rect(), cull_rect); - EXPECT_EQ(layer->parent_elevation(), parent_elevation); EXPECT_EQ(layer->parent_has_platform_view(), parent_has_platform_view); layer->Paint(paint_context()); diff --git a/flow/view_holder.cc b/flow/view_holder.cc index 7fd00500bb02c..c2011825c9474 100644 --- a/flow/view_holder.cc +++ b/flow/view_holder.cc @@ -4,6 +4,8 @@ #include "flutter/flow/view_holder.h" +#include + #include "flutter/fml/thread_local.h" namespace { @@ -98,18 +100,17 @@ ViewHolder::ViewHolder(fml::RefPtr ui_task_runner, FML_DCHECK(pending_view_holder_token_.value); } -void ViewHolder::UpdateScene(SceneUpdateContext& context, +void ViewHolder::UpdateScene(scenic::Session* session, + scenic::ContainerNode& container_node, const SkPoint& offset, const SkSize& size, SkAlpha opacity, bool hit_testable) { if (pending_view_holder_token_.value) { - entity_node_ = std::make_unique(context.session()); - opacity_node_ = - std::make_unique(context.session()); + entity_node_ = std::make_unique(session); + opacity_node_ = std::make_unique(session); view_holder_ = std::make_unique( - context.session(), std::move(pending_view_holder_token_), - "Flutter SceneHost"); + session, std::move(pending_view_holder_token_), "Flutter SceneHost"); opacity_node_->AddChild(*entity_node_); opacity_node_->SetLabel("flutter::ViewHolder"); entity_node_->Attach(*view_holder_); @@ -125,7 +126,7 @@ void ViewHolder::UpdateScene(SceneUpdateContext& context, FML_DCHECK(opacity_node_); FML_DCHECK(view_holder_); - context.top_entity()->embedder_node().AddChild(*opacity_node_); + container_node.AddChild(*opacity_node_); opacity_node_->SetOpacity(opacity / 255.0f); entity_node_->SetTranslation(offset.x(), offset.y(), -0.1f); entity_node_->SetHitTestBehavior( diff --git a/flow/view_holder.h b/flow/view_holder.h index bb8ff83d776ab..f25b205c7823c 100644 --- a/flow/view_holder.h +++ b/flow/view_holder.h @@ -9,17 +9,17 @@ #include #include #include +#include #include -#include "third_party/skia/include/core/SkMatrix.h" -#include "third_party/skia/include/core/SkPoint.h" -#include "third_party/skia/include/core/SkSize.h" #include -#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" #include "flutter/fml/memory/ref_counted.h" #include "flutter/fml/task_runner.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPoint.h" +#include "third_party/skia/include/core/SkSize.h" namespace flutter { @@ -54,7 +54,8 @@ class ViewHolder { // Creates or updates the contained ViewHolder resource using the specified // |SceneUpdateContext|. - void UpdateScene(SceneUpdateContext& context, + void UpdateScene(scenic::Session* session, + scenic::ContainerNode& container_node, const SkPoint& offset, const SkSize& size, SkAlpha opacity, diff --git a/fml/file.cc b/fml/file.cc index 3d9e4397ee6b0..b96bfe6341453 100644 --- a/fml/file.cc +++ b/fml/file.cc @@ -44,14 +44,21 @@ fml::UniqueFD CreateDirectory(const fml::UniqueFD& base_directory, return CreateDirectory(base_directory, components, permission, 0); } -ScopedTemporaryDirectory::ScopedTemporaryDirectory() { - path_ = CreateTemporaryDirectory(); +ScopedTemporaryDirectory::ScopedTemporaryDirectory() + : path_(CreateTemporaryDirectory()) { if (path_ != "") { dir_fd_ = OpenDirectory(path_.c_str(), false, FilePermission::kRead); } } ScopedTemporaryDirectory::~ScopedTemporaryDirectory() { + // POSIX requires the directory to be empty before UnlinkDirectory. + if (path_ != "") { + if (!RemoveFilesInDirectory(dir_fd_)) { + FML_LOG(ERROR) << "Could not clean directory: " << path_; + } + } + // Windows has to close UniqueFD first before UnlinkDirectory dir_fd_.reset(); if (path_ != "") { diff --git a/fml/logging.cc b/fml/logging.cc index 8c4796db0a504..d4273b9381da7 100644 --- a/fml/logging.cc +++ b/fml/logging.cc @@ -39,9 +39,8 @@ const char* StripPath(const char* path) { auto* p = strrchr(path, '/'); if (p) { return p + 1; - } else { - return path; } + return path; } } // namespace diff --git a/fml/memory/ref_counted_unittest.cc b/fml/memory/ref_counted_unittest.cc index 959117c488e38..4cefb8b5ff5f9 100644 --- a/fml/memory/ref_counted_unittest.cc +++ b/fml/memory/ref_counted_unittest.cc @@ -227,6 +227,8 @@ TEST(RefCountedTest, NullAssignmentToNull) { // No-op null assignment using move constructor. r1 = std::move(r2); EXPECT_TRUE(r1.get() == nullptr); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r2.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_FALSE(r1); EXPECT_FALSE(r2); @@ -272,6 +274,8 @@ TEST(RefCountedTest, NonNullAssignmentToNull) { RefPtr r2; // Move assignment (to null ref pointer). r2 = std::move(r1); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r1.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_EQ(created, r2.get()); EXPECT_FALSE(r1); @@ -336,6 +340,8 @@ TEST(RefCountedTest, NullAssignmentToNonNull) { // Null assignment using move constructor. r1 = std::move(r2); EXPECT_TRUE(r1.get() == nullptr); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r2.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_FALSE(r1); EXPECT_FALSE(r2); @@ -389,6 +395,8 @@ TEST(RefCountedTest, NonNullAssignmentToNonNull) { RefPtr r2(MakeRefCounted(nullptr, &was_destroyed2)); // Move assignment (to non-null ref pointer). r2 = std::move(r1); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r1.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_FALSE(r2.get() == nullptr); EXPECT_FALSE(r1); @@ -436,7 +444,13 @@ TEST(RefCountedTest, SelfAssignment) { { MyClass* created = nullptr; was_destroyed = false; - RefPtr r(MakeRefCounted(&created, &was_destroyed)); + // This line is marked NOLINT because the clang linter does not reason about + // the value of the reference count. In particular, the self-assignment + // below is handled in the copy constructor by a refcount increment then + // decrement. The linter sees only that the decrement might destroy the + // object. + RefPtr r(MakeRefCounted( // NOLINT + &created, &was_destroyed)); // Copy. ALLOW_SELF_ASSIGN_OVERLOADED(r = r); EXPECT_EQ(created, r.get()); diff --git a/fml/memory/weak_ptr_unittest.cc b/fml/memory/weak_ptr_unittest.cc index a1db3ae1d1988..e055ad9095409 100644 --- a/fml/memory/weak_ptr_unittest.cc +++ b/fml/memory/weak_ptr_unittest.cc @@ -38,6 +38,8 @@ TEST(WeakPtrTest, MoveConstruction) { WeakPtrFactory factory(&data); WeakPtr ptr = factory.GetWeakPtr(); WeakPtr ptr2(std::move(ptr)); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_EQ(nullptr, ptr.get()); // NOLINT EXPECT_EQ(&data, ptr2.get()); } @@ -60,6 +62,8 @@ TEST(WeakPtrTest, MoveAssignment) { WeakPtr ptr2; EXPECT_EQ(nullptr, ptr2.get()); ptr2 = std::move(ptr); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_EQ(nullptr, ptr.get()); // NOLINT EXPECT_EQ(&data, ptr2.get()); } diff --git a/fml/message_loop_task_queues.cc b/fml/message_loop_task_queues.cc index 8df08fa1b244d..1d5a9091083f3 100644 --- a/fml/message_loop_task_queues.cc +++ b/fml/message_loop_task_queues.cc @@ -246,7 +246,7 @@ bool MessageLoopTaskQueues::Unmerge(TaskQueueId owner) { bool MessageLoopTaskQueues::Owns(TaskQueueId owner, TaskQueueId subsumed) const { std::lock_guard guard(queue_mutex_); - return subsumed == queue_entries_.at(owner)->owner_of || owner == subsumed; + return subsumed == queue_entries_.at(owner)->owner_of; } // Subsumed queues will never have pending tasks. diff --git a/fml/message_loop_task_queues_unittests.cc b/fml/message_loop_task_queues_unittests.cc index a1c2df0a47f6c..1086bb28b8013 100644 --- a/fml/message_loop_task_queues_unittests.cc +++ b/fml/message_loop_task_queues_unittests.cc @@ -173,6 +173,13 @@ TEST(MessageLoopTaskQueue, NotifyObserversWhileCreatingQueues) { before_second_observer.Signal(); notify_observers.join(); } + +TEST(MessageLoopTaskQueue, QueueDoNotOwnItself) { + auto task_queue = fml::MessageLoopTaskQueues::GetInstance(); + auto queue_id = task_queue->CreateTaskQueue(); + ASSERT_FALSE(task_queue->Owns(queue_id, queue_id)); +} + // TODO(chunhtai): This unit-test is flaky and sometimes fails asynchronizely // after the test has finished. // https://github.com/flutter/flutter/issues/43858 diff --git a/fml/raster_thread_merger.cc b/fml/raster_thread_merger.cc index b29c303371b8a..62b696db70aa4 100644 --- a/fml/raster_thread_merger.cc +++ b/fml/raster_thread_merger.cc @@ -17,23 +17,41 @@ RasterThreadMerger::RasterThreadMerger(fml::TaskQueueId platform_queue_id, gpu_queue_id_(gpu_queue_id), task_queues_(fml::MessageLoopTaskQueues::GetInstance()), lease_term_(kLeaseNotSet) { - is_merged_ = task_queues_->Owns(platform_queue_id_, gpu_queue_id_); + FML_CHECK(!task_queues_->Owns(platform_queue_id_, gpu_queue_id_)); } void RasterThreadMerger::MergeWithLease(size_t lease_term) { + if (TaskQueuesAreSame()) { + return; + } + FML_DCHECK(lease_term > 0) << "lease_term should be positive."; - if (!is_merged_) { - is_merged_ = task_queues_->Merge(platform_queue_id_, gpu_queue_id_); + std::scoped_lock lock(lease_term_mutex_); + if (!IsMergedUnSafe()) { + bool success = task_queues_->Merge(platform_queue_id_, gpu_queue_id_); + FML_CHECK(success) << "Unable to merge the raster and platform threads."; lease_term_ = lease_term; } + merged_condition_.notify_one(); +} + +void RasterThreadMerger::UnMergeNow() { + if (TaskQueuesAreSame()) { + return; + } + + std::scoped_lock lock(lease_term_mutex_); + lease_term_ = 0; + bool success = task_queues_->Unmerge(platform_queue_id_); + FML_CHECK(success) << "Unable to un-merge the raster and platform threads."; } bool RasterThreadMerger::IsOnPlatformThread() const { return MessageLoop::GetCurrentTaskQueueId() == platform_queue_id_; } -bool RasterThreadMerger::IsOnRasterizingThread() const { - if (is_merged_) { +bool RasterThreadMerger::IsOnRasterizingThread() { + if (IsMergedUnSafe()) { return IsOnPlatformThread(); } else { return !IsOnPlatformThread(); @@ -41,24 +59,45 @@ bool RasterThreadMerger::IsOnRasterizingThread() const { } void RasterThreadMerger::ExtendLeaseTo(size_t lease_term) { - FML_DCHECK(lease_term > 0) << "lease_term should be positive."; + if (TaskQueuesAreSame()) { + return; + } + std::scoped_lock lock(lease_term_mutex_); + FML_DCHECK(IsMergedUnSafe()) << "lease_term should be positive."; if (lease_term_ != kLeaseNotSet && static_cast(lease_term) > lease_term_) { lease_term_ = lease_term; } } -bool RasterThreadMerger::IsMerged() const { - return is_merged_; +bool RasterThreadMerger::IsMerged() { + std::scoped_lock lock(lease_term_mutex_); + return IsMergedUnSafe(); } -RasterThreadStatus RasterThreadMerger::DecrementLease() { - if (!is_merged_) { - return RasterThreadStatus::kRemainsUnmerged; +bool RasterThreadMerger::IsMergedUnSafe() { + return lease_term_ > 0 || TaskQueuesAreSame(); +} + +bool RasterThreadMerger::TaskQueuesAreSame() { + return platform_queue_id_ == gpu_queue_id_; +} + +void RasterThreadMerger::WaitUntilMerged() { + if (TaskQueuesAreSame()) { + return; } + FML_CHECK(IsOnPlatformThread()); + std::unique_lock lock(lease_term_mutex_); + merged_condition_.wait(lock, [&] { return IsMergedUnSafe(); }); +} - // we haven't been set to merge. - if (lease_term_ == kLeaseNotSet) { +RasterThreadStatus RasterThreadMerger::DecrementLease() { + if (TaskQueuesAreSame()) { + return RasterThreadStatus::kRemainsMerged; + } + std::unique_lock lock(lease_term_mutex_); + if (!IsMergedUnSafe()) { return RasterThreadStatus::kRemainsUnmerged; } @@ -66,9 +105,9 @@ RasterThreadStatus RasterThreadMerger::DecrementLease() { << "lease_term should always be positive when merged."; lease_term_--; if (lease_term_ == 0) { - bool success = task_queues_->Unmerge(platform_queue_id_); - FML_CHECK(success) << "Unable to un-merge the raster and platform threads."; - is_merged_ = false; + // |UnMergeNow| is going to acquire the lock again. + lock.unlock(); + UnMergeNow(); return RasterThreadStatus::kUnmergedNow; } diff --git a/fml/raster_thread_merger.h b/fml/raster_thread_merger.h index 7c0318ff77d26..b01cf76a11436 100644 --- a/fml/raster_thread_merger.h +++ b/fml/raster_thread_merger.h @@ -5,6 +5,8 @@ #ifndef FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_ #define FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_ +#include +#include #include "flutter/fml/macros.h" #include "flutter/fml/memory/ref_counted.h" #include "flutter/fml/message_loop_task_queues.h" @@ -28,15 +30,37 @@ class RasterThreadMerger // When the caller merges with a lease term of say 2. The threads // are going to remain merged until 2 invocations of |DecreaseLease|, // unless an |ExtendLeaseTo| gets called. + // + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged this method becomes no-op. void MergeWithLease(size_t lease_term); + // Un-merges the threads now, and resets the lease term to 0. + // + // Must be executed on the raster task runner. + // + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged, we never unmerge them and + // this method becomes no-op. + void UnMergeNow(); + + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged this method becomes no-op. void ExtendLeaseTo(size_t lease_term); // Returns |RasterThreadStatus::kUnmergedNow| if this call resulted in // splitting the raster and platform threads. Reduces the lease term by 1. + // + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged this method becomes no-op. RasterThreadStatus DecrementLease(); - bool IsMerged() const; + bool IsMerged(); + + // Waits until the threads are merged. + // + // Must run on the platform task runner. + void WaitUntilMerged(); RasterThreadMerger(fml::TaskQueueId platform_queue_id, fml::TaskQueueId gpu_queue_id); @@ -44,7 +68,7 @@ class RasterThreadMerger // Returns true if the current thread owns rasterizing. // When the threads are merged, platform thread owns rasterizing. // When un-merged, raster thread owns rasterizing. - bool IsOnRasterizingThread() const; + bool IsOnRasterizingThread(); // Returns true if the current thread is the platform thread. bool IsOnPlatformThread() const; @@ -55,7 +79,13 @@ class RasterThreadMerger fml::TaskQueueId gpu_queue_id_; fml::RefPtr task_queues_; std::atomic_int lease_term_; - bool is_merged_; + std::condition_variable merged_condition_; + std::mutex lease_term_mutex_; + + bool IsMergedUnSafe(); + // The platform_queue_id and gpu_queue_id are exactly the same. + // We consider the threads are always merged and cannot be unmerged. + bool TaskQueuesAreSame(); FML_FRIEND_REF_COUNTED_THREAD_SAFE(RasterThreadMerger); FML_FRIEND_MAKE_REF_COUNTED(RasterThreadMerger); diff --git a/fml/raster_thread_merger_unittests.cc b/fml/raster_thread_merger_unittests.cc index a182723a79191..f3df1c1bb2259 100644 --- a/fml/raster_thread_merger_unittests.cc +++ b/fml/raster_thread_merger_unittests.cc @@ -208,3 +208,96 @@ TEST(RasterThreadMerger, LeaseExtension) { thread1.join(); thread2.join(); } + +TEST(RasterThreadMerger, WaitUntilMerged) { + fml::RefPtr raster_thread_merger; + + fml::AutoResetWaitableEvent create_thread_merger_latch; + fml::MessageLoop* loop_platform = nullptr; + fml::AutoResetWaitableEvent latch_platform; + fml::AutoResetWaitableEvent term_platform; + fml::AutoResetWaitableEvent latch_merged; + std::thread thread_platform([&]() { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + loop_platform = &fml::MessageLoop::GetCurrent(); + latch_platform.Signal(); + create_thread_merger_latch.Wait(); + raster_thread_merger->WaitUntilMerged(); + latch_merged.Signal(); + term_platform.Wait(); + }); + + const int kNumFramesMerged = 5; + fml::MessageLoop* loop_raster = nullptr; + fml::AutoResetWaitableEvent term_raster; + std::thread thread_raster([&]() { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + loop_raster = &fml::MessageLoop::GetCurrent(); + latch_platform.Wait(); + fml::TaskQueueId qid_platform = + loop_platform->GetTaskRunner()->GetTaskQueueId(); + fml::TaskQueueId qid_raster = + loop_raster->GetTaskRunner()->GetTaskQueueId(); + raster_thread_merger = + fml::MakeRefCounted(qid_platform, qid_raster); + ASSERT_FALSE(raster_thread_merger->IsMerged()); + create_thread_merger_latch.Signal(); + raster_thread_merger->MergeWithLease(kNumFramesMerged); + term_raster.Wait(); + }); + + latch_merged.Wait(); + ASSERT_TRUE(raster_thread_merger->IsMerged()); + + for (int i = 0; i < kNumFramesMerged; i++) { + ASSERT_TRUE(raster_thread_merger->IsMerged()); + raster_thread_merger->DecrementLease(); + } + + ASSERT_FALSE(raster_thread_merger->IsMerged()); + + term_platform.Signal(); + term_raster.Signal(); + thread_platform.join(); + thread_raster.join(); +} + +TEST(RasterThreadMerger, HandleTaskQueuesAreTheSame) { + fml::MessageLoop* loop1 = nullptr; + fml::AutoResetWaitableEvent latch1; + fml::AutoResetWaitableEvent term1; + std::thread thread1([&loop1, &latch1, &term1]() { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + loop1 = &fml::MessageLoop::GetCurrent(); + latch1.Signal(); + term1.Wait(); + }); + + latch1.Wait(); + + fml::TaskQueueId qid1 = loop1->GetTaskRunner()->GetTaskQueueId(); + fml::TaskQueueId qid2 = qid1; + const auto raster_thread_merger_ = + fml::MakeRefCounted(qid1, qid2); + // Statically merged. + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + + // Test decrement lease and unmerge are both no-ops. + // The task queues should be always merged. + const int kNumFramesMerged = 5; + raster_thread_merger_->MergeWithLease(kNumFramesMerged); + + for (int i = 0; i < kNumFramesMerged; i++) { + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + raster_thread_merger_->DecrementLease(); + } + + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + + // Wait until merged should also return immediately. + raster_thread_merger_->WaitUntilMerged(); + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + + term1.Signal(); + thread1.join(); +} diff --git a/lib/io/dart_io.cc b/lib/io/dart_io.cc index 6e5e538d74da0..d656e8b538943 100644 --- a/lib/io/dart_io.cc +++ b/lib/io/dart_io.cc @@ -16,19 +16,27 @@ using tonic::ToDart; namespace flutter { -void DartIO::InitForIsolate(bool disable_http) { - Dart_Handle result = Dart_SetNativeResolver( - Dart_LookupLibrary(ToDart("dart:io")), dart::bin::LookupIONative, - dart::bin::LookupIONativeSymbol); +void DartIO::InitForIsolate(bool may_insecurely_connect_to_all_domains, + std::string domain_network_policy) { + Dart_Handle io_lib = Dart_LookupLibrary(ToDart("dart:io")); + Dart_Handle result = Dart_SetNativeResolver(io_lib, dart::bin::LookupIONative, + dart::bin::LookupIONativeSymbol); FML_CHECK(!LogIfError(result)); - // The SDK expects this field to represent "allow http" so we switch the - // value. - Dart_Handle allow_http_value = disable_http ? Dart_False() : Dart_True(); - Dart_Handle set_field_result = - Dart_SetField(Dart_LookupLibrary(ToDart("dart:_http")), - ToDart("_embedderAllowsHttp"), allow_http_value); - FML_CHECK(!LogIfError(set_field_result)); + Dart_Handle embedder_config_type = + Dart_GetType(io_lib, ToDart("_EmbedderConfig"), 0, nullptr); + FML_CHECK(!LogIfError(embedder_config_type)); + + Dart_Handle allow_insecure_connections_result = Dart_SetField( + embedder_config_type, ToDart("_mayInsecurelyConnectToAllDomains"), + ToDart(may_insecurely_connect_to_all_domains)); + FML_CHECK(!LogIfError(allow_insecure_connections_result)); + + Dart_Handle dart_args[1]; + dart_args[0] = ToDart(domain_network_policy); + Dart_Handle set_domain_network_policy_result = Dart_Invoke( + embedder_config_type, ToDart("_setDomainPolicies"), 1, dart_args); + FML_CHECK(!LogIfError(set_domain_network_policy_result)); } } // namespace flutter diff --git a/lib/io/dart_io.h b/lib/io/dart_io.h index 27ce7aa65baeb..34bc8a54aaed9 100644 --- a/lib/io/dart_io.h +++ b/lib/io/dart_io.h @@ -6,6 +6,7 @@ #define FLUTTER_LIB_IO_DART_IO_H_ #include +#include #include "flutter/fml/macros.h" @@ -13,7 +14,8 @@ namespace flutter { class DartIO { public: - static void InitForIsolate(bool disable_http); + static void InitForIsolate(bool may_insecurely_connect_to_all_domains, + std::string domain_network_policy); private: FML_DISALLOW_IMPLICIT_CONSTRUCTORS(DartIO); diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index 103699d640692..decca25d81f90 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -6,7 +6,7 @@ import("//build/fuchsia/sdk.gni") import("//flutter/common/config.gni") import("//flutter/testing/testing.gni") -source_set_maybe_fuchsia_legacy("ui") { +source_set("ui") { sources = [ "compositing/scene.cc", "compositing/scene.h", @@ -93,6 +93,8 @@ source_set_maybe_fuchsia_legacy("ui") { "text/text_box.h", "ui_dart_state.cc", "ui_dart_state.h", + "window/platform_configuration.cc", + "window/platform_configuration.h", "window/platform_message.cc", "window/platform_message.h", "window/platform_message_response.cc", @@ -118,6 +120,7 @@ source_set_maybe_fuchsia_legacy("ui") { deps = [ "//flutter/assets", "//flutter/common", + "//flutter/flow", "//flutter/fml", "//flutter/runtime:test_font", "//flutter/third_party/tonic", @@ -130,18 +133,18 @@ source_set_maybe_fuchsia_legacy("ui") { defines = [ "FLUTTER_ENABLE_SKSHAPER" ] } - sources_legacy = [ - "compositing/scene_host.cc", - "compositing/scene_host.h", - ] - - deps_legacy = [ - "$fuchsia_sdk_root/pkg:async-cpp", - "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", - "//flutter/shell/platform/fuchsia/dart-pkg/zircon", - ] + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { + sources += [ + "compositing/scene_host.cc", + "compositing/scene_host.h", + ] - deps_legacy_and_next = [ "//flutter/flow:flow" ] + deps += [ + "$fuchsia_sdk_root/pkg:async-cpp", + "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", + "//flutter/shell/platform/fuchsia/dart-pkg/zircon", + ] + } } if (enable_unittests) { @@ -172,7 +175,7 @@ if (enable_unittests) { ] } - source_set_maybe_fuchsia_legacy("ui_unittests_common") { + executable("ui_unittests") { testonly = true public_configs = [ "//flutter:export_dynamic_symbols" ] @@ -180,48 +183,27 @@ if (enable_unittests) { sources = [ "painting/image_encoding_unittests.cc", "painting/vertices_unittests.cc", + "window/platform_configuration_unittests.cc", "window/pointer_data_packet_converter_unittests.cc", ] deps = [ + ":ui", ":ui_unittests_fixtures", "//flutter/common", + "//flutter/shell/common:shell_test_fixture_sources", "//flutter/testing", + "//flutter/testing:dart", + "//flutter/testing:fixture_test", "//flutter/third_party/tonic", "//third_party/dart/runtime/bin:elf_loader", ] - # TODO(): This test is hard-coded to use a TestGLSurface so it cannot run on fuchsia. + # TODO(https://github.com/flutter/flutter/issues/63837): This test is hard-coded to use a TestGLSurface so it cannot run on fuchsia. if (!is_fuchsia) { sources += [ "painting/image_decoder_unittests.cc" ] deps += [ "//flutter/testing:opengl" ] } - - deps_legacy_and_next = [ - ":ui", - "//flutter/shell/common:shell_test_fixture_sources", - "//flutter/testing:dart", - "//flutter/testing:fixture_test", - ] - } - - if (is_fuchsia) { - executable("ui_unittests") { - testonly = true - - deps = [ ":ui_unittests_common_fuchsia_legacy" ] - } - executable("ui_unittests_next") { - testonly = true - - deps = [ ":ui_unittests_common" ] - } - } else { - executable("ui_unittests") { - testonly = true - - deps = [ ":ui_unittests_common" ] - } } } diff --git a/lib/ui/channel_buffers.dart b/lib/ui/channel_buffers.dart index ba67c41269b9c..a32a557e1f121 100644 --- a/lib/ui/channel_buffers.dart +++ b/lib/ui/channel_buffers.dart @@ -25,7 +25,7 @@ class _StoredMessage { /// A fixed-size circular queue. class _RingBuffer { - /// The underlying data for the RingBuffer. ListQueue's dynamically resize, + /// The underlying data for the RingBuffer. ListQueues dynamically resize, /// [_RingBuffer]s do not. final collection.ListQueue _queue; diff --git a/lib/ui/compositing.dart b/lib/ui/compositing.dart index 4f8332f6efdfd..39b0d63fc2b36 100644 --- a/lib/ui/compositing.dart +++ b/lib/ui/compositing.dart @@ -699,12 +699,12 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// texture just before resizing the Android view and un-freezes it when it is /// certain that a frame with the new size is ready. void addTexture( - int/*!*/ textureId, { - Offset/*!*/ offset = Offset.zero, - double/*!*/ width = 0.0, - double/*!*/ height = 0.0, - bool/*!*/ freeze = false, - FilterQuality/*!*/ filterQuality = FilterQuality.low, + int textureId, { + Offset offset = Offset.zero, + double width = 0.0, + double height = 0.0, + bool freeze = false, + FilterQuality filterQuality = FilterQuality.low, }) { assert(offset != null, 'Offset argument was null'); // ignore: unnecessary_null_comparison _addTexture(offset.dx, offset.dy, width, height, textureId, freeze, filterQuality.index); diff --git a/lib/ui/compositing/scene.cc b/lib/ui/compositing/scene.cc index f5403ecae9a61..96eac5885d083 100644 --- a/lib/ui/compositing/scene.cc +++ b/lib/ui/compositing/scene.cc @@ -8,6 +8,7 @@ #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/picture.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/window.h" #include "third_party/skia/include/core/SkImageInfo.h" #include "third_party/skia/include/core/SkSurface.h" @@ -41,12 +42,14 @@ Scene::Scene(std::shared_ptr rootLayer, uint32_t rasterizerTracingThreshold, bool checkerboardRasterCacheImages, bool checkerboardOffscreenLayers) { - auto viewport_metrics = UIDartState::Current()->window()->viewport_metrics(); + auto viewport_metrics = UIDartState::Current() + ->platform_configuration() + ->window() + ->viewport_metrics(); layer_tree_ = std::make_unique( SkISize::Make(viewport_metrics.physical_width, viewport_metrics.physical_height), - static_cast(viewport_metrics.physical_depth), static_cast(viewport_metrics.device_pixel_ratio)); layer_tree_->set_root_layer(std::move(rootLayer)); layer_tree_->set_rasterizer_tracing_threshold(rasterizerTracingThreshold); diff --git a/lib/ui/compositing/scene_builder.cc b/lib/ui/compositing/scene_builder.cc index 1c0fa9bd5597f..b60a5f1c67caa 100644 --- a/lib/ui/compositing/scene_builder.cc +++ b/lib/ui/compositing/scene_builder.cc @@ -220,7 +220,7 @@ void SceneBuilder::addPicture(double dx, pictureRect.offset(offset.x(), offset.y()); auto layer = std::make_unique( offset, UIDartState::CreateGPUObject(picture->picture()), !!(hints & 1), - !!(hints & 2)); + !!(hints & 2), picture->GetAllocationSize()); AddLayer(std::move(layer)); } diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc index a02bf3523d612..f9f89d6f22d67 100644 --- a/lib/ui/dart_ui.cc +++ b/lib/ui/dart_ui.cc @@ -30,7 +30,7 @@ #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/text/paragraph.h" #include "flutter/lib/ui/text/paragraph_builder.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "third_party/tonic/converter/dart_converter.h" #include "third_party/tonic/logging/dart_error.h" @@ -85,7 +85,7 @@ void DartUI::InitForGlobal() { SemanticsUpdate::RegisterNatives(g_natives); SemanticsUpdateBuilder::RegisterNatives(g_natives); Vertices::RegisterNatives(g_natives); - Window::RegisterNatives(g_natives); + PlatformConfiguration::RegisterNatives(g_natives); #if defined(LEGACY_FUCHSIA_EMBEDDER) SceneHost::RegisterNatives(g_natives); #endif diff --git a/lib/ui/fixtures/ui_test.dart b/lib/ui/fixtures/ui_test.dart index 717f9b7fd335f..478c919066562 100644 --- a/lib/ui/fixtures/ui_test.dart +++ b/lib/ui/fixtures/ui_test.dart @@ -3,6 +3,7 @@ // 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:typed_data'; import 'dart:ui'; @@ -34,6 +35,7 @@ void createVertices() { ); _validateVertices(vertices); } + void _validateVertices(Vertices vertices) native 'ValidateVertices'; @pragma('vm:entry-point') @@ -42,8 +44,10 @@ void frameCallback(FrameInfo info) { } @pragma('vm:entry-point') -void messageCallback(dynamic data) { -} +void messageCallback(dynamic data) {} + +@pragma('vm:entry-point') +void validateConfiguration() native 'ValidateConfiguration'; // Draw a circle on a Canvas that has a PictureRecorder. Take the image from @@ -70,3 +74,59 @@ Future encodeImageProducesExternalUint8List() async { void _encodeImage(Image i, int format, void Function(Uint8List result)) native 'EncodeImage'; void _validateExternal(Uint8List result) native 'ValidateExternal'; + +@pragma('vm:entry-point') +Future pumpImage() async { + final FrameCallback renderBlank = (Duration duration) { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawRect(Rect.largest, Paint()); + final Picture picture = recorder.endRecording(); + + final SceneBuilder builder = SceneBuilder(); + builder.addPicture(Offset.zero, picture); + + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + window.onBeginFrame = (Duration duration) { + window.onDrawFrame = _onBeginFrameDone; + }; + window.scheduleFrame(); + }; + + final FrameCallback renderImage = (Duration duration) { + const int width = 8000; + const int height = 8000; + final Completer completer = Completer(); + decodeImageFromPixels( + Uint8List.fromList(List.filled(width * height * 4, 0xFF)), + width, + height, + PixelFormat.rgba8888, + (Image image) => completer.complete(image), + ); + completer.future.then((Image image) { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawImage(image, Offset.zero, Paint()); + final Picture picture = recorder.endRecording(); + + final SceneBuilder builder = SceneBuilder(); + builder.addPicture(Offset.zero, picture); + + _captureImageAndPicture(image, picture); + + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + window.onBeginFrame = renderBlank; + window.scheduleFrame(); + }); + }; + + window.onBeginFrame = renderImage; + window.scheduleFrame(); +} +void _captureImageAndPicture(Image image, Picture picture) native 'CaptureImageAndPicture'; +Future _onBeginFrameDone() native 'OnBeginFrameDone'; diff --git a/lib/ui/geometry.dart b/lib/ui/geometry.dart index a7404996cd674..23829b3efaaca 100644 --- a/lib/ui/geometry.dart +++ b/lib/ui/geometry.dart @@ -656,7 +656,7 @@ class Rect { /// Constructs a rectangle from its center point, width, and height. /// /// The `center` argument is assumed to be an offset from the origin. - Rect.fromCenter({ required Offset center/*!*/, required double width, required double height }) : this.fromLTRB( + Rect.fromCenter({ required Offset center, required double width, required double height }) : this.fromLTRB( center.dx - width / 2, center.dy - height / 2, center.dx + width / 2, diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 39bab12406fb8..a911d79f63a8d 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -14,7 +14,6 @@ void _updateWindowMetrics( double devicePixelRatio, double width, double height, - double depth, double viewPaddingTop, double viewPaddingRight, double viewPaddingBottom, @@ -31,7 +30,6 @@ void _updateWindowMetrics( window .._devicePixelRatio = devicePixelRatio .._physicalSize = Size(width, height) - .._physicalDepth = depth .._viewPadding = WindowPadding._( top: viewPaddingTop, right: viewPaddingRight, @@ -200,7 +198,7 @@ void _reportTimings(List timings) { assert(timings.length % FramePhase.values.length == 0); final List frameTimings = []; for (int i = 0; i < timings.length; i += FramePhase.values.length) { - frameTimings.add(FrameTiming(timings.sublist(i, i + FramePhase.values.length))); + frameTimings.add(FrameTiming._(timings.sublist(i, i + FramePhase.values.length))); } _invoke1(window.onReportTimings, window._onReportTimingsZone, frameTimings); } @@ -238,7 +236,7 @@ void _runMainZoned(Function startMainIsolateFunction, }, null); } -void _reportUnhandledException(String error, String stackTrace) native 'Window_reportUnhandledException'; +void _reportUnhandledException(String error, String stackTrace) native 'PlatformConfiguration_reportUnhandledException'; /// Invokes [callback] inside the given [zone]. void _invoke(void callback()?, Zone zone) { diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 454c8c34c4a4d..449d11c171445 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -3191,8 +3191,8 @@ class Gradient extends Shader { List colors, [ List? colorStops, TileMode tileMode = TileMode.clamp, - double startAngle/*?*/ = 0.0, - double endAngle/*!*/ = math.pi * 2, + double startAngle = 0.0, + double endAngle = math.pi * 2, Float64List? matrix4, ]) : assert(_offsetIsValid(center)), assert(colors != null), // ignore: unnecessary_null_comparison @@ -4026,13 +4026,128 @@ class Canvas extends NativeFieldWrapperClass2 { List? paintObjects, ByteData paintData) native 'Canvas_drawVertices'; - /// Draws part of an image - the [atlas] - onto the canvas. + /// Draws many parts of an image - the [atlas] - onto the canvas. + /// + /// This method allows for optimization when you want to draw many parts of an + /// image onto the canvas, such as when using sprites or zooming. It is more efficient + /// than using multiple calls to [drawImageRect] and provides more functionality + /// to individually transform each image part by a separate rotation or scale and + /// blend or modulate those parts with a solid color. + /// + /// The method takes a list of [Rect] objects that each define a piece of the + /// [atlas] image to be drawn independently. Each [Rect] is associated with an + /// [RSTransform] entry in the [transforms] list which defines the location, + /// rotation, and (uniform) scale with which to draw that portion of the image. + /// Each [Rect] can also be associated with an optional [Color] which will be + /// composed with the associated image part using the [blendMode] before blending + /// the result onto the canvas. The full operation can be broken down as: + /// + /// - Blend each rectangular portion of the image specified by an entry in the + /// [rects] argument with its associated entry in the [colors] list using the + /// [blendMode] argument (if a color is specified). In this part of the operation, + /// the image part will be considered the source of the operation and the associated + /// color will be considered the destination. + /// - Blend the result from the first step onto the canvas using the translation, + /// rotation, and scale properties expressed in the associated entry in the + /// [transforms] list using the properties of the [Paint] object. + /// + /// If the first stage of the operation which blends each part of the image with + /// a color is needed, then both the [colors] and [blendMode] arguments must + /// not be null and there must be an entry in the [colors] list for each + /// image part. If that stage is not needed, then the [colors] argument can + /// be either null or an empty list and the [blendMode] argument may also be null. + /// + /// The optional [cullRect] argument can provide an estimate of the bounds of the + /// coordinates rendered by all components of the atlas to be compared against + /// the clip to quickly reject the operation if it does not intersect. + /// + /// An example usage to render many sprites from a single sprite atlas with no + /// rotations or scales: /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// } /// - /// All parameters must not be null. + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, [ + /// for (Sprite sprite in allSprites) + /// RSTransform.fromComponents( + /// rotation: 0.0, + /// scale: 1.0, + /// // Center of the sprite relative to its rect + /// anchorX: 5.0, + /// anchorY: 5.0, + /// // Location at which to draw the center of the sprite + /// translateX: sprite.centerX, + /// translateY: sprite.centerY, + /// ), + /// ], [ + /// for (Sprite sprite in allSprites) + /// Rect.fromLTWH(sprite.index * 10.0, 0.0, 10.0, 10.0), + /// ], null, null, null, paint); + /// } + /// + /// ... + /// } + /// ``` + /// + /// Another example usage which renders sprites with an optional opacity and rotation: + /// + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// int alpha; + /// double rotation; + /// } + /// + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, [ + /// for (Sprite sprite in allSprites) + /// RSTransform.fromComponents( + /// rotation: sprite.rotation, + /// scale: 1.0, + /// // Center of the sprite relative to its rect + /// anchorX: 5.0, + /// anchorY: 5.0, + /// // Location at which to draw the center of the sprite + /// translateX: sprite.centerX, + /// translateY: sprite.centerY, + /// ), + /// ], [ + /// for (Sprite sprite in allSprites) + /// Rect.fromLTWH(sprite.index * 10.0, 0.0, 10.0, 10.0), + /// ], [ + /// for (Sprite sprite in allSprites) + /// Color.white.withAlpha(sprite.alpha), + /// ], BlendMode.srcIn, null, paint); + /// } + /// + /// ... + /// } + /// ``` + /// + /// The length of the [transforms] and [rects] lists must be equal and + /// if the [colors] argument is not null then it must either be empty or + /// have the same length as the other two lists. /// /// See also: /// @@ -4041,22 +4156,21 @@ class Canvas extends NativeFieldWrapperClass2 { void drawAtlas(Image atlas, List transforms, List rects, - List colors, - BlendMode blendMode, + List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(transforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || colors.isEmpty || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; if (transforms.length != rectCount) throw ArgumentError('"transforms" and "rects" lengths must match.'); - if (colors.isNotEmpty && colors.length != rectCount) + if (colors != null && colors.isNotEmpty && colors.length != rectCount) throw ArgumentError('If non-null, "colors" length must match that of "transforms" and "rects".'); final Float32List rstTransformBuffer = Float32List(rectCount * 4); @@ -4080,20 +4194,27 @@ class Canvas extends NativeFieldWrapperClass2 { rectBuffer[index3] = rect.bottom; } - final Int32List? colorBuffer = colors.isEmpty ? null : _encodeColorList(colors); + final Int32List? colorBuffer = (colors == null || colors.isEmpty) ? null : _encodeColorList(colors); final Float32List? cullRectBuffer = cullRect?._value32; _drawAtlas( paint._objects, paint._data, atlas, rstTransformBuffer, rectBuffer, - colorBuffer, blendMode.index, cullRectBuffer + colorBuffer, (blendMode ?? BlendMode.src).index, cullRectBuffer ); } - /// Draws part of an image - the [atlas] - onto the canvas. + /// Draws many parts of an image - the [atlas] - onto the canvas. + /// + /// This method allows for optimization when you want to draw many parts of an + /// image onto the canvas, such as when using sprites or zooming. It is more efficient + /// than using multiple calls to [drawImageRect] and provides more functionality + /// to individually transform each image part by a separate rotation or scale and + /// blend or modulate those parts with a solid color. It is also more efficient + /// than [drawAtlas] as the data in the arguments is already packed in a format + /// that can be directly used by the rendering code. /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. + /// A full description of how this method uses its arguments to draw onto the + /// canvas can be found in the description of the [drawAtlas] method. /// /// The [rstTransforms] argument is interpreted as a list of four-tuples, with /// each tuple being ([RSTransform.scos], [RSTransform.ssin], @@ -4103,7 +4224,121 @@ class Canvas extends NativeFieldWrapperClass2 { /// tuple being ([Rect.left], [Rect.top], [Rect.right], [Rect.bottom]). /// /// The [colors] argument, which can be null, is interpreted as a list of - /// 32-bit colors, with the same packing as [Color.value]. + /// 32-bit colors, with the same packing as [Color.value]. If the [colors] + /// argument is not null then the [blendMode] argument must also not be null. + /// + /// An example usage to render many sprites from a single sprite atlas with no rotations + /// or scales: + /// + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// } + /// + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// // For best advantage, these lists should be cached and only specific + /// // entries updated when the sprite information changes. This code is + /// // illustrative of how to set up the data and not a recommendation for + /// // optimal usage. + /// Float32List rectList = Float32List(allSprites.length * 4); + /// Float32List transformList = Float32List(allSprites.length * 4); + /// for (int i = 0; i < allSprites.length; i++) { + /// final double rectX = sprite.spriteIndex * 10.0; + /// rectList[i * 4 + 0] = rectX; + /// rectList[i * 4 + 1] = 0.0; + /// rectList[i * 4 + 2] = rectX + 10.0; + /// rectList[i * 4 + 3] = 10.0; + /// + /// // This example sets the RSTransform values directly for a common case of no + /// // rotations or scales and just a translation to position the atlas entry. For + /// // more complicated transforms one could use the RSTransform class to compute + /// // the necessary values or do the same math directly. + /// transformList[i * 4 + 0] = 1.0; + /// transformList[i * 4 + 1] = 0.0; + /// transformList[i * 4 + 2] = sprite.centerX - 5.0; + /// transformList[i * 4 + 2] = sprite.centerY - 5.0; + /// } + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, transformList, rectList, null, null, null, paint); + /// } + /// + /// ... + /// } + /// ``` + /// + /// Another example usage which renders sprites with an optional opacity and rotation: + /// + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// int alpha; + /// double rotation; + /// } + /// + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// // For best advantage, these lists should be cached and only specific + /// // entries updated when the sprite information changes. This code is + /// // illustrative of how to set up the data and not a recommendation for + /// // optimal usage. + /// Float32List rectList = Float32List(allSprites.length * 4); + /// Float32List transformList = Float32List(allSprites.length * 4); + /// Int32List colorList = Int32List(allSprites.length); + /// for (int i = 0; i < allSprites.length; i++) { + /// final double rectX = sprite.spriteIndex * 10.0; + /// rectList[i * 4 + 0] = rectX; + /// rectList[i * 4 + 1] = 0.0; + /// rectList[i * 4 + 2] = rectX + 10.0; + /// rectList[i * 4 + 3] = 10.0; + /// + /// // This example uses an RSTransform object to compute the necessary values for + /// // the transform using a factory helper method because the sprites contain + /// // rotation values which are not trivial to work with. But if the math for the + /// // values falls out from other calculations on the sprites then the values could + /// // possibly be generated directly from the sprite update code. + /// final RSTransform transform = RSTransform.fromComponents( + /// rotation: sprite.rotation, + /// scale: 1.0, + /// // Center of the sprite relative to its rect + /// anchorX: 5.0, + /// anchorY: 5.0, + /// // Location at which to draw the center of the sprite + /// translateX: sprite.centerX, + /// translateY: sprite.centerY, + /// ); + /// transformList[i * 4 + 0] = transform.scos; + /// transformList[i * 4 + 1] = transform.ssin; + /// transformList[i * 4 + 2] = transform.tx; + /// transformList[i * 4 + 2] = transform.ty; + /// + /// // This example computes the color value directly, but one could also compute + /// // an actual Color object and use its Color.value getter for the same result. + /// // Since we are using BlendMode.srcIn, only the alpha component matters for + /// // these colors which makes this a simple shift operation. + /// colorList[i] = sprite.alpha << 24; + /// } + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, transformList, rectList, colorList, BlendMode.srcIn, null, paint); + /// } + /// + /// ... + /// } + /// ``` /// /// See also: /// @@ -4112,16 +4347,15 @@ class Canvas extends NativeFieldWrapperClass2 { void drawRawAtlas(Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - BlendMode blendMode, + Int32List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(rstTransforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; @@ -4129,12 +4363,12 @@ class Canvas extends NativeFieldWrapperClass2 { throw ArgumentError('"rstTransforms" and "rects" lengths must match.'); if (rectCount % 4 != 0) throw ArgumentError('"rstTransforms" and "rects" lengths must be a multiple of four.'); - if (colors.length * 4 != rectCount) + if (colors != null && colors.length * 4 != rectCount) throw ArgumentError('If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); _drawAtlas( paint._objects, paint._data, atlas, rstTransforms, rects, - colors, blendMode.index, cullRect?._value32 + colors, (blendMode ?? BlendMode.src).index, cullRect?._value32 ); } diff --git a/lib/ui/painting/canvas.cc b/lib/ui/painting/canvas.cc index be28e8c964bbc..c68f0d6001ad6 100644 --- a/lib/ui/painting/canvas.cc +++ b/lib/ui/painting/canvas.cc @@ -11,6 +11,7 @@ #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/matrix.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/window.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkCanvas.h" @@ -326,7 +327,6 @@ void Canvas::drawImage(const CanvasImage* image, ToDart("Canvas.drawImage called with non-genuine Image.")); return; } - external_allocation_size_ += image->GetAllocationSize(); canvas_->drawImage(image->image(), x, y, paint.paint()); } @@ -351,7 +351,6 @@ void Canvas::drawImageRect(const CanvasImage* image, } SkRect src = SkRect::MakeLTRB(src_left, src_top, src_right, src_bottom); SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom); - external_allocation_size_ += image->GetAllocationSize(); canvas_->drawImageRect(image->image(), src, dst, paint.paint(), SkCanvas::kFast_SrcRectConstraint); } @@ -380,7 +379,6 @@ void Canvas::drawImageNine(const CanvasImage* image, SkIRect icenter; center.round(&icenter); SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom); - external_allocation_size_ += image->GetAllocationSize(); canvas_->drawImageNine(image->image(), icenter, dst, paint.paint()); } @@ -474,18 +472,21 @@ void Canvas::drawShadow(const CanvasPath* path, ToDart("Canvas.drawShader called with non-genuine Path.")); return; } - SkScalar dpr = - UIDartState::Current()->window()->viewport_metrics().device_pixel_ratio; + SkScalar dpr = UIDartState::Current() + ->platform_configuration() + ->window() + ->viewport_metrics() + .device_pixel_ratio; external_allocation_size_ += path->path().approximateBytesUsed(); flutter::PhysicalShapeLayer::DrawShadow(canvas_, path->path(), color, elevation, transparentOccluder, dpr); } void Canvas::Invalidate() { + canvas_ = nullptr; if (dart_wrapper()) { ClearDartWrapper(); } - canvas_ = nullptr; } } // namespace flutter diff --git a/lib/ui/painting/image.cc b/lib/ui/painting/image.cc index 126205530fa4a..7da7c0ad029ec 100644 --- a/lib/ui/painting/image.cc +++ b/lib/ui/painting/image.cc @@ -37,8 +37,8 @@ Dart_Handle CanvasImage::toByteData(int format, Dart_Handle callback) { } void CanvasImage::dispose() { - ClearDartWrapper(); image_.reset(); + ClearDartWrapper(); } size_t CanvasImage::GetAllocationSize() const { diff --git a/lib/ui/painting/multi_frame_codec.cc b/lib/ui/painting/multi_frame_codec.cc index 4965bd924b215..c40561836011a 100644 --- a/lib/ui/painting/multi_frame_codec.cc +++ b/lib/ui/painting/multi_frame_codec.cc @@ -75,7 +75,7 @@ static bool CopyToBitmap(SkBitmap* dst, } sk_sp MultiFrameCodec::State::GetNextFrameImage( - fml::WeakPtr resourceContext) { + fml::WeakPtr resourceContext) { SkBitmap bitmap = SkBitmap(); SkImageInfo info = generator_->getInfo().makeColorType(kN32_SkColorType); if (info.alphaType() == kUnpremul_SkAlphaType) { @@ -136,7 +136,7 @@ sk_sp MultiFrameCodec::State::GetNextFrameImage( void MultiFrameCodec::State::GetNextFrameAndInvokeCallback( std::unique_ptr callback, fml::RefPtr ui_task_runner, - fml::WeakPtr resourceContext, + fml::WeakPtr resourceContext, fml::RefPtr unref_queue, size_t trace_id) { fml::RefPtr frameInfo = NULL; diff --git a/lib/ui/painting/multi_frame_codec.h b/lib/ui/painting/multi_frame_codec.h index 5843876046e31..428d67c0bfeda 100644 --- a/lib/ui/painting/multi_frame_codec.h +++ b/lib/ui/painting/multi_frame_codec.h @@ -53,12 +53,13 @@ class MultiFrameCodec : public Codec { // The index of the last decoded required frame. int lastRequiredFrameIndex_ = -1; - sk_sp GetNextFrameImage(fml::WeakPtr resourceContext); + sk_sp GetNextFrameImage( + fml::WeakPtr resourceContext); void GetNextFrameAndInvokeCallback( std::unique_ptr callback, fml::RefPtr ui_task_runner, - fml::WeakPtr resourceContext, + fml::WeakPtr resourceContext, fml::RefPtr unref_queue, size_t trace_id); }; diff --git a/lib/ui/painting/picture.cc b/lib/ui/painting/picture.cc index 48dd11226cf26..1285a6b0921cb 100644 --- a/lib/ui/painting/picture.cc +++ b/lib/ui/painting/picture.cc @@ -56,8 +56,8 @@ Dart_Handle Picture::toImage(uint32_t width, } void Picture::dispose() { - ClearDartWrapper(); picture_.reset(); + ClearDartWrapper(); } size_t Picture::GetAllocationSize() const { diff --git a/lib/ui/text/font_collection.cc b/lib/ui/text/font_collection.cc index 38d931b402ea7..7405941da41ed 100644 --- a/lib/ui/text/font_collection.cc +++ b/lib/ui/text/font_collection.cc @@ -8,7 +8,7 @@ #include "flutter/lib/ui/text/asset_manager_font_provider.h" #include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/runtime/test_font_data.h" #include "rapidjson/document.h" #include "rapidjson/rapidjson.h" @@ -30,8 +30,10 @@ namespace { void LoadFontFromList(tonic::Uint8List& font_data, // NOLINT Dart_Handle callback, std::string family_name) { - FontCollection& font_collection = - UIDartState::Current()->window()->client()->GetFontCollection(); + FontCollection& font_collection = UIDartState::Current() + ->platform_configuration() + ->client() + ->GetFontCollection(); font_collection.LoadFontFromList(font_data.data(), font_data.num_elements(), family_name); font_data.Release(); diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 396ce50f5cfca..679fc887ac89a 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -10,7 +10,7 @@ #include "flutter/fml/task_runner.h" #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/third_party/txt/src/txt/font_style.h" #include "flutter/third_party/txt/src/txt/font_weight.h" #include "flutter/third_party/txt/src/txt/paragraph_style.h" @@ -288,8 +288,10 @@ ParagraphBuilder::ParagraphBuilder( style.locale = locale; } - FontCollection& font_collection = - UIDartState::Current()->window()->client()->GetFontCollection(); + FontCollection& font_collection = UIDartState::Current() + ->platform_configuration() + ->client() + ->GetFontCollection(); #if FLUTTER_ENABLE_SKSHAPER #define FLUTTER_PARAGRAPH_BUILDER txt::ParagraphBuilder::CreateSkiaBuilder diff --git a/lib/ui/ui_dart_state.cc b/lib/ui/ui_dart_state.cc index 1bd00e35ae972..b43a442f849e0 100644 --- a/lib/ui/ui_dart_state.cc +++ b/lib/ui/ui_dart_state.cc @@ -5,7 +5,7 @@ #include "flutter/lib/ui/ui_dart_state.h" #include "flutter/fml/message_loop.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "third_party/tonic/converter/dart_converter.h" #include "third_party/tonic/dart_message_handler.h" @@ -73,8 +73,9 @@ void UIDartState::ThrowIfUIOperationsProhibited() { void UIDartState::SetDebugName(const std::string debug_name) { debug_name_ = debug_name; - if (window_) { - window_->client()->UpdateIsolateDescription(debug_name_, main_port_); + if (platform_configuration_) { + platform_configuration_->client()->UpdateIsolateDescription(debug_name_, + main_port_); } } @@ -82,10 +83,12 @@ UIDartState* UIDartState::Current() { return static_cast(DartState::Current()); } -void UIDartState::SetWindow(std::unique_ptr window) { - window_ = std::move(window); - if (window_) { - window_->client()->UpdateIsolateDescription(debug_name_, main_port_); +void UIDartState::SetPlatformConfiguration( + std::unique_ptr platform_configuration) { + platform_configuration_ = std::move(platform_configuration); + if (platform_configuration_) { + platform_configuration_->client()->UpdateIsolateDescription(debug_name_, + main_port_); } } @@ -133,7 +136,7 @@ fml::WeakPtr UIDartState::GetSnapshotDelegate() const { return snapshot_delegate_; } -fml::WeakPtr UIDartState::GetResourceContext() const { +fml::WeakPtr UIDartState::GetResourceContext() const { if (!io_manager_) { return {}; } diff --git a/lib/ui/ui_dart_state.h b/lib/ui/ui_dart_state.h index fd21146d24dd4..71755931a30d1 100644 --- a/lib/ui/ui_dart_state.h +++ b/lib/ui/ui_dart_state.h @@ -27,7 +27,7 @@ namespace flutter { class FontSelector; -class Window; +class PlatformConfiguration; class UIDartState : public tonic::DartState { public: @@ -44,7 +44,9 @@ class UIDartState : public tonic::DartState { const std::string& logger_prefix() const { return logger_prefix_; } - Window* window() const { return window_.get(); } + PlatformConfiguration* platform_configuration() const { + return platform_configuration_.get(); + } const TaskRunners& GetTaskRunners() const; @@ -58,7 +60,7 @@ class UIDartState : public tonic::DartState { fml::WeakPtr GetSnapshotDelegate() const; - fml::WeakPtr GetResourceContext() const; + fml::WeakPtr GetResourceContext() const; fml::WeakPtr GetImageDecoder() const; @@ -97,7 +99,8 @@ class UIDartState : public tonic::DartState { ~UIDartState() override; - void SetWindow(std::unique_ptr window); + void SetPlatformConfiguration( + std::unique_ptr platform_configuration); const std::string& GetAdvisoryScriptURI() const; @@ -119,7 +122,7 @@ class UIDartState : public tonic::DartState { Dart_Port main_port_ = ILLEGAL_PORT; const bool is_root_isolate_; std::string debug_name_; - std::unique_ptr window_; + std::unique_ptr platform_configuration_; tonic::DartMicrotaskQueue microtask_queue_; UnhandledExceptionCallback unhandled_exception_callback_; const std::shared_ptr isolate_name_server_; diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 5d99e42781fd6..a39c5407ad45e 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -47,6 +47,11 @@ typedef _SetNeedsReportTimingsFunc = void Function(bool value); /// /// [FrameTiming] records a timestamp of each phase for performance analysis. enum FramePhase { + /// The timestamp of the vsync signal given by the operating system. + /// + /// See also [FrameTiming.vsyncOverhead]. + vsyncStart, + /// When the UI thread starts building a frame. /// /// See also [FrameTiming.buildDuration]. @@ -82,6 +87,26 @@ enum FramePhase { /// Therefore it's recommended to only monitor and analyze performance metrics /// in profile and release modes. class FrameTiming { + /// Construct [FrameTiming] with raw timestamps in microseconds. + /// + /// This constructor is used for unit test only. Real [FrameTiming]s should + /// be retrieved from [Window.onReportTimings]. + factory FrameTiming({ + required int vsyncStart, + required int buildStart, + required int buildFinish, + required int rasterStart, + required int rasterFinish, + }) { + return FrameTiming._([ + vsyncStart, + buildStart, + buildFinish, + rasterStart, + rasterFinish + ]); + } + /// Construct [FrameTiming] with raw timestamps in microseconds. /// /// List [timestamps] must have the same number of elements as @@ -89,7 +114,7 @@ class FrameTiming { /// /// This constructor is usually only called by the Flutter engine, or a test. /// To get the [FrameTiming] of your app, see [Window.onReportTimings]. - FrameTiming(List timestamps) + FrameTiming._(List timestamps) : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; /// This is a raw timestamp in microseconds from some epoch. The epoch in all @@ -121,14 +146,18 @@ class FrameTiming { /// {@macro dart.ui.FrameTiming.fps_milliseconds} Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart); - /// The timespan between build start and raster finish. + /// The duration between receiving the vsync signal and starting building the + /// frame. + Duration get vsyncOverhead => _rawDuration(FramePhase.buildStart) - _rawDuration(FramePhase.vsyncStart); + + /// The timespan between vsync start and raster finish. /// /// To achieve the lowest latency on an X fps display, this should not exceed /// 1000/X milliseconds. /// {@macro dart.ui.FrameTiming.fps_milliseconds} /// - /// See also [buildDuration] and [rasterDuration]. - Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.buildStart); + /// See also [vsyncOverhead], [buildDuration] and [rasterDuration]. + Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.vsyncStart); final List _timestamps; // in microseconds @@ -136,7 +165,7 @@ class FrameTiming { @override String toString() { - return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, totalSpan: ${_formatMS(totalSpan)})'; + return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, vsyncOverhead: ${_formatMS(vsyncOverhead)}, totalSpan: ${_formatMS(totalSpan)})'; } } @@ -627,20 +656,6 @@ class Window { Size get physicalSize => _physicalSize; Size _physicalSize = Size.zero; - /// The physical depth is the maximum elevation that the Window allows. - /// - /// Physical layers drawn at or above this elevation will have their elevation - /// clamped to this value. This can happen if the physical layer itself has - /// an elevation larger than available depth, or if some ancestor of the layer - /// causes it to have a cumulative elevation that is larger than the available - /// depth. - /// - /// The default value is [double.maxFinite], which is used for platforms that - /// do not specify a maximum elevation. This property is currently on expected - /// to be set to a non-default value on Fuchsia. - double get physicalDepth => _physicalDepth; - double _physicalDepth = double.maxFinite; - /// The number of physical pixels on each side of the display rectangle into /// which the application can render, but over which the operating system /// will likely place system UI, such as the keyboard, that fully obscures @@ -823,7 +838,7 @@ class Window { } return null; } - List _computePlatformResolvedLocale(List supportedLocalesData) native 'Window_computePlatformResolvedLocale'; + List _computePlatformResolvedLocale(List supportedLocalesData) native 'PlatformConfiguration_computePlatformResolvedLocale'; /// A callback that is invoked whenever [locale] changes value. /// @@ -1001,7 +1016,7 @@ class Window { } late _SetNeedsReportTimingsFunc _setNeedsReportTimings; - void _nativeSetNeedsReportTimings(bool value) native 'Window_setNeedsReportTimings'; + void _nativeSetNeedsReportTimings(bool value) native 'PlatformConfiguration_setNeedsReportTimings'; /// A callback that is invoked when pointer data is available. /// @@ -1027,23 +1042,19 @@ class Window { /// /// ## Android /// - /// On Android, calling - /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `createFlutterView` method in your `FlutterActivity` - /// subclass is a suitable time to set the value. The application's - /// `AndroidManifest.xml` file must also be updated to have a suitable - /// [``](https://developer.android.com/guide/topics/manifest/intent-filter-element.html). + /// On Android, the initial route can be set on the [initialRoute](/javadoc/io/flutter/embedding/android/FlutterActivity.NewEngineIntentBuilder.html#initialRoute-java.lang.String-) + /// method of the [FlutterActivity](/javadoc/io/flutter/embedding/android/FlutterActivity.html)'s + /// intent builder. + /// + /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen#initial-route-with-a-cached-engine. /// /// ## iOS /// - /// On iOS, calling - /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `application:didFinishLaunchingWithOptions:` method is a - /// suitable time to set this value. + /// On iOS, the initial route can be set on the `initialRoute` + /// parameter of the [FlutterViewController](/objcdoc/Classes/FlutterViewController.html)'s + /// initializer. + /// + /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/ios/add-flutter-screen#route. /// /// See also: /// @@ -1051,7 +1062,7 @@ class Window { /// * [SystemChannels.navigation], which handles subsequent navigation /// requests from the embedder. String get defaultRouteName => _defaultRouteName(); - String _defaultRouteName() native 'Window_defaultRouteName'; + String _defaultRouteName() native 'PlatformConfiguration_defaultRouteName'; /// Requests that, at the next appropriate opportunity, the [onBeginFrame] /// and [onDrawFrame] callbacks be invoked. @@ -1060,7 +1071,7 @@ class Window { /// /// * [SchedulerBinding], the Flutter framework class which manages the /// scheduling of frames. - void scheduleFrame() native 'Window_scheduleFrame'; + void scheduleFrame() native 'PlatformConfiguration_scheduleFrame'; /// Updates the application's rendering on the GPU with the newly provided /// [Scene]. This function must be called within the scope of the @@ -1086,7 +1097,7 @@ class Window { /// scheduling of frames. /// * [RendererBinding], the Flutter framework class which manages layout and /// painting. - void render(Scene scene) native 'Window_render'; + void render(Scene scene) native 'PlatformConfiguration_render'; /// Whether the user has requested that [updateSemantics] be called when /// the semantic contents of window changes. @@ -1126,7 +1137,7 @@ class Window { /// Additional accessibility features that may be enabled by the platform. AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; - // The zero value matches the default value in `window_data.h`. + // The zero value matches the default value in `platform_data.h`. AccessibilityFeatures _accessibilityFeatures = const AccessibilityFeatures._(0); /// A callback that is invoked when the value of [accessibilityFeatures] changes. @@ -1148,7 +1159,7 @@ class Window { /// /// In either case, this function disposes the given update, which means the /// semantics update cannot be used further. - void updateSemantics(SemanticsUpdate update) native 'Window_updateSemantics'; + void updateSemantics(SemanticsUpdate update) native 'PlatformConfiguration_updateSemantics'; /// Set the debug name associated with this window's root isolate. /// @@ -1158,7 +1169,7 @@ class Window { /// This can be combined with flutter tools `--isolate-filter` flag to debug /// specific root isolates. For example: `flutter attach --isolate-filter=[name]`. /// Note that this does not rename any child isolates of the root. - void setIsolateDebugName(String name) native 'Window_setIsolateDebugName'; + void setIsolateDebugName(String name) native 'PlatformConfiguration_setIsolateDebugName'; /// Sends a message to a platform-specific plugin. /// @@ -1179,7 +1190,7 @@ class Window { } String? _sendPlatformMessage(String name, PlatformMessageResponseCallback? callback, - ByteData? data) native 'Window_sendPlatformMessage'; + ByteData? data) native 'PlatformConfiguration_sendPlatformMessage'; /// Called whenever this window receives a message from a platform-specific /// plugin. @@ -1204,7 +1215,7 @@ class Window { /// Called by [_dispatchPlatformMessage]. void _respondToPlatformMessage(int responseId, ByteData? data) - native 'Window_respondToPlatformMessage'; + native 'PlatformConfiguration_respondToPlatformMessage'; /// Wraps the given [callback] in another callback that ensures that the /// original callback is called in the zone it was registered in. @@ -1230,7 +1241,7 @@ class Window { /// /// For asynchronous communication between the embedder and isolate, a /// platform channel may be used. - ByteData? getPersistentIsolateData() native 'Window_getPersistentIsolateData'; + ByteData? getPersistentIsolateData() native 'PlatformConfiguration_getPersistentIsolateData'; } /// Additional accessibility features that may be enabled by the platform. diff --git a/lib/ui/window/platform_configuration.cc b/lib/ui/window/platform_configuration.cc new file mode 100644 index 0000000000000..3ce180e3bce71 --- /dev/null +++ b/lib/ui/window/platform_configuration.cc @@ -0,0 +1,437 @@ +// 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. + +#include "flutter/lib/ui/window/platform_configuration.h" + +#include "flutter/lib/ui/compositing/scene.h" +#include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_message_response_dart.h" +#include "flutter/lib/ui/window/window.h" +#include "third_party/tonic/converter/dart_converter.h" +#include "third_party/tonic/dart_args.h" +#include "third_party/tonic/dart_library_natives.h" +#include "third_party/tonic/dart_microtask_queue.h" +#include "third_party/tonic/logging/dart_invoke.h" +#include "third_party/tonic/typed_data/dart_byte_data.h" + +namespace flutter { +namespace { + +void DefaultRouteName(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + std::string routeName = UIDartState::Current() + ->platform_configuration() + ->client() + ->DefaultRouteName(); + Dart_SetReturnValue(args, tonic::StdStringToDart(routeName)); +} + +void ScheduleFrame(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + UIDartState::Current()->platform_configuration()->client()->ScheduleFrame(); +} + +void Render(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + Scene* scene = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + UIDartState::Current()->platform_configuration()->client()->Render(scene); +} + +void UpdateSemantics(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + SemanticsUpdate* update = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + UIDartState::Current()->platform_configuration()->client()->UpdateSemantics( + update); +} + +void SetIsolateDebugName(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + const std::string name = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + UIDartState::Current()->SetDebugName(name); +} + +void SetNeedsReportTimings(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + bool value = tonic::DartConverter::FromArguments(args, 1, exception); + UIDartState::Current() + ->platform_configuration() + ->client() + ->SetNeedsReportTimings(value); +} + +void ReportUnhandledException(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + + Dart_Handle exception = nullptr; + + auto error_name = + tonic::DartConverter::FromArguments(args, 0, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + + auto stack_trace = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + + UIDartState::Current()->ReportUnhandledException(std::move(error_name), + std::move(stack_trace)); +} + +Dart_Handle SendPlatformMessage(Dart_Handle window, + const std::string& name, + Dart_Handle callback, + Dart_Handle data_handle) { + UIDartState* dart_state = UIDartState::Current(); + + if (!dart_state->platform_configuration()) { + return tonic::ToDart( + "Platform messages can only be sent from the main isolate"); + } + + fml::RefPtr response; + if (!Dart_IsNull(callback)) { + response = fml::MakeRefCounted( + tonic::DartPersistentValue(dart_state, callback), + dart_state->GetTaskRunners().GetUITaskRunner()); + } + if (Dart_IsNull(data_handle)) { + dart_state->platform_configuration()->client()->HandlePlatformMessage( + fml::MakeRefCounted(name, response)); + } else { + tonic::DartByteData data(data_handle); + const uint8_t* buffer = static_cast(data.data()); + dart_state->platform_configuration()->client()->HandlePlatformMessage( + fml::MakeRefCounted( + name, std::vector(buffer, buffer + data.length_in_bytes()), + response)); + } + + return Dart_Null(); +} + +void _SendPlatformMessage(Dart_NativeArguments args) { + tonic::DartCallStatic(&SendPlatformMessage, args); +} + +void RespondToPlatformMessage(Dart_Handle window, + int response_id, + const tonic::DartByteData& data) { + if (Dart_IsNull(data.dart_handle())) { + UIDartState::Current() + ->platform_configuration() + ->CompletePlatformMessageEmptyResponse(response_id); + } else { + // TODO(engine): Avoid this copy. + const uint8_t* buffer = static_cast(data.data()); + UIDartState::Current() + ->platform_configuration() + ->CompletePlatformMessageResponse( + response_id, + std::vector(buffer, buffer + data.length_in_bytes())); + } +} + +void _RespondToPlatformMessage(Dart_NativeArguments args) { + tonic::DartCallStatic(&RespondToPlatformMessage, args); +} + +void GetPersistentIsolateData(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + + auto persistent_isolate_data = UIDartState::Current() + ->platform_configuration() + ->client() + ->GetPersistentIsolateData(); + + if (!persistent_isolate_data) { + Dart_SetReturnValue(args, Dart_Null()); + return; + } + + Dart_SetReturnValue( + args, tonic::DartByteData::Create(persistent_isolate_data->GetMapping(), + persistent_isolate_data->GetSize())); +} + +Dart_Handle ToByteData(const std::vector& buffer) { + return tonic::DartByteData::Create(buffer.data(), buffer.size()); +} + +} // namespace + +PlatformConfigurationClient::~PlatformConfigurationClient() {} + +PlatformConfiguration::PlatformConfiguration( + PlatformConfigurationClient* client) + : client_(client) {} + +PlatformConfiguration::~PlatformConfiguration() {} + +void PlatformConfiguration::DidCreateIsolate() { + library_.Set(tonic::DartState::Current(), + Dart_LookupLibrary(tonic::ToDart("dart:ui"))); + window_.reset(new Window({1.0, 0.0, 0.0})); +} + +void PlatformConfiguration::UpdateLocales( + const std::vector& locales) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_updateLocales", + { + tonic::ToDart>(locales), + })); +} + +void PlatformConfiguration::UpdateUserSettingsData(const std::string& data) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), + "_updateUserSettingsData", + { + tonic::StdStringToDart(data), + })); +} + +void PlatformConfiguration::UpdateLifecycleState(const std::string& data) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + tonic::LogIfError(tonic::DartInvokeField(library_.value(), + "_updateLifecycleState", + { + tonic::StdStringToDart(data), + })); +} + +void PlatformConfiguration::UpdateSemanticsEnabled(bool enabled) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + UIDartState::ThrowIfUIOperationsProhibited(); + + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_updateSemanticsEnabled", {tonic::ToDart(enabled)})); +} + +void PlatformConfiguration::UpdateAccessibilityFeatures(int32_t values) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), + "_updateAccessibilityFeatures", + {tonic::ToDart(values)})); +} + +void PlatformConfiguration::DispatchPlatformMessage( + fml::RefPtr message) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + FML_DLOG(WARNING) + << "Dropping platform message for lack of DartState on channel: " + << message->channel(); + return; + } + tonic::DartState::Scope scope(dart_state); + Dart_Handle data_handle = + (message->hasData()) ? ToByteData(message->data()) : Dart_Null(); + if (Dart_IsError(data_handle)) { + FML_DLOG(WARNING) + << "Dropping platform message because of a Dart error on channel: " + << message->channel(); + return; + } + + int response_id = 0; + if (auto response = message->response()) { + response_id = next_response_id_++; + pending_responses_[response_id] = response; + } + + tonic::LogIfError( + tonic::DartInvokeField(library_.value(), "_dispatchPlatformMessage", + {tonic::ToDart(message->channel()), data_handle, + tonic::ToDart(response_id)})); +} + +void PlatformConfiguration::DispatchSemanticsAction(int32_t id, + SemanticsAction action, + std::vector args) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + Dart_Handle args_handle = (args.empty()) ? Dart_Null() : ToByteData(args); + + if (Dart_IsError(args_handle)) { + return; + } + + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_dispatchSemanticsAction", + {tonic::ToDart(id), tonic::ToDart(static_cast(action)), + args_handle})); +} + +void PlatformConfiguration::BeginFrame(fml::TimePoint frameTime) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + int64_t microseconds = (frameTime - fml::TimePoint()).ToMicroseconds(); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_beginFrame", + { + Dart_NewInteger(microseconds), + })); + + UIDartState::Current()->FlushMicrotasksNow(); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_drawFrame", {})); +} + +void PlatformConfiguration::ReportTimings(std::vector timings) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + Dart_Handle data_handle = + Dart_NewTypedData(Dart_TypedData_kInt64, timings.size()); + + Dart_TypedData_Type type; + void* data = nullptr; + intptr_t num_acquired = 0; + FML_CHECK(!Dart_IsError( + Dart_TypedDataAcquireData(data_handle, &type, &data, &num_acquired))); + FML_DCHECK(num_acquired == static_cast(timings.size())); + + memcpy(data, timings.data(), sizeof(int64_t) * timings.size()); + FML_CHECK(Dart_TypedDataReleaseData(data_handle)); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_reportTimings", + { + data_handle, + })); +} + +void PlatformConfiguration::CompletePlatformMessageEmptyResponse( + int response_id) { + if (!response_id) { + return; + } + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) { + return; + } + auto response = std::move(it->second); + pending_responses_.erase(it); + response->CompleteEmpty(); +} + +void PlatformConfiguration::CompletePlatformMessageResponse( + int response_id, + std::vector data) { + if (!response_id) { + return; + } + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) { + return; + } + auto response = std::move(it->second); + pending_responses_.erase(it); + response->Complete(std::make_unique(std::move(data))); +} + +Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) { + std::vector supportedLocales = + tonic::DartConverter>::FromDart( + supportedLocalesHandle); + + std::vector results = + *UIDartState::Current() + ->platform_configuration() + ->client() + ->ComputePlatformResolvedLocale(supportedLocales); + + return tonic::DartConverter>::ToDart(results); +} + +static void _ComputePlatformResolvedLocale(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle result = + ComputePlatformResolvedLocale(Dart_GetNativeArgument(args, 1)); + Dart_SetReturnValue(args, result); +} + +void PlatformConfiguration::RegisterNatives( + tonic::DartLibraryNatives* natives) { + natives->Register({ + {"PlatformConfiguration_defaultRouteName", DefaultRouteName, 1, true}, + {"PlatformConfiguration_scheduleFrame", ScheduleFrame, 1, true}, + {"PlatformConfiguration_sendPlatformMessage", _SendPlatformMessage, 4, + true}, + {"PlatformConfiguration_respondToPlatformMessage", + _RespondToPlatformMessage, 3, true}, + {"PlatformConfiguration_render", Render, 2, true}, + {"PlatformConfiguration_updateSemantics", UpdateSemantics, 2, true}, + {"PlatformConfiguration_setIsolateDebugName", SetIsolateDebugName, 2, + true}, + {"PlatformConfiguration_reportUnhandledException", + ReportUnhandledException, 2, true}, + {"PlatformConfiguration_setNeedsReportTimings", SetNeedsReportTimings, 2, + true}, + {"PlatformConfiguration_getPersistentIsolateData", + GetPersistentIsolateData, 1, true}, + {"PlatformConfiguration_computePlatformResolvedLocale", + _ComputePlatformResolvedLocale, 2, true}, + }); +} + +} // namespace flutter diff --git a/lib/ui/window/platform_configuration.h b/lib/ui/window/platform_configuration.h new file mode 100644 index 0000000000000..4652c7c5dcdbb --- /dev/null +++ b/lib/ui/window/platform_configuration.h @@ -0,0 +1,421 @@ +// 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. + +#ifndef FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ +#define FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ + +#include +#include +#include +#include +#include + +#include "flutter/fml/time/time_point.h" +#include "flutter/lib/ui/semantics/semantics_update.h" +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/lib/ui/window/pointer_data_packet.h" +#include "flutter/lib/ui/window/viewport_metrics.h" +#include "flutter/lib/ui/window/window.h" +#include "third_party/tonic/dart_persistent_value.h" + +namespace tonic { +class DartLibraryNatives; + +// So tonic::ToDart> returns List instead of +// List. +template <> +struct DartListFactory { + static Dart_Handle NewList(intptr_t length) { + return Dart_NewListOf(Dart_CoreType_Int, length); + } +}; + +} // namespace tonic + +namespace flutter { +class FontCollection; +class PlatformMessage; +class Scene; + +//-------------------------------------------------------------------------- +/// @brief An enum for defining the different kinds of accessibility features +/// that can be enabled by the platform. +/// +/// Must match the `AccessibilityFeatureFlag` enum in framework. +enum class AccessibilityFeatureFlag : int32_t { + kAccessibleNavigation = 1 << 0, + kInvertColors = 1 << 1, + kDisableAnimations = 1 << 2, + kBoldText = 1 << 3, + kReduceMotion = 1 << 4, + kHighContrast = 1 << 5, +}; + +//-------------------------------------------------------------------------- +/// @brief A client interface that the `RuntimeController` uses to define +/// handlers for `PlatformConfiguration` requests. +/// +/// @see `PlatformConfiguration` +/// +class PlatformConfigurationClient { + public: + //-------------------------------------------------------------------------- + /// @brief The route or path that the embedder requested when the + /// application was launched. + /// + /// This will be the string "`/`" if no particular route was + /// requested. + /// + virtual std::string DefaultRouteName() = 0; + + //-------------------------------------------------------------------------- + /// @brief Requests that, at the next appropriate opportunity, a new + /// frame be scheduled for rendering. + /// + virtual void ScheduleFrame() = 0; + + //-------------------------------------------------------------------------- + /// @brief Updates the client's rendering on the GPU with the newly + /// provided Scene. + /// + virtual void Render(Scene* scene) = 0; + + //-------------------------------------------------------------------------- + /// @brief Receives a updated semantics tree from the Framework. + /// + /// @param[in] update The updated semantic tree to apply. + /// + virtual void UpdateSemantics(SemanticsUpdate* update) = 0; + + //-------------------------------------------------------------------------- + /// @brief When the Flutter application has a message to send to the + /// underlying platform, the message needs to be forwarded to + /// the platform on the appropriate thread (via the platform + /// task runner). The PlatformConfiguration delegates this task + /// to the engine via this method. + /// + /// @see `PlatformView::HandlePlatformMessage` + /// + /// @param[in] message The message from the Flutter application to send to + /// the underlying platform. + /// + virtual void HandlePlatformMessage(fml::RefPtr message) = 0; + + //-------------------------------------------------------------------------- + /// @brief Returns the current collection of fonts available on the + /// platform. + /// + /// This function reads an XML file and makes font families and + /// collections of them. MinikinFontForTest is used for FontFamily + /// creation. + virtual FontCollection& GetFontCollection() = 0; + + //-------------------------------------------------------------------------- + /// @brief Notifies this client of the name of the root isolate and its + /// port when that isolate is launched, restarted (in the + /// cold-restart scenario) or the application itself updates the + /// name of the root isolate (via `Window.setIsolateDebugName` + /// in `window.dart`). The name of the isolate is meaningless to + /// the engine but is used in instrumentation and tooling. + /// Currently, this information is to update the service + /// protocol list of available root isolates running in the VM + /// and their names so that the appropriate isolate can be + /// selected in the tools for debugging and instrumentation. + /// + /// @param[in] isolate_name The isolate name + /// @param[in] isolate_port The isolate port + /// + virtual void UpdateIsolateDescription(const std::string isolate_name, + int64_t isolate_port) = 0; + + //-------------------------------------------------------------------------- + /// @brief Notifies this client that the application has an opinion about + /// whether its frame timings need to be reported backed to it. + /// Due to the asynchronous nature of rendering in Flutter, it is + /// not possible for the application to determine the total time + /// it took to render a specific frame. While the layer-tree is + /// constructed on the UI thread, it needs to be rendering on the + /// raster thread. Dart code cannot execute on this thread. So any + /// instrumentation about the frame times gathered on this thread + /// needs to be aggregated and sent back to the UI thread for + /// processing in Dart. + /// + /// When the application indicates that frame times need to be + /// reported, it collects this information till a specified number + /// of data points are gathered. Then this information is sent + /// back to Dart code via `Engine::ReportTimings`. + /// + /// This option is engine counterpart of the + /// `Window._setNeedsReportTimings` in `window.dart`. + /// + /// @param[in] needs_reporting If reporting information should be collected + /// and send back to Dart. + /// + virtual void SetNeedsReportTimings(bool value) = 0; + + //-------------------------------------------------------------------------- + /// @brief The embedder can specify data that the isolate can request + /// synchronously on launch. This accessor fetches that data. + /// + /// This data is persistent for the duration of the Flutter + /// application and is available even after isolate restarts. + /// Because of this lifecycle, the size of this data must be kept + /// to a minimum. + /// + /// For asynchronous communication between the embedder and + /// isolate, a platform channel may be used. + /// + /// @return A map of the isolate data that the framework can request upon + /// launch. + /// + virtual std::shared_ptr GetPersistentIsolateData() = 0; + + //-------------------------------------------------------------------------- + /// @brief Directly invokes platform-specific APIs to compute the + /// locale the platform would have natively resolved to. + /// + /// @param[in] supported_locale_data The vector of strings that represents + /// the locales supported by the app. + /// Each locale consists of three + /// strings: languageCode, countryCode, + /// and scriptCode in that order. + /// + /// @return A vector of 3 strings languageCode, countryCode, and + /// scriptCode that represents the locale selected by the + /// platform. Empty strings mean the value was unassigned. Empty + /// vector represents a null locale. + /// + virtual std::unique_ptr> + ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) = 0; + + protected: + virtual ~PlatformConfigurationClient(); +}; + +//---------------------------------------------------------------------------- +/// @brief A class for holding and distributing platform-level information +/// to and from the Dart code in Flutter's framework. +/// +/// It handles communication between the engine and the framework, +/// and owns the main window. +/// +/// It communicates with the RuntimeController through the use of a +/// PlatformConfigurationClient interface, which the +/// RuntimeController defines. +/// +class PlatformConfiguration final { + public: + //---------------------------------------------------------------------------- + /// @brief Creates a new PlatformConfiguration, typically created by the + /// RuntimeController. + /// + /// @param[in] client The `PlatformConfigurationClient` to be injected into + /// the PlatformConfiguration. This client is used to + /// forward requests to the RuntimeController. + /// + explicit PlatformConfiguration(PlatformConfigurationClient* client); + + // PlatformConfiguration is not copyable. + PlatformConfiguration(const PlatformConfiguration&) = delete; + PlatformConfiguration& operator=(const PlatformConfiguration&) = delete; + + ~PlatformConfiguration(); + + //---------------------------------------------------------------------------- + /// @brief Access to the platform configuration client (which typically + /// is implemented by the RuntimeController). + /// + /// @return Returns the client used to construct this + /// PlatformConfiguration. + /// + PlatformConfigurationClient* client() const { return client_; } + + //---------------------------------------------------------------------------- + /// @brief Called by the RuntimeController once it has created the root + /// isolate, so that the PlatformController can get a handle to + /// the 'dart:ui' library. + /// + /// It uses the handle to call the hooks in hooks.dart. + /// + void DidCreateIsolate(); + + //---------------------------------------------------------------------------- + /// @brief Update the specified locale data in the framework. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] locale_data The locale data. This should consist of groups of + /// 4 strings, each group representing a single locale. + /// + void UpdateLocales(const std::vector& locales); + + //---------------------------------------------------------------------------- + /// @brief Update the user settings data in the framework. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] data The user settings data. + /// + void UpdateUserSettingsData(const std::string& data); + + //---------------------------------------------------------------------------- + /// @brief Updates the lifecycle state data in the framework. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] data The lifecycle state data. + /// + void UpdateLifecycleState(const std::string& data); + + //---------------------------------------------------------------------------- + /// @brief Notifies the PlatformConfiguration that the embedder has + /// expressed an opinion about whether the accessibility tree + /// should be generated or not. This call originates in the + /// platform view and is forwarded to the PlatformConfiguration + /// here by the engine. + /// + /// @param[in] enabled Whether the accessibility tree is enabled or + /// disabled. + /// + void UpdateSemanticsEnabled(bool enabled); + + //---------------------------------------------------------------------------- + /// @brief Forward the preference of accessibility features that must be + /// enabled in the semantics tree to the framwork. + /// + /// @param[in] flags The accessibility features that must be generated in + /// the semantics tree. + /// + void UpdateAccessibilityFeatures(int32_t flags); + + //---------------------------------------------------------------------------- + /// @brief Notifies the PlatformConfiguration that the client has sent + /// it a message. This call originates in the platform view and + /// has been forwarded through the engine to here. + /// + /// @param[in] message The message sent from the embedder to the Dart + /// application. + /// + void DispatchPlatformMessage(fml::RefPtr message); + + //---------------------------------------------------------------------------- + /// @brief Notifies the framework that the embedder encountered an + /// accessibility related action on the specified node. This call + /// originates on the platform view and has been forwarded to the + /// platform configuration here by the engine. + /// + /// @param[in] id The identifier of the accessibility node. + /// @param[in] action The accessibility related action performed on the + /// node of the specified ID. + /// @param[in] args Optional data that applies to the specified action. + /// + void DispatchSemanticsAction(int32_t id, + SemanticsAction action, + std::vector args); + + //---------------------------------------------------------------------------- + /// @brief Notifies the framework that it is time to begin working on a + /// new + /// frame previously scheduled via a call to + /// `PlatformConfigurationClient::ScheduleFrame`. This call + /// originates in the animator. + /// + /// The frame time given as the argument indicates the point at + /// which the current frame interval began. It is very slightly + /// (because of scheduling overhead) in the past. If a new layer + /// tree is not produced and given to the GPU task runner within + /// one frame interval from this point, the Flutter application + /// will jank. + /// + /// This method calls the `::_beginFrame` method in `hooks.dart`. + /// + /// @param[in] frame_time The point at which the current frame interval + /// began. May be used by animation interpolators, + /// physics simulations, etc.. + /// + void BeginFrame(fml::TimePoint frame_time); + + //---------------------------------------------------------------------------- + /// @brief Dart code cannot fully measure the time it takes for a + /// specific frame to be rendered. This is because Dart code only + /// runs on the UI task runner. That is only a small part of the + /// overall frame workload. The GPU task runner frame workload is + /// executed on a thread where Dart code cannot run (and hence + /// instrument). Besides, due to the pipelined nature of rendering + /// in Flutter, there may be multiple frame workloads being + /// processed at any given time. However, for non-Timeline based + /// profiling, it is useful for trace collection and processing to + /// happen in Dart. To do this, the GPU task runner frame + /// workloads need to be instrumented separately. After a set + /// number of these profiles have been gathered, they need to be + /// reported back to Dart code. The engine reports this extra + /// instrumentation information back to the framework by invoking + /// this method at predefined intervals. + /// + /// @see `FrameTiming` + /// + /// @param[in] timings Collection of `FrameTiming::kCount` * `n` timestamps + /// for `n` frames whose timings have not been reported + /// yet. A collection of integers is reported here for + /// easier conversions to Dart objects. The timestamps + /// are measured against the system monotonic clock + /// measured in microseconds. + /// + void ReportTimings(std::vector timings); + + //---------------------------------------------------------------------------- + /// @brief Registers the native handlers for Dart functions that this + /// class handles. + /// + /// @param[in] natives The natives registry that the functions will be + /// registered with. + /// + static void RegisterNatives(tonic::DartLibraryNatives* natives); + + //---------------------------------------------------------------------------- + /// @brief Retrieves the Window managed by the PlatformConfiguration. + /// + /// @return a pointer to the Window. + /// + Window* window() const { return window_.get(); } + + //---------------------------------------------------------------------------- + /// @brief Responds to a previous platform message to the engine from the + /// framework. + /// + /// @param[in] response_id The unique id that identifies the original platform + /// message to respond to. + /// @param[in] data The data to send back in the response. + /// + void CompletePlatformMessageResponse(int response_id, + std::vector data); + + //---------------------------------------------------------------------------- + /// @brief Responds to a previous platform message to the engine from the + /// framework with an empty response. + /// + /// @param[in] response_id The unique id that identifies the original platform + /// message to respond to. + /// + void CompletePlatformMessageEmptyResponse(int response_id); + + private: + PlatformConfigurationClient* client_; + tonic::DartPersistentValue library_; + + std::unique_ptr window_; + + // We use id 0 to mean that no response is expected. + int next_response_id_ = 1; + std::unordered_map> + pending_responses_; +}; + +} // namespace flutter + +#endif // FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ diff --git a/lib/ui/window/platform_configuration_unittests.cc b/lib/ui/window/platform_configuration_unittests.cc new file mode 100644 index 0000000000000..903b6d748e2dc --- /dev/null +++ b/lib/ui/window/platform_configuration_unittests.cc @@ -0,0 +1,139 @@ +// 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. + +#define FML_USED_ON_EMBEDDER + +#include + +#include "flutter/lib/ui/window/platform_configuration.h" + +#include "flutter/common/task_runners.h" +#include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/lib/ui/painting/vertices.h" +#include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/shell_test.h" +#include "flutter/shell/common/thread_host.h" +#include "flutter/testing/testing.h" + +namespace flutter { +namespace testing { + +class DummyPlatformConfigurationClient : public PlatformConfigurationClient { + public: + DummyPlatformConfigurationClient() { + std::vector data; + isolate_data_.reset(new ::fml::DataMapping(data)); + } + std::string DefaultRouteName() override { return "TestRoute"; } + void ScheduleFrame() override {} + void Render(Scene* scene) override {} + void UpdateSemantics(SemanticsUpdate* update) override {} + void HandlePlatformMessage(fml::RefPtr message) override {} + FontCollection& GetFontCollection() override { return font_collection_; } + void UpdateIsolateDescription(const std::string isolate_name, + int64_t isolate_port) override {} + void SetNeedsReportTimings(bool value) override {} + std::shared_ptr GetPersistentIsolateData() override { + return isolate_data_; + } + std::unique_ptr> ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) override { + return nullptr; + }; + + private: + FontCollection font_collection_; + std::shared_ptr isolate_data_; +}; + +TEST_F(ShellTest, PlatformConfigurationInitialization) { + auto message_latch = std::make_shared(); + + auto nativeValidateConfiguration = [message_latch]( + Dart_NativeArguments args) { + PlatformConfiguration* configuration = + UIDartState::Current()->platform_configuration(); + ASSERT_NE(configuration->window(), nullptr); + ASSERT_EQ(configuration->window()->viewport_metrics().device_pixel_ratio, + 1.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_width, 0.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_height, 0.0); + + message_latch->Signal(); + }; + + Settings settings = CreateSettingsForFixture(); + TaskRunners task_runners("test", // label + GetCurrentTaskRunner(), // platform + CreateNewThread(), // raster + CreateNewThread(), // ui + CreateNewThread() // io + ); + + AddNativeCallback("ValidateConfiguration", + CREATE_NATIVE_ENTRY(nativeValidateConfiguration)); + + std::unique_ptr shell = + CreateShell(std::move(settings), std::move(task_runners)); + + ASSERT_TRUE(shell->IsSetup()); + auto run_configuration = RunConfiguration::InferFromSettings(settings); + run_configuration.SetEntrypoint("validateConfiguration"); + + shell->RunEngine(std::move(run_configuration), [&](auto result) { + ASSERT_EQ(result, Engine::RunStatus::Success); + }); + + message_latch->Wait(); + DestroyShell(std::move(shell), std::move(task_runners)); +} + +TEST_F(ShellTest, PlatformConfigurationWindowMetricsUpdate) { + auto message_latch = std::make_shared(); + + auto nativeValidateConfiguration = [message_latch]( + Dart_NativeArguments args) { + PlatformConfiguration* configuration = + UIDartState::Current()->platform_configuration(); + + ASSERT_NE(configuration->window(), nullptr); + configuration->window()->UpdateWindowMetrics( + ViewportMetrics{2.0, 10.0, 20.0}); + ASSERT_EQ(configuration->window()->viewport_metrics().device_pixel_ratio, + 2.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_width, 10.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_height, + 20.0); + + message_latch->Signal(); + }; + + Settings settings = CreateSettingsForFixture(); + TaskRunners task_runners("test", // label + GetCurrentTaskRunner(), // platform + CreateNewThread(), // raster + CreateNewThread(), // ui + CreateNewThread() // io + ); + + AddNativeCallback("ValidateConfiguration", + CREATE_NATIVE_ENTRY(nativeValidateConfiguration)); + + std::unique_ptr shell = + CreateShell(std::move(settings), std::move(task_runners)); + + ASSERT_TRUE(shell->IsSetup()); + auto run_configuration = RunConfiguration::InferFromSettings(settings); + run_configuration.SetEntrypoint("validateConfiguration"); + + shell->RunEngine(std::move(run_configuration), [&](auto result) { + ASSERT_EQ(result, Engine::RunStatus::Success); + }); + + message_latch->Wait(); + DestroyShell(std::move(shell), std::move(task_runners)); +} + +} // namespace testing +} // namespace flutter diff --git a/lib/ui/window/viewport_metrics.cc b/lib/ui/window/viewport_metrics.cc index 0b6dab6d4c1e0..f642bd116966f 100644 --- a/lib/ui/window/viewport_metrics.cc +++ b/lib/ui/window/viewport_metrics.cc @@ -8,6 +8,8 @@ namespace flutter { +ViewportMetrics::ViewportMetrics() = default; + ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, double p_physical_height, @@ -48,32 +50,10 @@ ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, - double p_physical_height, - double p_physical_depth, - double p_physical_padding_top, - double p_physical_padding_right, - double p_physical_padding_bottom, - double p_physical_padding_left, - double p_physical_view_inset_front, - double p_physical_view_inset_back, - double p_physical_view_inset_top, - double p_physical_view_inset_right, - double p_physical_view_inset_bottom, - double p_physical_view_inset_left) + double p_physical_height) : device_pixel_ratio(p_device_pixel_ratio), physical_width(p_physical_width), - physical_height(p_physical_height), - physical_depth(p_physical_depth), - physical_padding_top(p_physical_padding_top), - physical_padding_right(p_physical_padding_right), - physical_padding_bottom(p_physical_padding_bottom), - physical_padding_left(p_physical_padding_left), - physical_view_inset_top(p_physical_view_inset_top), - physical_view_inset_right(p_physical_view_inset_right), - physical_view_inset_bottom(p_physical_view_inset_bottom), - physical_view_inset_left(p_physical_view_inset_left), - physical_view_inset_front(p_physical_view_inset_front), - physical_view_inset_back(p_physical_view_inset_back) { + physical_height(p_physical_height) { // Ensure we don't have nonsensical dimensions. FML_DCHECK(physical_width >= 0); FML_DCHECK(physical_height >= 0); diff --git a/lib/ui/window/viewport_metrics.h b/lib/ui/window/viewport_metrics.h index f60adbfcee110..01081e3f345f1 100644 --- a/lib/ui/window/viewport_metrics.h +++ b/lib/ui/window/viewport_metrics.h @@ -5,21 +5,10 @@ #ifndef FLUTTER_LIB_UI_WINDOW_VIEWPORT_METRICS_H_ #define FLUTTER_LIB_UI_WINDOW_VIEWPORT_METRICS_H_ -#include - namespace flutter { -// This is the value of double.maxFinite from dart:core. -// Platforms that do not explicitly set a depth will use this value, which -// avoids the need to special case logic that wants to check the max depth on -// the Dart side. -static const double kUnsetDepth = 1.7976931348623157e+308; - struct ViewportMetrics { - ViewportMetrics() = default; - ViewportMetrics(const ViewportMetrics& other) = default; - - // Create a 2D ViewportMetrics instance. + ViewportMetrics(); ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, double p_physical_height, @@ -36,26 +25,15 @@ struct ViewportMetrics { double p_physical_system_gesture_inset_bottom, double p_physical_system_gesture_inset_left); - // Create a ViewportMetrics instance that contains z information. + // Create a ViewportMetrics instance that doesn't include depth, padding, or + // insets. ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, - double p_physical_height, - double p_physical_depth, - double p_physical_padding_top, - double p_physical_padding_right, - double p_physical_padding_bottom, - double p_physical_padding_left, - double p_physical_view_inset_front, - double p_physical_view_inset_back, - double p_physical_view_inset_top, - double p_physical_view_inset_right, - double p_physical_view_inset_bottom, - double p_physical_view_inset_left); + double p_physical_height); double device_pixel_ratio = 1.0; double physical_width = 0; double physical_height = 0; - double physical_depth = kUnsetDepth; double physical_padding_top = 0; double physical_padding_right = 0; double physical_padding_bottom = 0; @@ -64,8 +42,6 @@ struct ViewportMetrics { double physical_view_inset_right = 0; double physical_view_inset_bottom = 0; double physical_view_inset_left = 0; - double physical_view_inset_front = kUnsetDepth; - double physical_view_inset_back = kUnsetDepth; double physical_system_gesture_inset_top = 0; double physical_system_gesture_inset_right = 0; double physical_system_gesture_inset_bottom = 0; @@ -75,7 +51,6 @@ struct ViewportMetrics { struct LogicalSize { double width = 0.0; double height = 0.0; - double depth = kUnsetDepth; }; struct LogicalInset { @@ -83,14 +58,11 @@ struct LogicalInset { double top = 0.0; double right = 0.0; double bottom = 0.0; - double front = kUnsetDepth; - double back = kUnsetDepth; }; struct LogicalMetrics { LogicalSize size; double scale = 1.0; - double scale_z = 1.0; LogicalInset padding; LogicalInset view_inset; }; diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 9dc964ad6ef5c..1779998945554 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -4,183 +4,35 @@ #include "flutter/lib/ui/window/window.h" -#include "flutter/lib/ui/compositing/scene.h" -#include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/platform_message_response_dart.h" #include "third_party/tonic/converter/dart_converter.h" #include "third_party/tonic/dart_args.h" -#include "third_party/tonic/dart_library_natives.h" -#include "third_party/tonic/dart_microtask_queue.h" #include "third_party/tonic/logging/dart_invoke.h" #include "third_party/tonic/typed_data/dart_byte_data.h" namespace flutter { -namespace { -void DefaultRouteName(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - std::string routeName = - UIDartState::Current()->window()->client()->DefaultRouteName(); - Dart_SetReturnValue(args, tonic::StdStringToDart(routeName)); -} - -void ScheduleFrame(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - UIDartState::Current()->window()->client()->ScheduleFrame(); -} - -void Render(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - Scene* scene = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } - UIDartState::Current()->window()->client()->Render(scene); -} - -void UpdateSemantics(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - SemanticsUpdate* update = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } - UIDartState::Current()->window()->client()->UpdateSemantics(update); -} - -void SetIsolateDebugName(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - const std::string name = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } - UIDartState::Current()->SetDebugName(name); -} - -void SetNeedsReportTimings(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - bool value = tonic::DartConverter::FromArguments(args, 1, exception); - UIDartState::Current()->window()->client()->SetNeedsReportTimings(value); +Window::Window(ViewportMetrics metrics) : viewport_metrics_(metrics) { + library_.Set(tonic::DartState::Current(), + Dart_LookupLibrary(tonic::ToDart("dart:ui"))); } -void ReportUnhandledException(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - - Dart_Handle exception = nullptr; - - auto error_name = - tonic::DartConverter::FromArguments(args, 0, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } +Window::~Window() {} - auto stack_trace = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); +void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { return; } + tonic::DartState::Scope scope(dart_state); - UIDartState::Current()->ReportUnhandledException(std::move(error_name), - std::move(stack_trace)); -} - -Dart_Handle SendPlatformMessage(Dart_Handle window, - const std::string& name, - Dart_Handle callback, - Dart_Handle data_handle) { - UIDartState* dart_state = UIDartState::Current(); - - if (!dart_state->window()) { - return tonic::ToDart( - "Platform messages can only be sent from the main isolate"); - } - - fml::RefPtr response; - if (!Dart_IsNull(callback)) { - response = fml::MakeRefCounted( - tonic::DartPersistentValue(dart_state, callback), - dart_state->GetTaskRunners().GetUITaskRunner()); - } - if (Dart_IsNull(data_handle)) { - dart_state->window()->client()->HandlePlatformMessage( - fml::MakeRefCounted(name, response)); - } else { - tonic::DartByteData data(data_handle); - const uint8_t* buffer = static_cast(data.data()); - dart_state->window()->client()->HandlePlatformMessage( - fml::MakeRefCounted( - name, std::vector(buffer, buffer + data.length_in_bytes()), - response)); - } - - return Dart_Null(); -} - -void _SendPlatformMessage(Dart_NativeArguments args) { - tonic::DartCallStatic(&SendPlatformMessage, args); -} - -void RespondToPlatformMessage(Dart_Handle window, - int response_id, - const tonic::DartByteData& data) { - if (Dart_IsNull(data.dart_handle())) { - UIDartState::Current()->window()->CompletePlatformMessageEmptyResponse( - response_id); - } else { - // TODO(engine): Avoid this copy. - const uint8_t* buffer = static_cast(data.data()); - UIDartState::Current()->window()->CompletePlatformMessageResponse( - response_id, - std::vector(buffer, buffer + data.length_in_bytes())); - } -} - -void _RespondToPlatformMessage(Dart_NativeArguments args) { - tonic::DartCallStatic(&RespondToPlatformMessage, args); -} - -void GetPersistentIsolateData(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - - auto persistent_isolate_data = - UIDartState::Current()->window()->client()->GetPersistentIsolateData(); - - if (!persistent_isolate_data) { - Dart_SetReturnValue(args, Dart_Null()); + const std::vector& buffer = packet.data(); + Dart_Handle data_handle = + tonic::DartByteData::Create(buffer.data(), buffer.size()); + if (Dart_IsError(data_handle)) { return; } - - Dart_SetReturnValue( - args, tonic::DartByteData::Create(persistent_isolate_data->GetMapping(), - persistent_isolate_data->GetSize())); -} - -Dart_Handle ToByteData(const std::vector& buffer) { - return tonic::DartByteData::Create(buffer.data(), buffer.size()); -} - -} // namespace - -WindowClient::~WindowClient() {} - -Window::Window(WindowClient* client) : client_(client) {} - -Window::~Window() {} - -void Window::DidCreateIsolate() { - library_.Set(tonic::DartState::Current(), - Dart_LookupLibrary(tonic::ToDart("dart:ui"))); + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_dispatchPointerDataPacket", {data_handle})); } void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { @@ -197,7 +49,6 @@ void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { tonic::ToDart(metrics.device_pixel_ratio), tonic::ToDart(metrics.physical_width), tonic::ToDart(metrics.physical_height), - tonic::ToDart(metrics.physical_depth), tonic::ToDart(metrics.physical_padding_top), tonic::ToDart(metrics.physical_padding_right), tonic::ToDart(metrics.physical_padding_bottom), @@ -213,244 +64,4 @@ void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { })); } -void Window::UpdateLocales(const std::vector& locales) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_updateLocales", - { - tonic::ToDart>(locales), - })); -} - -void Window::UpdateUserSettingsData(const std::string& data) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), - "_updateUserSettingsData", - { - tonic::StdStringToDart(data), - })); -} - -void Window::UpdateLifecycleState(const std::string& data) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - tonic::LogIfError(tonic::DartInvokeField(library_.value(), - "_updateLifecycleState", - { - tonic::StdStringToDart(data), - })); -} - -void Window::UpdateSemanticsEnabled(bool enabled) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - UIDartState::ThrowIfUIOperationsProhibited(); - - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_updateSemanticsEnabled", {tonic::ToDart(enabled)})); -} - -void Window::UpdateAccessibilityFeatures(int32_t values) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), - "_updateAccessibilityFeatures", - {tonic::ToDart(values)})); -} - -void Window::DispatchPlatformMessage(fml::RefPtr message) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - FML_DLOG(WARNING) - << "Dropping platform message for lack of DartState on channel: " - << message->channel(); - return; - } - tonic::DartState::Scope scope(dart_state); - Dart_Handle data_handle = - (message->hasData()) ? ToByteData(message->data()) : Dart_Null(); - if (Dart_IsError(data_handle)) { - FML_DLOG(WARNING) - << "Dropping platform message because of a Dart error on channel: " - << message->channel(); - return; - } - - int response_id = 0; - if (auto response = message->response()) { - response_id = next_response_id_++; - pending_responses_[response_id] = response; - } - - tonic::LogIfError( - tonic::DartInvokeField(library_.value(), "_dispatchPlatformMessage", - {tonic::ToDart(message->channel()), data_handle, - tonic::ToDart(response_id)})); -} - -void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - Dart_Handle data_handle = ToByteData(packet.data()); - if (Dart_IsError(data_handle)) { - return; - } - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_dispatchPointerDataPacket", {data_handle})); -} - -void Window::DispatchSemanticsAction(int32_t id, - SemanticsAction action, - std::vector args) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - Dart_Handle args_handle = (args.empty()) ? Dart_Null() : ToByteData(args); - - if (Dart_IsError(args_handle)) { - return; - } - - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_dispatchSemanticsAction", - {tonic::ToDart(id), tonic::ToDart(static_cast(action)), - args_handle})); -} - -void Window::BeginFrame(fml::TimePoint frameTime) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - int64_t microseconds = (frameTime - fml::TimePoint()).ToMicroseconds(); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_beginFrame", - { - Dart_NewInteger(microseconds), - })); - - UIDartState::Current()->FlushMicrotasksNow(); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_drawFrame", {})); -} - -void Window::ReportTimings(std::vector timings) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - Dart_Handle data_handle = - Dart_NewTypedData(Dart_TypedData_kInt64, timings.size()); - - Dart_TypedData_Type type; - void* data = nullptr; - intptr_t num_acquired = 0; - FML_CHECK(!Dart_IsError( - Dart_TypedDataAcquireData(data_handle, &type, &data, &num_acquired))); - FML_DCHECK(num_acquired == static_cast(timings.size())); - - memcpy(data, timings.data(), sizeof(int64_t) * timings.size()); - FML_CHECK(Dart_TypedDataReleaseData(data_handle)); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_reportTimings", - { - data_handle, - })); -} - -void Window::CompletePlatformMessageEmptyResponse(int response_id) { - if (!response_id) { - return; - } - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) { - return; - } - auto response = std::move(it->second); - pending_responses_.erase(it); - response->CompleteEmpty(); -} - -void Window::CompletePlatformMessageResponse(int response_id, - std::vector data) { - if (!response_id) { - return; - } - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) { - return; - } - auto response = std::move(it->second); - pending_responses_.erase(it); - response->Complete(std::make_unique(std::move(data))); -} - -Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) { - std::vector supportedLocales = - tonic::DartConverter>::FromDart( - supportedLocalesHandle); - - std::vector results = - *UIDartState::Current() - ->window() - ->client() - ->ComputePlatformResolvedLocale(supportedLocales); - - return tonic::DartConverter>::ToDart(results); -} - -static void _ComputePlatformResolvedLocale(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle result = - ComputePlatformResolvedLocale(Dart_GetNativeArgument(args, 1)); - Dart_SetReturnValue(args, result); -} - -void Window::RegisterNatives(tonic::DartLibraryNatives* natives) { - natives->Register({ - {"Window_defaultRouteName", DefaultRouteName, 1, true}, - {"Window_scheduleFrame", ScheduleFrame, 1, true}, - {"Window_sendPlatformMessage", _SendPlatformMessage, 4, true}, - {"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true}, - {"Window_render", Render, 2, true}, - {"Window_updateSemantics", UpdateSemantics, 2, true}, - {"Window_setIsolateDebugName", SetIsolateDebugName, 2, true}, - {"Window_reportUnhandledException", ReportUnhandledException, 2, true}, - {"Window_setNeedsReportTimings", SetNeedsReportTimings, 2, true}, - {"Window_getPersistentIsolateData", GetPersistentIsolateData, 1, true}, - {"Window_computePlatformResolvedLocale", _ComputePlatformResolvedLocale, - 2, true}, - }); -} - } // namespace flutter diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index 1e68b09d8095a..172cf6b8c2ae5 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -9,102 +9,27 @@ #include #include -#include "flutter/fml/time/time_point.h" -#include "flutter/lib/ui/semantics/semantics_update.h" #include "flutter/lib/ui/window/platform_message.h" #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/viewport_metrics.h" #include "third_party/skia/include/gpu/GrDirectContext.h" #include "third_party/tonic/dart_persistent_value.h" -namespace tonic { -class DartLibraryNatives; - -// So tonice::ToDart> returns List instead of -// List. -template <> -struct DartListFactory { - static Dart_Handle NewList(intptr_t length) { - return Dart_NewListOf(Dart_CoreType_Int, length); - } -}; - -} // namespace tonic - namespace flutter { -class FontCollection; -class Scene; - -// Must match the AccessibilityFeatureFlag enum in window.dart. -enum class AccessibilityFeatureFlag : int32_t { - kAccessibleNavigation = 1 << 0, - kInvertColors = 1 << 1, - kDisableAnimations = 1 << 2, - kBoldText = 1 << 3, - kReduceMotion = 1 << 4, - kHighContrast = 1 << 5, -}; - -class WindowClient { - public: - virtual std::string DefaultRouteName() = 0; - virtual void ScheduleFrame() = 0; - virtual void Render(Scene* scene) = 0; - virtual void UpdateSemantics(SemanticsUpdate* update) = 0; - virtual void HandlePlatformMessage(fml::RefPtr message) = 0; - virtual FontCollection& GetFontCollection() = 0; - virtual void UpdateIsolateDescription(const std::string isolate_name, - int64_t isolate_port) = 0; - virtual void SetNeedsReportTimings(bool value) = 0; - virtual std::shared_ptr GetPersistentIsolateData() = 0; - virtual std::unique_ptr> - ComputePlatformResolvedLocale( - const std::vector& supported_locale_data) = 0; - - protected: - virtual ~WindowClient(); -}; - class Window final { public: - explicit Window(WindowClient* client); + explicit Window(ViewportMetrics metrics); ~Window(); - WindowClient* client() const { return client_; } + const ViewportMetrics& viewport_metrics() const { return viewport_metrics_; } - const ViewportMetrics& viewport_metrics() { return viewport_metrics_; } - - void DidCreateIsolate(); - void UpdateWindowMetrics(const ViewportMetrics& metrics); - void UpdateLocales(const std::vector& locales); - void UpdateUserSettingsData(const std::string& data); - void UpdateLifecycleState(const std::string& data); - void UpdateSemanticsEnabled(bool enabled); - void UpdateAccessibilityFeatures(int32_t flags); - void DispatchPlatformMessage(fml::RefPtr message); void DispatchPointerDataPacket(const PointerDataPacket& packet); - void DispatchSemanticsAction(int32_t id, - SemanticsAction action, - std::vector args); - void BeginFrame(fml::TimePoint frameTime); - void ReportTimings(std::vector timings); - - void CompletePlatformMessageResponse(int response_id, - std::vector data); - void CompletePlatformMessageEmptyResponse(int response_id); - - static void RegisterNatives(tonic::DartLibraryNatives* natives); + void UpdateWindowMetrics(const ViewportMetrics& metrics); private: - WindowClient* client_; tonic::DartPersistentValue library_; ViewportMetrics viewport_metrics_; - - // We use id 0 to mean that no response is expected. - int next_response_id_ = 1; - std::unordered_map> - pending_responses_; }; } // namespace flutter diff --git a/lib/web_ui/CODE_CONVENTIONS.md b/lib/web_ui/CODE_CONVENTIONS.md new file mode 100644 index 0000000000000..de91812be7b7f --- /dev/null +++ b/lib/web_ui/CODE_CONVENTIONS.md @@ -0,0 +1,62 @@ +# Web-specific coding conventions and terminology + +Here you will find various naming and structural conventions used in the Web +engine code. This is not a code style guide. For code style refer to +[Flutter's style guide][1]. This document does not apply outside the `web_ui` +directory. + +## CanvasKit Renderer + +All code specific to the CanvasKit renderer lives in `lib/src/engine/canvaskit`. + +CanvasKit bindings should use the exact names defined in CanvasKit's JavaScript +API, even if it violates Flutter's style guide, such as function names that +start with a capital letter (e.g. "MakeSkVertices"). This makes it easier to find +the relevant code in Skia's source code. CanvasKit bindings should all go in +the `canvaskit_api.dart` file. + +Files and directories should use all-lower-case "canvaskit", without +capitalization or punctuation (such as "canvasKit", "canvas-kit", "canvas_kit"). +This is consistent with Skia's conventions. + +Variable, function, method, and class names should use camel case, i.e. +"canvasKit", "CanvasKit". + +In documentation (doc comments, flutter.dev website, markdown files, +blog posts, etc) refer to Flutter's usage of CanvasKit as "CanvasKit renderer" +(to avoid confusion with CanvasKit as the standalone library, which can be used +without Flutter). + +Classes that wrap CanvasKit classes should replace the `Sk` class prefix with +`Ck` (which stands for "CanvasKit"), e.g. `CkPaint` wraps `SkPaint`, `CkImage` +wraps `SkImage`. + +## HTML Renderer + +All code specific to the HTML renderer lives in `lib/src/engine/html`. + +In documentation (doc comments, flutter.dev website, markdown files, +blog posts, etc) refer to Flutter's HTML implementation as "HTML renderer". We +include SVG, CSS, and Canvas 2D under the "HTML" umbrella. + +The implementation of the layer system uses the term "surface" to refer to +layers. We rely on persisting the DOM information across frames to gain +efficiency. Each concrete implementation of the `Surface` class should start +with the prefix `Persisted`, e.g. `PersistedOpacity`, `PersistedPicture`. + +## Semantics + +The semantics (accessibility) code is shared between CanvasKit and HTML. All +semantics code lives in `lib/src/engine/semantics`. + +## Text editing + +Text editing code is shared between CanvasKit and HTML, and it lives in +`lib/src/engine/text_editing`. + +## Common utilities + +Small common utilities do not need dedicated directories. It is OK to put all +such utilities in `lib/src/engine` (see, for example, `alarm_clock.dart`). + +[1]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo diff --git a/lib/web_ui/dev/README.md b/lib/web_ui/dev/README.md index 5903f07e9327a..73e181a9f8bb7 100644 --- a/lib/web_ui/dev/README.md +++ b/lib/web_ui/dev/README.md @@ -41,9 +41,7 @@ felt build [-w] -j 100 If you are a Google employee, you can use an internal instance of Goma to parallelize your builds. Because Goma compiles code on remote servers, this option is effective even on low-powered laptops. -By default, when compiling Dart code to JavaScript, we use 4 `dart2js` workers. -If you need to increase or reduce the number of workers, set the `BUILD_MAX_WORKERS_PER_TASK` -environment variable to the desired number. +By default, when compiling Dart code to JavaScript, we use 8 `dart2js` workers. ## Running web engine tests @@ -142,6 +140,7 @@ flutter/goldens updating the screenshots. Then update this file pointing to the new revision. ## Developing the `felt` tool + If you are making changes in the `felt` tool itself, you need to be aware of Dart snapshots. We create a Dart snapshot of the `felt` tool to make the startup faster. To make sure you are running the `felt` tool with your changes included, you would need to stop using the snapshot. This can be achived through the environment variable `FELT_USE_SNAPSHOT`: @@ -157,3 +156,22 @@ FELT_USE_SNAPSHOT=0 felt ``` _**Note**: if `FELT_USE_SNAPSHOT` is omitted or has any value other than "false" or "0", the snapshot mode will be enabled._ + +## Upgrade Browser Version + +Since the engine code and infra recipes do not live in the same repository there are few steps to follow in order to upgrade a browser's version. For now these instructins are most relevant to Chrome. + +1. Dowload the binaries for the new browser/driver for each operaing system (macOS, linux, windows). +2. Create CIPD packages for these packages. (More documentation is available for Googlers. go/cipd-flutter-web) +3. Add the new browser version to the recipe. Do not remove the old one. This recipe will apply to all PRs as soon as it is merged. However, not all PRs will have the up to date code for a while. +4. Update the version in this repo. Do this by changing the related fields in `browser_lock.yaml` file. +5. After a few days don't forget to remove the old version from the LUCI recipe. + +Note that for LUCI builders, both unit and integration tests are using the same browser. + +Some useful links: + +1. For Chrome downloads [link](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html) +2. Browser and driver CIPD [packages](https://chrome-infra-packages.appspot.com/p/flutter_internal) (Note: Access rights are restricted for these packages.) +3. LUCI web [recipe](https://flutter.googlesource.com/recipes/+/refs/heads/master/recipes/web_engine.py) +4. More general reading on CIPD packages [link](https://chromium.googlesource.com/chromium/src.git/+/master/docs/cipd.md#What-is-CIPD) diff --git a/lib/web_ui/dev/browser_lock.yaml b/lib/web_ui/dev/browser_lock.yaml index 57fbf7a6816fe..11b2baea1d55f 100644 --- a/lib/web_ui/dev/browser_lock.yaml +++ b/lib/web_ui/dev/browser_lock.yaml @@ -1,11 +1,21 @@ +## Driver version in use. +## For an integration test to run, the browser's major version and the driver's +## major version should be equal. Please make sure the major version of +## the binary for `chrome` is the same with `required_driver_version`. +## (Major version meaning: For a browser that has version 13.0.5, the major +## version is 13.) +## Please refer to README's `Upgrade Browser Version` section for more details +## on how to update the version number. +required_driver_version: + ## Make sure the major version of the binary in `browser_lock.yaml` is the + ## same for Chrome. + chrome: '84' chrome: # It seems Chrome can't always release from the same build for all operating # systems, so we specify per-OS build number. - # Note: 741412 binary is for Chrome Version 82. Driver for Chrome version 83 - # is not working with chrome.binary option. - Linux: 741412 - Mac: 735194 - Win: 735105 + Linux: 768968 # Major version 84 + Mac: 768985 # Major version 84 + Win: 768975 # Major version 84 firefox: version: '72.0' edge: diff --git a/lib/web_ui/dev/chrome_installer.dart b/lib/web_ui/dev/chrome_installer.dart index 6f46c2a57d903..3766488f497dd 100644 --- a/lib/web_ui/dev/chrome_installer.dart +++ b/lib/web_ui/dev/chrome_installer.dart @@ -3,8 +3,11 @@ // found in the LICENSE file. // @dart = 2.6 +import 'dart:async'; import 'dart:io' as io; +import 'package:archive/archive.dart'; +import 'package:archive/archive_io.dart'; import 'package:args/args.dart'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; @@ -186,7 +189,7 @@ class ChromeInstaller { } else if (versionDir.existsSync() && isLuci) { print('INFO: Chrome version directory in LUCI: ' '${versionDir.path}'); - } else if(!versionDir.existsSync() && isLuci) { + } else if (!versionDir.existsSync() && isLuci) { // Chrome should have been deployed as a CIPD package on LUCI. // Throw if it does not exists. throw StateError('Failed to locate Chrome on LUCI on path:' @@ -196,6 +199,8 @@ class ChromeInstaller { versionDir.createSync(recursive: true); } + print('INFO: Starting Chrome download.'); + final String url = PlatformBinding.instance.getChromeDownloadUrl(version); final StreamedResponse download = await client.send(Request( 'GET', @@ -206,17 +211,50 @@ class ChromeInstaller { io.File(path.join(versionDir.path, 'chrome.zip')); await download.stream.pipe(downloadedFile.openWrite()); - final io.ProcessResult unzipResult = await io.Process.run('unzip', [ - downloadedFile.path, - '-d', - versionDir.path, - ]); - - if (unzipResult.exitCode != 0) { - throw BrowserInstallerException( - 'Failed to unzip the downloaded Chrome archive ${downloadedFile.path}.\n' - 'With the version path ${versionDir.path}\n' - 'The unzip process exited with code ${unzipResult.exitCode}.'); + /// Windows LUCI bots does not have a `unzip`. Instead we are + /// using `archive` pub package. + /// + /// We didn't use `archieve` on Mac/Linux since the new files have + /// permission issues. For now we are not able change file permissions + /// from dart. + /// See: https://github.com/dart-lang/sdk/issues/15078. + if (io.Platform.isWindows) { + final Stopwatch stopwatch = Stopwatch()..start(); + + // Read the Zip file from disk. + final bytes = downloadedFile.readAsBytesSync(); + + final Archive archive = ZipDecoder().decodeBytes(bytes); + + // Extract the contents of the Zip archive to disk. + for (final ArchiveFile file in archive) { + final String filename = file.name; + if (file.isFile) { + final data = file.content as List; + io.File(path.join(versionDir.path, filename)) + ..createSync(recursive: true) + ..writeAsBytesSync(data); + } else { + io.Directory(path.join(versionDir.path, filename)) + ..create(recursive: true); + } + } + + stopwatch.stop(); + print('INFO: The unzip took ${stopwatch.elapsedMilliseconds ~/ 1000} seconds.'); + } else { + final io.ProcessResult unzipResult = + await io.Process.run('unzip', [ + downloadedFile.path, + '-d', + versionDir.path, + ]); + if (unzipResult.exitCode != 0) { + throw BrowserInstallerException( + 'Failed to unzip the downloaded Chrome archive ${downloadedFile.path}.\n' + 'With the version path ${versionDir.path}\n' + 'The unzip process exited with code ${unzipResult.exitCode}.'); + } } downloadedFile.deleteSync(); diff --git a/lib/web_ui/dev/common.dart b/lib/web_ui/dev/common.dart index 588fe67eedf6f..511794a1e92f3 100644 --- a/lib/web_ui/dev/common.dart +++ b/lib/web_ui/dev/common.dart @@ -59,11 +59,11 @@ class _WindowsBinding implements PlatformBinding { @override String getChromeDownloadUrl(String version) => - 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Win%2F${version}%2Fchrome-win32.zip?alt=media'; + 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Win%2F${version}%2Fchrome-win.zip?alt=media'; @override String getChromeExecutablePath(io.Directory versionDir) => - path.join(versionDir.path, 'chrome-win32', 'chrome'); + path.join(versionDir.path, 'chrome-win', 'chrome.exe'); @override String getFirefoxDownloadUrl(String version) => diff --git a/lib/web_ui/dev/driver_manager.dart b/lib/web_ui/dev/driver_manager.dart index de15c467c5510..595c965d20c80 100644 --- a/lib/web_ui/dev/driver_manager.dart +++ b/lib/web_ui/dev/driver_manager.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.6 import 'dart:io' as io; import 'package:meta/meta.dart'; @@ -20,19 +21,44 @@ import 'utils.dart'; /// /// This manager can be used for both macOS and Linux. class ChromeDriverManager extends DriverManager { - ChromeDriverManager(String browser) : super(browser); + /// Directory which contains the Chrome's major version. + /// + /// On LUCI we are using the CIPD packages to control Chrome binaries we use. + /// There will be multiple CIPD packages loaded at the same time. Yaml file + /// `driver_version.yaml` contains the version number we want to use. + /// + /// Initialized to the current first to avoid the `Non-nullable` error. + // TODO: https://github.com/flutter/flutter/issues/53179. Local integration + // tests are still using the system Chrome. + io.Directory _browserDriverDirWithVersion; + + ChromeDriverManager(String browser) : super(browser) { + final io.File lockFile = io.File(pathlib.join( + environment.webUiRootDir.path, 'dev', 'browser_lock.yaml')); + final YamlMap _configuration = + loadYaml(lockFile.readAsStringSync()) as YamlMap; + final String requiredChromeDriverVersion = + _configuration['required_driver_version']['chrome'] as String; + print('INFO: Major version for Chrome Driver $requiredChromeDriverVersion'); + _browserDriverDirWithVersion = io.Directory(pathlib.join( + environment.webUiDartToolDir.path, + 'drivers', + browser, + requiredChromeDriverVersion, + '${browser}driver-${io.Platform.operatingSystem.toString()}')); + } @override Future _installDriver() async { - if (_browserDriverDir.existsSync()) { - _browserDriverDir.deleteSync(recursive: true); + if (_browserDriverDirWithVersion.existsSync()) { + _browserDriverDirWithVersion.deleteSync(recursive: true); } - _browserDriverDir.createSync(recursive: true); + _browserDriverDirWithVersion.createSync(recursive: true); temporaryDirectories.add(_drivers); final io.Directory temp = io.Directory.current; - io.Directory.current = _browserDriverDir; + io.Directory.current = _browserDriverDirWithVersion; try { // TODO(nurhan): https://github.com/flutter/flutter/issues/53179 @@ -50,17 +76,17 @@ class ChromeDriverManager extends DriverManager { /// Driver should already exist on LUCI as a CIPD package. @override Future _verifyDriverForLUCI() { - if (!_browserDriverDir.existsSync()) { + if (!_browserDriverDirWithVersion.existsSync()) { throw StateError('Failed to locate Chrome driver on LUCI on path:' - '${_browserDriverDir.path}'); + '${_browserDriverDirWithVersion.path}'); } return Future.value(); } @override - Future _startDriver(String driverPath) async { + Future _startDriver() async { await startProcess('./chromedriver/chromedriver', ['--port=4444'], - workingDirectory: driverPath); + workingDirectory: _browserDriverDirWithVersion.path); print('INFO: Driver started'); } } @@ -106,9 +132,9 @@ class FirefoxDriverManager extends DriverManager { } @override - Future _startDriver(String driverPath) async { + Future _startDriver() async { await startProcess('./firefoxdriver/geckodriver', ['--port=4444'], - workingDirectory: driverPath); + workingDirectory: _browserDriverDir.path); print('INFO: Driver started'); } @@ -144,7 +170,7 @@ class SafariDriverManager extends DriverManager { } @override - Future _startDriver(String driverPath) async { + Future _startDriver() async { final SafariDriverRunner safariDriverRunner = SafariDriverRunner(); final io.Process process = @@ -184,7 +210,7 @@ abstract class DriverManager { } else { await _verifyDriverForLUCI(); } - await _startDriver(_browserDriverDir.path); + await _startDriver(); } /// Always re-install since driver can change frequently. @@ -198,7 +224,7 @@ abstract class DriverManager { Future _verifyDriverForLUCI(); @protected - Future _startDriver(String driverPath); + Future _startDriver(); static DriverManager chooseDriver(String browser) { if (browser == 'chrome') { diff --git a/lib/web_ui/dev/driver_version.yaml b/lib/web_ui/dev/driver_version.yaml index 174e69448ac7c..f2c3baf9391f8 100644 --- a/lib/web_ui/dev/driver_version.yaml +++ b/lib/web_ui/dev/driver_version.yaml @@ -1,4 +1,3 @@ - ## Map for driver versions to use for each browser version. ## See: https://chromedriver.chromium.org/downloads chrome: diff --git a/lib/web_ui/dev/felt_windows.bat b/lib/web_ui/dev/felt_windows.bat index bacd1b0d50975..2ae0e2ab8b125 100644 --- a/lib/web_ui/dev/felt_windows.bat +++ b/lib/web_ui/dev/felt_windows.bat @@ -57,15 +57,15 @@ IF %orTempValue%==0 ( :: TODO(nurhan): The batch script does not support snanphot option. :: Support snapshot option. CALL :installdeps -IF %1==test (%DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* --browser=edge) ELSE ( %DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* ) +IF %1==test (%DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* --browser=chrome) ELSE ( %DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* ) -EXIT /B 0 +EXIT /B %ERRORLEVEL% :installdeps ECHO "Running \`pub get\` in 'engine/src/flutter/web_sdk/web_engine_tester'" cd "%FLUTTER_DIR%web_sdk\web_engine_tester" CALL %PUB_DIR% get ECHO "Running \`pub get\` in 'engine/src/flutter/lib/web_ui'" -cd %WEB_UI_DIR% +cd %WEB_UI_DIR% CALL %PUB_DIR% get EXIT /B 0 diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 2d5ea68c65afa..7daac2c7ae55f 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 4fb2ce1ea4b3a0bd48b01d7b3724be87244964d6 +revision: f26d68c3596eece3d40112e9dff01dc55d9bae97 diff --git a/lib/web_ui/dev/macos_info.dart b/lib/web_ui/dev/macos_info.dart index 40742c1e84943..b406aa6a32e88 100644 --- a/lib/web_ui/dev/macos_info.dart +++ b/lib/web_ui/dev/macos_info.dart @@ -1,6 +1,8 @@ // 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. + +// @dart = 2.6 import 'dart:convert'; import 'utils.dart'; diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index 20e16be900217..341d9b245ee37 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -34,7 +34,6 @@ import 'package:test_core/src/runner/environment.dart'; // ignore: implementatio import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports -import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' as wip; @@ -407,15 +406,6 @@ Golden file $filename did not match the image generated by the test. throw ArgumentError('$browser is not a browser.'); } - var htmlPath = p.withoutExtension(path) + '.html'; - if (File(htmlPath).existsSync() && - !File(htmlPath).readAsStringSync().contains('packages/test/dart.js')) { - throw LoadException( - path, - '"${htmlPath}" must contain .'); - } - if (_closed) { return null; } @@ -811,15 +801,18 @@ class BrowserManager { controller = deserializeSuite(path, currentPlatform(_runtime), suiteConfig, await _environment, suiteChannel, message); - final String mapPath = p.join( - env.environment.webUiRootDir.path, - 'build', - '$path.browser_test.dart.js.map', - ); - PackageConfig packageConfig = await loadPackageConfigUri( - await Isolate.packageConfig); - Map packageMap = - {for (var p in packageConfig.packages) p.name: p.packageUriRoot}; + final String sourceMapFileName = + '${p.basename(path)}.browser_test.dart.js.map'; + final String pathToTest = p.dirname(path); + + final String mapPath = p.join(env.environment.webUiRootDir.path, + 'build', pathToTest, sourceMapFileName); + + PackageConfig packageConfig = + await loadPackageConfigUri(await Isolate.packageConfig); + Map packageMap = { + for (var p in packageConfig.packages) p.name: p.packageUriRoot + }; final JSStackTraceMapper mapper = JSStackTraceMapper( await File(mapPath).readAsString(), mapUrl: p.toUri(mapPath), diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index e9f57b32f0153..14a34d5eea4df 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -9,6 +9,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; +import 'package:pool/pool.dart'; import 'package:test_core/src/runner/hack_register_platform.dart' as hack; // ignore: implementation_imports import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports @@ -99,6 +100,9 @@ class TestCommand extends Command with ArgUtils { TestTypesRequested testTypesRequested = null; + /// How many dart2js build tasks are running at the same time. + final Pool _pool = Pool(8); + /// Check the flags to see what type of tests are requested. TestTypesRequested findTestType() { if (boolArg('unit-tests-only') && boolArg('integration-tests-only')) { @@ -130,6 +134,7 @@ class TestCommand extends Command with ArgUtils { /// Collect information on the bot. final MacOSInfo macOsInfo = new MacOSInfo(); await macOsInfo.printInformation(); + /// Tests may fail on the CI, therefore exit test_runner. if (isLuci) { return true; @@ -142,9 +147,7 @@ class TestCommand extends Command with ArgUtils { case TestTypesRequested.integration: return runIntegrationTests(); case TestTypesRequested.all: - // TODO(nurhan): https://github.com/flutter/flutter/issues/53322 - // TODO(nurhan): Expand browser matrix for felt integration tests. - if (runAllTests && (isChrome || isSafariOnMacOS || isFirefox)) { + if (runAllTests && isIntegrationTestsAvailable) { bool unitTestResult = await runUnitTests(); bool integrationTestResult = await runIntegrationTests(); if (integrationTestResult != unitTestResult) { @@ -240,12 +243,35 @@ class TestCommand extends Command with ArgUtils { } if (htmlTargets.isNotEmpty) { - await _buildTests(targets: htmlTargets, forCanvasKit: false); + await _buildTestsInParallel(targets: htmlTargets, forCanvasKit: false); } + // Currently iOS Safari tests are running on simulator, which does not + // support canvaskit backend. if (canvasKitTargets.isNotEmpty) { - await _buildTests(targets: canvasKitTargets, forCanvasKit: true); + await _buildTestsInParallel( + targets: canvasKitTargets, forCanvasKit: true); } + + // Copy image files from test/ to build/test/. + // A side effect is this file copies all the images even when only one + // target test is asked to run. + final List contents = + environment.webUiTestDir.listSync(recursive: true); + contents.whereType().forEach((final io.File entity) { + final String directoryPath = path.relative(path.dirname(entity.path), + from: environment.webUiRootDir.path); + final io.Directory directory = io.Directory( + path.join(environment.webUiBuildDir.path, directoryPath)); + if (!directory.existsSync()) { + directory.createSync(recursive: true); + } + final String pathRelativeToWebUi = path.relative(entity.absolute.path, + from: environment.webUiRootDir.path); + entity.copySync( + path.join(environment.webUiBuildDir.path, pathRelativeToWebUi)); + }); + stopwatch.stop(); print('The build took ${stopwatch.elapsedMilliseconds ~/ 1000} seconds.'); } @@ -279,8 +305,40 @@ class TestCommand extends Command with ArgUtils { bool get isFirefox => browser == 'firefox'; /// Whether [browser] is set to "safari". - bool get isSafariOnMacOS => browser == 'safari' - && io.Platform.isMacOS; + bool get isSafariOnMacOS => browser == 'safari' && io.Platform.isMacOS; + + /// Due to lack of resources Chrome integration tests only run on Linux on + /// LUCI. + /// + /// They run on all platforms for local. + bool get isChromeIntegrationTestAvailable => + (isChrome && isLuci && io.Platform.isLinux) || (isChrome && !isLuci); + + /// Due to efficiancy constraints, Firefox integration tests only run on + /// Linux on LUCI. + /// + /// For now Firefox integration tests only run on Linux and Mac on local. + /// + // TODO: https://github.com/flutter/flutter/issues/63832 + bool get isFirefoxIntegrationTestAvailable => + (isFirefox && isLuci && io.Platform.isLinux) || + (isFirefox && !isLuci && !io.Platform.isWindows); + + /// Latest versions of Safari Desktop are only available on macOS. + /// + /// Integration testing on LUCI is not supported at the moment. + // TODO: https://github.com/flutter/flutter/issues/63710 + bool get isSafariIntegrationTestAvailable => isSafariOnMacOS && !isLuci; + + /// Due to various factors integration tests might be missing on a given + /// platform and given environment. + /// See: [isChromeIntegrationTestAvailable] + /// See: [isSafariIntegrationTestAvailable] + /// See: [isFirefoxIntegrationTestAvailable] + bool get isIntegrationTestsAvailable => + isChromeIntegrationTestAvailable || + isFirefoxIntegrationTestAvailable || + isSafariIntegrationTestAvailable; /// Use system flutter instead of cloning the repository. /// @@ -308,8 +366,10 @@ class TestCommand extends Command with ArgUtils { 'test', )); - // Screenshot tests and smoke tests only run in Chrome. - if (isChrome) { + // Screenshot tests and smoke tests only run on: "Chrome locally" or + // "Chrome on a Linux bot". We can remove the Linux bot restriction after: + // TODO: https://github.com/flutter/flutter/issues/63710 + if ((isChrome && isLuci && io.Platform.isLinux) || (isChrome && !isLuci)) { // Separate screenshot tests from unit-tests. Screenshot tests must run // one at a time. Otherwise, they will end up screenshotting each other. // This is not an issue for unit-tests. @@ -448,69 +508,69 @@ class TestCommand extends Command with ArgUtils { timestampFile.writeAsStringSync(timestamp); } + Future _buildTestsInParallel( + {List targets, bool forCanvasKit = false}) async { + final List buildInputs = targets + .map((FilePath f) => TestBuildInput(f, forCanvasKit: forCanvasKit)) + .toList(); + + final results = _pool.forEach( + buildInputs, + _buildTest, + ); + await for (final bool isSuccess in results) { + if (!isSuccess) { + throw ToolException('Failed to compile tests.'); + } + } + } + /// Builds the specific test [targets]. /// /// [targets] must not be null. /// - /// When building for CanvasKit we have to use a separate `build.canvaskit.yaml` - /// config file. Otherwise, `build.html.yaml` is used. Because `build_runner` - /// overwrites the output directories, we redirect the CanvasKit output to a - /// separate directory, then copy the files back to `build/test`. - Future _buildTests({List targets, bool forCanvasKit}) async { - print( - 'Building ${targets.length} targets for ${forCanvasKit ? 'CanvasKit' : 'HTML'}'); - final String canvasKitOutputRelativePath = - path.join('.dart_tool', 'canvaskit_tests'); + /// Uses `dart2js` for building the test. + /// + /// When building for CanvasKit we have to use extra argument + /// `DFLUTTER_WEB_USE_SKIA=true`. + Future _buildTest(TestBuildInput input) async { + final targetFileName = + '${input.path.relativeToWebUi}.browser_test.dart.js'; + final String targetPath = path.join('build', targetFileName); + + final io.Directory directoryToTarget = io.Directory(path.join( + environment.webUiBuildDir.path, + path.dirname(input.path.relativeToWebUi))); + + if (!directoryToTarget.existsSync()) { + directoryToTarget.createSync(recursive: true); + } + List arguments = [ - 'run', - 'build_runner', - 'build', + '--no-minify', + '--disable-inlining', + '--enable-asserts', '--enable-experiment=non-nullable', - 'test', + '--no-sound-null-safety', + if (input.forCanvasKit) '-DFLUTTER_WEB_USE_SKIA=true', + '-O2', '-o', - forCanvasKit ? canvasKitOutputRelativePath : 'build', - '--config', - // CanvasKit uses `build.canvaskit.yaml`, which HTML Uses `build.html.yaml`. - forCanvasKit ? 'canvaskit' : 'html', - for (FilePath path in targets) ...[ - '--build-filter=${path.relativeToWebUi}.js', - '--build-filter=${path.relativeToWebUi}.browser_test.dart.js', - ], + targetPath, // target path. + '${input.path.relativeToWebUi}', // current path. ]; final int exitCode = await runProcess( - environment.pubExecutable, + environment.dart2jsExecutable, arguments, workingDirectory: environment.webUiRootDir.path, - environment: { - // This determines the number of concurrent dart2js processes. - // - // By default build_runner uses 4 workers. - // - // In a testing on a 32-core 132GB workstation increasing this number to - // 32 sped up the build from ~4min to ~1.5min. - if (io.Platform.environment.containsKey('BUILD_MAX_WORKERS_PER_TASK')) - 'BUILD_MAX_WORKERS_PER_TASK': - io.Platform.environment['BUILD_MAX_WORKERS_PER_TASK'], - }, ); if (exitCode != 0) { - throw ToolException( - 'Failed to compile tests. Compiler exited with exit code $exitCode'); - } - - if (forCanvasKit) { - final io.Directory canvasKitTemporaryOutputDirectory = io.Directory( - path.join(environment.webUiRootDir.path, canvasKitOutputRelativePath, - 'test', 'canvaskit')); - final io.Directory canvasKitOutputDirectory = io.Directory( - path.join(environment.webUiBuildDir.path, 'test', 'canvaskit')); - if (await canvasKitOutputDirectory.exists()) { - await canvasKitOutputDirectory.delete(recursive: true); - } - await canvasKitTemporaryOutputDirectory - .rename(canvasKitOutputDirectory.path); + io.stderr.writeln('ERROR: Failed to compile test ${input.path}. ' + 'Dart2js exited with exit code $exitCode'); + return false; + } else { + return true; } } @@ -582,3 +642,16 @@ void _copyTestFontsIntoWebUi() { sourceTtf.copySync(destinationTtfPath); } } + +/// Used as an input message to the PoolResources that are building a test. +class TestBuildInput { + /// Test to build. + final FilePath path; + + /// Whether these tests should be build for CanvasKit. + /// + /// `-DFLUTTER_WEB_USE_SKIA=true` is passed to dart2js for CanvasKit. + final bool forCanvasKit; + + TestBuildInput(this.path, {this.forCanvasKit = false}); +} diff --git a/lib/web_ui/lib/assets/houdini_painter.js b/lib/web_ui/lib/assets/houdini_painter.js deleted file mode 100644 index 74ec4a29e25cb..0000000000000 --- a/lib/web_ui/lib/assets/houdini_painter.js +++ /dev/null @@ -1,1069 +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. - -// TODO(yjbanov): Consider the following optimizations: -// - Switch from JSON to typed arrays. See: -// https://github.com/w3c/css-houdini-drafts/issues/136 -// - When there is no DOM-rendered content, then clipping in the canvas is more -// efficient than DOM-rendered clipping. -// - When DOM-rendered clip is the only option, then clipping _again_ in the -// canvas is superfluous. -// - When transform is a 2D transform and there is no DOM-rendered content, then -// canvas transform is more efficient than DOM-rendered transform. -// - If a transform must be DOM-rendered, then clipping in the canvas _again_ is -// superfluous. - -/** - * Applies paint commands to CSS Paint API (a.k.a. Houdini). - * - * This painter is driven by houdini_canvas.dart. This painter and the - * HoudiniCanvas class must be kept in sync with each other. - */ -class FlutterPainter { - /** - * Properties used by this painter. - * - * @return {string[]} list of CSS properties this painter depends on. - */ - static get inputProperties() { - return ['--flt']; - } - - /** - * Implements the painter interface. - */ - paint(ctx, geom, properties) { - let fltProp = properties.get('--flt').toString(); - if (!fltProp) { - // Nothing to paint. - return; - } - const commands = JSON.parse(fltProp); - for (let i = 0; i < commands.length; i++) { - let command = commands[i]; - // TODO(yjbanov): we should probably move command identifiers into an enum - switch (command[0]) { - case 1: - this._save(ctx, geom, command); - break; - case 2: - this._restore(ctx, geom, command); - break; - case 3: - this._translate(ctx, geom, command); - break; - case 4: - this._scale(ctx, geom, command); - break; - case 5: - this._rotate(ctx, geom, command); - break; - // Skip case 6: implemented in the DOM for now. - case 7: - this._skew(ctx, geom, command); - break; - case 8: - this._clipRect(ctx, geom, command); - break; - case 9: - this._clipRRect(ctx, geom, command); - break; - case 10: - this._clipPath(ctx, geom, command); - break; - case 11: - this._drawColor(ctx, geom, command); - break; - case 12: - this._drawLine(ctx, geom, command); - break; - case 13: - this._drawPaint(ctx, geom, command); - break; - case 14: - this._drawRect(ctx, geom, command); - break; - case 15: - this._drawRRect(ctx, geom, command); - break; - case 16: - this._drawDRRect(ctx, geom, command); - break; - case 17: - this._drawOval(ctx, geom, command); - break; - case 18: - this._drawCircle(ctx, geom, command); - break; - case 19: - this._drawPath(ctx, geom, command); - break; - case 20: - this._drawShadow(ctx, geom, command); - break; - default: - throw new Error(`Unsupported command ID: ${command[0]}`); - } - } - } - - _applyPaint(ctx, paint) { - let blendMode = _stringForBlendMode(paint.blendMode); - ctx.globalCompositeOperation = blendMode ? blendMode : 'source-over'; - ctx.lineWidth = paint.strokeWidth ? paint.strokeWidth : 1.0; - - let strokeCap = _stringForStrokeCap(paint.strokeCap); - ctx.lineCap = strokeCap ? strokeCap : 'butt'; - - if (paint.shader != null) { - let paintStyle = paint.shader.createPaintStyle(ctx); - ctx.fillStyle = paintStyle; - ctx.strokeStyle = paintStyle; - } else if (paint.color != null) { - let colorString = paint.color; - ctx.fillStyle = colorString; - ctx.strokeStyle = colorString; - } - if (paint.maskFilter != null) { - ctx.filter = `blur(${paint.maskFilter[1]}px)`; - } - } - - _strokeOrFill(ctx, paint, resetPaint) { - switch (paint.style) { - case PaintingStyle.stroke: - ctx.stroke(); - break; - case PaintingStyle.fill: - default: - ctx.fill(); - break; - } - if (resetPaint) { - this._resetPaint(ctx); - } - } - - _resetPaint(ctx) { - ctx.globalCompositeOperation = 'source-over'; - ctx.lineWidth = 1.0; - ctx.lineCap = 'butt'; - ctx.filter = 'none'; - ctx.fillStyle = null; - ctx.strokeStyle = null; - } - - _save(ctx, geom, command) { - ctx.save(); - } - - _restore(ctx, geom, command) { - ctx.restore(); - } - - _translate(ctx, geom, command) { - ctx.translate(command[1], command[2]); - } - - _scale(ctx, geom, command) { - ctx.translate(command[1], command[2]); - } - - _rotate(ctx, geom, command) { - ctx.rotate(command[1]); - } - - _skew(ctx, geom, command) { - ctx.translate(command[1], command[2]); - } - - _drawRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let rect = scanner.scanRect(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.rect(rect.left, rect.top, rect.width(), rect.height()); - this._strokeOrFill(ctx, paint, true); - } - - _drawRRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let rrect = scanner.scanRRect(); - let paint = scanner.scanPaint(); - - this._applyPaint(ctx, paint); - this._drawRRectPath(ctx, rrect, true); - this._strokeOrFill(ctx, paint, true); - } - - _drawDRRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let outer = scanner.scanRRect(); - let inner = scanner.scanRRect(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - this._drawRRectPath(ctx, outer, true); - this._drawRRectPathReverse(ctx, inner, false); - this._strokeOrFill(ctx, paint, true); - } - - _drawRRectPath(ctx, rrect, startNewPath) { - // TODO(mdebbar): there's a bug in this code, it doesn't correctly handle - // the case when the radius is greater than the width of the - // rect. When we fix that in BitmapCanvas, we need to fix it - // here too. - // To draw the rounded rectangle, perform the following 8 steps: - // 1. draw the line for the top - // 2. draw the arc for the top-right corner - // 3. draw the line for the right side - // 4. draw the arc for the bottom-right corner - // 5. draw the line for the bottom of the rectangle - // 6. draw the arc for the bottom-left corner - // 7. draw the line for the left side - // 8. draw the arc for the top-left corner - // - // After drawing, the current point will be the left side of the top of the - // rounded rectangle (after the corner). - // TODO(het): Confirm that this is the end point in Flutter for RRect - - if (startNewPath) { - ctx.beginPath(); - } - - ctx.moveTo(rrect.left + rrect.trRadiusX, rrect.top); - - // Top side and top-right corner - ctx.lineTo(rrect.right - rrect.trRadiusX, rrect.top); - ctx.ellipse( - rrect.right - rrect.trRadiusX, - rrect.top + rrect.trRadiusY, - rrect.trRadiusX, - rrect.trRadiusY, - 0, - 1.5 * Math.PI, - 2.0 * Math.PI, - false, - ); - - // Right side and bottom-right corner - ctx.lineTo(rrect.right, rrect.bottom - rrect.brRadiusY); - ctx.ellipse( - rrect.right - rrect.brRadiusX, - rrect.bottom - rrect.brRadiusY, - rrect.brRadiusX, - rrect.brRadiusY, - 0, - 0, - 0.5 * Math.PI, - false, - ); - - // Bottom side and bottom-left corner - ctx.lineTo(rrect.left + rrect.blRadiusX, rrect.bottom); - ctx.ellipse( - rrect.left + rrect.blRadiusX, - rrect.bottom - rrect.blRadiusY, - rrect.blRadiusX, - rrect.blRadiusY, - 0, - 0.5 * Math.PI, - Math.PI, - false, - ); - - // Left side and top-left corner - ctx.lineTo(rrect.left, rrect.top + rrect.tlRadiusY); - ctx.ellipse( - rrect.left + rrect.tlRadiusX, - rrect.top + rrect.tlRadiusY, - rrect.tlRadiusX, - rrect.tlRadiusY, - 0, - Math.PI, - 1.5 * Math.PI, - false, - ); - } - - _drawRRectPathReverse(ctx, rrect, startNewPath) { - // Draw the rounded rectangle, counterclockwise. - ctx.moveTo(rrect.right - rrect.trRadiusX, rrect.top); - - if (startNewPath) { - ctx.beginPath(); - } - - // Top side and top-left corner - ctx.lineTo(rrect.left + rrect.tlRadiusX, rrect.top); - ctx.ellipse( - rrect.left + rrect.tlRadiusX, - rrect.top + rrect.tlRadiusY, - rrect.tlRadiusX, - rrect.tlRadiusY, - 0, - 1.5 * Math.PI, - Math.PI, - true, - ); - - // Left side and bottom-left corner - ctx.lineTo(rrect.left, rrect.bottom - rrect.blRadiusY); - ctx.ellipse( - rrect.left + rrect.blRadiusX, - rrect.bottom - rrect.blRadiusY, - rrect.blRadiusX, - rrect.blRadiusY, - 0, - Math.PI, - 0.5 * Math.PI, - true, - ); - - // Bottom side and bottom-right corner - ctx.lineTo(rrect.right - rrect.brRadiusX, rrect.bottom); - ctx.ellipse( - rrect.right - rrect.brRadiusX, - rrect.bottom - rrect.brRadiusY, - rrect.brRadiusX, - rrect.brRadiusY, - 0, - 0.5 * Math.PI, - 0, - true, - ); - - // Right side and top-right corner - ctx.lineTo(rrect.right, rrect.top + rrect.trRadiusY); - ctx.ellipse( - rrect.right - rrect.trRadiusX, - rrect.top + rrect.trRadiusY, - rrect.trRadiusX, - rrect.trRadiusY, - 0, - 0, - 1.5 * Math.PI, - true, - ); - } - - _clipRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let rect = scanner.scanRect(); - ctx.beginPath(); - ctx.rect(rect.left, rect.top, rect.width(), rect.height()); - ctx.clip(); - } - - _clipRRect(ctx, geom, command) { - let path = new Path([]); - let commands = [new RRectCommand(command[1])]; - path.subpaths.push(new Subpath(commands)); - this._runPath(ctx, path); - ctx.clip(); - } - - _clipPath(ctx, geom, command) { - let scanner = _scanCommand(command); - let path = scanner.scanPath(); - this._runPath(ctx, path); - ctx.clip(); - } - - _drawCircle(ctx, geom, command) { - let scanner = _scanCommand(command); - let dx = scanner.scanNumber(); - let dy = scanner.scanNumber(); - let radius = scanner.scanNumber(); - let paint = scanner.scanPaint(); - - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.ellipse(dx, dy, radius, radius, 0, 0, 2.0 * Math.PI, false); - this._strokeOrFill(ctx, paint, true); - } - - _drawOval(ctx, geom, command) { - let scanner = _scanCommand(command); - let rect = scanner.scanRect(); - let paint = scanner.scanPaint(); - - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.ellipse( - (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, - rect.width / 2, rect.height / 2, 0, 0, 2.0 * Math.PI, false); - this._strokeOrFill(ctx, paint, true); - } - - _drawPath(ctx, geom, command) { - let scanner = _scanCommand(command); - let path = scanner.scanPath(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - this._runPath(ctx, path); - this._strokeOrFill(ctx, paint, true); - } - - _drawShadow(ctx, geom, command) { - // TODO: this is mostly a stub; implement properly. - let scanner = _scanCommand(command); - let path = scanner.scanPath(); - let color = scanner.scanArray(); - let elevation = scanner.scanNumber(); - let transparentOccluder = scanner.scanBool(); - - let shadows = _computeShadowsForElevation(elevation, color); - for (let i = 0; i < shadows.length; i++) { - let shadow = shadows[i]; - - let paint = new Paint( - null, // blendMode - PaintingStyle.fill, // style - 1.0, // strokeWidth - null, // strokeCap - true, // isAntialias - shadow.color, // color - null, // shader - [BlurStyle.normal, shadow.blur], // maskFilter - null, // filterQuality - null // colorFilter - ); - - ctx.save(); - ctx.translate(shadow.offsetX, shadow.offsetY); - this._applyPaint(ctx, paint); - this._runPath(ctx, path, true); - this._strokeOrFill(ctx, paint, false); - ctx.restore(); - } - this._resetPaint(ctx); - } - - _runPath(ctx, path) { - ctx.beginPath(); - for (let i = 0; i < path.subpaths.length; i++) { - let subpath = path.subpaths[i]; - for (let j = 0; j < subpath.commands.length; j++) { - let command = subpath.commands[j]; - switch (command.type()) { - case PathCommandType.bezierCurveTo: - ctx.bezierCurveTo( - command.x1, command.y1, command.x2, command.y2, command.x3, - command.y3); - break; - case PathCommandType.close: - ctx.closePath(); - break; - case PathCommandType.ellipse: - ctx.ellipse( - command.x, command.y, command.radiusX, command.radiusY, - command.rotation, command.startAngle, command.endAngle, - command.anticlockwise); - break; - case PathCommandType.lineTo: - ctx.lineTo(command.x, command.y); - break; - case PathCommandType.moveTo: - ctx.moveTo(command.x, command.y); - break; - case PathCommandType.rrect: - this._drawRRectPath(ctx, command.rrect, false); - break; - case PathCommandType.rect: - ctx.rect(command.x, command.y, command.width, command.height); - break; - case PathCommandType.quadraticCurveTo: - ctx.quadraticCurveTo( - command.x1, command.y1, command.x2, command.y2); - break; - default: - throw new Error(`Unknown path command ${command.type()}`); - } - } - } - } - - _drawColor(ctx, geom, command) { - ctx.globalCompositeOperation = _stringForBlendMode(command[2]); - ctx.fillStyle = command[1]; - - // Fill a virtually infinite rect with the color. - // - // We can't use (0, 0, width, height) because the current transform can - // cause it to not fill the entire clip. - ctx.fillRect(-10000, -10000, 20000, 20000); - this._resetPaint(ctx); - } - - _drawLine(ctx, geom, command) { - let scanner = _scanCommand(command); - let p1dx = scanner.scanNumber(); - let p1dy = scanner.scanNumber(); - let p2dx = scanner.scanNumber(); - let p2dy = scanner.scanNumber(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.moveTo(p1dx, p1dy); - ctx.lineTo(p2dx, p2dy); - ctx.stroke(); - this._resetPaint(ctx); - } - - _drawPaint(ctx, geom, command) { - let scanner = _scanCommand(command); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - ctx.beginPath(); - - // Fill a virtually infinite rect with the color. - // - // We can't use (0, 0, width, height) because the current transform can - // cause it to not fill the entire clip. - ctx.fillRect(-10000, -10000, 20000, 20000); - this._resetPaint(ctx); - } -} - -function _scanCommand(command) { - return new CommandScanner(command); -} - -const PaintingStyle = { - fill: 0, - stroke: 1, -}; - -/// A singleton used to parse serialized commands. -class CommandScanner { - constructor(command) { - // Skip the first element, which is always the command ID. - this.index = 1; - this.command = command; - } - - scanRect() { - let rect = this.command[this.index++]; - return new Rect(rect[0], rect[1], rect[2], rect[3]); - } - - scanRRect() { - let rrect = this.command[this.index++]; - return new RRect( - rrect[0], rrect[1], rrect[2], rrect[3], rrect[4], rrect[5], rrect[6], - rrect[7], rrect[8], rrect[9], rrect[10], rrect[11]); - } - - scanPaint() { - let paint = this.command[this.index++]; - return new Paint( - paint[0], paint[1], paint[2], paint[3], paint[4], paint[5], paint[6], - paint[7], paint[8], paint[9]); - } - - scanNumber() { - return this.command[this.index++]; - } - - scanString() { - return this.command[this.index++]; - } - - scanBool() { - return this.command[this.index++]; - } - - scanPath() { - let subpaths = this.command[this.index++]; - return new Path(subpaths); - } - - scanArray() { - return this.command[this.index++]; - } -} - -class Rect { - constructor(left, top, right, bottom) { - this.left = left; - this.top = top; - this.right = right; - this.bottom = bottom; - } - - width() { - return this.right - this.left; - } - - height() { - return this.bottom - this.top; - } -} - -class RRect { - constructor( - left, top, right, bottom, tlRadiusX, tlRadiusY, trRadiusX, trRadiusY, - brRadiusX, brRadiusY, blRadiusX, blRadiusY) { - this.left = left; - this.top = top; - this.right = right; - this.bottom = bottom; - this.tlRadiusX = tlRadiusX; - this.tlRadiusY = tlRadiusY; - this.trRadiusX = trRadiusX; - this.trRadiusY = trRadiusY; - this.brRadiusX = brRadiusX; - this.brRadiusY = brRadiusY; - this.blRadiusX = blRadiusX; - this.blRadiusY = blRadiusY; - } - - tallMiddleRect() { - let leftRadius = Math.max(this.blRadiusX, this.tlRadiusX); - let rightRadius = Math.max(this.trRadiusX, this.brRadiusX); - return new Rect( - this.left + leftRadius, this.top, this.right - rightRadius, - this.bottom); - } - - middleRect() { - let leftRadius = Math.max(this.blRadiusX, this.tlRadiusX); - let topRadius = Math.max(this.tlRadiusY, this.trRadiusY); - let rightRadius = Math.max(this.trRadiusX, this.brRadiusX); - let bottomRadius = Math.max(this.brRadiusY, this.blRadiusY); - return new Rect( - this.left + leftRadius, this.top + topRadius, this.right - rightRadius, - this.bottom - bottomRadius); - } - - wideMiddleRect() { - let topRadius = Math.max(this.tlRadiusY, this.trRadiusY); - let bottomRadius = Math.max(this.brRadiusY, this.blRadiusY); - return new Rect( - this.left, this.top + topRadius, this.right, - this.bottom - bottomRadius); - } -} - -class Paint { - constructor( - blendMode, style, strokeWidth, strokeCap, isAntialias, color, shader, - maskFilter, filterQuality, colorFilter) { - this.blendMode = blendMode; - this.style = style; - this.strokeWidth = strokeWidth; - this.strokeCap = strokeCap; - this.isAntialias = isAntialias; - this.color = color; - this.shader = _deserializeShader(shader); // TODO: deserialize - this.maskFilter = maskFilter; - this.filterQuality = filterQuality; - this.colorFilter = colorFilter; // TODO: deserialize - } -} - -function _deserializeShader(data) { - if (!data) { - return null; - } - - switch (data[0]) { - case 1: - return new GradientLinear(data); - default: - throw new Error(`Shader type not supported: ${data}`); - } -} - -class GradientLinear { - constructor(data) { - this.fromX = data[1]; - this.fromY = data[2]; - this.toX = data[3]; - this.toY = data[4]; - this.colors = data[5]; - this.colorStops = data[6]; - this.tileMode = data[7]; - } - - createPaintStyle(ctx) { - let gradient = - ctx.createLinearGradient(this.fromX, this.fromY, this.toX, this.toY); - if (this.colorStops == null) { - gradient.addColorStop(0, this.colors[0]); - gradient.addColorStop(1, this.colors[1]); - return gradient; - } - for (let i = 0; i < this.colors.length; i++) { - gradient.addColorStop(this.colorStops[i], this.colors[i]); - } - return gradient; - } -} - -const BlendMode = { - clear: 0, - src: 1, - dst: 2, - srcOver: 3, - dstOver: 4, - srcIn: 5, - dstIn: 6, - srcOut: 7, - dstOut: 8, - srcATop: 9, - dstATop: 10, - xor: 11, - plus: 12, - modulate: 13, - screen: 14, - overlay: 15, - darken: 16, - lighten: 17, - colorDodge: 18, - colorBurn: 19, - hardLight: 20, - softLight: 21, - difference: 22, - exclusion: 23, - multiply: 24, - hue: 25, - saturation: 26, - color: 27, - luminosity: 28, -}; - -function _stringForBlendMode(blendMode) { - if (blendMode == null) return null; - switch (blendMode) { - case BlendMode.srcOver: - return 'source-over'; - case BlendMode.srcIn: - return 'source-in'; - case BlendMode.srcOut: - return 'source-out'; - case BlendMode.srcATop: - return 'source-atop'; - case BlendMode.dstOver: - return 'destination-over'; - case BlendMode.dstIn: - return 'destination-in'; - case BlendMode.dstOut: - return 'destination-out'; - case BlendMode.dstATop: - return 'destination-atop'; - case BlendMode.plus: - return 'lighten'; - case BlendMode.src: - return 'copy'; - case BlendMode.xor: - return 'xor'; - case BlendMode.multiply: - // Falling back to multiply, ignoring alpha channel. - // TODO(flutter_web): only used for debug, find better fallback for web. - case BlendMode.modulate: - return 'multiply'; - case BlendMode.screen: - return 'screen'; - case BlendMode.overlay: - return 'overlay'; - case BlendMode.darken: - return 'darken'; - case BlendMode.lighten: - return 'lighten'; - case BlendMode.colorDodge: - return 'color-dodge'; - case BlendMode.colorBurn: - return 'color-burn'; - case BlendMode.hardLight: - return 'hard-light'; - case BlendMode.softLight: - return 'soft-light'; - case BlendMode.difference: - return 'difference'; - case BlendMode.exclusion: - return 'exclusion'; - case BlendMode.hue: - return 'hue'; - case BlendMode.saturation: - return 'saturation'; - case BlendMode.color: - return 'color'; - case BlendMode.luminosity: - return 'luminosity'; - default: - throw new Error( - 'Flutter web does not support the blend mode: $blendMode'); - } -} - -const StrokeCap = { - butt: 0, - round: 1, - square: 2, -}; - -function _stringForStrokeCap(strokeCap) { - if (strokeCap == null) return null; - switch (strokeCap) { - case StrokeCap.butt: - return 'butt'; - case StrokeCap.round: - return 'round'; - case StrokeCap.square: - default: - return 'square'; - } -} - -class Path { - constructor(serializedSubpaths) { - this.subpaths = []; - for (let i = 0; i < serializedSubpaths.length; i++) { - let subpath = serializedSubpaths[i]; - let pathCommands = []; - for (let j = 0; j < subpath.length; j++) { - let pathCommand = subpath[j]; - switch (pathCommand[0]) { - case 1: - pathCommands.push(new MoveTo(pathCommand)); - break; - case 2: - pathCommands.push(new LineTo(pathCommand)); - break; - case 3: - pathCommands.push(new Ellipse(pathCommand)); - break; - case 4: - pathCommands.push(new QuadraticCurveTo(pathCommand)); - break; - case 5: - pathCommands.push(new BezierCurveTo(pathCommand)); - break; - case 6: - pathCommands.push(new RectCommand(pathCommand)); - break; - case 7: - pathCommands.push(new RRectCommand(pathCommand)); - break; - case 8: - pathCommands.push(new CloseCommand()); - break; - default: - throw new Error(`Unsupported path command: ${pathCommand}`); - } - } - - this.subpaths.push(new Subpath(pathCommands)); - } - } -} - -class Subpath { - constructor(commands) { - this.commands = commands; - } -} - -class MoveTo { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - } - - type() { - return PathCommandType.moveTo; - } -} - -class LineTo { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - } - - type() { - return PathCommandType.lineTo; - } -} - -class Ellipse { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - this.radiusX = data[3]; - this.radiusY = data[4]; - this.rotation = data[5]; - this.startAngle = data[6]; - this.endAngle = data[7]; - this.anticlockwise = data[8]; - } - - type() { - return PathCommandType.ellipse; - } -} - -class QuadraticCurveTo { - constructor(data) { - this.x1 = data[1]; - this.y1 = data[2]; - this.x2 = data[3]; - this.y2 = data[4]; - } - - type() { - return PathCommandType.quadraticCurveTo; - } -} - -class BezierCurveTo { - constructor(data) { - this.x1 = data[1]; - this.y1 = data[2]; - this.x2 = data[3]; - this.y2 = data[4]; - this.x3 = data[5]; - this.y3 = data[6]; - } - - type() { - return PathCommandType.bezierCurveTo; - } -} - -class RectCommand { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - this.width = data[3]; - this.height = data[4]; - } - - type() { - return PathCommandType.rect; - } -} - -class RRectCommand { - constructor(data) { - let scanner = _scanCommand(data); - this.rrect = scanner.scanRRect(); - } - - type() { - return PathCommandType.rrect; - } -} - -class CloseCommand { - type() { - return PathCommandType.close; - } -} - -class CanvasShadow { - constructor(offsetX, offsetY, blur, spread, color) { - this.offsetX = offsetX; - this.offsetY = offsetY; - this.blur = blur; - this.spread = spread; - this.color = color; - } -} - -const _noShadows = []; - -function _computeShadowsForElevation(elevation, color) { - if (elevation <= 0.0) { - return _noShadows; - } else if (elevation <= 1.0) { - return _computeShadowElevation(2, color); - } else if (elevation <= 2.0) { - return _computeShadowElevation(4, color); - } else if (elevation <= 3.0) { - return _computeShadowElevation(6, color); - } else if (elevation <= 4.0) { - return _computeShadowElevation(8, color); - } else if (elevation <= 5.0) { - return _computeShadowElevation(16, color); - } else { - return _computeShadowElevation(24, color); - } -} - -function _computeShadowElevation(dp, color) { - // TODO(yjbanov): multiple shadows are very expensive. Find a more efficient - // method to render them. - let red = color[1]; - let green = color[2]; - let blue = color[3]; - - // let penumbraColor = `rgba(${red}, ${green}, ${blue}, 0.14)`; - // let ambientShadowColor = `rgba(${red}, ${green}, ${blue}, 0.12)`; - let umbraColor = `rgba(${red}, ${green}, ${blue}, 0.2)`; - - let result = []; - if (dp === 2) { - // result.push(new CanvasShadow(0.0, 2.0, 1.0, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 3.0, 0.5, -2.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 1.0, 2.5, 0.0, umbraColor)); - } else if (dp === 3) { - // result.push(new CanvasShadow(0.0, 1.5, 4.0, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 3.0, 1.5, -2.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 1.0, 4.0, 0.0, umbraColor)); - } else if (dp === 4) { - // result.push(new CanvasShadow(0.0, 4.0, 2.5, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 1.0, 5.0, 0.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 2.0, 2.0, -1.0, umbraColor)); - } else if (dp === 6) { - // result.push(new CanvasShadow(0.0, 6.0, 5.0, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 1.0, 9.0, 0.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 3.0, 2.5, -1.0, umbraColor)); - } else if (dp === 8) { - // result.push(new CanvasShadow(0.0, 4.0, 10.0, 1.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 3.0, 7.0, 2.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 5.0, 2.5, -3.0, umbraColor)); - } else if (dp === 12) { - // result.push(new CanvasShadow(0.0, 12.0, 8.5, 2.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 5.0, 11.0, 4.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 7.0, 4.0, -4.0, umbraColor)); - } else if (dp === 16) { - // result.push(new CanvasShadow(0.0, 16.0, 12.0, 2.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 6.0, 15.0, 5.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 0.0, 5.0, -5.0, umbraColor)); - } else { - // result.push(new CanvasShadow(0.0, 24.0, 18.0, 3.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 9.0, 23.0, 8.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 11.0, 7.5, -7.0, umbraColor)); - } - return result; -} - -const PathCommandType = { - moveTo: 0, - lineTo: 1, - ellipse: 2, - close: 3, - quadraticCurveTo: 4, - bezierCurveTo: 5, - rect: 6, - rrect: 7, -}; - -const TileMode = { - clamp: 0, - repeated: 1, -}; - -const BlurStyle = { - normal: 0, - solid: 1, - outer: 2, - inner: 3, -}; - -/// This makes the painter available as "background-image: paint(flt)". -registerPaint('flt', FlutterPainter); diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 165a672cebb57..232b00e4c6028 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -27,43 +27,70 @@ part 'engine/assets.dart'; part 'engine/bitmap_canvas.dart'; part 'engine/browser_detection.dart'; part 'engine/browser_location.dart'; +part 'engine/canvaskit/canvas.dart'; +part 'engine/canvaskit/canvaskit_canvas.dart'; +part 'engine/canvaskit/canvaskit_api.dart'; +part 'engine/canvaskit/color_filter.dart'; +part 'engine/canvaskit/embedded_views.dart'; +part 'engine/canvaskit/fonts.dart'; +part 'engine/canvaskit/image.dart'; +part 'engine/canvaskit/image_filter.dart'; +part 'engine/canvaskit/initialization.dart'; +part 'engine/canvaskit/layer.dart'; +part 'engine/canvaskit/layer_scene_builder.dart'; +part 'engine/canvaskit/layer_tree.dart'; +part 'engine/canvaskit/mask_filter.dart'; +part 'engine/canvaskit/n_way_canvas.dart'; +part 'engine/canvaskit/path.dart'; +part 'engine/canvaskit/painting.dart'; +part 'engine/canvaskit/path_metrics.dart'; +part 'engine/canvaskit/picture.dart'; +part 'engine/canvaskit/picture_recorder.dart'; +part 'engine/canvaskit/platform_message.dart'; +part 'engine/canvaskit/raster_cache.dart'; +part 'engine/canvaskit/rasterizer.dart'; +part 'engine/canvaskit/shader.dart'; +part 'engine/canvaskit/skia_object_cache.dart'; +part 'engine/canvaskit/surface.dart'; +part 'engine/canvaskit/text.dart'; +part 'engine/canvaskit/util.dart'; +part 'engine/canvaskit/vertices.dart'; +part 'engine/canvaskit/viewport_metrics.dart'; part 'engine/canvas_pool.dart'; part 'engine/clipboard.dart'; part 'engine/color_filter.dart'; -part 'engine/compositor/canvas.dart'; -part 'engine/compositor/canvas_kit_canvas.dart'; -part 'engine/compositor/canvaskit_api.dart'; -part 'engine/compositor/color_filter.dart'; -part 'engine/compositor/embedded_views.dart'; -part 'engine/compositor/fonts.dart'; -part 'engine/compositor/image.dart'; -part 'engine/compositor/image_filter.dart'; -part 'engine/compositor/initialization.dart'; -part 'engine/compositor/layer.dart'; -part 'engine/compositor/layer_scene_builder.dart'; -part 'engine/compositor/layer_tree.dart'; -part 'engine/compositor/mask_filter.dart'; -part 'engine/compositor/n_way_canvas.dart'; -part 'engine/compositor/path.dart'; -part 'engine/compositor/painting.dart'; -part 'engine/compositor/path_metrics.dart'; -part 'engine/compositor/picture.dart'; -part 'engine/compositor/picture_recorder.dart'; -part 'engine/compositor/platform_message.dart'; -part 'engine/compositor/raster_cache.dart'; -part 'engine/compositor/rasterizer.dart'; -part 'engine/compositor/skia_object_cache.dart'; -part 'engine/compositor/surface.dart'; -part 'engine/compositor/text.dart'; -part 'engine/compositor/util.dart'; -part 'engine/compositor/vertices.dart'; -part 'engine/compositor/viewport_metrics.dart'; part 'engine/dom_canvas.dart'; part 'engine/dom_renderer.dart'; part 'engine/engine_canvas.dart'; part 'engine/frame_reference.dart'; part 'engine/history.dart'; -part 'engine/houdini_canvas.dart'; +part 'engine/html/backdrop_filter.dart'; +part 'engine/html/canvas.dart'; +part 'engine/html/clip.dart'; +part 'engine/html/debug_canvas_reuse_overlay.dart'; +part 'engine/html/image_filter.dart'; +part 'engine/html/offset.dart'; +part 'engine/html/opacity.dart'; +part 'engine/html/painting.dart'; +part 'engine/html/path/conic.dart'; +part 'engine/html/path/cubic.dart'; +part 'engine/html/path/path.dart'; +part 'engine/html/path/path_metrics.dart'; +part 'engine/html/path/path_ref.dart'; +part 'engine/html/path/path_to_svg.dart'; +part 'engine/html/path/path_utils.dart'; +part 'engine/html/path/path_windings.dart'; +part 'engine/html/path/tangent.dart'; +part 'engine/html/picture.dart'; +part 'engine/html/platform_view.dart'; +part 'engine/html/recording_canvas.dart'; +part 'engine/html/render_vertices.dart'; +part 'engine/html/scene.dart'; +part 'engine/html/scene_builder.dart'; +part 'engine/html/shader.dart'; +part 'engine/html/surface.dart'; +part 'engine/html/surface_stats.dart'; +part 'engine/html/transform.dart'; part 'engine/html_image_codec.dart'; part 'engine/keyboard.dart'; part 'engine/mouse_cursor.dart'; @@ -90,34 +117,7 @@ part 'engine/services/buffers.dart'; part 'engine/services/message_codec.dart'; part 'engine/services/message_codecs.dart'; part 'engine/services/serialization.dart'; -part 'engine/shader.dart'; part 'engine/shadow.dart'; -part 'engine/surface/backdrop_filter.dart'; -part 'engine/surface/canvas.dart'; -part 'engine/surface/clip.dart'; -part 'engine/surface/debug_canvas_reuse_overlay.dart'; -part 'engine/surface/image_filter.dart'; -part 'engine/surface/offset.dart'; -part 'engine/surface/opacity.dart'; -part 'engine/surface/painting.dart'; -part 'engine/surface/path/conic.dart'; -part 'engine/surface/path/cubic.dart'; -part 'engine/surface/path/path.dart'; -part 'engine/surface/path/path_metrics.dart'; -part 'engine/surface/path/path_ref.dart'; -part 'engine/surface/path/path_to_svg.dart'; -part 'engine/surface/path/path_utils.dart'; -part 'engine/surface/path/path_windings.dart'; -part 'engine/surface/path/tangent.dart'; -part 'engine/surface/picture.dart'; -part 'engine/surface/platform_view.dart'; -part 'engine/surface/recording_canvas.dart'; -part 'engine/surface/render_vertices.dart'; -part 'engine/surface/scene.dart'; -part 'engine/surface/scene_builder.dart'; -part 'engine/surface/surface.dart'; -part 'engine/surface/surface_stats.dart'; -part 'engine/surface/transform.dart'; part 'engine/test_embedding.dart'; part 'engine/text/font_collection.dart'; part 'engine/text/line_break_properties.dart'; diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index bd3fc5390b8ea..1954fa5231363 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -193,9 +193,9 @@ class BitmapCanvas extends EngineCanvas { /// /// See also: /// - /// * [PersistedStandardPicture._applyBitmapPaint] which uses this method to + /// * [PersistedPicture._applyBitmapPaint] which uses this method to /// decide whether to reuse this canvas or not. - /// * [PersistedStandardPicture._recycleCanvas] which also uses this method + /// * [PersistedPicture._recycleCanvas] which also uses this method /// for the same reason. bool isReusable() { return _devicePixelRatio == EngineWindow.browserDevicePixelRatio; @@ -965,7 +965,9 @@ List _clipContent(List<_SaveClipEntry> clipStack, ..height = '${roundRect.bottom - clipOffsetY}px'; setElementTransform(curElement, newClipTransform.storage); } else if (entry.path != null) { - curElement.style.transform = matrix4ToCssTransform(newClipTransform); + curElement.style + ..transform = matrix4ToCssTransform(newClipTransform) + ..transformOrigin = '0 0 0'; String svgClipPath = createSvgClipDef(curElement as html.HtmlElement, entry.path!); final html.Element clipElement = html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer()); diff --git a/lib/web_ui/lib/src/engine/browser_detection.dart b/lib/web_ui/lib/src/engine/browser_detection.dart index 0a4f3cfac291e..1ada5e37ed512 100644 --- a/lib/web_ui/lib/src/engine/browser_detection.dart +++ b/lib/web_ui/lib/src/engine/browser_detection.dart @@ -159,3 +159,32 @@ bool get isDesktop => _desktopOperatingSystems.contains(operatingSystem); /// See [_desktopOperatingSystems]. /// See [isDesktop]. bool get isMobile => !isDesktop; + +int? _cachedWebGLVersion; + +/// The highest WebGL version supported by the current browser, or -1 if WebGL +/// is not supported. +int get webGLVersion => _cachedWebGLVersion ?? (_cachedWebGLVersion = _detectWebGLVersion()); + +/// Detects the highest WebGL version supported by the current browser, or +/// -1 if WebGL is not supported. +/// +/// Chrome reports that `WebGL2RenderingContext` is available even when WebGL 2 is +/// disabled due hardware-specific issues. This happens, for example, on Chrome on +/// Moto E5. Therefore checking for the presence of `WebGL2RenderingContext` or +/// using the current [browserEngine] is insufficient. +/// +/// Our CanvasKit backend is affected due to: https://github.com/emscripten-core/emscripten/issues/11819 +int _detectWebGLVersion() { + final html.CanvasElement canvas = html.CanvasElement( + width: 1, + height: 1, + ); + if (canvas.getContext('webgl2') != null) { + return 2; + } + if (canvas.getContext('webgl') != null) { + return 1; + } + return -1; +} diff --git a/lib/web_ui/lib/src/engine/compositor/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart similarity index 96% rename from lib/web_ui/lib/src/engine/compositor/canvas.dart rename to lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 690d5b29b355c..3426253473469 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -5,7 +5,10 @@ // @dart = 2.10 part of engine; -/// A Dart wrapper around Skia's SKCanvas. +/// A Dart wrapper around Skia's [SkCanvas]. +/// +/// This is intentionally not memory-managing the underlying [SkCanvas]. See +/// the docs on [SkCanvas], which explain the reason. class CkCanvas { final SkCanvas skCanvas; @@ -203,7 +206,7 @@ class CkCanvas { ui.Vertices vertices, ui.BlendMode blendMode, CkPaint paint) { CkVertices skVertices = vertices as CkVertices; skCanvas.drawVertices( - skVertices.skVertices, + skVertices.skiaObject, toSkBlendMode(blendMode), paint.skiaObject, ); diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart similarity index 83% rename from lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart rename to lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index ab51ab4134a44..8a67398f07181 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -4,7 +4,7 @@ /// Bindings for CanvasKit JavaScript API. /// -/// Prefer keeping the originl CanvasKit names so it is easier to locate +/// Prefer keeping the original CanvasKit names so it is easier to locate /// the API behind these bindings in the Skia source code. // @dart = 2.10 @@ -17,10 +17,11 @@ late CanvasKit canvasKit; /// static APIs. /// /// See, e.g. [SkPaint]. -@JS('window.flutter_canvas_kit') +@JS('window.flutterCanvasKit') external set windowFlutterCanvasKit(CanvasKit value); @JS() +@anonymous class CanvasKit { external SkBlendModeEnum get BlendMode; external SkPaintStyleEnum get PaintStyle; @@ -30,6 +31,8 @@ class CanvasKit { external SkBlurStyleEnum get BlurStyle; external SkTileModeEnum get TileMode; external SkFillTypeEnum get FillType; + external SkAlphaTypeEnum get AlphaType; + external SkColorTypeEnum get ColorType; external SkPathOpEnum get PathOp; external SkClipOpEnum get ClipOp; external SkPointModeEnum get PointMode; @@ -43,7 +46,8 @@ class CanvasKit { external SkFontSlantEnum get FontSlant; external SkAnimatedImage MakeAnimatedImageFromEncoded(Uint8List imageData); external SkShaderNamespace get SkShader; - external SkMaskFilter MakeBlurMaskFilter(SkBlurStyle blurStyle, double sigma, bool respectCTM); + external SkMaskFilter MakeBlurMaskFilter( + SkBlurStyle blurStyle, double sigma, bool respectCTM); external SkColorFilterNamespace get SkColorFilter; external SkImageFilterNamespace get SkImageFilter; external SkPath MakePathFromOp(SkPath path1, SkPath path2, SkPathOp pathOp); @@ -57,8 +61,16 @@ class CanvasKit { Uint16List? indices, ); external SkParagraphBuilderNamespace get ParagraphBuilder; - external SkParagraphStyle ParagraphStyle(SkParagraphStyleProperties properties); + external SkParagraphStyle ParagraphStyle( + SkParagraphStyleProperties properties); external SkTextStyle TextStyle(SkTextStyleProperties properties); + external SkSurface MakeSurface( + int width, + int height, + ); + external Uint8List getSkDataBytes( + SkData skData, + ); // Text decoration enum is embedded in the CanvasKit object itself. external int get NoDecoration; @@ -68,12 +80,14 @@ class CanvasKit { // End of text decoration enum. external SkFontMgrNamespace get SkFontMgr; - external int GetWebGLContext(html.CanvasElement canvas, SkWebGLContextOptions options); + external TypefaceFontProviderNamespace get TypefaceFontProvider; + external int GetWebGLContext( + html.CanvasElement canvas, SkWebGLContextOptions options); external SkGrContext MakeGrContext(int glContext); external SkSurface MakeOnScreenGLSurface( SkGrContext grContext, - double width, - double height, + int width, + int height, SkColorSpace colorSpace, ); external SkSurface MakeSWCanvasSurface(html.CanvasElement canvas); @@ -100,7 +114,7 @@ class CanvasKitInitPromise { external void then(CanvasKitInitCallback callback); } -@JS('window.flutter_canvas_kit.SkColorSpace.SRGB') +@JS('window.flutterCanvasKit.SkColorSpace.SRGB') external SkColorSpace get SkColorSpaceSRGB; @JS() @@ -111,6 +125,8 @@ class SkColorSpace {} class SkWebGLContextOptions { external factory SkWebGLContextOptions({ required int anitalias, + // WebGL version: 1 or 2. + required int majorVersion, }); } @@ -121,9 +137,11 @@ class SkSurface { external int width(); external int height(); external void dispose(); + external SkImage makeImageSnapshot(); } @JS() +@anonymous class SkGrContext { external void setResourceCacheLimitBytes(int limit); external void releaseResourcesAndAbandonContext(); @@ -569,7 +587,6 @@ SkStrokeJoin toSkStrokeJoin(ui.StrokeJoin strokeJoin) { return _skStrokeJoins[strokeJoin.index]; } - @JS() class SkFilterQualityEnum { external SkFilterQuality get None; @@ -594,7 +611,6 @@ SkFilterQuality toSkFilterQuality(ui.FilterQuality filterQuality) { return _skFilterQualitys[filterQuality.index]; } - @JS() class SkTileModeEnum { external SkTileMode get Clamp; @@ -618,14 +634,50 @@ SkTileMode toSkTileMode(ui.TileMode mode) { } @JS() +class SkAlphaTypeEnum { + external SkAlphaType get Opaque; + external SkAlphaType get Premul; + external SkAlphaType get Unpremul; +} + +@JS() +class SkAlphaType { + external int get value; +} + +@JS() +class SkColorTypeEnum { + external SkColorType get Alpha_8; + external SkColorType get RGB_565; + external SkColorType get ARGB_4444; + external SkColorType get RGBA_8888; + external SkColorType get RGB_888x; + external SkColorType get BGRA_8888; + external SkColorType get RGBA_1010102; + external SkColorType get RGB_101010x; + external SkColorType get Gray_8; + external SkColorType get RGBA_F16; + external SkColorType get RGBA_F32; +} + +@JS() +class SkColorType { + external int get value; +} + +@JS() +@anonymous class SkAnimatedImage { external int getFrameCount(); + /// Returns duration in milliseconds. external int getRepetitionCount(); external int decodeNextFrame(); external SkImage getCurrentFrame(); external int width(); external int height(); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); /// Deletes the C++ object. /// @@ -634,11 +686,18 @@ class SkAnimatedImage { } @JS() +@anonymous class SkImage { external void delete(); external int width(); external int height(); - external SkShader makeShader(SkTileMode tileModeX, SkTileMode tileModeY); + external SkShader makeShader( + SkTileMode tileModeX, + SkTileMode tileModeY, + Float32List? matrix, // 3x3 matrix + ); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); } @JS() @@ -672,18 +731,31 @@ class SkShaderNamespace { Float32List? matrix, // 3x3 matrix int flags, ); + + external SkShader MakeSweepGradient( + double cx, + double cy, + List colors, + Float32List colorStops, + SkTileMode tileMode, + Float32List? matrix, // 3x3 matrix + int flags, + double startAngle, + double endAngle, + ); } @JS() +@anonymous class SkShader { - + external void delete(); } // This needs to be bound to top-level because SkPaint is initialized // with `new`. Also in Dart you can't write this: // // external SkPaint SkPaint(); -@JS('window.flutter_canvas_kit.SkPaint') +@JS('window.flutterCanvasKit.SkPaint') class SkPaint { // TODO(yjbanov): implement invertColors, see paint.cc external SkPaint(); @@ -704,6 +776,7 @@ class SkPaint { } @JS() +@anonymous class SkMaskFilter { external void delete(); } @@ -719,6 +792,7 @@ class SkColorFilterNamespace { } @JS() +@anonymous class SkColorFilter { external void delete(); } @@ -740,6 +814,7 @@ class SkImageFilterNamespace { } @JS() +@anonymous class SkImageFilter { external void delete(); } @@ -816,7 +891,7 @@ external _NativeFloat32ArrayType get _nativeFloat32ArrayType; @JS() class _NativeFloat32ArrayType {} -@JS('window.flutter_canvas_kit.Malloc') +@JS('window.flutterCanvasKit.Malloc') external SkFloat32List _mallocFloat32List( _NativeFloat32ArrayType float32ListType, int size, @@ -835,7 +910,7 @@ SkFloat32List mallocFloat32List(int size) { /// The [list] is no longer usable after calling this function. /// /// Use this function to free lists owned by the engine. -@JS('window.flutter_canvas_kit.Free') +@JS('window.flutterCanvasKit.Free') external void freeFloat32List(SkFloat32List list); /// Wraps a [Float32List] backed by WASM memory. @@ -873,6 +948,7 @@ Float32List _populateSkColor(SkFloat32List skColor, ui.Color color) { Float32List toSharedSkColor1(ui.Color color) { return _populateSkColor(_sharedSkColor1, color); } + final SkFloat32List _sharedSkColor1 = mallocFloat32List(4); /// Unpacks the [color] into CanvasKit-compatible representation stored @@ -883,6 +959,7 @@ final SkFloat32List _sharedSkColor1 = mallocFloat32List(4); Float32List toSharedSkColor2(ui.Color color) { return _populateSkColor(_sharedSkColor2, color); } + final SkFloat32List _sharedSkColor2 = mallocFloat32List(4); /// Unpacks the [color] into CanvasKit-compatible representation stored @@ -893,6 +970,7 @@ final SkFloat32List _sharedSkColor2 = mallocFloat32List(4); Float32List toSharedSkColor3(ui.Color color) { return _populateSkColor(_sharedSkColor3, color); } + final SkFloat32List _sharedSkColor3 = mallocFloat32List(4); Uint32List toSkIntColorList(List colors) { @@ -928,7 +1006,7 @@ List encodeRawColorList(Int32List rawColors) { return toSkFloatColorList(colors); } -@JS('window.flutter_canvas_kit.SkPath') +@JS('window.flutterCanvasKit.SkPath') class SkPath { external SkPath([SkPath? other]); external void setFillType(SkFillType fillType); @@ -967,12 +1045,21 @@ class SkPath { external void addRect( SkRect rect, ); - external void arcTo( + external void arcToOval( SkRect oval, double startAngleDegrees, double sweepAngleDegrees, bool forceMoveTo, ); + external void arcToRotated( + double radiusX, + double radiusY, + double rotation, + bool useSmallArc, + bool counterClockWise, + double x, + double y, + ); external void close(); external void conicTo( double x1, @@ -1051,22 +1138,7 @@ class SkPath { ); } -/// A different view on [SkPath] used to overload [SkPath.arcTo]. -// TODO(yjbanov): this is a hack to get around https://github.com/flutter/flutter/issues/61305 -@JS() -class SkPathArcToPointOverload { - external void arcTo( - double radiusX, - double radiusY, - double rotation, - bool useSmallArc, - bool counterClockWise, - double x, - double y, - ); -} - -@JS('window.flutter_canvas_kit.SkContourMeasureIter') +@JS('window.flutterCanvasKit.SkContourMeasureIter') class SkContourMeasureIter { external SkContourMeasureIter(SkPath path, bool forceClosed, int startIndex); external SkContourMeasure? next(); @@ -1220,7 +1292,7 @@ Uint16List toUint16List(List ints) { return result; } -@JS('window.flutter_canvas_kit.SkPictureRecorder') +@JS('window.flutterCanvasKit.SkPictureRecorder') class SkPictureRecorder { external SkPictureRecorder(); external SkCanvas beginRecording(SkRect bounds); @@ -1228,6 +1300,11 @@ class SkPictureRecorder { external void delete(); } +/// We do not use the `delete` method (which may be removed in the future anyway). +/// +/// By Skia coding convention raw pointers should always be treated as +/// "borrowed", i.e. their memory is managed by other objects. In the case of +/// [SkCanvas] it is managed by [SkPictureRecorder]. @JS() class SkCanvas { external void clear(Float32List color); @@ -1382,6 +1459,7 @@ class SkCanvasSaveLayerWithFilterOverload { } @JS() +@anonymous class SkPicture { external void delete(); } @@ -1392,20 +1470,27 @@ class SkParagraphBuilderNamespace { SkParagraphStyle paragraphStyle, SkFontMgr? fontManager, ); + + external SkParagraphBuilder MakeFromFontProvider( + SkParagraphStyle paragraphStyle, + TypefaceFontProvider? fontManager, + ); } @JS() +@anonymous class SkParagraphBuilder { external void addText(String text); external void pushStyle(SkTextStyle textStyle); + external void pushPaintStyle( + SkTextStyle textStyle, SkPaint foreground, SkPaint background); external void pop(); external SkParagraph build(); external void delete(); } @JS() -class SkParagraphStyle { -} +class SkParagraphStyle {} @JS() @anonymous @@ -1433,9 +1518,7 @@ class SkParagraphStyleProperties { } @JS() -class SkTextStyle { - -} +class SkTextStyle {} @JS() @anonymous @@ -1476,12 +1559,20 @@ class SkFontStyle { } @JS() +@anonymous class SkFontMgr { external String? getFamilyName(int fontId); external void delete(); } +@JS('window.flutterCanvasKit.TypefaceFontProvider') +class TypefaceFontProvider extends SkFontMgr { + external TypefaceFontProvider(); + external void registerFont(Uint8List font, String family); +} + @JS() +@anonymous class SkParagraph { external double getAlphabeticBaseline(); external bool didExceedMaxLines(); @@ -1519,7 +1610,10 @@ class SkTextRange { } @JS() -class SkVertices { } +@anonymous +class SkVertices { + external void delete(); +} @JS() @anonymous @@ -1537,3 +1631,111 @@ class SkFontMgrNamespace { // TODO(yjbanov): can this be made non-null? It returns null in our unit-tests right now. external SkFontMgr? FromData(List fonts); } + +@JS() +class TypefaceFontProviderNamespace { + external TypefaceFontProvider Make(); +} + +Timer? _skObjectCollector; +List _skObjectDeleteQueue = []; + +final SkObjectFinalizationRegistry skObjectFinalizationRegistry = SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) { + _skObjectDeleteQueue.add(deletable); + _skObjectCollector ??= _scheduleSkObjectCollection(); +})); + +/// Schedules an asap timer to delete garbage-collected Skia objects. +/// +/// We use a timer for the following reasons: +/// +/// - Deleting the object immediately may lead to dangling pointer as the Skia +/// object may still be used by a function in the current frame. For example, +/// a `CkPaint` + `SkPaint` pair may be created by the framework, passed to +/// the engine, and the `CkPaint` dropped immediately. Because GC can kick in +/// any time, including in the middle of the event, we may delete `SkPaint` +/// prematurely. +/// - A microtask, while solves the problem above, would prevent the event from +/// yielding to the graphics system to render the frame on the screen if there +/// is a large number of objects to delete, causing jank. +Timer _scheduleSkObjectCollection() => Timer(Duration.zero, () { + html.window.performance.mark('SkObject collection-start'); + final int length = _skObjectDeleteQueue.length; + for (int i = 0; i < length; i++) { + _skObjectDeleteQueue[i].delete(); + } + _skObjectDeleteQueue = []; + + // Null out the timer so we can schedule a new one next time objects are + // scheduled for deletion. + _skObjectCollector = null; + html.window.performance.mark('SkObject collection-end'); + html.window.performance.measure('SkObject collection', 'SkObject collection-start', 'SkObject collection-end'); +}); + +typedef SkObjectFinalizer = void Function(T key); + +/// Any Skia object that has a `delete` method. +@JS() +@anonymous +class SkDeletable { + /// Deletes the C++ side object. + external void delete(); +} + +/// Attaches a weakly referenced object to another object and calls a finalizer +/// with the latter when weakly referenced object is garbage collected. +/// +/// We use this to delete Skia objects when their "Ck" wrapper is garbage +/// collected. +/// +/// Example sequence of events: +/// +/// 1. A (CkPaint, SkPaint) pair created. +/// 2. The paint is used to paint some picture. +/// 3. CkPaint is dropped by the app. +/// 4. GC decides to perform a GC cycle and collects CkPaint. +/// 5. The finalizer function is called with the SkPaint as the sole argument. +/// 6. We call `delete` on SkPaint. +@JS('window.FinalizationRegistry') +class SkObjectFinalizationRegistry { + external SkObjectFinalizationRegistry(SkObjectFinalizer finalizer); + external void register(Object ckObject, Object skObject); +} + +@JS('window.FinalizationRegistry') +external Object? get _finalizationRegistryConstructor; + +/// Whether the current browser supports `FinalizationRegistry`. +bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; + +@JS() +class SkData { + external int size(); + external bool isEmpty(); + external Uint8List bytes(); +} + +@JS() +@anonymous +class SkImageInfo { + external factory SkImageInfo({ + required int width, + required int height, + SkAlphaType alphaType, + SkColorSpace colorSpace, + SkColorType colorType, + }); + external SkAlphaType get alphaType; + external SkColorSpace get colorSpace; + external SkColorType get colorType; + external int get height; + external bool get isEmpty; + external bool get isOpaque; + external SkRect get bounds; + external int get width; + external SkImageInfo makeAlphaType(SkAlphaType alphaType); + external SkImageInfo makeColorSpace(SkColorSpace colorSpace); + external SkImageInfo makeColorType(SkColorType colorType); + external SkImageInfo makeWH(int width, int height); +} diff --git a/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart similarity index 95% rename from lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart rename to lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart index 4af4b9e935061..92c60e8ac2fbc 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart @@ -350,23 +350,22 @@ class CanvasKitCanvas implements ui.Canvas { ui.Image atlas, List transforms, List rects, - List colors, - ui.BlendMode blendMode, + List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(transforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || colors.isEmpty || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; if (transforms.length != rectCount) { throw ArgumentError('"transforms" and "rects" lengths must match.'); } - if (colors.isNotEmpty && colors.length != rectCount) { + if (colors != null && colors.isNotEmpty && colors.length != rectCount) { throw ArgumentError( 'If non-null, "colors" length must match that of "transforms" and "rects".'); } @@ -393,10 +392,10 @@ class CanvasKitCanvas implements ui.Canvas { } final List? colorBuffer = - colors.isEmpty ? null : toSkFloatColorList(colors); + (colors == null || colors.isEmpty) ? null : toSkFloatColorList(colors); _drawAtlas( - paint, atlas, rstTransformBuffer, rectBuffer, colorBuffer, blendMode); + paint, atlas, rstTransformBuffer, rectBuffer, colorBuffer, blendMode ?? ui.BlendMode.src); } @override @@ -404,16 +403,15 @@ class CanvasKitCanvas implements ui.Canvas { ui.Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - ui.BlendMode blendMode, + Int32List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(rstTransforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; @@ -422,12 +420,13 @@ class CanvasKitCanvas implements ui.Canvas { if (rectCount % 4 != 0) throw ArgumentError( '"rstTransforms" and "rects" lengths must be a multiple of four.'); - if (colors.length * 4 != rectCount) + if (colors != null && colors.length * 4 != rectCount) throw ArgumentError( 'If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); - _drawAtlas(paint, atlas, rstTransforms, rects, encodeRawColorList(colors), - blendMode); + final List? colorBuffer = colors == null ? null : encodeRawColorList(colors); + + _drawAtlas(paint, atlas, rstTransforms, rects, colorBuffer, blendMode ?? ui.BlendMode.src); } // TODO(hterkelsen): Pass a cull_rect once CanvasKit supports that. diff --git a/lib/web_ui/lib/src/engine/compositor/color_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart similarity index 96% rename from lib/web_ui/lib/src/engine/compositor/color_filter.dart rename to lib/web_ui/lib/src/engine/canvaskit/color_filter.dart index 341c5f5c02b56..2c219b4b19957 100644 --- a/lib/web_ui/lib/src/engine/compositor/color_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart @@ -6,7 +6,7 @@ part of engine; /// A [ui.ColorFilter] backed by Skia's [CkColorFilter]. -class CkColorFilter extends ResurrectableSkiaObject { +class CkColorFilter extends ManagedSkiaObject { final EngineColorFilter _engineFilter; CkColorFilter.mode(EngineColorFilter filter) : _engineFilter = filter; diff --git a/lib/web_ui/lib/src/engine/compositor/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart similarity index 99% rename from lib/web_ui/lib/src/engine/compositor/embedded_views.dart rename to lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 0a29ef7ef0495..5f8c4a9b8e73c 100644 --- a/lib/web_ui/lib/src/engine/compositor/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -49,7 +49,7 @@ class HtmlViewEmbedder { Map _clipCount = {}; /// The size of the frame, in physical pixels. - ui.Size _frameSize = _computeFrameSize(); + ui.Size _frameSize = ui.window.physicalSize; void set frameSize(ui.Size size) { if (_frameSize == size) { diff --git a/lib/web_ui/lib/src/engine/compositor/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart similarity index 69% rename from lib/web_ui/lib/src/engine/compositor/fonts.dart rename to lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 7d40185e35b37..a26a3192ae316 100644 --- a/lib/web_ui/lib/src/engine/compositor/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -20,28 +20,18 @@ class SkiaFontCollection { >[]; /// Fonts which have been registered and loaded. - final List<_RegisteredFont?> _registeredFonts = <_RegisteredFont?>[]; - - /// A mapping from the name a font was registered with, to the family name - /// embedded in the font's bytes (the font's "actual" name). - /// - /// For example, a font may be registered in Flutter assets with the name - /// "MaterialIcons", but if you read the family name out of the font's bytes - /// it is actually "Material Icons". Skia works with the actual names of the - /// fonts, so when we create a Skia Paragraph with Flutter font families, we - /// must convert them to their actual family name when we pass them to Skia. - final Map fontFamilyOverrides = {}; + final List<_RegisteredFont> _registeredFonts = <_RegisteredFont>[]; final Set registeredFamilies = {}; Future ensureFontsLoaded() async { await _loadFonts(); - _computeFontFamilyOverrides(); - final List fontBuffers = - _registeredFonts.map((f) => f!.bytes).toList(); + fontProvider = canvasKit.TypefaceFontProvider.Make(); - skFontMgr = canvasKit.SkFontMgr.FromData(fontBuffers); + for (var font in _registeredFonts) { + fontProvider!.registerFont(font.bytes, font.flutterFamily); + } } /// Loads all of the unloaded fonts in [_unloadedFonts] and adds them @@ -50,28 +40,14 @@ class SkiaFontCollection { if (_unloadedFonts.isEmpty) { return; } - - final List<_RegisteredFont?> loadedFonts = await Future.wait(_unloadedFonts); - _registeredFonts.addAll(loadedFonts.where((x) => x != null)); - _unloadedFonts.clear(); - } - - void _computeFontFamilyOverrides() { - fontFamilyOverrides.clear(); - - for (_RegisteredFont? font in _registeredFonts) { - if (fontFamilyOverrides.containsKey(font!.flutterFamily)) { - if (fontFamilyOverrides[font.flutterFamily] != font.actualFamily) { - html.window.console.warn('Fonts in family ${font.flutterFamily} ' - 'have different actual family names.'); - html.window.console.warn( - 'Current actual family: ${fontFamilyOverrides[font.flutterFamily]}'); - html.window.console.warn('New actual family: ${font.actualFamily}'); - } - } else { - fontFamilyOverrides[font.flutterFamily] = font.actualFamily; + final List<_RegisteredFont?> loadedFonts = + await Future.wait(_unloadedFonts); + for (_RegisteredFont? font in loadedFonts) { + if (font != null) { + _registeredFonts.add(font); } } + _unloadedFonts.clear(); } Future loadFontFromList(Uint8List list, {String? fontFamily}) async { @@ -118,8 +94,9 @@ class SkiaFontCollection { 'There was a problem trying to load FontManifest.json'); } - for (Map fontFamily in fontManifest.cast>()) { - final String? family = fontFamily['family']; + for (Map fontFamily + in fontManifest.cast>()) { + final String family = fontFamily['family']!; final List fontAssets = fontFamily['fonts']; registeredFamilies.add(family); @@ -141,10 +118,12 @@ class SkiaFontCollection { } } - Future<_RegisteredFont?> _registerFont(String url, String? family) async { + Future<_RegisteredFont?> _registerFont(String url, String family) async { ByteBuffer buffer; try { - buffer = await html.window.fetch(url).then(_getArrayBuffer as FutureOr Function(dynamic)); + buffer = await html.window + .fetch(url) + .then(_getArrayBuffer as FutureOr Function(dynamic)); } catch (e) { html.window.console.warn('Failed to load font $family at $url'); html.window.console.warn(e); @@ -160,7 +139,7 @@ class SkiaFontCollection { actualFamily = family; } - return _RegisteredFont(bytes, family!, actualFamily!); + return _RegisteredFont(bytes, family, actualFamily); } String? _readActualFamilyName(Uint8List bytes) { @@ -178,6 +157,7 @@ class SkiaFontCollection { } SkFontMgr? skFontMgr; + TypefaceFontProvider? fontProvider; } /// Represents a font that has been registered. diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart new file mode 100644 index 0000000000000..8bcb210d961ef --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -0,0 +1,185 @@ +// 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. + +// @dart = 2.10 +part of engine; + +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia. +void skiaInstantiateImageCodec(Uint8List list, Callback callback, + [int? width, int? height, int? format, int? rowBytes]) { + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(list); + final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); + final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); + callback(codec); +} + +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after requesting from URI. +void skiaInstantiateWebImageCodec(String src, Callback callback, + WebOnlyImageCodecChunkCallback? chunkCallback) { + chunkCallback?.call(0, 100); + //TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported. + html.HttpRequest.request( + src, + responseType: "arraybuffer", + ).then((html.HttpRequest response) { + chunkCallback?.call(100, 100); + final Uint8List list = + new Uint8List.view((response.response as ByteBuffer)); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(list); + final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); + final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); + callback(codec); + }); +} + +/// A wrapper for `SkAnimatedImage`. +class CkAnimatedImage implements ui.Image { + final SkAnimatedImage _skAnimatedImage; + + // Use a box because `SkImage` may be deleted either due to this object + // being garbage-collected, or by an explicit call to [delete]. + late final SkiaObjectBox box; + + CkAnimatedImage(this._skAnimatedImage) { + box = SkiaObjectBox(this, _skAnimatedImage as SkDeletable); + } + + @override + void dispose() { + box.delete(); + } + + int get frameCount => _skAnimatedImage.getFrameCount(); + + /// Decodes the next frame and returns the frame duration. + Duration decodeNextFrame() { + final int durationMillis = _skAnimatedImage.decodeNextFrame(); + return Duration(milliseconds: durationMillis); + } + + int get repetitionCount => _skAnimatedImage.getRepetitionCount(); + + CkImage get currentFrameAsImage { + return CkImage(_skAnimatedImage.getCurrentFrame()); + } + + @override + int get width => _skAnimatedImage.width(); + + @override + int get height => _skAnimatedImage.height(); + + @override + Future toByteData( + {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); + } +} + +/// A [ui.Image] backed by an `SkImage` from Skia. +class CkImage implements ui.Image { + final SkImage skImage; + + // Use a box because `SkImage` may be deleted either due to this object + // being garbage-collected, or by an explicit call to [delete]. + late final SkiaObjectBox box; + + CkImage(this.skImage) { + box = SkiaObjectBox(this, skImage as SkDeletable); + } + + @override + void dispose() { + box.delete(); + } + + @override + int get width => skImage.width(); + + @override + int get height => skImage.height(); + + @override + Future toByteData( + {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = skImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = skImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); + } +} + +/// A [Codec] that wraps an `SkAnimatedImage`. +class CkAnimatedImageCodec implements ui.Codec { + CkAnimatedImage animatedImage; + + CkAnimatedImageCodec(this.animatedImage); + + @override + void dispose() { + animatedImage.dispose(); + } + + @override + int get frameCount => animatedImage.frameCount; + + @override + int get repetitionCount => animatedImage.repetitionCount; + + @override + Future getNextFrame() { + final Duration duration = animatedImage.decodeNextFrame(); + final CkImage image = animatedImage.currentFrameAsImage; + return Future.value(AnimatedImageFrameInfo(duration, image)); + } +} + +/// Data for a single frame of an animated image. +class AnimatedImageFrameInfo implements ui.FrameInfo { + final Duration _duration; + final CkImage _image; + + AnimatedImageFrameInfo(this._duration, this._image); + + @override + Duration get duration => _duration; + + @override + ui.Image get image => _image; +} diff --git a/lib/web_ui/lib/src/engine/compositor/image_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart similarity index 92% rename from lib/web_ui/lib/src/engine/compositor/image_filter.dart rename to lib/web_ui/lib/src/engine/canvaskit/image_filter.dart index 55288800db721..02ebb4bccad4e 100644 --- a/lib/web_ui/lib/src/engine/compositor/image_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart @@ -8,7 +8,7 @@ part of engine; /// The CanvasKit implementation of [ui.ImageFilter]. /// /// Currently only supports `blur`. -class CkImageFilter extends ResurrectableSkiaObject implements ui.ImageFilter { +class CkImageFilter extends ManagedSkiaObject implements ui.ImageFilter { CkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) : _sigmaX = sigmaX, _sigmaY = sigmaY; diff --git a/lib/web_ui/lib/src/engine/compositor/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart similarity index 97% rename from lib/web_ui/lib/src/engine/compositor/initialization.dart rename to lib/web_ui/lib/src/engine/canvaskit/initialization.dart index fa6207b455871..c1d91ad525925 100644 --- a/lib/web_ui/lib/src/engine/compositor/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -20,7 +20,7 @@ const bool canvasKitForceCpuOnly = /// NPM, update this URL to `https://unpkg.com/canvaskit-wasm@0.34.0/bin/`. const String canvasKitBaseUrl = String.fromEnvironment( 'FLUTTER_WEB_CANVASKIT_URL', - defaultValue: 'https://unpkg.com/canvaskit-wasm@0.17.2/bin/', + defaultValue: 'https://unpkg.com/canvaskit-wasm@0.17.3/bin/', ); /// Initialize CanvasKit. diff --git a/lib/web_ui/lib/src/engine/compositor/layer.dart b/lib/web_ui/lib/src/engine/canvaskit/layer.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/layer.dart rename to lib/web_ui/lib/src/engine/canvaskit/layer.dart diff --git a/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart rename to lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart diff --git a/lib/web_ui/lib/src/engine/compositor/layer_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart similarity index 92% rename from lib/web_ui/lib/src/engine/compositor/layer_tree.dart rename to lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart index afde94d1c26ed..3b40be4728b39 100644 --- a/lib/web_ui/lib/src/engine/compositor/layer_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart @@ -11,7 +11,7 @@ class LayerTree { Layer? rootLayer; /// The size (in physical pixels) of the frame to paint this layer tree into. - final ui.Size frameSize = _computeFrameSize(); + final ui.Size frameSize = ui.window.physicalSize; /// The devicePixelRatio of the frame to paint this layer tree into. double? devicePixelRatio; @@ -54,14 +54,6 @@ class LayerTree { } } -ui.Size _computeFrameSize() { - final ui.Size physicalSize = ui.window.physicalSize; - return ui.Size( - physicalSize.width.truncate().toDouble(), - physicalSize.height.truncate().toDouble(), - ); -} - /// A single frame to be rendered. class Frame { /// The canvas to render this frame to. diff --git a/lib/web_ui/lib/src/engine/compositor/mask_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart similarity index 91% rename from lib/web_ui/lib/src/engine/compositor/mask_filter.dart rename to lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart index 51be8820dc61f..8c120d1520035 100644 --- a/lib/web_ui/lib/src/engine/compositor/mask_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart @@ -6,7 +6,7 @@ part of engine; /// The CanvasKit implementation of [ui.MaskFilter]. -class CkMaskFilter extends ResurrectableSkiaObject { +class CkMaskFilter extends ManagedSkiaObject { CkMaskFilter.blur(ui.BlurStyle blurStyle, double sigma) : _blurStyle = blurStyle, _sigma = sigma; diff --git a/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart rename to lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart diff --git a/lib/web_ui/lib/src/engine/compositor/painting.dart b/lib/web_ui/lib/src/engine/canvaskit/painting.dart similarity index 94% rename from lib/web_ui/lib/src/engine/compositor/painting.dart rename to lib/web_ui/lib/src/engine/canvaskit/painting.dart index f8c21b2b7dd02..550ff12351e40 100644 --- a/lib/web_ui/lib/src/engine/compositor/painting.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/painting.dart @@ -9,7 +9,7 @@ part of engine; /// /// This class is backed by a Skia object that must be explicitly /// deleted to avoid a memory leak. This is done by extending [SkiaObject]. -class CkPaint extends ResurrectableSkiaObject implements ui.Paint { +class CkPaint extends ManagedSkiaObject implements ui.Paint { CkPaint(); static const ui.Color _defaultPaintColor = ui.Color(0xFF000000); @@ -117,17 +117,17 @@ class CkPaint extends ResurrectableSkiaObject implements ui.Paint { bool _invertColors = false; @override - ui.Shader? get shader => _shader as ui.Shader?; + ui.Shader? get shader => _shader; @override set shader(ui.Shader? value) { if (_shader == value) { return; } - _shader = value as EngineShader?; - skiaObject.setShader(_shader?.createSkiaShader()); + _shader = value as CkShader?; + skiaObject.setShader(_shader?.skiaObject); } - EngineShader? _shader; + CkShader? _shader; @override ui.MaskFilter? get maskFilter => _maskFilter; @@ -222,7 +222,7 @@ class CkPaint extends ResurrectableSkiaObject implements ui.Paint { paint.setStrokeWidth(_strokeWidth); paint.setAntiAlias(_isAntiAlias); paint.setColorInt(_color.value); - paint.setShader(_shader?.createSkiaShader()); + paint.setShader(_shader?.skiaObject); paint.setMaskFilter(_ckMaskFilter?.skiaObject); paint.setColorFilter(_ckColorFilter?.skiaObject); paint.setImageFilter(_imageFilter?.skiaObject); diff --git a/lib/web_ui/lib/src/engine/compositor/path.dart b/lib/web_ui/lib/src/engine/canvaskit/path.dart similarity index 99% rename from lib/web_ui/lib/src/engine/compositor/path.dart rename to lib/web_ui/lib/src/engine/canvaskit/path.dart index 321c6a1246f39..723517eb5ce5e 100644 --- a/lib/web_ui/lib/src/engine/compositor/path.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/path.dart @@ -116,7 +116,7 @@ class CkPath implements ui.Path { void arcTo( ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { const double toDegrees = 180.0 / math.pi; - _skPath.arcTo( + _skPath.arcToOval( toSkRect(rect), startAngle * toDegrees, sweepAngle * toDegrees, @@ -130,7 +130,7 @@ class CkPath implements ui.Path { double rotation = 0.0, bool largeArc = false, bool clockwise = true}) { - (_skPath as SkPathArcToPointOverload).arcTo( + _skPath.arcToRotated( radius.x, radius.y, rotation, diff --git a/lib/web_ui/lib/src/engine/compositor/path_metrics.dart b/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/path_metrics.dart rename to lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart similarity index 65% rename from lib/web_ui/lib/src/engine/compositor/picture.dart rename to lib/web_ui/lib/src/engine/canvaskit/picture.dart index 49d6fea0c3731..2a0d292ebd45b 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -21,9 +21,13 @@ class CkPicture implements ui.Picture { } @override - Future toImage(int width, int height) { - throw UnsupportedError( - 'Picture.toImage not yet implemented for CanvasKit and HTML'); + Future toImage(int width, int height) async { + final SkSurface skSurface = canvasKit.MakeSurface(width, height); + final SkCanvas skCanvas = skSurface.getCanvas(); + skCanvas.drawPicture(skiaObject.skiaObject); + final SkImage skImage = skSurface.makeImageSnapshot(); + skSurface.dispose(); + return CkImage(skImage); } } @@ -32,6 +36,6 @@ class SkPictureSkiaObject extends OneShotSkiaObject { @override void delete() { - rawSkiaObject?.delete(); + rawSkiaObject.delete(); } } diff --git a/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/picture_recorder.dart rename to lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart diff --git a/lib/web_ui/lib/src/engine/compositor/platform_message.dart b/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/platform_message.dart rename to lib/web_ui/lib/src/engine/canvaskit/platform_message.dart diff --git a/lib/web_ui/lib/src/engine/compositor/raster_cache.dart b/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/raster_cache.dart rename to lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart diff --git a/lib/web_ui/lib/src/engine/compositor/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/rasterizer.dart rename to lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart diff --git a/lib/web_ui/lib/src/engine/canvaskit/shader.dart b/lib/web_ui/lib/src/engine/canvaskit/shader.dart new file mode 100644 index 0000000000000..6b0074b609d4c --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/shader.dart @@ -0,0 +1,186 @@ +// 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. + +// @dart = 2.10 +part of engine; + +abstract class CkShader extends ManagedSkiaObject implements ui.Shader { + @override + void delete() { + rawSkiaObject?.delete(); + } +} + +class CkGradientSweep extends CkShader implements ui.Gradient { + CkGradientSweep(this.center, this.colors, this.colorStops, this.tileMode, + this.startAngle, this.endAngle, this.matrix4) + : assert(_offsetIsValid(center)), + assert(colors != null), // ignore: unnecessary_null_comparison + assert(tileMode != null), // ignore: unnecessary_null_comparison + assert(startAngle != null), // ignore: unnecessary_null_comparison + assert(endAngle != null), // ignore: unnecessary_null_comparison + assert(startAngle < endAngle), + assert(matrix4 == null || _matrix4IsValid(matrix4)) { + _validateColorStops(colors, colorStops); + } + + final ui.Offset center; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final double startAngle; + final double endAngle; + final Float32List? matrix4; + + @override + SkShader createDefault() { + return canvasKit.SkShader.MakeSweepGradient( + center.dx, + center.dy, + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, + 0, + startAngle, + endAngle, + ); + } + + @override + SkShader resurrect() { + return createDefault(); + } +} + +class CkGradientLinear extends CkShader implements ui.Gradient { + CkGradientLinear( + this.from, + this.to, + this.colors, + this.colorStops, + this.tileMode, + Float64List? matrix, + ) : assert(_offsetIsValid(from)), + assert(_offsetIsValid(to)), + assert(colors != null), // ignore: unnecessary_null_comparison + assert(tileMode != null), // ignore: unnecessary_null_comparison + this.matrix4 = matrix == null ? null : _FastMatrix64(matrix) { + if (assertionsEnabled) { + _validateColorStops(colors, colorStops); + } + } + + final ui.Offset from; + final ui.Offset to; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final _FastMatrix64? matrix4; + + @override + SkShader createDefault() { + assert(experimentalUseSkia); + + return canvasKit.SkShader.MakeLinearGradient( + toSkPoint(from), + toSkPoint(to), + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + ); + } + + @override + SkShader resurrect() => createDefault(); +} + +class CkGradientRadial extends CkShader implements ui.Gradient { + CkGradientRadial(this.center, this.radius, this.colors, this.colorStops, + this.tileMode, this.matrix4); + + final ui.Offset center; + final double radius; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final Float32List? matrix4; + + @override + SkShader createDefault() { + assert(experimentalUseSkia); + + return canvasKit.SkShader.MakeRadialGradient( + toSkPoint(center), + radius, + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, + 0, + ); + } + + @override + SkShader resurrect() => createDefault(); +} + +class CkGradientConical extends CkShader implements ui.Gradient { + CkGradientConical(this.focal, this.focalRadius, this.center, this.radius, + this.colors, this.colorStops, this.tileMode, this.matrix4); + + final ui.Offset focal; + final double focalRadius; + final ui.Offset center; + final double radius; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final Float32List? matrix4; + + @override + SkShader createDefault() { + assert(experimentalUseSkia); + return canvasKit.SkShader.MakeTwoPointConicalGradient( + toSkPoint(focal), + focalRadius, + toSkPoint(center), + radius, + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, + 0, + ); + } + + @override + SkShader resurrect() => createDefault(); +} + +class CkImageShader extends CkShader implements ui.ImageShader { + CkImageShader( + ui.Image image, this.tileModeX, this.tileModeY, this.matrix4) + : _skImage = image as CkImage; + + final ui.TileMode tileModeX; + final ui.TileMode tileModeY; + final Float64List matrix4; + final CkImage _skImage; + + @override + SkShader createDefault() => _skImage.skImage.makeShader( + toSkTileMode(tileModeX), + toSkTileMode(tileModeY), + toSkMatrixFromFloat64(matrix4), + ); + + @override + SkShader resurrect() => createDefault(); + + @override + void delete() { + rawSkiaObject?.delete(); + } +} diff --git a/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart similarity index 69% rename from lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart rename to lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart index b94908be6e8df..160ee172f2f7d 100644 --- a/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart @@ -86,7 +86,7 @@ class SkiaObjectCache { /// WebAssembly heap. /// /// These objects are automatically deleted when no longer used. -abstract class SkiaObject { +abstract class SkiaObject { /// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap. T get skiaObject; @@ -99,16 +99,19 @@ abstract class SkiaObject { void didDelete(); } -/// A [SkiaObject] that can resurrect its C++ counterpart. +/// A [SkiaObject] that manages the lifecycle of its C++ counterpart. /// -/// Because there is no feedback from JavaScript's GC (no destructors or -/// finalizers), we pessimistically delete the underlying C++ object before -/// the Dart object is garbage-collected. The current algorithm deletes objects -/// at the end of every frame. This allows reusing the C++ objects within the -/// frame. In the future we may add smarter strategies that will allow us to -/// reuse C++ objects across frames. +/// In browsers that support weak references we use feedback from the garbage +/// collector to determine when it is safe to release the C++ object. /// -/// The lifecycle of a C++ object is as follows: +/// In browsers that do not support weak references we pessimistically delete +/// the underlying C++ object before the Dart object is garbage-collected. The +/// current algorithm deletes objects at the end of every frame. This allows +/// reusing the C++ objects within the frame. If the object is used again after +/// is was deleted, we [resurrect] it based on the data available on the +/// JavaScript side. +/// +/// The lifecycle of a resurrectable C++ object is as follows: /// /// - Create default: when instantiating a C++ object for a Dart object for the /// first time, the C++ object is populated with default data (the defaults are @@ -120,13 +123,22 @@ abstract class SkiaObject { /// [resurrect] method. /// - Final delete: if a Dart object is never reused, it is GC'd after its /// underlying C++ object is deleted. This is implemented by [SkiaObjects]. -abstract class ResurrectableSkiaObject extends SkiaObject { - ResurrectableSkiaObject() { - rawSkiaObject = createDefault(); - if (isResurrectionExpensive) { - SkiaObjects.manageExpensive(this); +abstract class ManagedSkiaObject extends SkiaObject { + ManagedSkiaObject() { + final T defaultObject = createDefault(); + rawSkiaObject = defaultObject; + if (browserSupportsFinalizationRegistry) { + // If FinalizationRegistry is supported we will only ever need the + // default object, as we know precisely when to delete it. + skObjectFinalizationRegistry.register(this, defaultObject); } else { - SkiaObjects.manageResurrectable(this); + // If FinalizationRegistry is _not_ supported we may need to delete + // and resurrect the object multiple times before deleting it forever. + if (isResurrectionExpensive) { + SkiaObjects.manageExpensive(this); + } else { + SkiaObjects.manageResurrectable(this); + } } } @@ -134,6 +146,7 @@ abstract class ResurrectableSkiaObject extends SkiaObject { T get skiaObject => rawSkiaObject ?? _doResurrect(); T _doResurrect() { + assert(!browserSupportsFinalizationRegistry); final T skiaObject = resurrect(); rawSkiaObject = skiaObject; if (isResurrectionExpensive) { @@ -146,6 +159,7 @@ abstract class ResurrectableSkiaObject extends SkiaObject { @override void didDelete() { + assert(!browserSupportsFinalizationRegistry); rawSkiaObject = null; } @@ -182,7 +196,11 @@ abstract class ResurrectableSkiaObject extends SkiaObject { // use. This issue discusses ways to address this: // https://github.com/flutter/flutter/issues/60401 /// A [SkiaObject] which is deleted once and cannot be used again. -abstract class OneShotSkiaObject extends SkiaObject { +/// +/// In browsers that support weak references we use feedback from the garbage +/// collector to determine when it is safe to release the C++ object. Otherwise, +/// we use an LRU cache (see [SkiaObjects.manageOneShot]). +abstract class OneShotSkiaObject extends SkiaObject { /// Returns the current skia object as is without attempting to /// resurrect it. /// @@ -191,34 +209,85 @@ abstract class OneShotSkiaObject extends SkiaObject { /// /// Use this field instead of the [skiaObject] getter when implementing /// the [delete] method. - T? rawSkiaObject; + T rawSkiaObject; + + bool _isDeleted = false; - OneShotSkiaObject(this.rawSkiaObject) { - SkiaObjects.manageOneShot(this); + OneShotSkiaObject(T skObject) : this.rawSkiaObject = skObject { + if (browserSupportsFinalizationRegistry) { + skObjectFinalizationRegistry.register(this, skObject); + } else { + SkiaObjects.manageOneShot(this); + } } @override T get skiaObject { - if (rawSkiaObject == null) { + if (browserSupportsFinalizationRegistry) { + return rawSkiaObject; + } + if (_isDeleted) { throw StateError('Attempting to use a Skia object that has been freed.'); } SkiaObjects.oneShotCache.markUsed(this); - return rawSkiaObject!; + return rawSkiaObject; } @override void didDelete() { - rawSkiaObject = null; + _isDeleted = true; + } +} + +/// Manages the lifecycle of a Skia object owned by a wrapper object. +/// +/// When the wrapper is garbage collected, deletes the corresponding +/// [skObject] (only in browsers that support weak references). +/// +/// The [delete] method can be used to eagerly delete the [skObject] +/// before the wrapper is garbage collected. +/// +/// The [delete] method may be called any number of times. The box +/// will only delete the object once. +class SkiaObjectBox { + SkiaObjectBox(Object wrapper, this.skObject) { + if (browserSupportsFinalizationRegistry) { + boxRegistry.register(wrapper, this); + } + } + + /// The Skia object whose lifecycle is being managed. + final SkDeletable skObject; + + /// Whether this object has been deleted. + bool get isDeleted => _isDeleted; + bool _isDeleted = false; + + /// Deletes Skia objects when their wrappers are garbage collected. + static final SkObjectFinalizationRegistry boxRegistry = + SkObjectFinalizationRegistry( + js.allowInterop((SkiaObjectBox box) { + box.delete(); + })); + + /// Deletes the [skObject]. + /// + /// Does nothing if the object has already been deleted. + void delete() { + if (_isDeleted) { + return; + } + _isDeleted = true; + _skObjectDeleteQueue.add(skObject); + _skObjectCollector ??= _scheduleSkObjectCollection(); } } /// Singleton that manages the lifecycles of [SkiaObject] instances. class SkiaObjects { - // TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects - // beyond a single frame. @visibleForTesting - static final List resurrectableObjects = - []; + static final List resurrectableObjects = + []; @visibleForTesting static int maximumCacheSize = 8192; @@ -247,7 +316,7 @@ class SkiaObjects { /// Starts managing the lifecycle of resurrectable [object]. /// /// These can safely be deleted at any time. - static void manageResurrectable(ResurrectableSkiaObject object) { + static void manageResurrectable(ManagedSkiaObject object) { registerCleanupCallback(); resurrectableObjects.add(object); } @@ -265,7 +334,7 @@ class SkiaObjects { /// /// Since it's expensive to resurrect, we shouldn't just delete it after every /// frame. Instead, add it to a cache and only delete it when the cache fills. - static void manageExpensive(ResurrectableSkiaObject object) { + static void manageExpensive(ManagedSkiaObject object) { registerCleanupCallback(); expensiveCache.add(object); } diff --git a/lib/web_ui/lib/src/engine/compositor/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart similarity index 80% rename from lib/web_ui/lib/src/engine/compositor/surface.dart rename to lib/web_ui/lib/src/engine/canvaskit/surface.dart index bacbe5e1c09da..17aac05770ecd 100644 --- a/lib/web_ui/lib/src/engine/compositor/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -89,22 +89,20 @@ class Surface { _addedToScene = true; } + ui.Size? _currentSize; + void _createOrUpdateSurfaces(ui.Size size) { if (size.isEmpty) { throw CanvasKitError('Cannot create surfaces of empty size.'); } - final CkSurface? currentSurface = _surface; - if (currentSurface != null) { - final bool isSameSize = size.width == currentSurface.width() && - size.height == currentSurface.height(); - if (isSameSize) { - // The existing surface is still reusable. - return; - } + if (size == _currentSize) { + // The existing surface is still reusable. + return; } - currentSurface?.dispose(); + _currentSize = size; + _surface?.dispose(); _surface = null; htmlElement?.remove(); htmlElement = null; @@ -113,17 +111,31 @@ class Surface { _surface = _wrapHtmlCanvas(size); } - CkSurface _wrapHtmlCanvas(ui.Size size) { - final ui.Size logicalSize = size / ui.window.devicePixelRatio; + CkSurface _wrapHtmlCanvas(ui.Size physicalSize) { + // If `physicalSize` is not precise, use a slightly bigger canvas. This way + // we ensure that the rendred picture covers the entire browser window. + final int pixelWidth = physicalSize.width.ceil(); + final int pixelHeight = physicalSize.height.ceil(); final html.CanvasElement htmlCanvas = html.CanvasElement( - width: size.width.ceil(), height: size.height.ceil()); + width: pixelWidth, + height: pixelHeight, + ); + + // The logical size of the canvas is not based on the size of the window + // but on the size of the canvas, which, due to `ceil()` above, may not be + // the same as the window. We do not round/floor/ceil the logical size as + // CSS pixels can contain more than one physical pixel and therefore to + // match the size of the window precisely we use the most precise floating + // point value we can get. + final double logicalWidth = pixelWidth / ui.window.devicePixelRatio; + final double logicalHeight = pixelHeight / ui.window.devicePixelRatio; htmlCanvas.style ..position = 'absolute' - ..width = '${logicalSize.width.ceil()}px' - ..height = '${logicalSize.height.ceil()}px'; + ..width = '${logicalWidth}px' + ..height = '${logicalHeight}px'; htmlElement = htmlCanvas; - if (canvasKitForceCpuOnly) { + if (webGLVersion == -1 || canvasKitForceCpuOnly) { return _makeSoftwareCanvasSurface(htmlCanvas); } else { // Try WebGL first. @@ -133,6 +145,7 @@ class Surface { // Default to no anti-aliasing. Paint commands can be explicitly // anti-aliased by setting their `Paint` object's `antialias` property. anitalias: 0, + majorVersion: webGLVersion, ), ); @@ -152,8 +165,8 @@ class Surface { SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface( _grContext!, - size.width, - size.height, + pixelWidth, + pixelHeight, SkColorSpaceSRGB, ); diff --git a/lib/web_ui/lib/src/engine/compositor/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart similarity index 90% rename from lib/web_ui/lib/src/engine/compositor/text.dart rename to lib/web_ui/lib/src/engine/canvaskit/text.dart index 1b99d2c9944ee..1079e814ae9b3 100644 --- a/lib/web_ui/lib/src/engine/compositor/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -20,17 +20,17 @@ class CkParagraphStyle implements ui.ParagraphStyle { String? ellipsis, ui.Locale? locale, }) : skParagraphStyle = toSkParagraphStyle( - textAlign, - textDirection, - maxLines, - fontFamily, - fontSize, - height, - textHeightBehavior, - fontWeight, - fontStyle, - ellipsis, - ) { + textAlign, + textDirection, + maxLines, + fontFamily, + fontSize, + height, + textHeightBehavior, + fontWeight, + fontStyle, + ellipsis, + ) { assert(skParagraphStyle != null); _textDirection = textDirection ?? ui.TextDirection.ltr; _fontFamily = fontFamily; @@ -59,9 +59,6 @@ class CkParagraphStyle implements ui.ParagraphStyle { !skiaFontCollection.registeredFamilies.contains(fontFamily)) { fontFamily = 'Roboto'; } - if (skiaFontCollection.fontFamilyOverrides.containsKey(fontFamily)) { - fontFamily = skiaFontCollection.fontFamilyOverrides[fontFamily]!; - } skTextStyle.fontFamilies = [fontFamily]; return skTextStyle; @@ -114,6 +111,8 @@ class CkParagraphStyle implements ui.ParagraphStyle { class CkTextStyle implements ui.TextStyle { SkTextStyle skTextStyle; + CkPaint? background; + CkPaint? foreground; factory CkTextStyle({ ui.Color? color, @@ -173,9 +172,6 @@ class CkTextStyle implements ui.TextStyle { fontFamily = 'Roboto'; } - if (skiaFontCollection.fontFamilyOverrides.containsKey(fontFamily)) { - fontFamily = skiaFontCollection.fontFamilyOverrides[fontFamily]!; - } List fontFamilies = [fontFamily]; if (fontFamilyFallback != null && !fontFamilyFallback.every((font) => fontFamily == font)) { @@ -202,14 +198,14 @@ class CkTextStyle implements ui.TextStyle { // - locale // - shadows // - fontFeatures - return CkTextStyle._(canvasKit.TextStyle(properties)); + return CkTextStyle._( + canvasKit.TextStyle(properties), foreground, background); } - CkTextStyle._(this.skTextStyle); + CkTextStyle._(this.skTextStyle, this.foreground, this.background); } -SkFontStyle toSkFontStyle( - ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) { +SkFontStyle toSkFontStyle(ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) { final style = SkFontStyle(); if (fontWeight != null) { style.weight = toSkFontWeight(fontWeight); @@ -220,7 +216,8 @@ SkFontStyle toSkFontStyle( return style; } -class CkParagraph extends ResurrectableSkiaObject implements ui.Paragraph { +class CkParagraph extends ManagedSkiaObject + implements ui.Paragraph { CkParagraph( this._initialParagraph, this._paragraphStyle, this._paragraphCommands); @@ -285,8 +282,7 @@ class CkParagraph extends ResurrectableSkiaObject implements ui.Par bool get isResurrectionExpensive => true; @override - double get alphabeticBaseline => - skiaObject.getAlphabeticBaseline(); + double get alphabeticBaseline => skiaObject.getAlphabeticBaseline(); @override bool get didExceedMaxLines => skiaObject.didExceedMaxLines(); @@ -295,8 +291,7 @@ class CkParagraph extends ResurrectableSkiaObject implements ui.Par double get height => skiaObject.getHeight(); @override - double get ideographicBaseline => - skiaObject.getIdeographicBaseline(); + double get ideographicBaseline => skiaObject.getIdeographicBaseline(); @override double get longestLine => skiaObject.getLongestLine(); @@ -414,9 +409,9 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { CkParagraphBuilder(ui.ParagraphStyle style) : _commands = <_ParagraphCommand>[], _style = style as CkParagraphStyle, - _paragraphBuilder = canvasKit.ParagraphBuilder.Make( + _paragraphBuilder = canvasKit.ParagraphBuilder.MakeFromFontProvider( style.skParagraphStyle, - skiaFontCollection.skFontMgr, + skiaFontCollection.fontProvider, ); // TODO(hterkelsen): Implement placeholders. @@ -468,7 +463,14 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { void pushStyle(ui.TextStyle style) { final CkTextStyle skStyle = style as CkTextStyle; _commands.add(_ParagraphCommand.pushStyle(skStyle)); - _paragraphBuilder.pushStyle(skStyle.skTextStyle); + if (skStyle.foreground != null || skStyle.background != null) { + final SkPaint foreground = skStyle.foreground?.skiaObject ?? SkPaint(); + final SkPaint background = skStyle.background?.skiaObject ?? SkPaint(); + _paragraphBuilder.pushPaintStyle( + skStyle.skTextStyle, foreground, background); + } else { + _paragraphBuilder.pushStyle(skStyle.skTextStyle); + } } } diff --git a/lib/web_ui/lib/src/engine/compositor/util.dart b/lib/web_ui/lib/src/engine/canvaskit/util.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/util.dart rename to lib/web_ui/lib/src/engine/canvaskit/util.dart diff --git a/lib/web_ui/lib/src/engine/compositor/vertices.dart b/lib/web_ui/lib/src/engine/canvaskit/vertices.dart similarity index 65% rename from lib/web_ui/lib/src/engine/compositor/vertices.dart rename to lib/web_ui/lib/src/engine/canvaskit/vertices.dart index 2c05c91d02465..ec13910db9c39 100644 --- a/lib/web_ui/lib/src/engine/compositor/vertices.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/vertices.dart @@ -5,17 +5,16 @@ // @dart = 2.10 part of engine; -class CkVertices implements ui.Vertices { - late SkVertices skVertices; - - CkVertices( +class CkVertices extends ManagedSkiaObject implements ui.Vertices { + factory CkVertices( ui.VertexMode mode, List positions, { List? textureCoordinates, List? colors, List? indices, - }) : assert(mode != null), // ignore: unnecessary_null_comparison - assert(positions != null) { // ignore: unnecessary_null_comparison + }) { + assert(mode != null); // ignore: unnecessary_null_comparison + assert(positions != null); // ignore: unnecessary_null_comparison if (textureCoordinates != null && textureCoordinates.length != positions.length) throw ArgumentError( @@ -27,7 +26,7 @@ class CkVertices implements ui.Vertices { throw ArgumentError( '"indices" values must be valid indices in the positions list.'); - skVertices = canvasKit.MakeSkVertices( + return CkVertices._( toSkVertexMode(mode), toSkPoints2d(positions), textureCoordinates != null ? toSkPoints2d(textureCoordinates) : null, @@ -36,14 +35,15 @@ class CkVertices implements ui.Vertices { ); } - CkVertices.raw( + factory CkVertices.raw( ui.VertexMode mode, Float32List positions, { Float32List? textureCoordinates, Int32List? colors, Uint16List? indices, - }) : assert(mode != null), // ignore: unnecessary_null_comparison - assert(positions != null) { // ignore: unnecessary_null_comparison + }) { + assert(mode != null); // ignore: unnecessary_null_comparison + assert(positions != null); // ignore: unnecessary_null_comparison if (textureCoordinates != null && textureCoordinates.length != positions.length) throw ArgumentError( @@ -55,7 +55,7 @@ class CkVertices implements ui.Vertices { throw ArgumentError( '"indices" values must be valid indices in the positions list.'); - skVertices = canvasKit.MakeSkVertices( + return CkVertices._( toSkVertexMode(mode), rawPointsToSkPoints2d(positions), textureCoordinates != null ? rawPointsToSkPoints2d(textureCoordinates) : null, @@ -63,4 +63,39 @@ class CkVertices implements ui.Vertices { indices, ); } + + CkVertices._( + this._mode, + this._positions, + this._textureCoordinates, + this._colors, + this._indices, + ); + + final SkVertexMode _mode; + final List _positions; + final List? _textureCoordinates; + final List? _colors; + final Uint16List? _indices; + + @override + SkVertices createDefault() { + return canvasKit.MakeSkVertices( + _mode, + _positions, + _textureCoordinates, + _colors, + _indices, + ); + } + + @override + SkVertices resurrect() { + return createDefault(); + } + + @override + void delete() { + rawSkiaObject?.delete(); + } } diff --git a/lib/web_ui/lib/src/engine/compositor/viewport_metrics.dart b/lib/web_ui/lib/src/engine/canvaskit/viewport_metrics.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/viewport_metrics.dart rename to lib/web_ui/lib/src/engine/canvaskit/viewport_metrics.dart diff --git a/lib/web_ui/lib/src/engine/color_filter.dart b/lib/web_ui/lib/src/engine/color_filter.dart index 0181695ca2d5a..230cdf9a2ae25 100644 --- a/lib/web_ui/lib/src/engine/color_filter.dart +++ b/lib/web_ui/lib/src/engine/color_filter.dart @@ -169,8 +169,4 @@ class EngineColorFilter implements ui.ColorFilter { return 'Unknown ColorFilter type. This is an error. If you\'re seeing this, please file an issue at https://github.com/flutter/flutter/issues/new.'; } } - - List webOnlySerializeToCssPaint() { - throw UnsupportedError('ColorFilter for CSS paint not yet supported'); - } } diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart deleted file mode 100644 index 61ed3c13c866d..0000000000000 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ /dev/null @@ -1,117 +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. - -// @dart = 2.10 -part of engine; - -/// Instantiates a [ui.Codec] backed by an `SkImage` from Skia. -void skiaInstantiateImageCodec(Uint8List list, Callback callback, - [int? width, int? height, int? format, int? rowBytes]) { - final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(list); - final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); - final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); - callback(codec); -} - -/// A wrapper for `SkAnimatedImage`. -class CkAnimatedImage implements ui.Image { - final SkAnimatedImage _skAnimatedImage; - - CkAnimatedImage(this._skAnimatedImage); - - @override - void dispose() { - _skAnimatedImage.delete(); - } - - int get frameCount => _skAnimatedImage.getFrameCount(); - - /// Decodes the next frame and returns the frame duration. - Duration decodeNextFrame() { - final int durationMillis = _skAnimatedImage.decodeNextFrame(); - return Duration(milliseconds: durationMillis); - } - - int get repetitionCount => _skAnimatedImage.getRepetitionCount(); - - CkImage get currentFrameAsImage { - return CkImage(_skAnimatedImage.getCurrentFrame()); - } - - @override - int get width => _skAnimatedImage.width(); - - @override - int get height => _skAnimatedImage.height(); - - @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; - } -} - -/// A [ui.Image] backed by an `SkImage` from Skia. -class CkImage implements ui.Image { - final SkImage skImage; - - CkImage(this.skImage); - - @override - void dispose() { - skImage.delete(); - } - - @override - int get width => skImage.width(); - - @override - int get height => skImage.height(); - - @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; - } -} - -/// A [Codec] that wraps an `SkAnimatedImage`. -class CkAnimatedImageCodec implements ui.Codec { - CkAnimatedImage? animatedImage; - - CkAnimatedImageCodec(this.animatedImage); - - @override - void dispose() { - animatedImage!.dispose(); - animatedImage = null; - } - - @override - int get frameCount => animatedImage!.frameCount; - - @override - int get repetitionCount => animatedImage!.repetitionCount; - - @override - Future getNextFrame() { - final Duration duration = animatedImage!.decodeNextFrame(); - final CkImage image = animatedImage!.currentFrameAsImage; - return Future.value(AnimatedImageFrameInfo(duration, image)); - } -} - -/// Data for a single frame of an animated image. -class AnimatedImageFrameInfo implements ui.FrameInfo { - final Duration _duration; - final CkImage _image; - - AnimatedImageFrameInfo(this._duration, this._image); - - @override - Duration get duration => _duration; - - @override - ui.Image get image => _image; -} diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index 1e38b05360297..5b0f7d31d8167 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -282,3 +282,127 @@ html.Element _drawParagraphElement( } return paragraphElement; } + +class _SaveElementStackEntry { + _SaveElementStackEntry({ + required this.savedElement, + required this.transform, + }); + + final html.Element savedElement; + final Matrix4 transform; +} + +/// Provides save stack tracking functionality to implementations of +/// [EngineCanvas]. +mixin SaveElementStackTracking on EngineCanvas { + static final Vector3 _unitZ = Vector3(0.0, 0.0, 1.0); + + final List<_SaveElementStackEntry> _saveStack = <_SaveElementStackEntry>[]; + + /// The element at the top of the element stack, or [rootElement] if the stack + /// is empty. + html.Element get currentElement => + _elementStack.isEmpty ? rootElement : _elementStack.last; + + /// The stack that maintains the DOM elements used to express certain paint + /// operations, such as clips. + final List _elementStack = []; + + /// Pushes the [element] onto the element stack for the purposes of applying + /// a paint effect using a DOM element, e.g. for clipping. + /// + /// The [restore] method automatically pops the element off the stack. + void pushElement(html.Element element) { + _elementStack.add(element); + } + + /// Empties the save stack and the element stack, and resets the transform + /// and clip parameters. + /// + /// Classes that override this method must call `super.clear()`. + @override + void clear() { + _saveStack.clear(); + _elementStack.clear(); + _currentTransform = Matrix4.identity(); + } + + /// The current transformation matrix. + Matrix4 get currentTransform => _currentTransform; + Matrix4 _currentTransform = Matrix4.identity(); + + /// Saves current clip and transform on the save stack. + /// + /// Classes that override this method must call `super.save()`. + @override + void save() { + _saveStack.add(_SaveElementStackEntry( + savedElement: currentElement, + transform: _currentTransform.clone(), + )); + } + + /// Restores current clip and transform from the save stack. + /// + /// Classes that override this method must call `super.restore()`. + @override + void restore() { + if (_saveStack.isEmpty) { + return; + } + final _SaveElementStackEntry entry = _saveStack.removeLast(); + _currentTransform = entry.transform; + + // Pop out of any clips. + while (currentElement != entry.savedElement) { + _elementStack.removeLast(); + } + } + + /// Multiplies the [currentTransform] matrix by a translation. + /// + /// Classes that override this method must call `super.translate()`. + @override + void translate(double dx, double dy) { + _currentTransform.translate(dx, dy); + } + + /// Scales the [currentTransform] matrix. + /// + /// Classes that override this method must call `super.scale()`. + @override + void scale(double sx, double sy) { + _currentTransform.scale(sx, sy); + } + + /// Rotates the [currentTransform] matrix. + /// + /// Classes that override this method must call `super.rotate()`. + @override + void rotate(double radians) { + _currentTransform.rotate(_unitZ, radians); + } + + /// Skews the [currentTransform] matrix. + /// + /// Classes that override this method must call `super.skew()`. + @override + void skew(double sx, double sy) { + // DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians, + // but in our case they are transform matrix values. + final Matrix4 skewMatrix = Matrix4.identity(); + final Float32List storage = skewMatrix.storage; + storage[1] = sy; + storage[4] = sx; + _currentTransform.multiply(skewMatrix); + } + + /// Multiplies the [currentTransform] matrix by another matrix. + /// + /// Classes that override this method must call `super.transform()`. + @override + void transform(Float32List matrix4) { + _currentTransform.multiply(Matrix4.fromFloat32List(matrix4)); + } +} diff --git a/lib/web_ui/lib/src/engine/houdini_canvas.dart b/lib/web_ui/lib/src/engine/houdini_canvas.dart deleted file mode 100644 index 1f83140a69983..0000000000000 --- a/lib/web_ui/lib/src/engine/houdini_canvas.dart +++ /dev/null @@ -1,370 +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. - -// TODO(yjbanov): optimization opportunities (see also houdini_painter.js) -// - collapse non-drawing paint operations -// - avoid producing DOM-based clips if there is no text -// - evaluate using stylesheets for static CSS properties -// - evaluate reusing houdini canvases - -// @dart = 2.10 -part of engine; - -/// A canvas that renders to a combination of HTML DOM and CSS Custom Paint API. -/// -/// This canvas produces paint commands for houdini_painter.js to apply. This -/// class must be kept in sync with houdini_painter.js. -class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking { - @override - final html.Element rootElement = html.Element.tag('flt-houdini'); - - /// The rectangle positioned relative to the parent layer's coordinate system - /// where this canvas paints. - /// - /// Painting outside the bounds of this rectangle is cropped. - final ui.Rect? bounds; - - HoudiniCanvas(this.bounds) { - // TODO(yjbanov): would it be faster to specify static values in a - // stylesheet and let the browser apply them? - rootElement.style - ..position = 'absolute' - ..top = '0' - ..left = '0' - ..width = '${bounds!.size.width}px' - ..height = '${bounds!.size.height}px' - ..backgroundImage = 'paint(flt)'; - } - - /// Prepare to reuse this canvas by clearing it's current contents. - @override - void clear() { - super.clear(); - _serializedCommands = >[]; - // TODO(yjbanov): we should measure if reusing old elements is beneficial. - domRenderer.clearDom(rootElement); - } - - /// Paint commands serialized for sending to the CSS custom painter. - List> _serializedCommands = >[]; - - void apply(PaintCommand command) { - // Some commands are applied purely in HTML DOM and do not need to be - // serialized. - if (command is! PaintDrawParagraph && - command is! PaintDrawImageRect && - command is! PaintTransform) { - command.serializeToCssPaint(_serializedCommands); - } - command.apply(this); - } - - /// Sends the paint commands to the CSS custom painter for painting. - void commit() { - if (_serializedCommands.isNotEmpty) { - rootElement.style.setProperty('--flt', json.encode(_serializedCommands)); - } else { - rootElement.style.removeProperty('--flt'); - } - } - - @override - void clipRect(ui.Rect rect) { - final html.Element clip = html.Element.tag('flt-clip-rect'); - final String cssTransform = matrix4ToCssTransform( - transformWithOffset(currentTransform, ui.Offset(rect.left, rect.top))); - clip.style - ..overflow = 'hidden' - ..position = 'absolute' - ..transform = cssTransform - ..width = '${rect.width}px' - ..height = '${rect.height}px'; - - // The clipping element will translate the coordinate system as well, which - // is not what a clip should do. To offset that we translate in the opposite - // direction. - super.translate(-rect.left, -rect.top); - - currentElement.append(clip); - pushElement(clip); - } - - @override - void clipRRect(ui.RRect rrect) { - final ui.Rect outer = rrect.outerRect; - if (rrect.isRect) { - clipRect(outer); - return; - } - - final html.Element clip = html.Element.tag('flt-clip-rrect'); - final html.CssStyleDeclaration style = clip.style; - style - ..overflow = 'hidden' - ..position = 'absolute' - ..transform = 'translate(${outer.left}px, ${outer.right}px)' - ..width = '${outer.width}px' - ..height = '${outer.height}px'; - - if (rrect.tlRadiusY == rrect.tlRadiusX) { - style.borderTopLeftRadius = '${rrect.tlRadiusX}px'; - } else { - style.borderTopLeftRadius = '${rrect.tlRadiusX}px ${rrect.tlRadiusY}px'; - } - - if (rrect.trRadiusY == rrect.trRadiusX) { - style.borderTopRightRadius = '${rrect.trRadiusX}px'; - } else { - style.borderTopRightRadius = '${rrect.trRadiusX}px ${rrect.trRadiusY}px'; - } - - if (rrect.brRadiusY == rrect.brRadiusX) { - style.borderBottomRightRadius = '${rrect.brRadiusX}px'; - } else { - style.borderBottomRightRadius = - '${rrect.brRadiusX}px ${rrect.brRadiusY}px'; - } - - if (rrect.blRadiusY == rrect.blRadiusX) { - style.borderBottomLeftRadius = '${rrect.blRadiusX}px'; - } else { - style.borderBottomLeftRadius = - '${rrect.blRadiusX}px ${rrect.blRadiusY}px'; - } - - // The clipping element will translate the coordinate system as well, which - // is not what a clip should do. To offset that we translate in the opposite - // direction. - super.translate(-rrect.left, -rrect.top); - - currentElement.append(clip); - pushElement(clip); - } - - @override - void clipPath(ui.Path path) { - // TODO(yjbanov): implement. - } - - @override - void drawColor(ui.Color color, ui.BlendMode blendMode) { - // Drawn using CSS Paint. - } - - @override - void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawPaint(SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawRect(ui.Rect rect, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawRRect(ui.RRect rrect, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawOval(ui.Rect rect, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawPath(ui.Path path, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawShadow(ui.Path path, ui.Color color, double elevation, - bool transparentOccluder) { - // Drawn using CSS Paint. - } - - @override - void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { - // TODO(yjbanov): implement. - } - - @override - void drawImageRect( - ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) { - // TODO(yjbanov): implement src rectangle - final HtmlImage htmlImage = image as HtmlImage; - final html.Element imageBox = html.Element.tag('flt-img'); - final String cssTransform = matrix4ToCssTransform( - transformWithOffset(currentTransform, ui.Offset(dst.left, dst.top))); - imageBox.style - ..position = 'absolute' - ..transformOrigin = '0 0 0' - ..width = '${dst.width.toInt()}px' - ..height = '${dst.height.toInt()}px' - ..transform = cssTransform - ..backgroundImage = 'url(${htmlImage.imgElement.src})' - ..backgroundRepeat = 'norepeat' - ..backgroundSize = '${dst.width}px ${dst.height}px'; - currentElement.append(imageBox); - } - - @override - void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { - final html.Element paragraphElement = - _drawParagraphElement(paragraph as EngineParagraph, offset, transform: currentTransform); - currentElement.append(paragraphElement); - } - - @override - void drawVertices( - ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { - // TODO(flutter_web): implement. - } - - @override - void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) { - // TODO(flutter_web): implement. - } - - @override - void endOfPaint() {} -} - -class _SaveElementStackEntry { - _SaveElementStackEntry({ - required this.savedElement, - required this.transform, - }); - - final html.Element savedElement; - final Matrix4 transform; -} - -/// Provides save stack tracking functionality to implementations of -/// [EngineCanvas]. -mixin SaveElementStackTracking on EngineCanvas { - static final Vector3 _unitZ = Vector3(0.0, 0.0, 1.0); - - final List<_SaveElementStackEntry> _saveStack = <_SaveElementStackEntry>[]; - - /// The element at the top of the element stack, or [rootElement] if the stack - /// is empty. - html.Element get currentElement => - _elementStack.isEmpty ? rootElement : _elementStack.last; - - /// The stack that maintains the DOM elements used to express certain paint - /// operations, such as clips. - final List _elementStack = []; - - /// Pushes the [element] onto the element stack for the purposes of applying - /// a paint effect using a DOM element, e.g. for clipping. - /// - /// The [restore] method automatically pops the element off the stack. - void pushElement(html.Element element) { - _elementStack.add(element); - } - - /// Empties the save stack and the element stack, and resets the transform - /// and clip parameters. - /// - /// Classes that override this method must call `super.clear()`. - @override - void clear() { - _saveStack.clear(); - _elementStack.clear(); - _currentTransform = Matrix4.identity(); - } - - /// The current transformation matrix. - Matrix4 get currentTransform => _currentTransform; - Matrix4 _currentTransform = Matrix4.identity(); - - /// Saves current clip and transform on the save stack. - /// - /// Classes that override this method must call `super.save()`. - @override - void save() { - _saveStack.add(_SaveElementStackEntry( - savedElement: currentElement, - transform: _currentTransform.clone(), - )); - } - - /// Restores current clip and transform from the save stack. - /// - /// Classes that override this method must call `super.restore()`. - @override - void restore() { - if (_saveStack.isEmpty) { - return; - } - final _SaveElementStackEntry entry = _saveStack.removeLast(); - _currentTransform = entry.transform; - - // Pop out of any clips. - while (currentElement != entry.savedElement) { - _elementStack.removeLast(); - } - } - - /// Multiplies the [currentTransform] matrix by a translation. - /// - /// Classes that override this method must call `super.translate()`. - @override - void translate(double dx, double dy) { - _currentTransform.translate(dx, dy); - } - - /// Scales the [currentTransform] matrix. - /// - /// Classes that override this method must call `super.scale()`. - @override - void scale(double sx, double sy) { - _currentTransform.scale(sx, sy); - } - - /// Rotates the [currentTransform] matrix. - /// - /// Classes that override this method must call `super.rotate()`. - @override - void rotate(double radians) { - _currentTransform.rotate(_unitZ, radians); - } - - /// Skews the [currentTransform] matrix. - /// - /// Classes that override this method must call `super.skew()`. - @override - void skew(double sx, double sy) { - // DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians, - // but in our case they are transform matrix values. - final Matrix4 skewMatrix = Matrix4.identity(); - final Float32List storage = skewMatrix.storage; - storage[1] = sy; - storage[4] = sx; - _currentTransform.multiply(skewMatrix); - } - - /// Multiplies the [currentTransform] matrix by another matrix. - /// - /// Classes that override this method must call `super.transform()`. - @override - void transform(Float32List matrix4) { - _currentTransform.multiply(Matrix4.fromFloat32List(matrix4)); - } -} diff --git a/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart b/lib/web_ui/lib/src/engine/html/backdrop_filter.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/backdrop_filter.dart rename to lib/web_ui/lib/src/engine/html/backdrop_filter.dart diff --git a/lib/web_ui/lib/src/engine/surface/canvas.dart b/lib/web_ui/lib/src/engine/html/canvas.dart similarity index 97% rename from lib/web_ui/lib/src/engine/surface/canvas.dart rename to lib/web_ui/lib/src/engine/html/canvas.dart index 4f3fc554ab034..b5b942dc84d16 100644 --- a/lib/web_ui/lib/src/engine/surface/canvas.dart +++ b/lib/web_ui/lib/src/engine/html/canvas.dart @@ -490,8 +490,8 @@ class SurfaceCanvas implements ui.Canvas { ui.Image atlas, List transforms, List rects, - List colors, - ui.BlendMode blendMode, + List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint, ) { @@ -499,15 +499,14 @@ class SurfaceCanvas implements ui.Canvas { assert(atlas != null); // atlas is checked on the engine side assert(transforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || colors.isEmpty || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; if (transforms.length != rectCount) { throw ArgumentError('"transforms" and "rects" lengths must match.'); } - if (colors.isNotEmpty && colors.length != rectCount) { + if (colors != null && colors.isNotEmpty && colors.length != rectCount) { throw ArgumentError( 'If non-null, "colors" length must match that of "transforms" and "rects".'); } @@ -521,8 +520,8 @@ class SurfaceCanvas implements ui.Canvas { ui.Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - ui.BlendMode blendMode, + Int32List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint, ) { @@ -530,8 +529,7 @@ class SurfaceCanvas implements ui.Canvas { assert(atlas != null); // atlas is checked on the engine side assert(rstTransforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; @@ -542,7 +540,7 @@ class SurfaceCanvas implements ui.Canvas { throw ArgumentError( '"rstTransforms" and "rects" lengths must be a multiple of four.'); } - if (colors.length * 4 != rectCount) { + if (colors != null && colors.length * 4 != rectCount) { throw ArgumentError( 'If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); } diff --git a/lib/web_ui/lib/src/engine/surface/clip.dart b/lib/web_ui/lib/src/engine/html/clip.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/clip.dart rename to lib/web_ui/lib/src/engine/html/clip.dart index 5b7a871d77ec6..d2fd64fed746f 100644 --- a/lib/web_ui/lib/src/engine/surface/clip.dart +++ b/lib/web_ui/lib/src/engine/html/clip.dart @@ -325,6 +325,8 @@ class PersistedPhysicalShape extends PersistedContainerSurface } if (oldSurface.path != path) { oldSurface._clipElement?.remove(); + // Reset style on prior element since we may have switched between + // rect/rrect and arbitrary path. domRenderer.setElementStyle(rootElement!, 'clip-path', ''); domRenderer.setElementStyle(rootElement!, '-webkit-clip-path', ''); _applyShape(); diff --git a/lib/web_ui/lib/src/engine/surface/debug_canvas_reuse_overlay.dart b/lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/debug_canvas_reuse_overlay.dart rename to lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart diff --git a/lib/web_ui/lib/src/engine/surface/image_filter.dart b/lib/web_ui/lib/src/engine/html/image_filter.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/image_filter.dart rename to lib/web_ui/lib/src/engine/html/image_filter.dart diff --git a/lib/web_ui/lib/src/engine/surface/offset.dart b/lib/web_ui/lib/src/engine/html/offset.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/offset.dart rename to lib/web_ui/lib/src/engine/html/offset.dart diff --git a/lib/web_ui/lib/src/engine/surface/opacity.dart b/lib/web_ui/lib/src/engine/html/opacity.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/opacity.dart rename to lib/web_ui/lib/src/engine/html/opacity.dart diff --git a/lib/web_ui/lib/src/engine/surface/painting.dart b/lib/web_ui/lib/src/engine/html/painting.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/painting.dart rename to lib/web_ui/lib/src/engine/html/painting.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/conic.dart b/lib/web_ui/lib/src/engine/html/path/conic.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/conic.dart rename to lib/web_ui/lib/src/engine/html/path/conic.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/cubic.dart b/lib/web_ui/lib/src/engine/html/path/cubic.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/cubic.dart rename to lib/web_ui/lib/src/engine/html/path/cubic.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/path.dart b/lib/web_ui/lib/src/engine/html/path/path.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/path/path.dart rename to lib/web_ui/lib/src/engine/html/path/path.dart index 36b8e77be672a..33ece6bba1fc3 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path.dart +++ b/lib/web_ui/lib/src/engine/html/path/path.dart @@ -1542,12 +1542,6 @@ class SurfacePath implements ui.Path { ui.Rect? get webOnlyPathAsCircle => pathRef.isOval == -1 ? null : pathRef.getBounds(); - /// Serializes this path to a value that's sent to a CSS custom painter for - /// painting. - List webOnlySerializeToCssPaint() { - throw UnimplementedError(); - } - /// Returns if Path is empty. /// Empty Path may have FillType but has no points, verbs or weights. /// Constructor, reset and rewind makes SkPath empty. diff --git a/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart b/lib/web_ui/lib/src/engine/html/path/path_metrics.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/path_metrics.dart rename to lib/web_ui/lib/src/engine/html/path/path_metrics.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/path_ref.dart b/lib/web_ui/lib/src/engine/html/path/path_ref.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/path/path_ref.dart rename to lib/web_ui/lib/src/engine/html/path/path_ref.dart index 078f3d6de0561..2b9c9f094ca09 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_ref.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_ref.dart @@ -367,7 +367,7 @@ class PathRef { } else { _conicWeights!.setAll(0, ref._conicWeights!); } - assert(verbCount == 0 || _fVerbs[0] != 0); + assert(verbCount == 0 || _fVerbs[0] == ref._fVerbs[0]); fBoundsIsDirty = ref.fBoundsIsDirty; if (!fBoundsIsDirty) { fBounds = ref.fBounds; diff --git a/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart b/lib/web_ui/lib/src/engine/html/path/path_to_svg.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart rename to lib/web_ui/lib/src/engine/html/path/path_to_svg.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/path_utils.dart b/lib/web_ui/lib/src/engine/html/path/path_utils.dart similarity index 97% rename from lib/web_ui/lib/src/engine/surface/path/path_utils.dart rename to lib/web_ui/lib/src/engine/html/path/path_utils.dart index 18abeefa23968..9908cf52370c8 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_utils.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_utils.dart @@ -15,12 +15,12 @@ class SPathSegmentMask { /// Types of path operations. class SPathVerb { - static const int kMove = 1; // 1 point - static const int kLine = 2; // 2 points - static const int kQuad = 3; // 3 points - static const int kConic = 4; // 3 points + 1 weight - static const int kCubic = 5; // 4 points - static const int kClose = 6; // 0 points + static const int kMove = 0; // 1 point + static const int kLine = 1; // 2 points + static const int kQuad = 2; // 3 points + static const int kConic = 3; // 3 points + 1 weight + static const int kCubic = 4; // 4 points + static const int kClose = 5; // 0 points } class SPath { diff --git a/lib/web_ui/lib/src/engine/surface/path/path_windings.dart b/lib/web_ui/lib/src/engine/html/path/path_windings.dart similarity index 96% rename from lib/web_ui/lib/src/engine/surface/path/path_windings.dart rename to lib/web_ui/lib/src/engine/html/path/path_windings.dart index 23790890da780..554a5413c8e36 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_windings.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_windings.dart @@ -145,7 +145,8 @@ class PathWinding { } _QuadRoots quadRoots = _QuadRoots(); - final int n = quadRoots.findRoots(startY - 2 * y1 + endY, 2 * (y1 - startY), endY - y); + final int n = quadRoots.findRoots( + startY - 2 * y1 + endY, 2 * (y1 - startY), endY - y); assert(n <= 1); double xt; if (0 == n) { @@ -377,6 +378,9 @@ class PathIterator { int _verbIndex = 0; int _pointIndex = 0; + /// Maximum buffer size required for points in [next] calls. + static const int kMaxBufferSize = 8; + /// Returns true if first contour on path is closed. bool isClosedContour() { if (_verbCount == 0 || _verbIndex == _verbCount) { @@ -438,7 +442,17 @@ class PathIterator { pathRef.points[_pointIndex - 2], pathRef.points[_pointIndex - 1]); } - int peek() => pathRef._fVerbs[_verbIndex]; + int peek() { + if (_verbIndex < pathRef.countVerbs()) { + return pathRef._fVerbs[_verbIndex]; + } + if (_needClose && _segmentState == SPathSegmentState.kAfterPrimitive) { + return (_lastPointX != _moveToX || _lastPointY != _moveToY) + ? SPath.kLineVerb + : SPath.kCloseVerb; + } + return SPath.kDoneVerb; + } // Returns next verb and reads associated points into [outPts]. int next(Float32List outPts) { diff --git a/lib/web_ui/lib/src/engine/surface/path/tangent.dart b/lib/web_ui/lib/src/engine/html/path/tangent.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/tangent.dart rename to lib/web_ui/lib/src/engine/html/path/tangent.dart diff --git a/lib/web_ui/lib/src/engine/surface/picture.dart b/lib/web_ui/lib/src/engine/html/picture.dart similarity index 87% rename from lib/web_ui/lib/src/engine/surface/picture.dart rename to lib/web_ui/lib/src/engine/html/picture.dart index 0249071a6fc93..520a4378f1408 100644 --- a/lib/web_ui/lib/src/engine/surface/picture.dart +++ b/lib/web_ui/lib/src/engine/html/picture.dart @@ -71,292 +71,9 @@ void _recycleCanvas(EngineCanvas? canvas) { } } - -/// Signature of a function that instantiates a [PersistedPicture]. -typedef PersistedPictureFactory = PersistedPicture Function( - double dx, - double dy, - ui.Picture picture, - int hints, -); - -/// Function used by the [SceneBuilder] to instantiate a picture layer. -PersistedPictureFactory persistedPictureFactory = standardPictureFactory; - -/// Instantiates an implementation of a picture layer that uses DOM, CSS, and -/// 2D canvas for painting. -PersistedStandardPicture standardPictureFactory( - double dx, double dy, ui.Picture picture, int hints) { - return PersistedStandardPicture(dx, dy, picture, hints); -} - -/// Instantiates an implementation of a picture layer that uses CSS Paint API -/// (part of Houdini) for painting. -PersistedHoudiniPicture houdiniPictureFactory( - double dx, double dy, ui.Picture picture, int hints) { - return PersistedHoudiniPicture(dx, dy, picture, hints); -} - -class PersistedHoudiniPicture extends PersistedPicture { - PersistedHoudiniPicture(double dx, double dy, ui.Picture picture, int hints) - : super(dx, dy, picture as EnginePicture, hints) { - if (!_cssPainterRegistered) { - _registerCssPainter(); - } - } - - static bool _cssPainterRegistered = false; - - @override - double matchForUpdate(PersistedPicture existingSurface) { - // Houdini is display list-based so all pictures are cheap to repaint. - // However, if the picture hasn't changed at all then it's completely - // free. - return existingSurface.picture == picture ? 0.0 : 1.0; - } - - static void _registerCssPainter() { - _cssPainterRegistered = true; - final dynamic css = js_util.getProperty(html.window, 'CSS'); - final dynamic paintWorklet = js_util.getProperty(css, 'paintWorklet'); - if (paintWorklet == null) { - html.window.console.warn( - 'WARNING: CSS.paintWorklet not available. Paint worklets are only ' - 'supported on sites served from https:// or http://localhost.'); - return; - } - js_util.callMethod( - paintWorklet, - 'addModule', - [ - '/packages/flutter_web_ui/assets/houdini_painter.js', - ], - ); - } - - /// Houdini does not paint to bitmap. - @override - int get bitmapPixelCount => 0; - - @override - void applyPaint(EngineCanvas? oldCanvas) { - _recycleCanvas(oldCanvas); - final HoudiniCanvas canvas = HoudiniCanvas(_optimalLocalCullRect); - _canvas = canvas; - domRenderer.clearDom(rootElement!); - rootElement!.append(_canvas!.rootElement); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - canvas.commit(); - } -} - -class PersistedStandardPicture extends PersistedPicture { - PersistedStandardPicture(double dx, double dy, ui.Picture picture, int hints) - : super(dx, dy, picture as EnginePicture, hints); - - @override - double matchForUpdate(PersistedStandardPicture existingSurface) { - if (existingSurface.picture == picture) { - // Picture is the same, return perfect score. - return 0.0; - } - - if (!existingSurface.picture.recordingCanvas!.didDraw) { - // The previous surface didn't draw anything and therefore has no - // resources to reuse. - return 1.0; - } - - final bool didRequireBitmap = - existingSurface.picture.recordingCanvas!.hasArbitraryPaint; - final bool requiresBitmap = picture.recordingCanvas!.hasArbitraryPaint; - if (didRequireBitmap != requiresBitmap) { - // Switching canvas types is always expensive. - return 1.0; - } else if (!requiresBitmap) { - // Currently DomCanvas is always expensive to repaint, as we always throw - // out all the DOM we rendered before. This may change in the future, at - // which point we may return other values here. - return 1.0; - } else { - final BitmapCanvas? oldCanvas = existingSurface._canvas as BitmapCanvas?; - if (oldCanvas == null) { - // We did not allocate a canvas last time. This can happen when the - // picture is completely clipped out of the view. - return 1.0; - } else if (!oldCanvas.doesFitBounds(_exactLocalCullRect!)) { - // The canvas needs to be resized before painting. - return 1.0; - } else { - final int newPixelCount = BitmapCanvas._widthToPhysical(_exactLocalCullRect!.width) - * BitmapCanvas._heightToPhysical(_exactLocalCullRect!.height); - final int oldPixelCount = - oldCanvas._widthInBitmapPixels * oldCanvas._heightInBitmapPixels; - - if (oldPixelCount == 0) { - return 1.0; - } - - final double pixelCountRatio = newPixelCount / oldPixelCount; - assert(0 <= pixelCountRatio && pixelCountRatio <= 1.0, - 'Invalid pixel count ratio $pixelCountRatio'); - return 1.0 - pixelCountRatio; - } - } - } - - @override - Matrix4? get localTransformInverse => null; - - @override - int get bitmapPixelCount { - if (_canvas is! BitmapCanvas) { - return 0; - } - - final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; - return bitmapCanvas.bitmapPixelCount; - } - - @override - void applyPaint(EngineCanvas? oldCanvas) { - if (picture.recordingCanvas!.hasArbitraryPaint) { - _applyBitmapPaint(oldCanvas); - } else { - _applyDomPaint(oldCanvas); - } - } - - void _applyDomPaint(EngineCanvas? oldCanvas) { - _recycleCanvas(oldCanvas); - _canvas = DomCanvas(); - domRenderer.clearDom(rootElement!); - rootElement!.append(_canvas!.rootElement); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - } - - void _applyBitmapPaint(EngineCanvas? oldCanvas) { - if (oldCanvas is BitmapCanvas && - oldCanvas.doesFitBounds(_optimalLocalCullRect!) && - oldCanvas.isReusable()) { - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.keptCount++; - } - oldCanvas.bounds = _optimalLocalCullRect!; - _canvas = oldCanvas; - oldCanvas.setElementCache(_elementCache); - _canvas!.clear(); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - } else { - // We can't use the old canvas because the size has changed, so we put - // it in a cache for later reuse. - _recycleCanvas(oldCanvas); - // We cannot paint immediately because not all canvases that we may be - // able to reuse have been released yet. So instead we enqueue this - // picture to be painted after the update cycle is done syncing the layer - // tree then reuse canvases that were freed up. - _paintQueue.add(_PaintRequest( - canvasSize: _optimalLocalCullRect!.size, - paintCallback: () { - _canvas = _findOrCreateCanvas(_optimalLocalCullRect!); - assert(_canvas is BitmapCanvas - && (_canvas as BitmapCanvas?)!._elementCache == _elementCache); - if (_debugExplainSurfaceStats) { - final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; - _surfaceStatsFor(this).paintPixelCount += - bitmapCanvas.bitmapPixelCount; - } - domRenderer.clearDom(rootElement!); - rootElement!.append(_canvas!.rootElement); - _canvas!.clear(); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - }, - )); - } - } - - /// Attempts to reuse a canvas from the [_recycledCanvases]. Allocates a new - /// one if unable to reuse. - /// - /// The best recycled canvas is one that: - /// - /// - Fits the requested [canvasSize]. This is a hard requirement. Otherwise - /// we risk clipping the picture. - /// - Is the smallest among all possible reusable canvases. This makes canvas - /// reuse more efficient. - /// - Contains no more than twice the number of requested pixels. This makes - /// sure we do not use too much memory for small canvases. - BitmapCanvas _findOrCreateCanvas(ui.Rect bounds) { - final ui.Size canvasSize = bounds.size; - BitmapCanvas? bestRecycledCanvas; - double lastPixelCount = double.infinity; - for (int i = 0; i < _recycledCanvases.length; i++) { - final BitmapCanvas candidate = _recycledCanvases[i]; - if (!candidate.isReusable()) { - continue; - } - - final ui.Size candidateSize = candidate.size; - final double candidatePixelCount = - candidateSize.width * candidateSize.height; - - final bool fits = candidate.doesFitBounds(bounds); - final bool isSmaller = candidatePixelCount < lastPixelCount; - if (fits && isSmaller) { - // [isTooSmall] is used to make sure that a small picture doesn't - // reuse and hold onto memory of a large canvas. - final double requestedPixelCount = bounds.width * bounds.height; - final bool isTooSmall = isSmaller && - requestedPixelCount > 1 && - (candidatePixelCount / requestedPixelCount) > 4; - if (!isTooSmall) { - bestRecycledCanvas = candidate; - lastPixelCount = candidatePixelCount; - final bool fitsExactly = candidateSize.width == canvasSize.width && - candidateSize.height == canvasSize.height; - if (fitsExactly) { - // No need to keep looking any more. - break; - } - } - } - } - - if (bestRecycledCanvas != null) { - if (_debugExplainSurfaceStats) { - _surfaceStatsFor(this).reuseCanvasCount++; - } - _recycledCanvases.remove(bestRecycledCanvas); - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.inRecycleCount = - _recycledCanvases.length; - } - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.reusedCount++; - } - bestRecycledCanvas.bounds = bounds; - bestRecycledCanvas.setElementCache(_elementCache); - return bestRecycledCanvas; - } - - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.createdCount++; - } - final BitmapCanvas canvas = BitmapCanvas(bounds); - canvas.setElementCache(_elementCache); - if (_debugExplainSurfaceStats) { - _surfaceStatsFor(this) - ..allocateBitmapCanvasCount += 1 - ..allocatedBitmapSizeInPixels = - canvas._widthInBitmapPixels * canvas._heightInBitmapPixels; - } - return canvas; - } -} - /// A surface that uses a combination of ``, `
` and `

` elements /// to draw shapes and text. -abstract class PersistedPicture extends PersistedLeafSurface { +class PersistedPicture extends PersistedLeafSurface { PersistedPicture(this.dx, this.dy, this.picture, this.hints) : localPaintBounds = picture.recordingCanvas!.pictureBounds; @@ -553,7 +270,14 @@ abstract class PersistedPicture extends PersistedLeafSurface { /// /// If the implementation does not paint onto a bitmap canvas, it should /// return zero. - int get bitmapPixelCount; + int get bitmapPixelCount { + if (_canvas is! BitmapCanvas) { + return 0; + } + + final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; + return bitmapCanvas.bitmapPixelCount; + } void _applyPaint(PersistedPicture? oldSurface) { final EngineCanvas? oldCanvas = oldSurface?._canvas; @@ -575,8 +299,193 @@ abstract class PersistedPicture extends PersistedLeafSurface { applyPaint(oldCanvas); } - /// Concrete implementations implement this method to do actual painting. - void applyPaint(EngineCanvas? oldCanvas); + @override + double matchForUpdate(PersistedPicture existingSurface) { + if (existingSurface.picture == picture) { + // Picture is the same, return perfect score. + return 0.0; + } + + if (!existingSurface.picture.recordingCanvas!.didDraw) { + // The previous surface didn't draw anything and therefore has no + // resources to reuse. + return 1.0; + } + + final bool didRequireBitmap = + existingSurface.picture.recordingCanvas!.hasArbitraryPaint; + final bool requiresBitmap = picture.recordingCanvas!.hasArbitraryPaint; + if (didRequireBitmap != requiresBitmap) { + // Switching canvas types is always expensive. + return 1.0; + } else if (!requiresBitmap) { + // Currently DomCanvas is always expensive to repaint, as we always throw + // out all the DOM we rendered before. This may change in the future, at + // which point we may return other values here. + return 1.0; + } else { + final BitmapCanvas? oldCanvas = existingSurface._canvas as BitmapCanvas?; + if (oldCanvas == null) { + // We did not allocate a canvas last time. This can happen when the + // picture is completely clipped out of the view. + return 1.0; + } else if (!oldCanvas.doesFitBounds(_exactLocalCullRect!)) { + // The canvas needs to be resized before painting. + return 1.0; + } else { + final int newPixelCount = BitmapCanvas._widthToPhysical(_exactLocalCullRect!.width) + * BitmapCanvas._heightToPhysical(_exactLocalCullRect!.height); + final int oldPixelCount = + oldCanvas._widthInBitmapPixels * oldCanvas._heightInBitmapPixels; + + if (oldPixelCount == 0) { + return 1.0; + } + + final double pixelCountRatio = newPixelCount / oldPixelCount; + assert(0 <= pixelCountRatio && pixelCountRatio <= 1.0, + 'Invalid pixel count ratio $pixelCountRatio'); + return 1.0 - pixelCountRatio; + } + } + } + + @override + Matrix4? get localTransformInverse => null; + + void applyPaint(EngineCanvas? oldCanvas) { + if (picture.recordingCanvas!.hasArbitraryPaint) { + _applyBitmapPaint(oldCanvas); + } else { + _applyDomPaint(oldCanvas); + } + } + + void _applyDomPaint(EngineCanvas? oldCanvas) { + _recycleCanvas(oldCanvas); + _canvas = DomCanvas(); + domRenderer.clearDom(rootElement!); + rootElement!.append(_canvas!.rootElement); + picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); + } + + void _applyBitmapPaint(EngineCanvas? oldCanvas) { + if (oldCanvas is BitmapCanvas && + oldCanvas.doesFitBounds(_optimalLocalCullRect!) && + oldCanvas.isReusable()) { + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.keptCount++; + } + oldCanvas.bounds = _optimalLocalCullRect!; + _canvas = oldCanvas; + oldCanvas.setElementCache(_elementCache); + _canvas!.clear(); + picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); + } else { + // We can't use the old canvas because the size has changed, so we put + // it in a cache for later reuse. + _recycleCanvas(oldCanvas); + // We cannot paint immediately because not all canvases that we may be + // able to reuse have been released yet. So instead we enqueue this + // picture to be painted after the update cycle is done syncing the layer + // tree then reuse canvases that were freed up. + _paintQueue.add(_PaintRequest( + canvasSize: _optimalLocalCullRect!.size, + paintCallback: () { + _canvas = _findOrCreateCanvas(_optimalLocalCullRect!); + assert(_canvas is BitmapCanvas + && (_canvas as BitmapCanvas?)!._elementCache == _elementCache); + if (_debugExplainSurfaceStats) { + final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; + _surfaceStatsFor(this).paintPixelCount += + bitmapCanvas.bitmapPixelCount; + } + domRenderer.clearDom(rootElement!); + rootElement!.append(_canvas!.rootElement); + _canvas!.clear(); + picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); + }, + )); + } + } + + /// Attempts to reuse a canvas from the [_recycledCanvases]. Allocates a new + /// one if unable to reuse. + /// + /// The best recycled canvas is one that: + /// + /// - Fits the requested [canvasSize]. This is a hard requirement. Otherwise + /// we risk clipping the picture. + /// - Is the smallest among all possible reusable canvases. This makes canvas + /// reuse more efficient. + /// - Contains no more than twice the number of requested pixels. This makes + /// sure we do not use too much memory for small canvases. + BitmapCanvas _findOrCreateCanvas(ui.Rect bounds) { + final ui.Size canvasSize = bounds.size; + BitmapCanvas? bestRecycledCanvas; + double lastPixelCount = double.infinity; + for (int i = 0; i < _recycledCanvases.length; i++) { + final BitmapCanvas candidate = _recycledCanvases[i]; + if (!candidate.isReusable()) { + continue; + } + + final ui.Size candidateSize = candidate.size; + final double candidatePixelCount = + candidateSize.width * candidateSize.height; + + final bool fits = candidate.doesFitBounds(bounds); + final bool isSmaller = candidatePixelCount < lastPixelCount; + if (fits && isSmaller) { + // [isTooSmall] is used to make sure that a small picture doesn't + // reuse and hold onto memory of a large canvas. + final double requestedPixelCount = bounds.width * bounds.height; + final bool isTooSmall = isSmaller && + requestedPixelCount > 1 && + (candidatePixelCount / requestedPixelCount) > 4; + if (!isTooSmall) { + bestRecycledCanvas = candidate; + lastPixelCount = candidatePixelCount; + final bool fitsExactly = candidateSize.width == canvasSize.width && + candidateSize.height == canvasSize.height; + if (fitsExactly) { + // No need to keep looking any more. + break; + } + } + } + } + + if (bestRecycledCanvas != null) { + if (_debugExplainSurfaceStats) { + _surfaceStatsFor(this).reuseCanvasCount++; + } + _recycledCanvases.remove(bestRecycledCanvas); + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.inRecycleCount = + _recycledCanvases.length; + } + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.reusedCount++; + } + bestRecycledCanvas.bounds = bounds; + bestRecycledCanvas.setElementCache(_elementCache); + return bestRecycledCanvas; + } + + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.createdCount++; + } + final BitmapCanvas canvas = BitmapCanvas(bounds); + canvas.setElementCache(_elementCache); + if (_debugExplainSurfaceStats) { + _surfaceStatsFor(this) + ..allocateBitmapCanvasCount += 1 + ..allocatedBitmapSizeInPixels = + canvas._widthInBitmapPixels * canvas._heightInBitmapPixels; + } + return canvas; + } void _applyTranslate() { rootElement!.style.transform = 'translate(${dx}px, ${dy}px)'; diff --git a/lib/web_ui/lib/src/engine/surface/platform_view.dart b/lib/web_ui/lib/src/engine/html/platform_view.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/platform_view.dart rename to lib/web_ui/lib/src/engine/html/platform_view.dart diff --git a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart b/lib/web_ui/lib/src/engine/html/recording_canvas.dart similarity index 87% rename from lib/web_ui/lib/src/engine/surface/recording_canvas.dart rename to lib/web_ui/lib/src/engine/html/recording_canvas.dart index c9288b7aca27b..a7c994990ad5d 100644 --- a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/recording_canvas.dart @@ -608,8 +608,6 @@ abstract class PaintCommand { const PaintCommand(); void apply(EngineCanvas? canvas); - - void serializeToCssPaint(List> serializedCommands); } /// A [PaintCommand] that affect pixels on the screen (unlike, for example, the @@ -665,11 +663,6 @@ class PaintSave extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add(const [1]); - } } class PaintRestore extends PaintCommand { @@ -688,11 +681,6 @@ class PaintRestore extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add(const [2]); - } } class PaintTranslate extends PaintCommand { @@ -714,11 +702,6 @@ class PaintTranslate extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([3, dx, dy]); - } } class PaintScale extends PaintCommand { @@ -740,11 +723,6 @@ class PaintScale extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([4, sx, sy]); - } } class PaintRotate extends PaintCommand { @@ -765,11 +743,6 @@ class PaintRotate extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([5, radians]); - } } class PaintTransform extends PaintCommand { @@ -790,11 +763,6 @@ class PaintTransform extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([6]..addAll(matrix4)); - } } class PaintSkew extends PaintCommand { @@ -816,11 +784,6 @@ class PaintSkew extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([7, sx, sy]); - } } class PaintClipRect extends DrawCommand { @@ -841,11 +804,6 @@ class PaintClipRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([8, _serializeRectToCssPaint(rect)]); - } } class PaintClipRRect extends DrawCommand { @@ -866,14 +824,6 @@ class PaintClipRRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 9, - _serializeRRectToCssPaint(rrect), - ]); - } } class PaintClipPath extends DrawCommand { @@ -894,11 +844,6 @@ class PaintClipPath extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([10, path.webOnlySerializeToCssPaint()]); - } } class PaintDrawColor extends DrawCommand { @@ -920,12 +865,6 @@ class PaintDrawColor extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands - .add([11, colorToCssString(color), blendMode.index]); - } } class PaintDrawLine extends DrawCommand { @@ -948,18 +887,6 @@ class PaintDrawLine extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 12, - p1.dx, - p1.dy, - p2.dx, - p2.dy, - _serializePaintToCssPaint(paint) - ]); - } } class PaintDrawPaint extends DrawCommand { @@ -980,11 +907,6 @@ class PaintDrawPaint extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([13, _serializePaintToCssPaint(paint)]); - } } class PaintDrawVertices extends DrawCommand { @@ -1006,11 +928,6 @@ class PaintDrawVertices extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - throw UnimplementedError(); - } } class PaintDrawPoints extends DrawCommand { @@ -1032,11 +949,6 @@ class PaintDrawPoints extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - throw UnimplementedError(); - } } class PaintDrawRect extends DrawCommand { @@ -1058,15 +970,6 @@ class PaintDrawRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 14, - _serializeRectToCssPaint(rect), - _serializePaintToCssPaint(paint) - ]); - } } class PaintDrawRRect extends DrawCommand { @@ -1088,15 +991,6 @@ class PaintDrawRRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 15, - _serializeRRectToCssPaint(rrect), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawDRRect extends DrawCommand { @@ -1125,16 +1019,6 @@ class PaintDrawDRRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 16, - _serializeRRectToCssPaint(outer), - _serializeRRectToCssPaint(inner), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawOval extends DrawCommand { @@ -1156,15 +1040,6 @@ class PaintDrawOval extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 17, - _serializeRectToCssPaint(rect), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawCircle extends DrawCommand { @@ -1187,17 +1062,6 @@ class PaintDrawCircle extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 18, - c.dx, - c.dy, - radius, - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawPath extends DrawCommand { @@ -1219,15 +1083,6 @@ class PaintDrawPath extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 19, - path.webOnlySerializeToCssPaint(), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawShadow extends DrawCommand { @@ -1252,22 +1107,6 @@ class PaintDrawShadow extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 20, - path.webOnlySerializeToCssPaint(), - [ - color.alpha, - color.red, - color.green, - color.blue, - ], - elevation, - transparentOccluder, - ]); - } } class PaintDrawImage extends DrawCommand { @@ -1290,13 +1129,6 @@ class PaintDrawImage extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - if (assertionsEnabled) { - throw UnsupportedError('drawImage not serializable'); - } - } } class PaintDrawImageRect extends DrawCommand { @@ -1320,13 +1152,6 @@ class PaintDrawImageRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - if (assertionsEnabled) { - throw UnsupportedError('drawImageRect not serializable'); - } - } } class PaintDrawParagraph extends DrawCommand { @@ -1348,55 +1173,6 @@ class PaintDrawParagraph extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - if (assertionsEnabled) { - throw UnsupportedError('drawParagraph not serializable'); - } - } -} - -List _serializePaintToCssPaint(SurfacePaintData paint) { - final EngineGradient? engineShader = paint.shader as EngineGradient?; - return [ - paint.blendMode?.index, - paint.style?.index, - paint.strokeWidth, - paint.strokeCap?.index, - paint.isAntiAlias, - colorToCssString(paint.color), - engineShader?.webOnlySerializeToCssPaint(), - paint.maskFilter?.webOnlySerializeToCssPaint(), - paint.filterQuality?.index, - paint.colorFilter?.webOnlySerializeToCssPaint(), - ]; -} - -List _serializeRectToCssPaint(ui.Rect rect) { - return [ - rect.left, - rect.top, - rect.right, - rect.bottom, - ]; -} - -List _serializeRRectToCssPaint(ui.RRect rrect) { - return [ - rrect.left, - rrect.top, - rrect.right, - rrect.bottom, - rrect.tlRadiusX, - rrect.tlRadiusY, - rrect.trRadiusX, - rrect.trRadiusY, - rrect.brRadiusX, - rrect.brRadiusY, - rrect.blRadiusX, - rrect.blRadiusY, - ]; } class Subpath { @@ -1421,14 +1197,6 @@ class Subpath { return result; } - List serializeToCssPaint() { - final List serialization = []; - for (int i = 0; i < commands.length; i++) { - serialization.add(commands[i].serializeToCssPaint()); - } - return serialization; - } - @override String toString() { if (assertionsEnabled) { @@ -1439,26 +1207,11 @@ class Subpath { } } -/// ! Houdini implementation relies on indices here. Keep in sync. -class PathCommandTypes { - static const int moveTo = 0; - static const int lineTo = 1; - static const int ellipse = 2; - static const int close = 3; - static const int quadraticCurveTo = 4; - static const int bezierCurveTo = 5; - static const int rect = 6; - static const int rRect = 7; -} - abstract class PathCommand { - final int type; - const PathCommand(this.type); + const PathCommand(); PathCommand shifted(ui.Offset offset); - List serializeToCssPaint(); - /// Transform the command and add to targetPath. void transform(Float32List matrix4, SurfacePath targetPath); @@ -1472,18 +1225,13 @@ class MoveTo extends PathCommand { final double x; final double y; - const MoveTo(this.x, this.y) : super(PathCommandTypes.moveTo); + const MoveTo(this.x, this.y); @override MoveTo shifted(ui.Offset offset) { return MoveTo(x + offset.dx, y + offset.dy); } - @override - List serializeToCssPaint() { - return [1, x, y]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); @@ -1504,18 +1252,13 @@ class LineTo extends PathCommand { final double x; final double y; - const LineTo(this.x, this.y) : super(PathCommandTypes.lineTo); + const LineTo(this.x, this.y); @override LineTo shifted(ui.Offset offset) { return LineTo(x + offset.dx, y + offset.dy); } - @override - List serializeToCssPaint() { - return [2, x, y]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); @@ -1543,8 +1286,7 @@ class Ellipse extends PathCommand { final bool anticlockwise; const Ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotation, - this.startAngle, this.endAngle, this.anticlockwise) - : super(PathCommandTypes.ellipse); + this.startAngle, this.endAngle, this.anticlockwise); @override Ellipse shifted(ui.Offset offset) { @@ -1552,21 +1294,6 @@ class Ellipse extends PathCommand { startAngle, endAngle, anticlockwise); } - @override - List serializeToCssPaint() { - return [ - 3, - x, - y, - radiusX, - radiusY, - rotation, - startAngle, - endAngle, - anticlockwise, - ]; - } - @override void transform(Float32List matrix4, SurfacePath targetPath) { final ui.Path bezierPath = ui.Path(); @@ -1686,8 +1413,7 @@ class QuadraticCurveTo extends PathCommand { final double x2; final double y2; - const QuadraticCurveTo(this.x1, this.y1, this.x2, this.y2) - : super(PathCommandTypes.quadraticCurveTo); + const QuadraticCurveTo(this.x1, this.y1, this.x2, this.y2); @override QuadraticCurveTo shifted(ui.Offset offset) { @@ -1695,11 +1421,6 @@ class QuadraticCurveTo extends PathCommand { x1 + offset.dx, y1 + offset.dy, x2 + offset.dx, y2 + offset.dy); } - @override - List serializeToCssPaint() { - return [4, x1, y1, x2, y2]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final double m0 = matrix4[0]; @@ -1734,8 +1455,7 @@ class BezierCurveTo extends PathCommand { final double x3; final double y3; - const BezierCurveTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3) - : super(PathCommandTypes.bezierCurveTo); + const BezierCurveTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); @override BezierCurveTo shifted(ui.Offset offset) { @@ -1743,11 +1463,6 @@ class BezierCurveTo extends PathCommand { y2 + offset.dy, x3 + offset.dx, y3 + offset.dy); } - @override - List serializeToCssPaint() { - return [5, x1, y1, x2, y2, x3, y3]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final double s0 = matrix4[0]; @@ -1782,8 +1497,7 @@ class RectCommand extends PathCommand { final double width; final double height; - const RectCommand(this.x, this.y, this.width, this.height) - : super(PathCommandTypes.rect); + const RectCommand(this.x, this.y, this.width, this.height); @override RectCommand shifted(ui.Offset offset) { @@ -1824,11 +1538,6 @@ class RectCommand extends PathCommand { } } - @override - List serializeToCssPaint() { - return [6, x, y, width, height]; - } - @override String toString() { if (assertionsEnabled) { @@ -1842,18 +1551,13 @@ class RectCommand extends PathCommand { class RRectCommand extends PathCommand { final ui.RRect rrect; - const RRectCommand(this.rrect) : super(PathCommandTypes.rRect); + const RRectCommand(this.rrect); @override RRectCommand shifted(ui.Offset offset) { return RRectCommand(rrect.shift(offset)); } - @override - List serializeToCssPaint() { - return [7, _serializeRRectToCssPaint(rrect)]; - } - @override void transform(Float32List matrix4, SurfacePath targetPath) { final ui.Path roundRectPath = ui.Path(); @@ -1872,18 +1576,11 @@ class RRectCommand extends PathCommand { } class CloseCommand extends PathCommand { - const CloseCommand() : super(PathCommandTypes.close); - @override CloseCommand shifted(ui.Offset offset) { return this; } - @override - List serializeToCssPaint() { - return [8]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { targetPath.close(); diff --git a/lib/web_ui/lib/src/engine/surface/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/render_vertices.dart rename to lib/web_ui/lib/src/engine/html/render_vertices.dart index e5ce5d1044e88..11d400bfc5f5b 100644 --- a/lib/web_ui/lib/src/engine/surface/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -163,8 +163,7 @@ class _WebGlRenderer implements _GlRenderer { } _GlContext gl = _OffscreenCanvas.createGlContext(widthInPixels, heightInPixels)!; - final bool isWebKit = (browserEngine == BrowserEngine.webkit); - _GlProgram glProgram = isWebKit + _GlProgram glProgram = webGLVersion == 1 ? gl.useAndCacheProgram( _vertexShaderTriangleEs1, _fragmentShaderTriangleEs1)! : gl.useAndCacheProgram( @@ -499,13 +498,10 @@ class _GlContext { switch (mode) { case ui.VertexMode.triangles: return kTriangles; - break; case ui.VertexMode.triangleFan: return kTriangleFan; - break; case ui.VertexMode.triangleStrip: return kTriangleStrip; - break; } } diff --git a/lib/web_ui/lib/src/engine/surface/scene.dart b/lib/web_ui/lib/src/engine/html/scene.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/scene.dart rename to lib/web_ui/lib/src/engine/html/scene.dart diff --git a/lib/web_ui/lib/src/engine/surface/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart similarity index 98% rename from lib/web_ui/lib/src/engine/surface/scene_builder.dart rename to lib/web_ui/lib/src/engine/html/scene_builder.dart index fd38e403e38b8..e6cb5d185f7b0 100644 --- a/lib/web_ui/lib/src/engine/surface/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -78,9 +78,6 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { Float64List matrix4, { ui.TransformEngineLayer? oldLayer, }) { - if (matrix4 == null) { // ignore: unnecessary_null_comparison - throw ArgumentError('"matrix4" argument cannot be null'); - } if (matrix4.length != 16) { throw ArgumentError('"matrix4" must have 16 entries.'); } @@ -363,7 +360,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { if (willChangeHint) { hints |= 2; } - _addSurface(persistedPictureFactory(offset.dx, offset.dy, picture, hints)); + _addSurface(PersistedPicture(offset.dx, offset.dy, picture as EnginePicture, hints)); } /// Adds a backend texture to the scene. diff --git a/lib/web_ui/lib/src/engine/shader.dart b/lib/web_ui/lib/src/engine/html/shader.dart similarity index 64% rename from lib/web_ui/lib/src/engine/shader.dart rename to lib/web_ui/lib/src/engine/html/shader.dart index 46d7153f9b4ec..4dae88f674bf9 100644 --- a/lib/web_ui/lib/src/engine/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shader.dart @@ -5,34 +5,12 @@ // @dart = 2.10 part of engine; -bool _offsetIsValid(ui.Offset offset) { - assert(offset != null, 'Offset argument was null.'); // ignore: unnecessary_null_comparison - assert(!offset.dx.isNaN && !offset.dy.isNaN, - 'Offset argument contained a NaN value.'); - return true; -} - -bool _matrix4IsValid(Float32List matrix4) { - assert(matrix4 != null, 'Matrix4 argument was null.'); // ignore: unnecessary_null_comparison - assert(matrix4.length == 16, 'Matrix4 must have 16 entries.'); - return true; -} - -abstract class EngineShader { - /// Create a shader for use in the Skia backend. - SkShader createSkiaShader(); -} - -abstract class EngineGradient implements ui.Gradient, EngineShader { +abstract class EngineGradient implements ui.Gradient { /// Hidden constructor to prevent subclassing. EngineGradient._(); /// Creates a fill style to be used in painting. Object createPaintStyle(html.CanvasRenderingContext2D? ctx); - - List webOnlySerializeToCssPaint() { - throw UnsupportedError('CSS paint not implemented for this shader type'); - } } class GradientSweep extends EngineGradient { @@ -61,23 +39,6 @@ class GradientSweep extends EngineGradient { final double startAngle; final double endAngle; final Float32List? matrix4; - - @override - SkShader createSkiaShader() { - throw UnimplementedError(); - } -} - -void _validateColorStops(List colors, List? colorStops) { - if (colorStops == null) { - if (colors.length != 2) - throw ArgumentError( - '"colors" must have length 2 if "colorStops" is omitted.'); - } else { - if (colors.length != colorStops.length) - throw ArgumentError( - '"colors" and "colorStops" arguments must have equal length.'); - } } class GradientLinear extends EngineGradient { @@ -135,37 +96,6 @@ class GradientLinear extends EngineGradient { } return gradient; } - - @override - List webOnlySerializeToCssPaint() { - final List serializedColors = []; - for (int i = 0; i < colors.length; i++) { - serializedColors.add(colorToCssString(colors[i])); - } - return [ - 1, - from.dx, - from.dy, - to.dx, - to.dy, - serializedColors, - colorStops, - tileMode.index - ]; - } - - @override - SkShader createSkiaShader() { - assert(experimentalUseSkia); - - return canvasKit.SkShader.MakeLinearGradient( - toSkPoint(from), - toSkPoint(to), - toSkFloatColorList(colors), - toSkColorStops(colorStops), - toSkTileMode(tileMode), - ); - } } // TODO(flutter_web): For transforms and tile modes implement as webgl @@ -207,21 +137,6 @@ class GradientRadial extends EngineGradient { } return gradient; } - - @override - SkShader createSkiaShader() { - assert(experimentalUseSkia); - - return canvasKit.SkShader.MakeRadialGradient( - toSkPoint(center), - radius, - toSkFloatColorList(colors), - toSkColorStops(colorStops), - toSkTileMode(tileMode), - matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, - 0, - ); - } } class GradientConical extends EngineGradient { @@ -242,22 +157,6 @@ class GradientConical extends EngineGradient { Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { throw UnimplementedError(); } - - @override - SkShader createSkiaShader() { - assert(experimentalUseSkia); - return canvasKit.SkShader.MakeTwoPointConicalGradient( - toSkPoint(focal), - focalRadius, - toSkPoint(center), - radius, - toSkFloatColorList(colors), - toSkColorStops(colorStops), - toSkTileMode(tileMode), - matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, - 0, - ); - } } /// Backend implementation of [ui.ImageFilter]. @@ -284,20 +183,3 @@ class EngineImageFilter implements ui.ImageFilter { return 'ImageFilter.blur($sigmaX, $sigmaY)'; } } - -/// Backend implementation of [ui.ImageShader]. -class EngineImageShader implements ui.ImageShader, EngineShader { - EngineImageShader( - ui.Image image, this.tileModeX, this.tileModeY, this.matrix4) - : _skImage = image as CkImage; - - final ui.TileMode tileModeX; - final ui.TileMode tileModeY; - final Float64List matrix4; - final CkImage _skImage; - - SkShader createSkiaShader() => _skImage.skImage.makeShader( - toSkTileMode(tileModeX), - toSkTileMode(tileModeY), - ); -} diff --git a/lib/web_ui/lib/src/engine/surface/surface.dart b/lib/web_ui/lib/src/engine/html/surface.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/surface.dart rename to lib/web_ui/lib/src/engine/html/surface.dart diff --git a/lib/web_ui/lib/src/engine/surface/surface_stats.dart b/lib/web_ui/lib/src/engine/html/surface_stats.dart similarity index 98% rename from lib/web_ui/lib/src/engine/surface/surface_stats.dart rename to lib/web_ui/lib/src/engine/html/surface_stats.dart index 2fc0487f1a123..911a825ad03ab 100644 --- a/lib/web_ui/lib/src/engine/surface/surface_stats.dart +++ b/lib/web_ui/lib/src/engine/html/surface_stats.dart @@ -237,7 +237,7 @@ void _debugPrintSurfaceStats(PersistedScene scene, int frameNumber) { elementReuseCount += stats.reuseElementCount; totalAllocatedDomNodeCount += stats.allocatedDomNodeCount; - if (surface is PersistedStandardPicture) { + if (surface is PersistedPicture) { pictureCount += 1; paintCount += stats.paintCount; @@ -291,8 +291,8 @@ void _debugPrintSurfaceStats(PersistedScene scene, int frameNumber) { final int pixelCount = canvasElements .cast() .map((html.CanvasElement e) { - final int pixels = e.width * e.height; - canvasInfo.writeln(' - ${e.width} x ${e.height} = $pixels pixels'); + final int pixels = e.width! * e.height!; + canvasInfo.writeln(' - ${e.width!} x ${e.height!} = $pixels pixels'); return pixels; }).fold(0, (int total, int pixels) => total + pixels); final double physicalScreenWidth = diff --git a/lib/web_ui/lib/src/engine/surface/transform.dart b/lib/web_ui/lib/src/engine/html/transform.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/transform.dart rename to lib/web_ui/lib/src/engine/html/transform.dart 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 cc8b30c1d27ef..6a0595d66ae50 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -32,17 +32,13 @@ class HtmlCodec implements ui.Codec { // Currently there is no way to watch decode progress, so // we add 0/100 , 100/100 progress callbacks to enable loading progress // builders to create UI. - if (chunkCallback != null) { - chunkCallback!(0, 100); - } + chunkCallback?.call(0, 100); if (_supportsDecode) { final html.ImageElement imgElement = html.ImageElement(); imgElement.src = src; js_util.setProperty(imgElement, 'decoding', 'async'); imgElement.decode().then((dynamic _) { - if (chunkCallback != null) { - chunkCallback!(100, 100); - } + chunkCallback?.call(100, 100); final HtmlImage image = HtmlImage( imgElement, imgElement.naturalWidth, @@ -133,13 +129,13 @@ class HtmlImage implements ui.Image { final int height; @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - return futurize((Callback callback) { - return _toByteData(format.index, (Uint8List? encoded) { - callback(encoded?.buffer.asByteData()); - }); - }); + Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + if (imgElement.src?.startsWith('data:') == true) { + final data = UriData.fromUri(Uri.parse(imgElement.src!)); + return Future.value(data.contentAsBytes().buffer.asByteData()); + } else { + return Future.value(null); + } } // Returns absolutely positioned actual image element on first call and @@ -153,12 +149,4 @@ class HtmlImage implements ui.Image { return imgElement; } } - - // TODO(het): Support this for asset images and images generated from - // `Picture`s. - /// Returns an error message on failure, null on success. - String _toByteData(int format, Callback callback) { - callback(null); - return 'Image.toByteData is not supported in Flutter for Web'; - } } diff --git a/lib/web_ui/lib/src/engine/rrect_renderer.dart b/lib/web_ui/lib/src/engine/rrect_renderer.dart index 767c0666c6649..3ccc054331d41 100644 --- a/lib/web_ui/lib/src/engine/rrect_renderer.dart +++ b/lib/web_ui/lib/src/engine/rrect_renderer.dart @@ -7,7 +7,6 @@ part of engine; /// Renders an RRect using path primitives. abstract class _RRectRenderer { - // TODO(mdebbar): Backport the overlapping corners fix to houdini_painter.js // To draw the rounded rectangle, perform the following steps: // 0. Ensure border radius don't overlap // 1. Flip left,right top,bottom since web doesn't support flipped diff --git a/lib/web_ui/lib/src/engine/text/line_breaker.dart b/lib/web_ui/lib/src/engine/text/line_breaker.dart index df4049f653888..50970980bf518 100644 --- a/lib/web_ui/lib/src/engine/text/line_breaker.dart +++ b/lib/web_ui/lib/src/engine/text/line_breaker.dart @@ -21,11 +21,93 @@ enum LineBreakType { } /// Acts as a tuple that encapsulates information about a line break. +/// +/// It contains multiple indices that are helpful when it comes to measuring the +/// width of a line of text. +/// +/// [indexWithoutTrailingSpaces] <= [indexWithoutTrailingNewlines] <= [index] +/// +/// Example: for the string "foo \nbar " here are the indices: +/// ``` +/// f o o \n b a r +/// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ +/// 0 1 2 3 4 5 6 7 8 9 +/// ``` +/// It contains two line breaks: +/// ``` +/// // The first line break: +/// LineBreakResult(5, 4, 3, LineBreakType.mandatory) +/// +/// // Second line break: +/// LineBreakResult(9, 9, 8, LineBreakType.mandatory) +/// ``` class LineBreakResult { - LineBreakResult(this.index, this.type); - + const LineBreakResult( + this.index, + this.indexWithoutTrailingNewlines, + this.indexWithoutTrailingSpaces, + this.type, + ): assert(indexWithoutTrailingSpaces <= indexWithoutTrailingNewlines), + assert(indexWithoutTrailingNewlines <= index); + + /// Creates a [LineBreakResult] where all indices are the same (i.e. there are + /// no trailing spaces or new lines). + const LineBreakResult.sameIndex(this.index, this.type) + : indexWithoutTrailingNewlines = index, + indexWithoutTrailingSpaces = index; + + /// The true index at which the line break should occur, including all spaces + /// and new lines. final int index; + + /// The index of the line break excluding any trailing new lines. + final int indexWithoutTrailingNewlines; + + /// The index of the line break excluding any trailing spaces. + final int indexWithoutTrailingSpaces; + + /// The type of line break is useful to determine the behavior in text + /// measurement. + /// + /// For example, a mandatory line break always causes a line break regardless + /// of width constraints. But a line break opportunity requires further checks + /// to decide whether to take the line break or not. final LineBreakType type; + + @override + int get hashCode => ui.hashValues( + index, + indexWithoutTrailingNewlines, + indexWithoutTrailingSpaces, + type, + ); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is LineBreakResult && + other.index == index && + other.indexWithoutTrailingNewlines == indexWithoutTrailingNewlines && + other.indexWithoutTrailingSpaces == indexWithoutTrailingSpaces && + other.type == type; + } + + @override + String toString() { + if (assertionsEnabled) { + return 'LineBreakResult(index: $index, ' + 'without new lines: $indexWithoutTrailingNewlines, ' + 'without spaces: $indexWithoutTrailingSpaces, ' + 'type: $type)'; + } else { + return super.toString(); + } + } } bool _isHardBreak(LineCharProperty? prop) { @@ -49,7 +131,7 @@ bool _isKoreanSyllable(LineCharProperty? prop) { prop == LineCharProperty.H3; } -/// Whether the given char code has an Easter Asian width property of F, W or H. +/// Whether the given char code has an Eastern Asian width property of F, W or H. /// /// See: /// - https://www.unicode.org/reports/tr14/tr14-45.html#LB30 @@ -62,6 +144,18 @@ bool _hasEastAsianWidthFWH(int charCode) { /// Finds the next line break in the given [text] starting from [index]. /// +/// Wethink about indices as pointing between characters, and they go all the +/// way from 0 to the string length. For example, here are the indices for the +/// string "foo bar": +/// +/// ``` +/// f o o b a r +/// ^ ^ ^ ^ ^ ^ ^ ^ +/// 0 1 2 3 4 5 6 7 +/// ``` +/// +/// This way the indices work well with [String.substring()]. +/// /// Useful resources: /// /// * https://www.unicode.org/reports/tr14/tr14-45.html#Algorithm @@ -80,6 +174,12 @@ LineBreakResult nextLineBreak(String text, int index) { // sequence. LineCharProperty? baseOfSpaceSequence; + /// The index of the last character that wasn't a space. + int lastNonSpaceIndex = index; + + /// The index of the last character that wasn't a new line. + int lastNonNewlineIndex = index; + // When the text/line starts with SP, we should treat the begining of text/line // as if it were a WJ (word joiner). if (curr == LineCharProperty.SP) { @@ -131,12 +231,15 @@ LineBreakResult nextLineBreak(String text, int index) { // LB4: BK ! // // Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks. - // LB5: CR × LF - // CR ! - // LF ! + // LB5: LF ! // NL ! if (_isHardBreak(prev1)) { - return LineBreakResult(index, LineBreakType.mandatory); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.mandatory, + ); } if (prev1 == LineCharProperty.CR) { @@ -145,10 +248,21 @@ LineBreakResult nextLineBreak(String text, int index) { continue; } else { // LB5: CR ! - return LineBreakResult(index, LineBreakType.mandatory); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.mandatory, + ); } } + // At this point, we know for sure the prev character wasn't a new line. + lastNonNewlineIndex = index; + if (prev1 != LineCharProperty.SP) { + lastNonSpaceIndex = index; + } + // Do not break before hard line breaks. // LB6: × ( BK | CR | LF | NL ) if (_isHardBreak(curr) || curr == LineCharProperty.CR) { @@ -158,7 +272,12 @@ LineBreakResult nextLineBreak(String text, int index) { // Always break at the end of text. // LB3: ! eot if (index >= text.length) { - return LineBreakResult(text.length, LineBreakType.endOfText); + return LineBreakResult( + text.length, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.endOfText, + ); } // Do not break before spaces or zero width space. @@ -186,7 +305,12 @@ LineBreakResult nextLineBreak(String text, int index) { // LB8: ZW SP* ÷ if (prev1 == LineCharProperty.ZW || baseOfSpaceSequence == LineCharProperty.ZW) { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } // Do not break a combining character sequence; treat it as if it has the @@ -292,7 +416,12 @@ LineBreakResult nextLineBreak(String text, int index) { // Break after spaces. // LB18: SP ÷ if (prev1 == LineCharProperty.SP) { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } // Do not break before or after quotation marks, such as ‘”’. @@ -306,7 +435,12 @@ LineBreakResult nextLineBreak(String text, int index) { // LB20: ÷ CB // CB ÷ if (prev1 == LineCharProperty.CB || curr == LineCharProperty.CB) { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } // Do not break before hyphen-minus, other hyphens, fixed-width spaces, @@ -471,7 +605,12 @@ LineBreakResult nextLineBreak(String text, int index) { if (regionalIndicatorCount.isOdd) { continue; } else { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } } @@ -484,7 +623,17 @@ LineBreakResult nextLineBreak(String text, int index) { // Break everywhere else. // LB31: ALL ÷ // ÷ ALL - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } - return LineBreakResult(text.length, LineBreakType.endOfText); + return LineBreakResult( + text.length, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.endOfText, + ); } diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart index c4fae1e0af282..ac2de9edc3110 100644 --- a/lib/web_ui/lib/src/engine/text/measurement.dart +++ b/lib/web_ui/lib/src/engine/text/measurement.dart @@ -16,19 +16,13 @@ const double _baselineRatioHack = 1.1662499904632568; /// Signature of a function that takes a character and returns true or false. typedef CharPredicate = bool Function(int char); -bool _whitespacePredicate(int char) { +bool _newlinePredicate(int char) { final LineCharProperty prop = lineLookup.findForChar(char); - return prop == LineCharProperty.SP || - prop == LineCharProperty.BK || + return prop == LineCharProperty.BK || prop == LineCharProperty.LF || prop == LineCharProperty.CR; } -bool _newlinePredicate(int char) { - final LineCharProperty prop = lineLookup.findForChar(char); - return prop == LineCharProperty.BK || prop == LineCharProperty.LF || prop == LineCharProperty.CR; -} - /// Manages [ParagraphRuler] instances and caches them per unique /// [ParagraphGeometricStyle]. /// @@ -435,6 +429,7 @@ class DomTextMeasurementService extends TextMeasurementService { _excludeTrailing(text, 0, text.length, _newlinePredicate), hardBreak: true, width: lineWidth, + widthWithTrailingSpaces: lineWidth, left: alignOffset, lineNumber: 0, ), @@ -453,6 +448,7 @@ class DomTextMeasurementService extends TextMeasurementService { alphabeticBaseline: alphabeticBaseline, ideographicBaseline: ideographicBaseline, lines: lines, + placeholderBoxes: ruler.measurePlaceholderBoxes(), textAlign: paragraph._textAlign, textDirection: paragraph._textDirection, ); @@ -503,6 +499,7 @@ class DomTextMeasurementService extends TextMeasurementService { alphabeticBaseline: alphabeticBaseline, ideographicBaseline: ideographicBaseline, lines: null, + placeholderBoxes: ruler.measurePlaceholderBoxes(), textAlign: paragraph._textAlign, textDirection: paragraph._textDirection, ); @@ -614,6 +611,7 @@ class CanvasTextMeasurementService extends TextMeasurementService { maxIntrinsicWidth: maxIntrinsicCalculator.value, width: constraints.width, lines: linesCalculator.lines, + placeholderBoxes: [], textAlign: paragraph._textAlign, textDirection: paragraph._textDirection, ); @@ -688,8 +686,8 @@ double _measureSubstring( final double letterSpacing = style.letterSpacing ?? 0.0; final String sub = start == 0 && end == text.length ? text : text.substring(start, end); - final double width = - _canvasContext.measureText(sub).width! + letterSpacing * sub.length as double; + final double width = _canvasContext.measureText(sub).width! + + letterSpacing * sub.length as double; // What we are doing here is we are rounding to the nearest 2nd decimal // point. So 39.999423 becomes 40, and 11.243982 becomes 11.24. @@ -736,8 +734,17 @@ class LinesCalculator { /// The lines that have been consumed so far. List lines = []; - int _lineStart = 0; - int _chunkStart = 0; + /// The last line break regardless of whether it was optional or mandatory, or + /// whether we took it or not. + LineBreakResult _lastBreak = + const LineBreakResult.sameIndex(0, LineBreakType.mandatory); + + /// The last line break that actually caused a new line to exist. + LineBreakResult _lastTakenBreak = + const LineBreakResult.sameIndex(0, LineBreakType.mandatory); + + int get _lineStart => _lastTakenBreak.index; + int get _chunkStart => _lastBreak.index; bool _reachedMaxLines = false; double? _cachedEllipsisWidth; @@ -755,8 +762,8 @@ class LinesCalculator { final bool isHardBreak = brk.type == LineBreakType.mandatory || brk.type == LineBreakType.endOfText; final int chunkEnd = brk.index; - final int chunkEndWithoutSpace = - _excludeTrailing(_text!, _chunkStart, chunkEnd, _whitespacePredicate); + final int chunkEndWithoutNewlines = brk.indexWithoutTrailingNewlines; + final int chunkEndWithoutSpace = brk.indexWithoutTrailingSpaces; // A single chunk of text could be force-broken into multiple lines if it // doesn't fit in a single line. That's why we need a loop. @@ -800,10 +807,10 @@ class LinesCalculator { _text!.substring(_lineStart, breakingPoint) + _style.ellipsis!, startIndex: _lineStart, endIndex: chunkEnd, - endIndexWithoutNewlines: - _excludeTrailing(_text!, _chunkStart, chunkEnd, _newlinePredicate), + endIndexWithoutNewlines: chunkEndWithoutNewlines, hardBreak: false, width: widthOfResultingLine, + widthWithTrailingSpaces: widthOfResultingLine, left: alignOffset, lineNumber: lines.length, )); @@ -821,12 +828,14 @@ class LinesCalculator { // [isHardBreak] check below) to break the line. break; } - _addLineBreak(lineEnd: breakingPoint, isHardBreak: false); - _chunkStart = breakingPoint; + _addLineBreak(LineBreakResult.sameIndex( + breakingPoint, + LineBreakType.opportunity, + )); } else { // The control case of current line exceeding [_maxWidth], we break the // line. - _addLineBreak(lineEnd: _chunkStart, isHardBreak: false); + _addLineBreak(_lastBreak); } } @@ -835,46 +844,38 @@ class LinesCalculator { } if (isHardBreak) { - _addLineBreak(lineEnd: chunkEnd, isHardBreak: true); + _addLineBreak(brk); } - _chunkStart = chunkEnd; + _lastBreak = brk; } - void _addLineBreak({ - required int lineEnd, - required bool isHardBreak, - }) { - final int endWithoutNewlines = _excludeTrailing( - _text!, - _lineStart, - lineEnd, - _newlinePredicate, - ); - final int endWithoutSpace = _excludeTrailing( - _text!, - _lineStart, - endWithoutNewlines, - _whitespacePredicate, - ); + void _addLineBreak(LineBreakResult brk) { final int lineNumber = lines.length; - final double lineWidth = measureSubstring(_lineStart, endWithoutSpace); + final double lineWidth = + measureSubstring(_lineStart, brk.indexWithoutTrailingSpaces); + final double lineWidthWithTrailingSpaces = + measureSubstring(_lineStart, brk.indexWithoutTrailingNewlines); final double alignOffset = _calculateAlignOffsetForLine( paragraph: _paragraph, lineWidth: lineWidth, maxWidth: _maxWidth, ); + final bool isHardBreak = brk.type == LineBreakType.mandatory || + brk.type == LineBreakType.endOfText; + final EngineLineMetrics metrics = EngineLineMetrics.withText( - _text!.substring(_lineStart, endWithoutNewlines), + _text!.substring(_lineStart, brk.indexWithoutTrailingNewlines), startIndex: _lineStart, - endIndex: lineEnd, - endIndexWithoutNewlines: endWithoutNewlines, + endIndex: brk.index, + endIndexWithoutNewlines: brk.indexWithoutTrailingNewlines, hardBreak: isHardBreak, width: lineWidth, + widthWithTrailingSpaces: lineWidthWithTrailingSpaces, left: alignOffset, lineNumber: lineNumber, ); lines.add(metrics); - _lineStart = lineEnd; + _lastTakenBreak = _lastBreak = brk; if (lines.length == _style.maxLines) { _reachedMaxLines = true; } @@ -943,10 +944,13 @@ class MinIntrinsicCalculator { /// [value] will contain the final minimum intrinsic width. void update(LineBreakResult brk) { final int chunkEnd = brk.index; - final int chunkEndWithoutSpace = - _excludeTrailing(_text, _lastChunkEnd, chunkEnd, _whitespacePredicate); final double width = _measureSubstring( - _canvasContext, _style, _text, _lastChunkEnd, chunkEndWithoutSpace); + _canvasContext, + _style, + _text, + _lastChunkEnd, + brk.indexWithoutTrailingSpaces, + ); if (width > value) { value = width; } @@ -977,24 +981,17 @@ class MaxIntrinsicCalculator { return; } - final int hardLineEnd = brk.index; - final int hardLineEndWithoutNewlines = _excludeTrailing( - _text, - _lastHardLineEnd, - hardLineEnd, - _newlinePredicate, - ); final double lineWidth = _measureSubstring( _canvasContext, _style, _text, _lastHardLineEnd, - hardLineEndWithoutNewlines, + brk.indexWithoutTrailingNewlines, ); if (lineWidth > value) { value = lineWidth; } - _lastHardLineEnd = hardLineEnd; + _lastHardLineEnd = brk.index; } } diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index a3d09e9c57795..862f6789a4d35 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -7,6 +7,8 @@ part of engine; const ui.Color _defaultTextColor = ui.Color(0xFFFF0000); +const String _placeholderClass = 'paragraph-placeholder'; + class EngineLineMetrics implements ui.LineMetrics { EngineLineMetrics({ required this.hardBreak, @@ -21,7 +23,8 @@ class EngineLineMetrics implements ui.LineMetrics { }) : displayText = null, startIndex = -1, endIndex = -1, - endIndexWithoutNewlines = -1; + endIndexWithoutNewlines = -1, + widthWithTrailingSpaces = width; EngineLineMetrics.withText( String this.displayText, { @@ -30,6 +33,7 @@ class EngineLineMetrics implements ui.LineMetrics { required this.endIndexWithoutNewlines, required this.hardBreak, required this.width, + required this.widthWithTrailingSpaces, required this.left, required this.lineNumber, }) : assert(displayText != null), // ignore: unnecessary_null_comparison @@ -80,6 +84,18 @@ class EngineLineMetrics implements ui.LineMetrics { @override final double width; + /// The full width of the line including all trailing space but not new lines. + /// + /// The difference between [width] and [widthWithTrailingSpaces] is that + /// [widthWithTrailingSpaces] includes trailing spaces in the width + /// calculation while [width] doesn't. + /// + /// For alignment purposes for example, the [width] property is the right one + /// to use because trailing spaces shouldn't affect the centering of text. + /// But for placing cursors in text fields, we do care about trailing + /// spaces so [widthWithTrailingSpaces] is more suitable. + final double widthWithTrailingSpaces; + @override final double left; @@ -160,6 +176,7 @@ class EngineParagraph implements ui.Paragraph { required ui.TextAlign textAlign, required ui.TextDirection textDirection, required ui.Paint? background, + required this.placeholderCount, }) : assert((plainText == null && paint == null) || (plainText != null && paint != null)), _paragraphElement = paragraphElement, @@ -178,6 +195,8 @@ class EngineParagraph implements ui.Paragraph { final ui.TextDirection _textDirection; final SurfacePaint? _background; + final int placeholderCount; + @visibleForTesting String? get plainText => _plainText; @@ -318,7 +337,8 @@ class EngineParagraph implements ui.Paragraph { @override List getBoxesForPlaceholders() { - return const []; + assert(_isLaidOut); + return _measurementResult!.placeholderBoxes; } /// Returns `true` if this paragraph can be directly painted to the canvas. @@ -453,7 +473,7 @@ class EngineParagraph implements ui.Paragraph { return ui.TextBox.fromLTRBD( line.left + widthBeforeBox, top, - line.left + line.width - widthAfterBox, + line.left + line.widthWithTrailingSpaces - widthAfterBox, top + _lineHeight, _textDirection, ); @@ -468,6 +488,7 @@ class EngineParagraph implements ui.Paragraph { textAlign: _textAlign, textDirection: _textDirection, background: _background, + placeholderCount: placeholderCount, ); } @@ -1044,11 +1065,11 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { @override int get placeholderCount => _placeholderCount; - late int _placeholderCount; + int _placeholderCount = 0; @override List get placeholderScales => _placeholderScales; - List _placeholderScales = []; + final List _placeholderScales = []; @override void addPlaceholder( @@ -1059,8 +1080,20 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { double? baselineOffset, ui.TextBaseline? baseline, }) { - // TODO(garyq): Implement stub_ui version of this. - throw UnimplementedError(); + // Require a baseline to be specified if using a baseline-based alignment. + assert((alignment == ui.PlaceholderAlignment.aboveBaseline || + alignment == ui.PlaceholderAlignment.belowBaseline || + alignment == ui.PlaceholderAlignment.baseline) ? baseline != null : true); + + _placeholderCount++; + _placeholderScales.add(scale); + _ops.add(ParagraphPlaceholder( + width * scale, + height * scale, + alignment, + baselineOffset: (baselineOffset ?? height) * scale, + baseline: baseline ?? ui.TextBaseline.alphabetic, + )); } // TODO(yjbanov): do we need to do this? @@ -1239,6 +1272,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { textAlign: textAlign, textDirection: textDirection, background: cumulativeStyle._background, + placeholderCount: placeholderCount, ); } @@ -1293,6 +1327,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { textAlign: textAlign, textDirection: textDirection, background: cumulativeStyle._background, + placeholderCount: placeholderCount, ); } @@ -1301,6 +1336,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { final List elementStack = []; dynamic currentElement() => elementStack.isNotEmpty ? elementStack.last : _paragraphElement; + for (int i = 0; i < _ops.length; i++) { final dynamic op = _ops[i]; if (op is EngineTextStyle) { @@ -1313,6 +1349,11 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { elementStack.add(span); } else if (op is String) { domRenderer.appendText(currentElement(), op); + } else if (op is ParagraphPlaceholder) { + domRenderer.append( + currentElement(), + _createPlaceholderElement(placeholder: op), + ); } else if (identical(op, _paragraphBuilderPop)) { elementStack.removeLast(); } else { @@ -1336,10 +1377,42 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { textAlign: _paragraphStyle._effectiveTextAlign, textDirection: _paragraphStyle._effectiveTextDirection, background: null, + placeholderCount: placeholderCount, ); } } +/// Holds information for a placeholder in a paragraph. +/// +/// [width], [height] and [baselineOffset] are expected to be already scaled. +class ParagraphPlaceholder { + ParagraphPlaceholder( + this.width, + this.height, + this.alignment, { + required this.baselineOffset, + required this.baseline, + }); + + /// The scaled width of the placeholder. + final double width; + + /// The scaled height of the placeholder. + final double height; + + /// Specifies how the placeholder rectangle will be vertically aligned with + /// the surrounding text. + final ui.PlaceholderAlignment alignment; + + /// When the [alignment] value is [ui.PlaceholderAlignment.baseline], the + /// [baselineOffset] indicates the distance from the baseline to the top of + /// the placeholder rectangle. + final double baselineOffset; + + /// Dictates whether to use alphabetic or ideographic baseline. + final ui.TextBaseline baseline; +} + /// Converts [fontWeight] to its CSS equivalent value. String? fontWeightToCss(ui.FontWeight? fontWeight) { if (fontWeight == null) { @@ -1554,6 +1627,52 @@ void _applyTextStyleToElement({ } } +html.Element _createPlaceholderElement({ + required ParagraphPlaceholder placeholder, +}) { + final html.Element element = domRenderer.createElement('span'); + element.className = _placeholderClass; + final html.CssStyleDeclaration style = element.style; + style + ..display = 'inline-block' + ..width = '${placeholder.width}px' + ..height = '${placeholder.height}px' + ..verticalAlign = _placeholderAlignmentToCssVerticalAlign(placeholder); + + return element; +} + +String _placeholderAlignmentToCssVerticalAlign( + ParagraphPlaceholder placeholder, +) { + // For more details about the vertical-align CSS property, see: + // - https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align + switch (placeholder.alignment) { + case ui.PlaceholderAlignment.top: + return 'top'; + + case ui.PlaceholderAlignment.middle: + return 'middle'; + + case ui.PlaceholderAlignment.bottom: + return 'bottom'; + + case ui.PlaceholderAlignment.aboveBaseline: + return 'baseline'; + + case ui.PlaceholderAlignment.belowBaseline: + return '-${placeholder.height}px'; + + case ui.PlaceholderAlignment.baseline: + // In CSS, the placeholder is already placed above the baseline. But + // Flutter's `baselineOffset` assumes the placeholder is placed below the + // baseline. That's why we need to subtract the placeholder's height from + // `baselineOffset`. + final double offset = placeholder.baselineOffset - placeholder.height; + return '${offset}px'; + } +} + String _shadowListToCss(List shadows) { if (shadows.isEmpty) { return ''; @@ -1689,7 +1808,6 @@ String? textAlignToCssValue(ui.TextAlign? align, ui.TextDirection textDirection) case ui.TextDirection.rtl: return 'left'; } - break; default: // including ui.TextAlign.start switch (textDirection) { case ui.TextDirection.ltr: @@ -1697,7 +1815,6 @@ String? textAlignToCssValue(ui.TextAlign? align, ui.TextDirection textDirection) case ui.TextDirection.rtl: return 'right'; } - break; } } diff --git a/lib/web_ui/lib/src/engine/text/ruler.dart b/lib/web_ui/lib/src/engine/text/ruler.dart index 74500dc9d2acb..32018f2515917 100644 --- a/lib/web_ui/lib/src/engine/text/ruler.dart +++ b/lib/web_ui/lib/src/engine/text/ruler.dart @@ -628,6 +628,31 @@ class ParagraphRuler { ); } + List measurePlaceholderBoxes() { + assert(!_debugIsDisposed); + assert(_paragraph != null); + + if (_paragraph!.placeholderCount == 0) { + return const []; + } + + final List placeholderElements = + constrainedDimensions._element.querySelectorAll('.$_placeholderClass'); + final List boxes = []; + + for (final html.Element element in placeholderElements) { + final html.Rectangle rect = element.getBoundingClientRect(); + boxes.add(ui.TextBox.fromLTRBD( + rect.left as double, + rect.top as double, + rect.right as double, + rect.bottom as double, + _paragraph!._textDirection, + )); + } + return boxes; + } + /// Returns text position in a paragraph that contains multiple /// nested spans given an offset. int hitTest(ui.ParagraphConstraints constraints, ui.Offset offset) { @@ -905,6 +930,8 @@ class MeasurementResult { /// of each laid out line. final List? lines; + final List placeholderBoxes; + /// The text align value of the paragraph. final ui.TextAlign textAlign; @@ -923,6 +950,7 @@ class MeasurementResult { required this.alphabeticBaseline, required this.ideographicBaseline, required this.lines, + required this.placeholderBoxes, required ui.TextAlign? textAlign, required ui.TextDirection? textDirection, }) : assert(constraintWidth != null), // ignore: unnecessary_null_comparison diff --git a/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart b/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart index 9b8007ee0599d..a9f04137bb0bb 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart @@ -37,7 +37,7 @@ class TextCapitalizationConfig { const TextCapitalizationConfig.defaultCapitalization() : textCapitalization = TextCapitalization.none; - TextCapitalizationConfig.fromInputConfiguration(String inputConfiguration) + const TextCapitalizationConfig.fromInputConfiguration(String inputConfiguration) : this.textCapitalization = inputConfiguration == 'TextCapitalization.words' ? TextCapitalization.words 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 e29d24f1c3134..c46e15c22d3a4 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 @@ -6,7 +6,7 @@ part of engine; /// Make the content editable span visible to facilitate debugging. -const bool _debugVisibleTextEditing = false; +bool _debugVisibleTextEditing = false; /// The `keyCode` of the "Enter" key. const int _kReturnKeyCode = 13; @@ -55,7 +55,8 @@ void _setStaticStyleAttributes(html.HtmlElement domElement) { /// element. /// /// They are assigned once during the creation of the DOM element. -void _hideAutofillElements(html.HtmlElement domElement) { +void _hideAutofillElements(html.HtmlElement domElement, + {bool isOffScreen = false}) { final html.CssStyleDeclaration elementStyle = domElement.style; elementStyle ..whiteSpace = 'pre-wrap' @@ -73,6 +74,12 @@ void _hideAutofillElements(html.HtmlElement domElement) { ..textShadow = 'transparent' ..transformOrigin = '0 0 0'; + if (isOffScreen) { + elementStyle + ..top = '-9999px' + ..left = '-9999px'; + } + /// This property makes the input's blinking cursor transparent. elementStyle.setProperty('caret-color', 'transparent'); } @@ -82,14 +89,27 @@ void _hideAutofillElements(html.HtmlElement domElement) { /// These values are to be used when autofill is enabled and there is a group of /// text fields with more than one text field. class EngineAutofillForm { - EngineAutofillForm({this.formElement, this.elements, this.items}); + EngineAutofillForm( + {required this.formElement, + this.elements, + this.items, + this.formIdentifier = ''}); - final html.FormElement? formElement; + final html.FormElement formElement; final Map? elements; final Map? items; + /// Identifier for the form. + /// + /// It is constructed by concatenating unique ids of input elements on the + /// form. + /// + /// It is used for storing the form until submission. + /// See [formsOnTheDom]. + final String formIdentifier; + static EngineAutofillForm? fromFrameworkMessage( Map? focusedElementAutofill, List? fields, @@ -109,9 +129,22 @@ class EngineAutofillForm { // Validation is in the framework side. formElement.noValidate = true; + formElement.method = 'post'; + formElement.action = '#'; + formElement.addEventListener('submit', (e) { + e.preventDefault(); + }); _hideAutofillElements(formElement); + // We keep the ids in a list then sort them later, in case the text fields' + // locations are re-ordered on the framework side. + final List ids = List.empty(growable: true); + + // The focused text editing element will not be created here. + final AutofillInfo focusedElement = + AutofillInfo.fromFrameworkMessage(focusedElementAutofill); + if (fields != null) { for (Map field in fields.cast>()) { final Map autofillInfo = field['autofill']; @@ -120,9 +153,8 @@ class EngineAutofillForm { textCapitalization: TextCapitalizationConfig.fromInputConfiguration( field['textCapitalization'])); - // The focused text editing element will not be created here. - final AutofillInfo focusedElement = - AutofillInfo.fromFrameworkMessage(focusedElementAutofill); + ids.add(autofill.uniqueIdentifier); + if (autofill.uniqueIdentifier != focusedElement.uniqueIdentifier) { EngineInputType engineInputType = EngineInputType.fromName(field['inputType']['name']); @@ -137,22 +169,57 @@ class EngineAutofillForm { formElement.append(htmlElement); } } + } else { + // There is one input element in the form. + ids.add(focusedElement.uniqueIdentifier); } + ids.sort(); + final StringBuffer idBuffer = StringBuffer(); + + // Add a seperator between element identifiers. + for (final String id in ids) { + if (idBuffer.length > 0) { + idBuffer.write('*'); + } + idBuffer.write(id); + } + + final String formIdentifier = idBuffer.toString(); + + // If a form with the same Autofill elements is already on the dom, remove + // it from DOM. + if (formsOnTheDom[formIdentifier] != null) { + final html.FormElement form = + formsOnTheDom[formIdentifier] as html.FormElement; + form.remove(); + } + + // In order to submit the form when Framework sends a `TextInput.commit` + // message, we add a submit button to the form. + final html.InputElement submitButton = html.InputElement(); + _hideAutofillElements(submitButton, isOffScreen: true); + submitButton.className = 'submitBtn'; + submitButton.type = 'submit'; + + formElement.append(submitButton); + return EngineAutofillForm( formElement: formElement, elements: elements, items: items, + formIdentifier: formIdentifier, ); } void placeForm(html.HtmlElement mainTextEditingElement) { - formElement!.append(mainTextEditingElement); - domRenderer.glassPaneElement!.append(formElement!); + formElement.append(mainTextEditingElement); + domRenderer.glassPaneElement!.append(formElement); } - void removeForm() { - formElement!.remove(); + void storeForm() { + formsOnTheDom[formIdentifier] = this.formElement; + _hideAutofillElements(formElement, isOffScreen: true); } /// Listens to `onInput` event on the form fields. @@ -226,9 +293,11 @@ class AutofillInfo { /// The current text and selection state of a text field. final EditingState editingState; - /// Unique value set by the developer. + /// Unique value set by the developer or generated by the framework. /// /// Used as id of the text field. + /// + /// An example an id generated by the framework: `EditableText-285283643`. final String uniqueIdentifier; /// Information on how should autofilled text capitalized. @@ -272,20 +341,17 @@ class AutofillInfo { if (domElement is html.InputElement) { html.InputElement element = domElement; element.name = hint; - element.id = uniqueIdentifier; + element.id = hint; element.autocomplete = hint; - // Do not change the element type for the focused element. - if (focusedElement == false) { - if (hint.contains('password')) { - element.type = 'password'; - } else { - element.type = 'text'; - } + if (hint.contains('password')) { + element.type = 'password'; + } else { + element.type = 'text'; } } else if (domElement is html.TextAreaElement) { html.TextAreaElement element = domElement; element.name = hint; - element.id = uniqueIdentifier; + element.id = hint; element.setAttribute('autocomplete', hint); } } @@ -424,11 +490,13 @@ class EditingState { /// This corresponds to Flutter's [TextInputConfiguration]. class InputConfiguration { InputConfiguration({ - required this.inputType, - required this.inputAction, - required this.obscureText, - required this.autocorrect, - required this.textCapitalization, + this.inputType = EngineInputType.text, + this.inputAction = 'TextInputAction.done', + this.obscureText = false, + this.readOnly = false, + this.autocorrect = true, + this.textCapitalization = + const TextCapitalizationConfig.defaultCapitalization(), this.autofill, this.autofillGroup, }); @@ -436,14 +504,17 @@ class InputConfiguration { InputConfiguration.fromFrameworkMessage( Map flutterInputConfiguration) : inputType = EngineInputType.fromName( - flutterInputConfiguration['inputType']['name'], - isDecimal: - flutterInputConfiguration['inputType']['decimal'] ?? false), - inputAction = flutterInputConfiguration['inputAction'], - obscureText = flutterInputConfiguration['obscureText'], - autocorrect = flutterInputConfiguration['autocorrect'], + flutterInputConfiguration['inputType']['name'], + isDecimal: flutterInputConfiguration['inputType']['decimal'] ?? false, + ), + inputAction = + flutterInputConfiguration['inputAction'] ?? 'TextInputAction.done', + obscureText = flutterInputConfiguration['obscureText'] ?? false, + readOnly = flutterInputConfiguration['readOnly'] ?? false, + autocorrect = flutterInputConfiguration['autocorrect'] ?? true, textCapitalization = TextCapitalizationConfig.fromInputConfiguration( - flutterInputConfiguration['textCapitalization']), + flutterInputConfiguration['textCapitalization'], + ), autofill = flutterInputConfiguration.containsKey('autofill') ? AutofillInfo.fromFrameworkMessage( flutterInputConfiguration['autofill']) @@ -456,7 +527,12 @@ class InputConfiguration { final EngineInputType inputType; /// The default action for the input field. - final String? inputAction; + final String inputAction; + + /// Whether the text field can be edited or not. + /// + /// Defaults to false. + final bool readOnly; /// Whether to hide the text being edited. final bool? obscureText; @@ -468,7 +544,7 @@ class InputConfiguration { /// /// For future manual tests, note that autocorrect is an attribute only /// supported by Safari. - final bool? autocorrect; + final bool autocorrect; final AutofillInfo? autofill; @@ -538,12 +614,17 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy { @override void placeElement() { - super.placeElement(); if (hasAutofillGroup) { _geometry?.applyToDomElement(focusedFormElement!); placeForm(); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } // On Chrome, when a form is focused, it opens an autofill menu - // immeddiately. + // immediately. // Flutter framework sends `setEditableSizeAndTransform` for informing // the engine about the location of the text field. This call will // arrive after `show` call. @@ -551,13 +632,68 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy { // `setEditableSizeAndTransform` method is called and focus on the form // only after placing it to the correct position. Hence autofill menu // does not appear on top-left of the page. + // Refocus on the elements after applying the geometry. focusedFormElement!.focus(); + domElement.focus(); } else { _geometry?.applyToDomElement(domElement); } } } +/// A [TextEditingStrategy] for Safari Desktop Browser. +/// +/// It places its [domElement] assuming no prior transform or sizing is applied +/// to it. +/// +/// In case of an autofill enabled form, it does not append the form element +/// to the DOM, until the geometry information is updated. +/// +/// This implementation is used by text editables when semantics is not +/// enabled. With semantics enabled the placement is provided by the semantics +/// tree. +class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy { + SafariDesktopTextEditingStrategy(HybridTextEditing owner) : super(owner); + + /// Appending an element on the DOM for Safari Desktop Browser. + /// + /// This method is only called when geometry information is updated by + /// 'TextInput.setEditableSizeAndTransform' message. + /// + /// This method is similar to the [GloballyPositionedTextEditingStrategy]. + /// The only part different: this method does not call `super.placeElement()`, + /// which in current state calls `domElement.focus()`. + /// + /// Making an extra `focus` request causes flickering in Safari. + @override + void placeElement() { + _geometry?.applyToDomElement(domElement); + if (hasAutofillGroup) { + placeForm(); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } + // On Safari Desktop, when a form is focused, it opens an autofill menu + // immediately. + // Flutter framework sends `setEditableSizeAndTransform` for informing + // the engine about the location of the text field. This call will + // arrive after `show` call. Therefore form is placed, when + // `setEditableSizeAndTransform` method is called and focus called on the + // form only after placing it to the correct position and only once after + // that. Calling focus multiple times causes flickering. + focusedFormElement!.focus(); + } + } + + @override + void initializeElementPlacement() { + domElement.focus(); + } +} + /// Class implementing the default editing strategies for text editing. /// /// This class uses a DOM element to provide text editing capabilities. @@ -611,6 +747,10 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { bool get hasAutofillGroup => _inputConfiguration.autofillGroup != null; + /// Whether the focused input element is part of a form. + bool get appendedToForm => _appendedToForm; + bool _appendedToForm = false; + html.FormElement? get focusedFormElement => _inputConfiguration.autofillGroup?.formElement; @@ -625,6 +765,9 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { this._inputConfiguration = inputConfig; _domElement = inputConfig.inputType.createDomElement(); + if (inputConfig.readOnly) { + domElement.setAttribute('readonly', 'readonly'); + } if (inputConfig.obscureText!) { domElement.setAttribute('type', 'password'); } @@ -636,12 +779,14 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _setStaticStyleAttributes(domElement); _style?.applyToDomElement(domElement); + if (!hasAutofillGroup) { // If there is an Autofill Group the `FormElement`, it will be appended to the // DOM later, when the first location information arrived. // Otherwise, on Blink based Desktop browsers, the autofill menu appears // on top left of the screen. domRenderer.glassPaneElement!.append(domElement); + _appendedToForm = false; } initializeElementPlacement(); @@ -709,9 +854,19 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _subscriptions[i].cancel(); } _subscriptions.clear(); - domElement.remove(); + // If focused element is a part of a form, it needs to stay on the DOM + // until the autofill context of the form is finalized. + // More details on `TextInput.finishAutofillContext` call. + if (_appendedToForm && + _inputConfiguration.autofillGroup?.formElement != null) { + // Subscriptions are removed, listeners won't be triggered. + domElement.blur(); + _hideAutofillElements(domElement, isOffScreen: true); + _inputConfiguration.autofillGroup?.storeForm(); + } else { + domElement.remove(); + } _domElement = null; - _inputConfiguration.autofillGroup?.removeForm(); } @mustCallSuper @@ -730,6 +885,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void placeForm() { _inputConfiguration.autofillGroup!.placeForm(domElement); + _appendedToForm = true; } void _handleChange(html.Event event) { @@ -857,8 +1013,6 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy { inputConfig.inputType.configureInputMode(domElement); if (hasAutofillGroup) { placeForm(); - } else { - domRenderer.glassPaneElement!.append(domElement); } inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement); } @@ -1045,8 +1199,6 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy { onChange: onChange, onAction: onAction); if (hasAutofillGroup) { placeForm(); - } else { - domRenderer.glassPaneElement!.append(domElement); } } @@ -1097,6 +1249,12 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy { void placeElement() { domElement.focus(); _geometry?.applyToDomElement(domElement); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } } } @@ -1156,8 +1314,14 @@ class TextEditingChannel { break; case 'TextInput.finishAutofillContext': - // TODO(nurhan): Handle saving autofill information on web. - // https://github.com/flutter/flutter/issues/59378 + final bool saveForm = call.arguments as bool; + // Close the text editing connection. Form is finalizing. + implementation.sendTextConnectionClosedToFrameworkIfAny(); + if (saveForm) { + saveForms(); + } + // Clean the forms from DOM after submitting them. + cleanForms(); break; default: @@ -1167,6 +1331,30 @@ class TextEditingChannel { window._replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); } + /// Used for submitting the forms attached on the DOM. + /// + /// Browser will save the information entered to the form. + /// + /// Called when the form is finalized with save option `true`. + /// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277 + void saveForms() { + formsOnTheDom.forEach((String identifier, html.FormElement form) { + final html.InputElement submitBtn = + form.getElementsByClassName('submitBtn').first as html.InputElement; + submitBtn.click(); + }); + } + + /// Used for removing the forms on the DOM. + /// + /// Called when the form is finalized. + void cleanForms() { + for (html.FormElement form in formsOnTheDom.values) { + form.remove(); + } + formsOnTheDom.clear(); + } + /// Sends the 'TextInputClient.updateEditingState' message to the framework. void updateEditingState(int? clientId, EditingState? editingState) { if (window._onPlatformMessage != null) { @@ -1219,6 +1407,15 @@ class TextEditingChannel { /// Text editing singleton. final HybridTextEditing textEditing = HybridTextEditing(); +/// Map for storing forms left attached on the DOM. +/// +/// Used for keeping the form elements on the DOM until user confirms to +/// save or cancel them. +/// +/// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277 +final Map formsOnTheDom = + Map(); + /// Should be used as a singleton to provide support for text editing in /// Flutter Web. /// @@ -1236,6 +1433,8 @@ class HybridTextEditing { if (browserEngine == BrowserEngine.webkit && operatingSystem == OperatingSystem.iOs) { this._defaultEditingElement = IOSTextEditingStrategy(this); + } else if (browserEngine == BrowserEngine.webkit) { + this._defaultEditingElement = SafariDesktopTextEditingStrategy(this); } else if (browserEngine == BrowserEngine.blink && operatingSystem == OperatingSystem.android) { this._defaultEditingElement = AndroidTextEditingStrategy(this); diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index ba8061fc061a6..b747583c6d3fd 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -527,3 +527,28 @@ double convertSigmaToRadius(double sigma) { bool isUnsoundNull(dynamic object) { return object == null; } + +bool _offsetIsValid(ui.Offset offset) { + assert(offset != null, 'Offset argument was null.'); // ignore: unnecessary_null_comparison + assert(!offset.dx.isNaN && !offset.dy.isNaN, + 'Offset argument contained a NaN value.'); + return true; +} + +bool _matrix4IsValid(Float32List matrix4) { + assert(matrix4 != null, 'Matrix4 argument was null.'); // ignore: unnecessary_null_comparison + assert(matrix4.length == 16, 'Matrix4 must have 16 entries.'); + return true; +} + +void _validateColorStops(List colors, List? colorStops) { + if (colorStops == null) { + if (colors.length != 2) + throw ArgumentError( + '"colors" must have length 2 if "colorStops" is omitted.'); + } else { + if (colors.length != colorStops.length) + throw ArgumentError( + '"colors" and "colorStops" arguments must have equal length.'); + } +} diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 6463e6043502b..7dbb1497c716d 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -123,14 +123,19 @@ class EngineWindow extends ui.Window { height = html.window.innerHeight! * devicePixelRatio; width = html.window.innerWidth! * devicePixelRatio; } - // First confirm both heught and width is effected. - if (_physicalSize!.height != height && _physicalSize!.width != width) { - // If prior to rotation height is bigger than width it should be the - // opposite after the rotation and vice versa. - if ((_physicalSize!.height > _physicalSize!.width && height < width) || - (_physicalSize!.width > _physicalSize!.height && width < height)) { - // Rotation detected - return true; + + // This method compares the new dimensions with the previous ones. + // Return false if the previous dimensions are not set. + if(_physicalSize != null) { + // First confirm both height and width are effected. + if (_physicalSize!.height != height && _physicalSize!.width != width) { + // If prior to rotation height is bigger than width it should be the + // opposite after the rotation and vice versa. + if ((_physicalSize!.height > _physicalSize!.width && height < width) || + (_physicalSize!.width > _physicalSize!.height && width < height)) { + // Rotation detected + return true; + } } } return false; @@ -146,9 +151,6 @@ class EngineWindow extends ui.Window { /// Overrides the value of [physicalSize] in tests. ui.Size? webOnlyDebugPhysicalSizeOverride; - @override - double get physicalDepth => double.maxFinite; - /// Handles the browser history integration to allow users to use the back /// button, etc. final BrowserHistory _browserHistory = BrowserHistory(); diff --git a/lib/web_ui/lib/src/ui/annotations.dart b/lib/web_ui/lib/src/ui/annotations.dart index 7dac0c5670212..c2670d5282cc7 100644 --- a/lib/web_ui/lib/src/ui/annotations.dart +++ b/lib/web_ui/lib/src/ui/annotations.dart @@ -9,36 +9,6 @@ part of ui; // TODO(dnfield): Update this if/when we default this to on in the tool, // see: https://github.com/flutter/flutter/issues/52759 -/// Annotation used by Flutter's Dart compiler to indicate that an -/// [Object.toString] override should not be replaced with a supercall. -/// -/// Since `dart:ui` and `package:flutter` override `toString` purely for -/// debugging purposes, the frontend compiler is instructed to replace all -/// `toString` bodies with `return super.toString()` during compilation. This -/// significantly reduces release code size, and would make it impossible to -/// implement a meaningful override of `toString` for release mode without -/// disabling the feature and losing the size savings. If a package uses this -/// feature and has some unavoidable need to keep the `toString` implementation -/// for a specific class, applying this annotation will direct the compiler -/// to leave the method body as-is. -/// -/// For example, in the following class the `toString` method will remain as -/// `return _buffer.toString();`, even if the `--delete-tostring-package-uri` -/// option would otherwise apply and replace it with `return super.toString()`. -/// -/// ```dart -/// class MyStringBuffer { -/// StringBuffer _buffer = StringBuffer(); -/// -/// // ... -/// -/// @keepToString -/// @override -/// String toString() { -/// return _buffer.toString(); -/// } -/// } -/// ``` const _KeepToString keepToString = _KeepToString(); class _KeepToString { diff --git a/lib/web_ui/lib/src/ui/canvas.dart b/lib/web_ui/lib/src/ui/canvas.dart index ee185ef29ce7d..1699a1a287410 100644 --- a/lib/web_ui/lib/src/ui/canvas.dart +++ b/lib/web_ui/lib/src/ui/canvas.dart @@ -5,70 +5,24 @@ // @dart = 2.10 part of ui; -/// Defines how a list of points is interpreted when drawing a set of points. -/// -/// Used by [Canvas.drawPoints]. enum PointMode { - /// Draw each point separately. - /// - /// If the [Paint.strokeCap] is [StrokeCap.round], then each point is drawn - /// as a circle with the diameter of the [Paint.strokeWidth], filled as - /// described by the [Paint] (ignoring [Paint.style]). - /// - /// Otherwise, each point is drawn as an axis-aligned square with sides of - /// length [Paint.strokeWidth], filled as described by the [Paint] (ignoring - /// [Paint.style]). points, - - /// Draw each sequence of two points as a line segment. - /// - /// If the number of points is odd, then the last point is ignored. - /// - /// The lines are stroked as described by the [Paint] (ignoring - /// [Paint.style]). lines, - - /// Draw the entire sequence of point as one line. - /// - /// The lines are stroked as described by the [Paint] (ignoring - /// [Paint.style]). polygon, } -/// Defines how a new clip region should be merged with the existing clip -/// region. -/// -/// Used by [Canvas.clipRect]. enum ClipOp { - /// Subtract the new region from the existing region. difference, - - /// Intersect the new region from the existing region. intersect, } enum VertexMode { - /// Draw each sequence of three points as the vertices of a triangle. triangles, - - /// Draw each sliding window of three points as the vertices of a triangle. triangleStrip, - - /// Draw the first point and each sliding window of two points as the vertices of a triangle. triangleFan, } -/// A set of vertex data used by [Canvas.drawVertices]. class Vertices { - /// Creates a set of vertex data for use with [Canvas.drawVertices]. - /// - /// The [mode] and [positions] parameters must not be null. - /// - /// If the [textureCoordinates] or [colors] parameters are provided, they must - /// be the same length as [positions]. - /// - /// If the [indices] parameter is provided, all values in the list must be - /// valid index values for [positions]. factory Vertices( VertexMode mode, List positions, { @@ -77,34 +31,21 @@ class Vertices { List? indices, }) { if (engine.experimentalUseSkia) { - return engine.CkVertices(mode, positions, - textureCoordinates: textureCoordinates, - colors: colors, - indices: indices); - } - return engine.SurfaceVertices(mode, positions, + return engine.CkVertices( + mode, + positions, + textureCoordinates: textureCoordinates, colors: colors, - indices: indices); + indices: indices, + ); + } + return engine.SurfaceVertices( + mode, + positions, + colors: colors, + indices: indices, + ); } - - /// Creates a set of vertex data for use with [Canvas.drawVertices], directly - /// using the encoding methods of [new Vertices]. - /// - /// The [mode] parameter must not be null. - /// - /// The [positions] list is interpreted as a list of repeated pairs of x,y - /// coordinates. It must not be null. - /// - /// The [textureCoordinates] list is interpreted as a list of repeated pairs - /// of x,y coordinates, and must be the same length of [positions] if it - /// is not null. - /// - /// The [colors] list is interpreted as a list of RGBA encoded colors, similar - /// to [Color.value]. It must be half length of [positions] if it is not - /// null. - /// - /// If the [indices] list is provided, all values in the list must be - /// valid index values for [positions]. factory Vertices.raw( VertexMode mode, Float32List positions, { @@ -113,25 +54,24 @@ class Vertices { Uint16List? indices, }) { if (engine.experimentalUseSkia) { - return engine.CkVertices.raw(mode, positions, - textureCoordinates: textureCoordinates, - colors: colors, - indices: indices); - } - return engine.SurfaceVertices.raw(mode, positions, + return engine.CkVertices.raw( + mode, + positions, + textureCoordinates: textureCoordinates, colors: colors, - indices: indices); + indices: indices, + ); + } + return engine.SurfaceVertices.raw( + mode, + positions, + colors: colors, + indices: indices, + ); } } -/// Records a [Picture] containing a sequence of graphical operations. -/// -/// To begin recording, construct a [Canvas] to record the commands. -/// To end recording, use the [PictureRecorder.endRecording] method. abstract class PictureRecorder { - /// Creates a new idle PictureRecorder. To associate it with a - /// [Canvas] and begin recording, pass this [PictureRecorder] to the - /// [Canvas] constructor. factory PictureRecorder() { if (engine.experimentalUseSkia) { return engine.CkPictureRecorder(); @@ -139,41 +79,10 @@ abstract class PictureRecorder { return engine.EnginePictureRecorder(); } } - - /// Whether this object is currently recording commands. - /// - /// Specifically, this returns true if a [Canvas] object has been - /// created to record commands and recording has not yet ended via a - /// call to [endRecording], and false if either this - /// [PictureRecorder] has not yet been associated with a [Canvas], - /// or the [endRecording] method has already been called. bool get isRecording; - - /// Finishes recording graphical operations. - /// - /// Returns a picture containing the graphical operations that have been - /// recorded thus far. After calling this function, both the picture recorder - /// and the canvas objects are invalid and cannot be used further. Picture endRecording(); } -/// An interface for recording graphical operations. -/// -/// [Canvas] objects are used in creating [Picture] objects, which can -/// themselves be used with a [SceneBuilder] to build a [Scene]. In -/// normal usage, however, this is all handled by the framework. -/// -/// A canvas has a current transformation matrix which is applied to all -/// operations. Initially, the transformation matrix is the identity transform. -/// It can be modified using the [translate], [scale], [rotate], [skew], -/// and [transform] methods. -/// -/// A canvas also has a current clip region which is applied to all operations. -/// Initially, the clip region is infinite. It can be modified using the -/// [clipRect], [clipRRect], and [clipPath] methods. -/// -/// The current transform and clip can be saved and restored using the stack -/// managed by the [save], [saveLayer], and [restore] methods. abstract class Canvas { factory Canvas(PictureRecorder recorder, [Rect? cullRect]) { if (engine.experimentalUseSkia) { @@ -182,403 +91,55 @@ abstract class Canvas { return engine.SurfaceCanvas(recorder as engine.EnginePictureRecorder, cullRect); } } - - /// Saves a copy of the current transform and clip on the save stack. - /// - /// Call [restore] to pop the save stack. - /// - /// See also: - /// - /// * [saveLayer], which does the same thing but additionally also groups the - /// commands done until the matching [restore]. void save(); - - /// Saves a copy of the current transform and clip on the save stack, and then - /// creates a new group which subsequent calls will become a part of. When the - /// save stack is later popped, the group will be flattened into a layer and - /// have the given `paint`'s [Paint.colorFilter] and [Paint.blendMode] - /// applied. - /// - /// This lets you create composite effects, for example making a group of - /// drawing commands semi-transparent. Without using [saveLayer], each part of - /// the group would be painted individually, so where they overlap would be - /// darker than where they do not. By using [saveLayer] to group them - /// together, they can be drawn with an opaque color at first, and then the - /// entire group can be made transparent using the [saveLayer]'s paint. - /// - /// Call [restore] to pop the save stack and apply the paint to the group. - /// - /// ## Using saveLayer with clips - /// - /// When a rectangular clip operation (from [clipRect]) is not axis-aligned - /// with the raster buffer, or when the clip operation is not rectalinear (e.g. - /// because it is a rounded rectangle clip created by [clipRRect] or an - /// arbitrarily complicated path clip created by [clipPath]), the edge of the - /// clip needs to be anti-aliased. - /// - /// If two draw calls overlap at the edge of such a clipped region, without - /// using [saveLayer], the first drawing will be anti-aliased with the - /// background first, and then the second will be anti-aliased with the result - /// of blending the first drawing and the background. On the other hand, if - /// [saveLayer] is used immediately after establishing the clip, the second - /// drawing will cover the first in the layer, and thus the second alone will - /// be anti-aliased with the background when the layer is clipped and - /// composited (when [restore] is called). - /// - /// For example, this [CustomPainter.paint] method paints a clean white - /// rounded rectangle: - /// - /// ```dart - /// void paint(Canvas canvas, Size size) { - /// Rect rect = Offset.zero & size; - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(rect, 100.0, 100.0)); - /// canvas.saveLayer(rect, new Paint()); - /// canvas.drawPaint(new Paint()..color = Colors.red); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// canvas.restore(); - /// } - /// ``` - /// - /// On the other hand, this one renders a red outline, the result of the red - /// paint being anti-aliased with the background at the clip edge, then the - /// white paint being similarly anti-aliased with the background _including - /// the clipped red paint_: - /// - /// ```dart - /// void paint(Canvas canvas, Size size) { - /// // (this example renders poorly, prefer the example above) - /// Rect rect = Offset.zero & size; - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(rect, 100.0, 100.0)); - /// canvas.drawPaint(new Paint()..color = Colors.red); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// } - /// ``` - /// - /// This point is moot if the clip only clips one draw operation. For example, - /// the following paint method paints a pair of clean white rounded - /// rectangles, even though the clips are not done on a separate layer: - /// - /// ```dart - /// void paint(Canvas canvas, Size size) { - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(Offset.zero & (size / 2.0), 50.0, 50.0)); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(size.center(Offset.zero) & (size / 2.0), 50.0, 50.0)); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// } - /// ``` - /// - /// (Incidentally, rather than using [clipRRect] and [drawPaint] to draw - /// rounded rectangles like this, prefer the [drawRRect] method. These - /// examples are using [drawPaint] as a proxy for "complicated draw operations - /// that will get clipped", to illustrate the point.) - /// - /// ## Performance considerations - /// - /// Generally speaking, [saveLayer] is relatively expensive. - /// - /// There are a several different hardware architectures for GPUs (graphics - /// processing units, the hardware that handles graphics), but most of them - /// involve batching commands and reordering them for performance. When layers - /// are used, they cause the rendering pipeline to have to switch render - /// target (from one layer to another). Render target switches can flush the - /// GPU's command buffer, which typically means that optimizations that one - /// could get with larger batching are lost. Render target switches also - /// generate a lot of memory churn because the GPU needs to copy out the - /// current frame buffer contents from the part of memory that's optimized for - /// writing, and then needs to copy it back in once the previous render target - /// (layer) is restored. - /// - /// See also: - /// - /// * [save], which saves the current state, but does not create a new layer - /// for subsequent commands. - /// * [BlendMode], which discusses the use of [Paint.blendMode] with - /// [saveLayer]. void saveLayer(Rect? bounds, Paint paint); - - /// Pops the current save stack, if there is anything to pop. - /// Otherwise, does nothing. - /// - /// Use [save] and [saveLayer] to push state onto the stack. - /// - /// If the state was pushed with with [saveLayer], then this call will also - /// cause the new layer to be composited into the previous layer. void restore(); - - /// Returns the number of items on the save stack, including the - /// initial state. This means it returns 1 for a clean canvas, and - /// that each call to [save] and [saveLayer] increments it, and that - /// each matching call to [restore] decrements it. - /// - /// This number cannot go below 1. int getSaveCount(); - - /// Add a translation to the current transform, shifting the coordinate space - /// horizontally by the first argument and vertically by the second argument. void translate(double dx, double dy); - - /// Add an axis-aligned scale to the current transform, scaling by the first - /// argument in the horizontal direction and the second in the vertical - /// direction. - /// - /// If [sy] is unspecified, [sx] will be used for the scale in both - /// directions. void scale(double sx, [double? sy]); - - /// Add a rotation to the current transform. The argument is in radians clockwise. void rotate(double radians); - - /// Add an axis-aligned skew to the current transform, with the first argument - /// being the horizontal skew in radians clockwise around the origin, and the - /// second argument being the vertical skew in radians clockwise around the - /// origin. void skew(double sx, double sy); - - /// Multiply the current transform by the specified 4⨉4 transformation matrix - /// specified as a list of values in column-major order. void transform(Float64List matrix4); - - /// Reduces the clip region to the intersection of the current clip and the - /// given rectangle. - /// - /// If [doAntiAlias] is true, then the clip will be anti-aliased. - /// - /// If multiple draw commands intersect with the clip boundary, this can result - /// in incorrect blending at the clip boundary. See [saveLayer] for a - /// discussion of how to address that. - /// - /// Use [ClipOp.difference] to subtract the provided rectangle from the - /// current clip. - void clipRect(Rect rect, - {ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true}); - - /// Reduces the clip region to the intersection of the current clip and the - /// given rounded rectangle. - /// - /// If [doAntiAlias] is true, then the clip will be anti-aliased. - /// - /// If multiple draw commands intersect with the clip boundary, this can result - /// in incorrect blending at the clip boundary. See [saveLayer] for a - /// discussion of how to address that and some examples of using [clipRRect]. + void clipRect(Rect rect, {ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true}); void clipRRect(RRect rrect, {bool doAntiAlias = true}); - - /// Reduces the clip region to the intersection of the current clip and the - /// given [Path]. - /// - /// If [doAntiAlias] is true, then the clip will be anti-aliased. - /// - /// If multiple draw commands intersect with the clip boundary, this can result - /// multiple draw commands intersect with the clip boundary, this can result - /// in incorrect blending at the clip boundary. See [saveLayer] for a - /// discussion of how to address that. void clipPath(Path path, {bool doAntiAlias = true}); - - /// Paints the given [Color] onto the canvas, applying the given - /// [BlendMode], with the given color being the source and the background - /// being the destination. void drawColor(Color color, BlendMode blendMode); - - /// Draws a line between the given points using the given paint. The line is - /// stroked, the value of the [Paint.style] is ignored for this call. - /// - /// The `p1` and `p2` arguments are interpreted as offsets from the origin. void drawLine(Offset p1, Offset p2, Paint paint); - - /// Fills the canvas with the given [Paint]. - /// - /// To fill the canvas with a solid color and blend mode, consider - /// [drawColor] instead. void drawPaint(Paint paint); - - /// Draws a rectangle with the given [Paint]. Whether the rectangle is filled - /// or stroked (or both) is controlled by [Paint.style]. void drawRect(Rect rect, Paint paint); - - /// Draws a rounded rectangle with the given [Paint]. Whether the rectangle is - /// filled or stroked (or both) is controlled by [Paint.style]. void drawRRect(RRect rrect, Paint paint); - - /// Draws a shape consisting of the difference between two rounded rectangles - /// with the given [Paint]. Whether this shape is filled or stroked (or both) - /// is controlled by [Paint.style]. - /// - /// This shape is almost but not quite entirely unlike an annulus. void drawDRRect(RRect outer, RRect inner, Paint paint); - - /// Draws an axis-aligned oval that fills the given axis-aligned rectangle - /// with the given [Paint]. Whether the oval is filled or stroked (or both) is - /// controlled by [Paint.style]. void drawOval(Rect rect, Paint paint); - - /// Draws a circle centered at the point given by the first argument and - /// that has the radius given by the second argument, with the [Paint] given in - /// the third argument. Whether the circle is filled or stroked (or both) is - /// controlled by [Paint.style]. void drawCircle(Offset c, double radius, Paint paint); - - /// Draw an arc scaled to fit inside the given rectangle. It starts from - /// startAngle radians around the oval up to startAngle + sweepAngle - /// radians around the oval, with zero radians being the point on - /// the right hand side of the oval that crosses the horizontal line - /// that intersects the center of the rectangle and with positive - /// angles going clockwise around the oval. If useCenter is true, the arc is - /// closed back to the center, forming a circle sector. Otherwise, the arc is - /// not closed, forming a circle segment. - /// - /// This method is optimized for drawing arcs and should be faster than [Path.arcTo]. - void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, - Paint paint); - - /// Draws the given [Path] with the given [Paint]. Whether this shape is - /// filled or stroked (or both) is controlled by [Paint.style]. If the path is - /// filled, then subpaths within it are implicitly closed (see [Path.close]). + void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint); void drawPath(Path path, Paint paint); - - /// Draws the given [Image] into the canvas with its top-left corner at the - /// given [Offset]. The image is composited into the canvas using the given [Paint]. void drawImage(Image image, Offset offset, Paint paint); - - /// Draws the subset of the given image described by the `src` argument into - /// the canvas in the axis-aligned rectangle given by the `dst` argument. - /// - /// This might sample from outside the `src` rect by up to half the width of - /// an applied filter. - /// - /// Multiple calls to this method with different arguments (from the same - /// image) can be batched into a single call to [drawAtlas] to improve - /// performance. void drawImageRect(Image image, Rect src, Rect dst, Paint paint); - - /// Draws the given [Image] into the canvas using the given [Paint]. - /// - /// The image is drawn in nine portions described by splitting the image by - /// drawing two horizontal lines and two vertical lines, where the `center` - /// argument describes the rectangle formed by the four points where these - /// four lines intersect each other. (This forms a 3-by-3 grid of regions, - /// the center region being described by the `center` argument.) - /// - /// The four regions in the corners are drawn, without scaling, in the four - /// corners of the destination rectangle described by `dst`. The remaining - /// five regions are drawn by stretching them to fit such that they exactly - /// cover the destination rectangle while maintaining their relative - /// positions. void drawImageNine(Image image, Rect center, Rect dst, Paint paint); - - /// Draw the given picture onto the canvas. To create a picture, see - /// [PictureRecorder]. void drawPicture(Picture picture); - - /// Draws the text in the given [Paragraph] into this canvas at the given - /// [Offset]. - /// - /// The [Paragraph] object must have had [Paragraph.layout] called on it - /// first. - /// - /// To align the text, set the `textAlign` on the [ParagraphStyle] object - /// passed to the [new ParagraphBuilder] constructor. For more details see - /// [TextAlign] and the discussion at [new ParagraphStyle]. - /// - /// If the text is left aligned or justified, the left margin will be at the - /// position specified by the `offset` argument's [Offset.dx] coordinate. - /// - /// If the text is right aligned or justified, the right margin will be at the - /// position described by adding the [ParagraphConstraints.width] given to - /// [Paragraph.layout], to the `offset` argument's [Offset.dx] coordinate. - /// - /// If the text is centered, the centering axis will be at the position - /// described by adding half of the [ParagraphConstraints.width] given to - /// [Paragraph.layout], to the `offset` argument's [Offset.dx] coordinate. void drawParagraph(Paragraph paragraph, Offset offset); - - /// Draws a sequence of points according to the given [PointMode]. - /// - /// The `points` argument is interpreted as offsets from the origin. - /// - /// See also: - /// - /// * [drawRawPoints], which takes `points` as a [Float32List] rather than a - /// [List]. void drawPoints(PointMode pointMode, List points, Paint paint); - - /// Draws a sequence of points according to the given [PointMode]. - /// - /// The `points` argument is interpreted as a list of pairs of floating point - /// numbers, where each pair represents an x and y offset from the origin. - /// - /// See also: - /// - /// * [drawPoints], which takes `points` as a [List] rather than a - /// [List]. void drawRawPoints(PointMode pointMode, Float32List points, Paint paint); void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint); - - /// Draws part of an image - the [atlas] - onto the canvas. - /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. - /// - /// All parameters must not be null. - /// - /// See also: - /// - /// * [drawRawAtlas], which takes its arguments as typed data lists rather - /// than objects. void drawAtlas( Image atlas, List transforms, List rects, - List colors, - BlendMode blendMode, + List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint, ); - - /// Draws part of an image - the [atlas] - onto the canvas. - /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. - /// - /// The [rstTransforms] argument is interpreted as a list of four-tuples, with - /// each tuple being ([RSTransform.scos], [RSTransform.ssin], - /// [RSTransform.tx], [RSTransform.ty]). - /// - /// The [rects] argument is interpreted as a list of four-tuples, with each - /// tuple being ([Rect.left], [Rect.top], [Rect.right], [Rect.bottom]). - /// - /// The [colors] argument, which can be null, is interpreted as a list of - /// 32-bit colors, with the same packing as [Color.value]. - /// - /// See also: - /// - /// * [drawAtlas], which takes its arguments as objects rather than typed - /// data lists. void drawRawAtlas( Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - BlendMode blendMode, + Int32List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint, ); - - /// Draws a shadow for a [Path] representing the given material elevation. - /// - /// The `transparentOccluder` argument should be true if the occluding object - /// is not opaque. - /// - /// The arguments must not be null. void drawShadow( Path path, Color color, @@ -587,116 +148,22 @@ abstract class Canvas { ); } -/// An object representing a sequence of recorded graphical operations. -/// -/// To create a [Picture], use a [PictureRecorder]. -/// -/// A [Picture] can be placed in a [Scene] using a [SceneBuilder], via -/// the [SceneBuilder.addPicture] method. A [Picture] can also be -/// drawn into a [Canvas], using the [Canvas.drawPicture] method. abstract class Picture { - /// Creates an image from this picture. - /// - /// The returned image will be `width` pixels wide and `height` pixels high. - /// The picture is rasterized within the 0 (left), 0 (top), `width` (right), - /// `height` (bottom) bounds. Content outside these bounds is clipped. - /// - /// Although the image is returned synchronously, the picture is actually - /// rasterized the first time the image is drawn and then cached. Future toImage(int width, int height); - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. void dispose(); - - /// Returns the approximate number of bytes allocated for this object. - /// - /// The actual size of this picture may be larger, particularly if it contains - /// references to image or other large objects. int get approximateBytesUsed; } -/// Determines the winding rule that decides how the interior of a [Path] is -/// calculated. -/// -/// This enum is used by the [Path.fillType] property. enum PathFillType { - /// The interior is defined by a non-zero sum of signed edge crossings. - /// - /// For a given point, the point is considered to be on the inside of the path - /// if a line drawn from the point to infinity crosses lines going clockwise - /// around the point a different number of times than it crosses lines going - /// counter-clockwise around that point. - /// - /// See: nonZero, - - /// The interior is defined by an odd number of edge crossings. - /// - /// For a given point, the point is considered to be on the inside of the path - /// if a line drawn from the point to infinity crosses an odd number of lines. - /// - /// See: evenOdd, } - -/// Strategies for combining paths. -/// -/// See also: -/// -/// * [Path.combine], which uses this enum to decide how to combine two paths. // Must be kept in sync with SkPathOp + enum PathOperation { - /// Subtract the second path from the first path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be a crescent portion of the - /// first circle that was not overlapped by the second circle. - /// - /// See also: - /// - /// * [reverseDifference], which is the same but subtracting the first path - /// from the second. difference, - - /// Create a new path that is the intersection of the two paths, leaving the - /// overlapping pieces of the path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be only the overlapping portion - /// of the two circles. - /// - /// See also: - /// * [xor], which is the inverse of this operation intersect, - - /// Create a new path that is the union (inclusive-or) of the two paths. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be a figure-eight like shape - /// matching the outer boundaries of both circles. union, - - /// Create a new path that is the exclusive-or of the two paths, leaving - /// everything but the overlapping pieces of the path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the figure-eight like shape less the overlapping - /// parts - /// - /// See also: - /// * [intersect], which is the inverse of this operation xor, - - /// Subtract the first path from the second path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be a crescent portion of the - /// second circle that was not overlapped by the first circle. - /// - /// See also: - /// - /// * [difference], which is the same but subtracting the second path - /// from the first. reverseDifference, } diff --git a/lib/web_ui/lib/src/ui/channel_buffers.dart b/lib/web_ui/lib/src/ui/channel_buffers.dart index 6ce0bc962f1f4..2dc98120bd11d 100644 --- a/lib/web_ui/lib/src/ui/channel_buffers.dart +++ b/lib/web_ui/lib/src/ui/channel_buffers.dart @@ -5,50 +5,27 @@ // @dart = 2.10 part of ui; -/// A saved platform message for a channel with its callback. class _StoredMessage { - /// Default constructor, takes in a [ByteData] that represents the - /// payload of the message and a [PlatformMessageResponseCallback] - /// that represents the callback that will be called when the message - /// is handled. _StoredMessage(this._data, this._callback); - - /// Representation of the message's payload. final ByteData? _data; ByteData? get data => _data; - - /// Callback to be called when the message is received. final PlatformMessageResponseCallback _callback; PlatformMessageResponseCallback get callback => _callback; } -/// A fixed-size circular queue. class _RingBuffer { - /// The underlying data for the RingBuffer. ListQueue's dynamically resize, - /// [_RingBuffer]s do not. final collection.ListQueue _queue; - _RingBuffer(this._capacity) - : _queue = collection.ListQueue(_capacity); - - /// Returns the number of items in the [_RingBuffer]. + _RingBuffer(this._capacity) : _queue = collection.ListQueue(_capacity); int get length => _queue.length; - - /// The number of items that can be stored in the [_RingBuffer]. int _capacity; int get capacity => _capacity; - - /// Returns true if there are no items in the [_RingBuffer]. bool get isEmpty => _queue.isEmpty; - - /// A callback that get's called when items are ejected from the [_RingBuffer] - /// by way of an overflow or a resizing. Function(T)? _dropItemCallback; set dropItemCallback(Function(T) callback) { _dropItemCallback = callback; } - /// Returns true on overflow. bool push(T val) { if (_capacity <= 0) { return true; @@ -59,13 +36,10 @@ class _RingBuffer { } } - /// Returns null when empty. T? pop() { return _queue.isEmpty ? null : _queue.removeFirst(); } - /// Removes items until then length reaches [lengthLimit] and returns - /// the number of items removed. int _dropOverflowItems(int lengthLimit) { int result = 0; while (_queue.length > lengthLimit) { @@ -76,46 +50,20 @@ class _RingBuffer { return result; } - /// Returns the number of discarded items resulting from resize. int resize(int newSize) { _capacity = newSize; return _dropOverflowItems(newSize); } } -/// Signature for [ChannelBuffers.drain]. typedef DrainChannelCallback = Future Function(ByteData?, PlatformMessageResponseCallback); -/// Storage of channel messages until the channels are completely routed, -/// i.e. when a message handler is attached to the channel on the framework side. -/// -/// Each channel has a finite buffer capacity and in a FIFO manner messages will -/// be deleted if the capacity is exceeded. The intention is that these buffers -/// will be drained once a callback is setup on the BinaryMessenger in the -/// Flutter framework. -/// -/// Clients of Flutter shouldn't need to allocate their own ChannelBuffers -/// and should only access this package's [channelBuffers] if they are writing -/// their own custom [BinaryMessenger]. class ChannelBuffers { - /// By default we store one message per channel. There are tradeoffs associated - /// with any size. The correct size should be chosen for the semantics of your - /// channel. - /// - /// Size 0 implies you want to ignore any message that gets sent before the engine - /// is ready (keeping in mind there is no way to know when the engine is ready). - /// - /// Size 1 implies that you only care about the most recent value. - /// - /// Size >1 means you want to process every single message and want to chose a - /// buffer size that will avoid any overflows. static const int kDefaultBufferSize = 1; static const String kControlChannelName = 'dev.flutter/channel-buffers'; - - /// A mapping between a channel name and its associated [_RingBuffer]. final Map?> _messages = - ?>{}; + ?>{}; _RingBuffer<_StoredMessage> _makeRingBuffer(int size) { final _RingBuffer<_StoredMessage> result = _RingBuffer<_StoredMessage>(size); @@ -127,7 +75,6 @@ class ChannelBuffers { message.callback(null); } - /// Returns true on overflow. bool push(String channel, ByteData? data, PlatformMessageResponseCallback callback) { _RingBuffer<_StoredMessage>? queue = _messages[channel]; if (queue == null) { @@ -147,7 +94,6 @@ class ChannelBuffers { return didOverflow; } - /// Returns null on underflow. _StoredMessage? _pop(String channel) { final _RingBuffer<_StoredMessage>? queue = _messages[channel]; final _StoredMessage? result = queue?.pop(); @@ -159,10 +105,6 @@ class ChannelBuffers { return (queue == null) ? true : queue.isEmpty; } - /// Changes the capacity of the queue associated with the given channel. - /// - /// This could result in the dropping of messages if newSize is less - /// than the current length of the queue. void _resize(String channel, int newSize) { _RingBuffer<_StoredMessage>? queue = _messages[channel]; if (queue == null) { @@ -176,10 +118,6 @@ class ChannelBuffers { } } - /// Remove and process all stored messages for a given channel. - /// - /// This should be called once a channel is prepared to handle messages - /// (i.e. when a message handler is setup in the framework). Future drain(String channel, DrainChannelCallback callback) async { while (!_isEmpty(channel)) { final _StoredMessage message = _pop(channel)!; @@ -193,15 +131,6 @@ class ChannelBuffers { return utf8.decode(list); } - /// Handle a control message. - /// - /// This is intended to be called by the platform messages dispatcher. - /// - /// Available messages: - /// - Name: resize - /// Arity: 2 - /// Format: `resize\r\r` - /// Description: Allows you to set the size of a channel's buffer. void handleMessage(ByteData data) { final List command = _getString(data).split('\r'); if (command.length == /*arity=*/2 + 1 && command[0] == 'resize') { @@ -212,10 +141,4 @@ class ChannelBuffers { } } -/// [ChannelBuffer]s that allow the storage of messages between the -/// Engine and the Framework. Typically messages that can't be delivered -/// are stored here until the Framework is able to process them. -/// -/// See also: -/// * [BinaryMessenger] - The place where ChannelBuffers are typically read. final ChannelBuffers channelBuffers = ChannelBuffers(); diff --git a/lib/web_ui/lib/src/ui/compositing.dart b/lib/web_ui/lib/src/ui/compositing.dart index 11a0e1199d63a..665ebbe14c68d 100644 --- a/lib/web_ui/lib/src/ui/compositing.dart +++ b/lib/web_ui/lib/src/ui/compositing.dart @@ -5,113 +5,34 @@ // @dart = 2.10 part of ui; -/// An opaque object representing a composited scene. -/// -/// To create a Scene object, use a [SceneBuilder]. -/// -/// Scene objects can be displayed on the screen using the -/// [Window.render] method. abstract class Scene { - /// Creates a raster image representation of the current state of the scene. - /// This is a slow operation that is performed on a background thread. Future toImage(int width, int height); - - /// Releases the resources used by this scene. - /// - /// After calling this function, the scene is cannot be used further. void dispose(); } -/// An opaque handle to a transform engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushTransform]. -/// -/// {@template dart.ui.sceneBuilder.oldLayerCompatibility} -/// `oldLayer` parameter in [SceneBuilder] methods only accepts objects created -/// by the engine. [SceneBuilder] will throw an [AssertionError] if you pass it -/// a custom implementation of this class. -/// {@endtemplate} abstract class TransformEngineLayer implements EngineLayer {} -/// An opaque handle to an offset engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushOffset]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class OffsetEngineLayer implements EngineLayer {} -/// An opaque handle to a clip rect engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushClipRect]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ClipRectEngineLayer implements EngineLayer {} -/// An opaque handle to a clip rounded rect engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushClipRRect]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ClipRRectEngineLayer implements EngineLayer {} -/// An opaque handle to a clip path engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushClipPath]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ClipPathEngineLayer implements EngineLayer {} -/// An opaque handle to an opacity engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushOpacity]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class OpacityEngineLayer implements EngineLayer {} -/// An opaque handle to a color filter engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushColorFilter]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ColorFilterEngineLayer implements EngineLayer {} -/// An opaque handle to an image filter engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushImageFilter]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ImageFilterEngineLayer implements EngineLayer {} -/// An opaque handle to a backdrop filter engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushBackdropFilter]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class BackdropFilterEngineLayer implements EngineLayer {} -/// An opaque handle to a shader mask engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushShaderMask]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ShaderMaskEngineLayer implements EngineLayer {} -/// An opaque handle to a physical shape engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushPhysicalShape]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class PhysicalShapeEngineLayer implements EngineLayer {} -/// Builds a [Scene] containing the given visuals. -/// -/// A [Scene] can then be rendered using [Window.render]. -/// -/// To draw graphical operations onto a [Scene], first create a -/// [Picture] using a [PictureRecorder] and a [Canvas], and then add -/// it to the scene using [addPicture]. abstract class SceneBuilder { - /// Creates an empty [SceneBuilder] object. factory SceneBuilder() { if (engine.experimentalUseSkia) { return engine.LayerSceneBuilder(); @@ -119,142 +40,53 @@ abstract class SceneBuilder { return engine.SurfaceSceneBuilder(); } } - - /// Pushes an offset operation onto the operation stack. - /// - /// This is equivalent to [pushTransform] with a matrix with only translation. - /// - /// See [pop] for details about the operation stack. OffsetEngineLayer? pushOffset( double dx, double dy, { OffsetEngineLayer? oldLayer, }); - - /// Pushes a transform operation onto the operation stack. - /// - /// The objects are transformed by the given matrix before rasterization. - /// - /// See [pop] for details about the operation stack. TransformEngineLayer? pushTransform( Float64List matrix4, { TransformEngineLayer? oldLayer, }); - - /// Pushes a rectangular clip operation onto the operation stack. - /// - /// Rasterization outside the given rectangle is discarded. - /// - /// See [pop] for details about the operation stack, and [Clip] for different clip modes. - /// By default, the clip will be anti-aliased (clip = [Clip.antiAlias]). ClipRectEngineLayer? pushClipRect( Rect rect, { Clip clipBehavior = Clip.antiAlias, ClipRectEngineLayer? oldLayer, }); - - /// Pushes a rounded-rectangular clip operation onto the operation stack. - /// - /// Rasterization outside the given rounded rectangle is discarded. - /// - /// See [pop] for details about the operation stack. ClipRRectEngineLayer? pushClipRRect( RRect rrect, { required Clip clipBehavior, ClipRRectEngineLayer? oldLayer, }); - - /// Pushes a path clip operation onto the operation stack. - /// - /// Rasterization outside the given path is discarded. - /// - /// See [pop] for details about the operation stack. ClipPathEngineLayer? pushClipPath( Path path, { Clip clipBehavior = Clip.antiAlias, ClipPathEngineLayer? oldLayer, }); - - /// Pushes an opacity operation onto the operation stack. - /// - /// The given alpha value is blended into the alpha value of the objects' - /// rasterization. An alpha value of 0 makes the objects entirely invisible. - /// An alpha value of 255 has no effect (i.e., the objects retain the current - /// opacity). - /// - /// See [pop] for details about the operation stack. OpacityEngineLayer? pushOpacity( int alpha, { Offset offset = Offset.zero, OpacityEngineLayer? oldLayer, }); - - /// Pushes a color filter operation onto the operation stack. - /// - /// The given color is applied to the objects' rasterization using the given - /// blend mode. - /// - /// {@macro dart.ui.sceneBuilder.oldLayer} - /// - /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} - /// - /// See [pop] for details about the operation stack. ColorFilterEngineLayer? pushColorFilter( ColorFilter filter, { ColorFilterEngineLayer? oldLayer, }); - - /// Pushes an image filter operation onto the operation stack. - /// - /// The given filter is applied to the children's rasterization before compositing them into - /// the scene. - /// - /// {@macro dart.ui.sceneBuilder.oldLayer} - /// - /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} - /// - /// See [pop] for details about the operation stack. ImageFilterEngineLayer? pushImageFilter( ImageFilter filter, { ImageFilterEngineLayer? oldLayer, }); - - /// Pushes a backdrop filter operation onto the operation stack. - /// - /// The given filter is applied to the current contents of the scene prior to - /// rasterizing the given objects. - /// - /// See [pop] for details about the operation stack. BackdropFilterEngineLayer? pushBackdropFilter( ImageFilter filter, { BackdropFilterEngineLayer? oldLayer, }); - - /// Pushes a shader mask operation onto the operation stack. - /// - /// The given shader is applied to the object's rasterization in the given - /// rectangle using the given blend mode. - /// - /// See [pop] for details about the operation stack. ShaderMaskEngineLayer? pushShaderMask( Shader shader, Rect maskRect, BlendMode blendMode, { ShaderMaskEngineLayer? oldLayer, }); - - /// Pushes a physical layer operation for an arbitrary shape onto the - /// operation stack. - /// - /// By default, the layer's content will not be clipped (clip = [Clip.none]). - /// If clip equals [Clip.hardEdge], [Clip.antiAlias], or [Clip.antiAliasWithSaveLayer], - /// then the content is clipped to the given shape defined by [path]. - /// - /// If [elevation] is greater than 0.0, then a shadow is drawn around the layer. - /// [shadowColor] defines the color of the shadow if present and [color] defines the - /// color of the layer background. - /// - /// See [pop] for details about the operation stack, and [Clip] for different clip modes. PhysicalShapeEngineLayer? pushPhysicalShape({ required Path path, required double elevation, @@ -263,64 +95,15 @@ abstract class SceneBuilder { Clip clipBehavior = Clip.none, PhysicalShapeEngineLayer? oldLayer, }); - - /// Add a retained engine layer subtree from previous frames. - /// - /// All the engine layers that are in the subtree of the retained layer will - /// be automatically appended to the current engine layer tree. - /// - /// Therefore, when implementing a subclass of the [Layer] concept defined in - /// the rendering layer of Flutter's framework, once this is called, there's - /// no need to call [addToScene] for its children layers. void addRetained(EngineLayer retainedLayer); - - /// Ends the effect of the most recently pushed operation. - /// - /// Internally the scene builder maintains a stack of operations. Each of the - /// operations in the stack applies to each of the objects added to the scene. - /// Calling this function removes the most recently added operation from the - /// stack. void pop(); - - /// Adds an object to the scene that displays performance statistics. - /// - /// Useful during development to assess the performance of the application. - /// The enabledOptions controls which statistics are displayed. The bounds - /// controls where the statistics are displayed. - /// - /// enabledOptions is a bit field with the following bits defined: - /// - 0x01: displayRasterizerStatistics - show raster thread frame time - /// - 0x02: visualizeRasterizerStatistics - graph raster thread frame times - /// - 0x04: displayEngineStatistics - show UI thread frame time - /// - 0x08: visualizeEngineStatistics - graph UI thread frame times - /// Set enabledOptions to 0x0F to enable all the currently defined features. - /// - /// The "UI thread" is the thread that includes all the execution of - /// the main Dart isolate (the isolate that can call - /// [Window.render]). The UI thread frame time is the total time - /// spent executing the [Window.onBeginFrame] callback. The "raster - /// thread" is the thread (running on the CPU) that subsequently - /// processes the [Scene] provided by the Dart code to turn it into - /// GPU commands and send it to the GPU. - /// - /// See also the [PerformanceOverlayOption] enum in the rendering library. - /// for more details. void addPerformanceOverlay(int enabledOptions, Rect bounds); - - /// Adds a [Picture] to the scene. - /// - /// The picture is rasterized at the given offset. void addPicture( Offset offset, Picture picture, { bool isComplexHint = false, bool willChangeHint = false, }); - - /// Adds a backend texture to the scene. - /// - /// The texture is scaled to the given size and rasterized at the given - /// offset. void addTexture( int textureId, { Offset offset = Offset.zero, @@ -329,32 +112,12 @@ abstract class SceneBuilder { bool freeze = false, FilterQuality filterQuality = FilterQuality.low, }); - - /// Adds a platform view (e.g an iOS UIView) to the scene. - /// - /// Only supported on iOS, this is currently a no-op on other platforms. - /// - /// On iOS this layer splits the current output surface into two surfaces, one for the scene nodes - /// preceding the platform view, and one for the scene nodes following the platform view. - /// - /// ## Performance impact - /// - /// Adding an additional surface doubles the amount of graphics memory directly used by Flutter - /// for output buffers. Quartz might allocated extra buffers for compositing the Flutter surfaces - /// and the platform view. - /// - /// With a platform view in the scene, Quartz has to composite the two Flutter surfaces and the - /// embedded UIView. In addition to that, on iOS versions greater than 9, the Flutter frames are - /// synchronized with the UIView frames adding additional performance overhead. void addPlatformView( int viewId, { Offset offset = Offset.zero, double width = 0.0, double height = 0.0, }); - - /// (Fuchsia-only) Adds a scene rendered by another application to the scene - /// for this application. void addChildScene({ Offset offset = Offset.zero, double width = 0.0, @@ -362,52 +125,10 @@ abstract class SceneBuilder { required SceneHost sceneHost, bool hitTestable = true, }); - - /// Sets a threshold after which additional debugging information should be - /// recorded. - /// - /// Currently this interface is difficult to use by end-developers. If you're - /// interested in using this feature, please contact [flutter-dev](https://groups.google.com/forum/#!forum/flutter-dev). - /// We'll hopefully be able to figure out how to make this feature more useful - /// to you. void setRasterizerTracingThreshold(int frameInterval); - - /// Sets whether the raster cache should checkerboard cached entries. This is - /// only useful for debugging purposes. - /// - /// The compositor can sometimes decide to cache certain portions of the - /// widget hierarchy. Such portions typically don't change often from frame to - /// frame and are expensive to render. This can speed up overall rendering. - /// However, there is certain upfront cost to constructing these cache - /// entries. And, if the cache entries are not used very often, this cost may - /// not be worth the speedup in rendering of subsequent frames. If the - /// developer wants to be certain that populating the raster cache is not - /// causing stutters, this option can be set. Depending on the observations - /// made, hints can be provided to the compositor that aid it in making better - /// decisions about caching. - /// - /// Currently this interface is difficult to use by end-developers. If you're - /// interested in using this feature, please contact [flutter-dev](https://groups.google.com/forum/#!forum/flutter-dev). void setCheckerboardRasterCacheImages(bool checkerboard); - - /// Sets whether the compositor should checkerboard layers that are rendered - /// to offscreen bitmaps. - /// - /// This is only useful for debugging purposes. void setCheckerboardOffscreenLayers(bool checkerboard); - - /// Finishes building the scene. - /// - /// Returns a [Scene] containing the objects that have been added to - /// this scene builder. The [Scene] can then be displayed on the - /// screen with [Window.render]. - /// - /// After calling this function, the scene builder object is invalid and - /// cannot be used further. Scene build(); - - /// Set properties on the linked scene. These properties include its bounds, - /// as well as whether it can be the target of focus events or not. void setProperties( double width, double height, @@ -419,36 +140,16 @@ abstract class SceneBuilder { ); } -/// A handle for the framework to hold and retain an engine layer across frames. class EngineLayer {} -//// (Fuchsia-only) Hosts content provided by another application. class SceneHost { - /// Creates a host for a child scene's content. - /// - /// The ViewHolder token is bound to a ViewHolder scene graph node which acts - /// as a container for the child's content. The creator of the SceneHost is - /// responsible for sending the corresponding ViewToken to the child. - /// - /// The ViewHolder token is a dart:zircon Handle, but that type isn't - /// available here. This is called by ChildViewConnection in - /// //topaz/public/dart/fuchsia_scenic_flutter/. - /// - /// The SceneHost takes ownership of the provided ViewHolder token. SceneHost( dynamic viewHolderToken, void Function() viewConnectedCallback, void Function() viewDisconnectedCallback, void Function(bool) viewStateChangedCallback, ); - - /// Releases the resources associated with the SceneHost. - /// - /// After calling this function, the SceneHost cannot be used further. void dispose() {} - - /// Set properties on the linked scene. These properties include its bounds, - /// as well as whether it can be the target of focus events or not. void setProperties( double width, double height, diff --git a/lib/web_ui/lib/src/ui/geometry.dart b/lib/web_ui/lib/src/ui/geometry.dart index c528c8c73db93..0ec3c06550a20 100644 --- a/lib/web_ui/lib/src/ui/geometry.dart +++ b/lib/web_ui/lib/src/ui/geometry.dart @@ -5,83 +5,19 @@ // @dart = 2.10 part of ui; -/// Base class for [Size] and [Offset], which are both ways to describe -/// a distance as a two-dimensional axis-aligned vector. abstract class OffsetBase { - /// Abstract const constructor. This constructor enables subclasses to provide - /// const constructors so that they can be used in const expressions. - /// - /// The first argument sets the horizontal component, and the second the - /// vertical component. const OffsetBase(this._dx, this._dy) : assert(_dx != null), // ignore: unnecessary_null_comparison assert(_dy != null); // ignore: unnecessary_null_comparison final double _dx; final double _dy; - - /// Returns true if either component is [double.infinity], and false if both - /// are finite (or negative infinity, or NaN). - /// - /// This is different than comparing for equality with an instance that has - /// _both_ components set to [double.infinity]. - /// - /// See also: - /// - /// * [isFinite], which is true if both components are finite (and not NaN). bool get isInfinite => _dx >= double.infinity || _dy >= double.infinity; - - /// Whether both components are finite (neither infinite nor NaN). - /// - /// See also: - /// - /// * [isInfinite], which returns true if either component is equal to - /// positive infinity. bool get isFinite => _dx.isFinite && _dy.isFinite; - - /// Less-than operator. Compares an [Offset] or [Size] to another [Offset] or - /// [Size], and returns true if both the horizontal and vertical values of the - /// left-hand-side operand are smaller than the horizontal and vertical values - /// of the right-hand-side operand respectively. Returns false otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator <(OffsetBase other) => _dx < other._dx && _dy < other._dy; - - /// Less-than-or-equal-to operator. Compares an [Offset] or [Size] to another - /// [Offset] or [Size], and returns true if both the horizontal and vertical - /// values of the left-hand-side operand are smaller than or equal to the - /// horizontal and vertical values of the right-hand-side operand - /// respectively. Returns false otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator <=(OffsetBase other) => _dx <= other._dx && _dy <= other._dy; - - /// Greater-than operator. Compares an [Offset] or [Size] to another [Offset] - /// or [Size], and returns true if both the horizontal and vertical values of - /// the left-hand-side operand are bigger than the horizontal and vertical - /// values of the right-hand-side operand respectively. Returns false - /// otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator >(OffsetBase other) => _dx > other._dx && _dy > other._dy; - - /// Greater-than-or-equal-to operator. Compares an [Offset] or [Size] to - /// another [Offset] or [Size], and returns true if both the horizontal and - /// vertical values of the left-hand-side operand are bigger than or equal to - /// the horizontal and vertical values of the right-hand-side operand - /// respectively. Returns false otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator >=(OffsetBase other) => _dx >= other._dx && _dy >= other._dy; - - /// Equality operator. Compares an [Offset] or [Size] to another [Offset] or - /// [Size], and returns true if the horizontal and vertical values of the - /// left-hand-side operand are equal to the horizontal and vertical values of - /// the right-hand-side operand respectively. Returns false otherwise. @override bool operator ==(Object other) { return other is OffsetBase @@ -96,224 +32,29 @@ abstract class OffsetBase { String toString() => 'OffsetBase(${_dx.toStringAsFixed(1)}, ${_dy.toStringAsFixed(1)})'; } -/// An immutable 2D floating-point offset. -/// -/// Generally speaking, Offsets can be interpreted in two ways: -/// -/// 1. As representing a point in Cartesian space a specified distance from a -/// separately-maintained origin. For example, the top-left position of -/// children in the [RenderBox] protocol is typically represented as an -/// [Offset] from the top left of the parent box. -/// -/// 2. As a vector that can be applied to coordinates. For example, when -/// painting a [RenderObject], the parent is passed an [Offset] from the -/// screen's origin which it can add to the offsets of its children to find -/// the [Offset] from the screen's origin to each of the children. -/// -/// Because a particular [Offset] can be interpreted as one sense at one time -/// then as the other sense at a later time, the same class is used for both -/// senses. -/// -/// See also: -/// -/// * [Size], which represents a vector describing the size of a rectangle. class Offset extends OffsetBase { - /// Creates an offset. The first argument sets [dx], the horizontal component, - /// and the second sets [dy], the vertical component. const Offset(double dx, double dy) : super(dx, dy); - - /// Creates an offset from its [direction] and [distance]. - /// - /// The direction is in radians clockwise from the positive x-axis. - /// - /// The distance can be omitted, to create a unit vector (distance = 1.0). factory Offset.fromDirection(double direction, [ double distance = 1.0 ]) { return Offset(distance * math.cos(direction), distance * math.sin(direction)); } - - /// The x component of the offset. - /// - /// The y component is given by [dy]. double get dx => _dx; - - /// The y component of the offset. - /// - /// The x component is given by [dx]. double get dy => _dy; - - /// The magnitude of the offset. - /// - /// If you need this value to compare it to another [Offset]'s distance, - /// consider using [distanceSquared] instead, since it is cheaper to compute. double get distance => math.sqrt(dx * dx + dy * dy); - - /// The square of the magnitude of the offset. - /// - /// This is cheaper than computing the [distance] itself. double get distanceSquared => dx * dx + dy * dy; - - /// The angle of this offset as radians clockwise from the positive x-axis, in - /// the range -[pi] to [pi], assuming positive values of the x-axis go to the - /// left and positive values of the y-axis go down. - /// - /// Zero means that [dy] is zero and [dx] is zero or positive. - /// - /// Values from zero to [pi]/2 indicate positive values of [dx] and [dy], the - /// bottom-right quadrant. - /// - /// Values from [pi]/2 to [pi] indicate negative values of [dx] and positive - /// values of [dy], the bottom-left quadrant. - /// - /// Values from zero to -[pi]/2 indicate positive values of [dx] and negative - /// values of [dy], the top-right quadrant. - /// - /// Values from -[pi]/2 to -[pi] indicate negative values of [dx] and [dy], - /// the top-left quadrant. - /// - /// When [dy] is zero and [dx] is negative, the [direction] is [pi]. - /// - /// When [dx] is zero, [direction] is [pi]/2 if [dy] is positive and -[pi]/2 - /// if [dy] is negative. - /// - /// See also: - /// - /// * [distance], to compute the magnitude of the vector. - /// * [Canvas.rotate], which uses the same convention for its angle. double get direction => math.atan2(dy, dx); - - /// An offset with zero magnitude. - /// - /// This can be used to represent the origin of a coordinate space. static const Offset zero = Offset(0.0, 0.0); - - /// An offset with infinite x and y components. - /// - /// See also: - /// - /// * [isInfinite], which checks whether either component is infinite. - /// * [isFinite], which checks whether both components are finite. // This is included for completeness, because [Size.infinite] exists. static const Offset infinite = Offset(double.infinity, double.infinity); - - /// Returns a new offset with the x component scaled by `scaleX` and the y - /// component scaled by `scaleY`. - /// - /// If the two scale arguments are the same, consider using the `*` operator - /// instead: - /// - /// ```dart - /// Offset a = const Offset(10.0, 10.0); - /// Offset b = a * 2.0; // same as: a.scale(2.0, 2.0) - /// ``` - /// - /// If the two arguments are -1, consider using the unary `-` operator - /// instead: - /// - /// ```dart - /// Offset a = const Offset(10.0, 10.0); - /// Offset b = -a; // same as: a.scale(-1.0, -1.0) - /// ``` Offset scale(double scaleX, double scaleY) => Offset(dx * scaleX, dy * scaleY); - - /// Returns a new offset with translateX added to the x component and - /// translateY added to the y component. - /// - /// If the arguments come from another [Offset], consider using the `+` or `-` - /// operators instead: - /// - /// ```dart - /// Offset a = const Offset(10.0, 10.0); - /// Offset b = const Offset(10.0, 10.0); - /// Offset c = a + b; // same as: a.translate(b.dx, b.dy) - /// Offset d = a - b; // same as: a.translate(-b.dx, -b.dy) - /// ``` Offset translate(double translateX, double translateY) => Offset(dx + translateX, dy + translateY); - - /// Unary negation operator. - /// - /// Returns an offset with the coordinates negated. - /// - /// If the [Offset] represents an arrow on a plane, this operator returns the - /// same arrow but pointing in the reverse direction. Offset operator -() => Offset(-dx, -dy); - - /// Binary subtraction operator. - /// - /// Returns an offset whose [dx] value is the left-hand-side operand's [dx] - /// minus the right-hand-side operand's [dx] and whose [dy] value is the - /// left-hand-side operand's [dy] minus the right-hand-side operand's [dy]. - /// - /// See also [translate]. Offset operator -(Offset other) => Offset(dx - other.dx, dy - other.dy); - - /// Binary addition operator. - /// - /// Returns an offset whose [dx] value is the sum of the [dx] values of the - /// two operands, and whose [dy] value is the sum of the [dy] values of the - /// two operands. - /// - /// See also [translate]. Offset operator +(Offset other) => Offset(dx + other.dx, dy + other.dy); - - /// Multiplication operator. - /// - /// Returns an offset whose coordinates are the coordinates of the - /// left-hand-side operand (an Offset) multiplied by the scalar - /// right-hand-side operand (a double). - /// - /// See also [scale]. Offset operator *(double operand) => Offset(dx * operand, dy * operand); - - /// Division operator. - /// - /// Returns an offset whose coordinates are the coordinates of the - /// left-hand-side operand (an Offset) divided by the scalar right-hand-side - /// operand (a double). - /// - /// See also [scale]. Offset operator /(double operand) => Offset(dx / operand, dy / operand); - - /// Integer (truncating) division operator. - /// - /// Returns an offset whose coordinates are the coordinates of the - /// left-hand-side operand (an Offset) divided by the scalar right-hand-side - /// operand (a double), rounded towards zero. Offset operator ~/(double operand) => Offset((dx ~/ operand).toDouble(), (dy ~/ operand).toDouble()); - - /// Modulo (remainder) operator. - /// - /// Returns an offset whose coordinates are the remainder of dividing the - /// coordinates of the left-hand-side operand (an Offset) by the scalar - /// right-hand-side operand (a double). Offset operator %(double operand) => Offset(dx % operand, dy % operand); - - /// Rectangle constructor operator. - /// - /// Combines an [Offset] and a [Size] to form a [Rect] whose top-left - /// coordinate is the point given by adding this offset, the left-hand-side - /// operand, to the origin, and whose size is the right-hand-side operand. - /// - /// ```dart - /// Rect myRect = Offset.zero & const Size(100.0, 100.0); - /// // same as: Rect.fromLTWH(0.0, 0.0, 100.0, 100.0) - /// ``` Rect operator &(Size other) => Rect.fromLTWH(dx, dy, other.width, other.height); - - /// Linearly interpolate between two offsets. - /// - /// If either offset is null, this function interpolates from [Offset.zero]. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Offset? lerp(Offset? a, Offset? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -331,7 +72,6 @@ class Offset extends OffsetBase { } } - /// Compares two Offsets for equality. @override bool operator ==(Object other) { return other is Offset @@ -346,61 +86,16 @@ class Offset extends OffsetBase { String toString() => 'Offset(${dx.toStringAsFixed(1)}, ${dy.toStringAsFixed(1)})'; } -/// Holds a 2D floating-point size. -/// -/// You can think of this as an [Offset] from the origin. class Size extends OffsetBase { - /// Creates a [Size] with the given [width] and [height]. const Size(double width, double height) : super(width, height); - - /// Creates an instance of [Size] that has the same values as another. // Used by the rendering library's _DebugSize hack. Size.copy(Size source) : super(source.width, source.height); - - /// Creates a square [Size] whose [width] and [height] are the given dimension. - /// - /// See also: - /// - /// * [Size.fromRadius], which is more convenient when the available size - /// is the radius of a circle. const Size.square(double dimension) : super(dimension, dimension); - - /// Creates a [Size] with the given [width] and an infinite [height]. const Size.fromWidth(double width) : super(width, double.infinity); - - /// Creates a [Size] with the given [height] and an infinite [width]. const Size.fromHeight(double height) : super(double.infinity, height); - - /// Creates a square [Size] whose [width] and [height] are twice the given - /// dimension. - /// - /// This is a square that contains a circle with the given radius. - /// - /// See also: - /// - /// * [Size.square], which creates a square with the given dimension. const Size.fromRadius(double radius) : super(radius * 2.0, radius * 2.0); - - /// The horizontal extent of this size. double get width => _dx; - - /// The vertical extent of this size. double get height => _dy; - - /// The aspect ratio of this size. - /// - /// This returns the [width] divided by the [height]. - /// - /// If the [width] is zero, the result will be zero. If the [height] is zero - /// (and the [width] is not), the result will be [double.infinity] or - /// [double.negativeInfinity] as determined by the sign of [width]. - /// - /// See also: - /// - /// * [AspectRatio], a widget for giving a child widget a specific aspect - /// ratio. - /// * [FittedBox], a widget that (in most modes) attempts to maintain a - /// child widget's aspect ratio while changing its size. double get aspectRatio { if (height != 0.0) return width / height; @@ -411,38 +106,9 @@ class Size extends OffsetBase { return 0.0; } - /// An empty size, one with a zero width and a zero height. static const Size zero = Size(0.0, 0.0); - - /// A size whose [width] and [height] are infinite. - /// - /// See also: - /// - /// * [isInfinite], which checks whether either dimension is infinite. - /// * [isFinite], which checks whether both dimensions are finite. static const Size infinite = Size(double.infinity, double.infinity); - - /// Whether this size encloses a non-zero area. - /// - /// Negative areas are considered empty. bool get isEmpty => width <= 0.0 || height <= 0.0; - - /// Binary subtraction operator for [Size]. - /// - /// Subtracting a [Size] from a [Size] returns the [Offset] that describes how - /// much bigger the left-hand-side operand is than the right-hand-side - /// operand. Adding that resulting [Offset] to the [Size] that was the - /// right-hand-side operand would return a [Size] equal to the [Size] that was - /// the left-hand-side operand. (i.e. if `sizeA - sizeB -> offsetA`, then - /// `offsetA + sizeB -> sizeA`) - /// - /// Subtracting an [Offset] from a [Size] returns the [Size] that is smaller than - /// the [Size] operand by the difference given by the [Offset] operand. In other - /// words, the returned [Size] has a [width] consisting of the [width] of the - /// left-hand-side operand minus the [Offset.dx] dimension of the - /// right-hand-side operand, and a [height] consisting of the [height] of the - /// left-hand-side operand minus the [Offset.dy] dimension of the - /// right-hand-side operand. OffsetBase operator -(OffsetBase other) { if (other is Size) return Offset(width - other.width, height - other.height); @@ -451,140 +117,30 @@ class Size extends OffsetBase { throw ArgumentError(other); } - /// Binary addition operator for adding an [Offset] to a [Size]. - /// - /// Returns a [Size] whose [width] is the sum of the [width] of the - /// left-hand-side operand, a [Size], and the [Offset.dx] dimension of the - /// right-hand-side operand, an [Offset], and whose [height] is the sum of the - /// [height] of the left-hand-side operand and the [Offset.dy] dimension of - /// the right-hand-side operand. Size operator +(Offset other) => Size(width + other.dx, height + other.dy); - - /// Multiplication operator. - /// - /// Returns a [Size] whose dimensions are the dimensions of the left-hand-side - /// operand (a [Size]) multiplied by the scalar right-hand-side operand (a - /// [double]). Size operator *(double operand) => Size(width * operand, height * operand); - - /// Division operator. - /// - /// Returns a [Size] whose dimensions are the dimensions of the left-hand-side - /// operand (a [Size]) divided by the scalar right-hand-side operand (a - /// [double]). Size operator /(double operand) => Size(width / operand, height / operand); - - /// Integer (truncating) division operator. - /// - /// Returns a [Size] whose dimensions are the dimensions of the left-hand-side - /// operand (a [Size]) divided by the scalar right-hand-side operand (a - /// [double]), rounded towards zero. Size operator ~/(double operand) => Size((width ~/ operand).toDouble(), (height ~/ operand).toDouble()); - - /// Modulo (remainder) operator. - /// - /// Returns a [Size] whose dimensions are the remainder of dividing the - /// left-hand-side operand (a [Size]) by the scalar right-hand-side operand (a - /// [double]). Size operator %(double operand) => Size(width % operand, height % operand); - - /// The lesser of the magnitudes of the [width] and the [height]. double get shortestSide => math.min(width.abs(), height.abs()); - - /// The greater of the magnitudes of the [width] and the [height]. double get longestSide => math.max(width.abs(), height.abs()); // Convenience methods that do the equivalent of calling the similarly named // methods on a Rect constructed from the given origin and this size. - - /// The offset to the intersection of the top and left edges of the rectangle - /// described by the given [Offset] (which is interpreted as the top-left corner) - /// and this [Size]. - /// - /// See also [Rect.topLeft]. Offset topLeft(Offset origin) => origin; - - /// The offset to the center of the top edge of the rectangle described by the - /// given offset (which is interpreted as the top-left corner) and this size. - /// - /// See also [Rect.topCenter]. Offset topCenter(Offset origin) => Offset(origin.dx + width / 2.0, origin.dy); - - /// The offset to the intersection of the top and right edges of the rectangle - /// described by the given offset (which is interpreted as the top-left corner) - /// and this size. - /// - /// See also [Rect.topRight]. Offset topRight(Offset origin) => Offset(origin.dx + width, origin.dy); - - /// The offset to the center of the left edge of the rectangle described by the - /// given offset (which is interpreted as the top-left corner) and this size. - /// - /// See also [Rect.centerLeft]. Offset centerLeft(Offset origin) => Offset(origin.dx, origin.dy + height / 2.0); - - /// The offset to the point halfway between the left and right and the top and - /// bottom edges of the rectangle described by the given offset (which is - /// interpreted as the top-left corner) and this size. - /// - /// See also [Rect.center]. Offset center(Offset origin) => Offset(origin.dx + width / 2.0, origin.dy + height / 2.0); - - /// The offset to the center of the right edge of the rectangle described by the - /// given offset (which is interpreted as the top-left corner) and this size. - /// - /// See also [Rect.centerLeft]. Offset centerRight(Offset origin) => Offset(origin.dx + width, origin.dy + height / 2.0); - - /// The offset to the intersection of the bottom and left edges of the - /// rectangle described by the given offset (which is interpreted as the - /// top-left corner) and this size. - /// - /// See also [Rect.bottomLeft]. Offset bottomLeft(Offset origin) => Offset(origin.dx, origin.dy + height); - - /// The offset to the center of the bottom edge of the rectangle described by - /// the given offset (which is interpreted as the top-left corner) and this - /// size. - /// - /// See also [Rect.bottomLeft]. Offset bottomCenter(Offset origin) => Offset(origin.dx + width / 2.0, origin.dy + height); - - /// The offset to the intersection of the bottom and right edges of the - /// rectangle described by the given offset (which is interpreted as the - /// top-left corner) and this size. - /// - /// See also [Rect.bottomRight]. Offset bottomRight(Offset origin) => Offset(origin.dx + width, origin.dy + height); - - /// Whether the point specified by the given offset (which is assumed to be - /// relative to the top left of the size) lies between the left and right and - /// the top and bottom edges of a rectangle of this size. - /// - /// Rectangles include their top and left edges but exclude their bottom and - /// right edges. bool contains(Offset offset) { return offset.dx >= 0.0 && offset.dx < width && offset.dy >= 0.0 && offset.dy < height; } - /// A [Size] with the [width] and [height] swapped. Size get flipped => Size(height, width); - - /// Linearly interpolate between two sizes - /// - /// If either size is null, this function interpolates from [Size.zero]. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Size? lerp(Size? a, Size? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -602,7 +158,6 @@ class Size extends OffsetBase { } } - /// Compares two Sizes for equality. // We don't compare the runtimeType because of _DebugSize in the framework. @override bool operator ==(Object other) { @@ -618,95 +173,51 @@ class Size extends OffsetBase { String toString() => 'Size(${width.toStringAsFixed(1)}, ${height.toStringAsFixed(1)})'; } -/// An immutable, 2D, axis-aligned, floating-point rectangle whose coordinates -/// are relative to a given origin. -/// -/// A Rect can be created with one its constructors or from an [Offset] and a -/// [Size] using the `&` operator: -/// -/// ```dart -/// Rect myRect = const Offset(1.0, 2.0) & const Size(3.0, 4.0); -/// ``` class Rect { - /// Construct a rectangle from its left, top, right, and bottom edges. const Rect.fromLTRB(this.left, this.top, this.right, this.bottom) : assert(left != null), // ignore: unnecessary_null_comparison assert(top != null), // ignore: unnecessary_null_comparison assert(right != null), // ignore: unnecessary_null_comparison assert(bottom != null); // ignore: unnecessary_null_comparison - /// Construct a rectangle from its left and top edges, its width, and its - /// height. - /// - /// To construct a [Rect] from an [Offset] and a [Size], you can use the - /// rectangle constructor operator `&`. See [Offset.&]. - const Rect.fromLTWH(double left, double top, double width, double height) : this.fromLTRB(left, top, left + width, top + height); - - /// Construct a rectangle that bounds the given circle. - /// - /// The `center` argument is assumed to be an offset from the origin. - Rect.fromCircle({ required Offset center, required double radius }) : this.fromCenter( - center: center, - width: radius * 2, - height: radius * 2, - ); - - /// Constructs a rectangle from its center point, width, and height. - /// - /// The `center` argument is assumed to be an offset from the origin. - Rect.fromCenter({ required Offset center, required double width, required double height }) : this.fromLTRB( - center.dx - width / 2, - center.dy - height / 2, - center.dx + width / 2, - center.dy + height / 2, - ); - - /// Construct the smallest rectangle that encloses the given offsets, treating - /// them as vectors from the origin. - Rect.fromPoints(Offset a, Offset b) : this.fromLTRB( - math.min(a.dx, b.dx), - math.min(a.dy, b.dy), - math.max(a.dx, b.dx), - math.max(a.dy, b.dy), - ); - - /// The offset of the left edge of this rectangle from the x axis. - final double left; + const Rect.fromLTWH(double left, double top, double width, double height) + : this.fromLTRB(left, top, left + width, top + height); - /// The offset of the top edge of this rectangle from the y axis. - final double top; + Rect.fromCircle({ required Offset center, required double radius }) + : this.fromCenter( + center: center, + width: radius * 2, + height: radius * 2, + ); - /// The offset of the right edge of this rectangle from the x axis. - final double right; + Rect.fromCenter({ required Offset center, required double width, required double height }) + : this.fromLTRB( + center.dx - width / 2, + center.dy - height / 2, + center.dx + width / 2, + center.dy + height / 2, + ); - /// The offset of the bottom edge of this rectangle from the y axis. - final double bottom; + Rect.fromPoints(Offset a, Offset b) + : this.fromLTRB( + math.min(a.dx, b.dx), + math.min(a.dy, b.dy), + math.max(a.dx, b.dx), + math.max(a.dy, b.dy), + ); - /// The distance between the left and right edges of this rectangle. + final double left; + final double top; + final double right; + final double bottom; double get width => right - left; - - /// The distance between the top and bottom edges of this rectangle. double get height => bottom - top; - - /// The distance between the upper-left corner and the lower-right corner of - /// this rectangle. Size get size => Size(width, height); - - /// Whether any of the dimensions are `NaN`. bool get hasNaN => left.isNaN || top.isNaN || right.isNaN || bottom.isNaN; - - /// A rectangle with left, top, right, and bottom edges all at zero. static const Rect zero = Rect.fromLTRB(0.0, 0.0, 0.0, 0.0); static const double _giantScalar = 1.0E+9; // matches kGiantRect from layer.h - - /// A rectangle that covers the entire coordinate space. - /// - /// This covers the space from -1e9,-1e9 to 1e9,1e9. - /// This is the space over which graphics operations are valid. static const Rect largest = Rect.fromLTRB(-_giantScalar, -_giantScalar, _giantScalar, _giantScalar); - - /// Whether any of the coordinates of this rectangle are equal to positive infinity. // included for consistency with Offset and Size bool get isInfinite { return left >= double.infinity @@ -715,63 +226,39 @@ class Rect { || bottom >= double.infinity; } - /// Whether all coordinates of this rectangle are finite. bool get isFinite => left.isFinite && top.isFinite && right.isFinite && bottom.isFinite; - - /// Whether this rectangle encloses a non-zero area. Negative areas are - /// considered empty. bool get isEmpty => left >= right || top >= bottom; - - /// Returns a new rectangle translated by the given offset. - /// - /// To translate a rectangle by separate x and y components rather than by an - /// [Offset], consider [translate]. Rect shift(Offset offset) { return Rect.fromLTRB(left + offset.dx, top + offset.dy, right + offset.dx, bottom + offset.dy); } - /// Returns a new rectangle with translateX added to the x components and - /// translateY added to the y components. - /// - /// To translate a rectangle by an [Offset] rather than by separate x and y - /// components, consider [shift]. Rect translate(double translateX, double translateY) { return Rect.fromLTRB(left + translateX, top + translateY, right + translateX, bottom + translateY); } - /// Returns a new rectangle with edges moved outwards by the given delta. Rect inflate(double delta) { return Rect.fromLTRB(left - delta, top - delta, right + delta, bottom + delta); } - /// Returns a new rectangle with edges moved inwards by the given delta. Rect deflate(double delta) => inflate(-delta); - - /// Returns a new rectangle that is the intersection of the given - /// rectangle and this rectangle. The two rectangles must overlap - /// for this to be meaningful. If the two rectangles do not overlap, - /// then the resulting Rect will have a negative width or height. Rect intersect(Rect other) { return Rect.fromLTRB( math.max(left, other.left), math.max(top, other.top), math.min(right, other.right), - math.min(bottom, other.bottom) + math.min(bottom, other.bottom), ); } - /// Returns a new rectangle which is the bounding box containing this - /// rectangle and the given rectangle. Rect expandToInclude(Rect other) { return Rect.fromLTRB( - math.min(left, other.left), - math.min(top, other.top), - math.max(right, other.right), - math.max(bottom, other.bottom), + math.min(left, other.left), + math.min(top, other.top), + math.max(right, other.right), + math.max(bottom, other.bottom), ); } - /// Whether `other` has a nonzero area of overlap with this rectangle. bool overlaps(Rect other) { if (right <= other.left || other.right <= left) return false; @@ -780,85 +267,21 @@ class Rect { return true; } - /// The lesser of the magnitudes of the [width] and the [height] of this - /// rectangle. double get shortestSide => math.min(width.abs(), height.abs()); - - /// The greater of the magnitudes of the [width] and the [height] of this - /// rectangle. double get longestSide => math.max(width.abs(), height.abs()); - - /// The offset to the intersection of the top and left edges of this rectangle. - /// - /// See also [Size.topLeft]. Offset get topLeft => Offset(left, top); - - /// The offset to the center of the top edge of this rectangle. - /// - /// See also [Size.topCenter]. Offset get topCenter => Offset(left + width / 2.0, top); - - /// The offset to the intersection of the top and right edges of this rectangle. - /// - /// See also [Size.topRight]. Offset get topRight => Offset(right, top); - - /// The offset to the center of the left edge of this rectangle. - /// - /// See also [Size.centerLeft]. Offset get centerLeft => Offset(left, top + height / 2.0); - - /// The offset to the point halfway between the left and right and the top and - /// bottom edges of this rectangle. - /// - /// See also [Size.center]. Offset get center => Offset(left + width / 2.0, top + height / 2.0); - - /// The offset to the center of the right edge of this rectangle. - /// - /// See also [Size.centerLeft]. Offset get centerRight => Offset(right, top + height / 2.0); - - /// The offset to the intersection of the bottom and left edges of this rectangle. - /// - /// See also [Size.bottomLeft]. Offset get bottomLeft => Offset(left, bottom); - - /// The offset to the center of the bottom edge of this rectangle. - /// - /// See also [Size.bottomLeft]. Offset get bottomCenter => Offset(left + width / 2.0, bottom); - - /// The offset to the intersection of the bottom and right edges of this rectangle. - /// - /// See also [Size.bottomRight]. Offset get bottomRight => Offset(right, bottom); - - /// Whether the point specified by the given offset (which is assumed to be - /// relative to the origin) lies between the left and right and the top and - /// bottom edges of this rectangle. - /// - /// Rectangles include their top and left edges but exclude their bottom and - /// right edges. bool contains(Offset offset) { return offset.dx >= left && offset.dx < right && offset.dy >= top && offset.dy < bottom; } - /// Linearly interpolate between two rectangles. - /// - /// If either rect is null, [Rect.zero] is used as a substitute. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Rect? lerp(Rect? a, Rect? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -902,92 +325,19 @@ class Rect { String toString() => 'Rect.fromLTRB(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)})'; } -/// A radius for either circular or elliptical shapes. class Radius { - /// Constructs a circular radius. [x] and [y] will have the same radius value. const Radius.circular(double radius) : this.elliptical(radius, radius); - - /// Constructs an elliptical radius with the given radii. const Radius.elliptical(this.x, this.y); - - /// The radius value on the horizontal axis. final double x; - - /// The radius value on the vertical axis. final double y; - - /// A radius with [x] and [y] values set to zero. - /// - /// You can use [Radius.zero] with [RRect] to have right-angle corners. static const Radius zero = Radius.circular(0.0); - - /// Unary negation operator. - /// - /// Returns a Radius with the distances negated. - /// - /// Radiuses with negative values aren't geometrically meaningful, but could - /// occur as part of expressions. For example, negating a radius of one pixel - /// and then adding the result to another radius is equivalent to subtracting - /// a radius of one pixel from the other. Radius operator -() => Radius.elliptical(-x, -y); - - /// Binary subtraction operator. - /// - /// Returns a radius whose [x] value is the left-hand-side operand's [x] - /// minus the right-hand-side operand's [x] and whose [y] value is the - /// left-hand-side operand's [y] minus the right-hand-side operand's [y]. Radius operator -(Radius other) => Radius.elliptical(x - other.x, y - other.y); - - /// Binary addition operator. - /// - /// Returns a radius whose [x] value is the sum of the [x] values of the - /// two operands, and whose [y] value is the sum of the [y] values of the - /// two operands. Radius operator +(Radius other) => Radius.elliptical(x + other.x, y + other.y); - - /// Multiplication operator. - /// - /// Returns a radius whose coordinates are the coordinates of the - /// left-hand-side operand (a radius) multiplied by the scalar - /// right-hand-side operand (a double). Radius operator *(double operand) => Radius.elliptical(x * operand, y * operand); - - /// Division operator. - /// - /// Returns a radius whose coordinates are the coordinates of the - /// left-hand-side operand (a radius) divided by the scalar right-hand-side - /// operand (a double). Radius operator /(double operand) => Radius.elliptical(x / operand, y / operand); - - /// Integer (truncating) division operator. - /// - /// Returns a radius whose coordinates are the coordinates of the - /// left-hand-side operand (a radius) divided by the scalar right-hand-side - /// operand (a double), rounded towards zero. Radius operator ~/(double operand) => Radius.elliptical((x ~/ operand).toDouble(), (y ~/ operand).toDouble()); - - /// Modulo (remainder) operator. - /// - /// Returns a radius whose coordinates are the remainder of dividing the - /// coordinates of the left-hand-side operand (a radius) by the scalar - /// right-hand-side operand (a double). Radius operator %(double operand) => Radius.elliptical(x % operand, y % operand); - - /// Linearly interpolate between two radii. - /// - /// If either is null, this function substitutes [Radius.zero] instead. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Radius? lerp(Radius? a, Radius? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -1032,32 +382,37 @@ class Radius { } } -/// An immutable rounded rectangle with the custom radii for all four corners. class RRect { - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and the same radii along its horizontal axis and its vertical axis. - const RRect.fromLTRBXY(double left, double top, double right, double bottom, - double radiusX, double radiusY) : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: radiusX, - tlRadiusY: radiusY, - trRadiusX: radiusX, - trRadiusY: radiusY, - blRadiusX: radiusX, - blRadiusY: radiusY, - brRadiusX: radiusX, - brRadiusY: radiusY, - uniformRadii: radiusX == radiusY, - ); - - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and the same radius in each corner. - RRect.fromLTRBR(double left, double top, double right, double bottom, - Radius radius) - : this._raw( + const RRect.fromLTRBXY( + double left, + double top, + double right, + double bottom, + double radiusX, + double radiusY, + ) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + uniformRadii: radiusX == radiusY, + ); + + RRect.fromLTRBR( + double left, + double top, + double right, + double bottom, + Radius radius, + ) : this._raw( top: top, left: left, right: right, @@ -1073,8 +428,6 @@ class RRect { uniformRadii: radius.x == radius.y, ); - /// Construct a rounded rectangle from its bounding box and the same radii - /// along its horizontal axis and its vertical axis. RRect.fromRectXY(Rect rect, double radiusX, double radiusY) : this._raw( top: rect.top, @@ -1092,8 +445,6 @@ class RRect { uniformRadii: radiusX == radiusY, ); - /// Construct a rounded rectangle from its bounding box and a radius that is - /// the same in each corner. RRect.fromRectAndRadius(Rect rect, Radius radius) : this._raw( top: rect.top, @@ -1111,10 +462,6 @@ class RRect { uniformRadii: radius.x == radius.y, ); - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and topLeft, topRight, bottomRight, and bottomLeft radii. - /// - /// The corner radii default to [Radius.zero], i.e. right-angled corners. RRect.fromLTRBAndCorners( double left, double top, @@ -1146,19 +493,13 @@ class RRect { topLeft.x == bottomRight.y, ); - /// Construct a rounded rectangle from its bounding box and and topLeft, - /// topRight, bottomRight, and bottomLeft radii. - /// - /// The corner radii default to [Radius.zero], i.e. right-angled corners RRect.fromRectAndCorners( - Rect rect, - { - Radius topLeft = Radius.zero, - Radius topRight = Radius.zero, - Radius bottomRight = Radius.zero, - Radius bottomLeft = Radius.zero - } - ) : this._raw( + Rect rect, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( top: rect.top, left: rect.left, right: rect.right, @@ -1208,62 +549,26 @@ class RRect { assert(blRadiusY != null), // ignore: unnecessary_null_comparison this.webOnlyUniformRadii = uniformRadii; - /// The offset of the left edge of this rectangle from the x axis. final double left; - - /// The offset of the top edge of this rectangle from the y axis. final double top; - - /// The offset of the right edge of this rectangle from the x axis. final double right; - - /// The offset of the bottom edge of this rectangle from the y axis. final double bottom; - - /// The top-left horizontal radius. final double tlRadiusX; - - /// The top-left vertical radius. final double tlRadiusY; - - /// The top-left [Radius]. Radius get tlRadius => Radius.elliptical(tlRadiusX, tlRadiusY); - - /// The top-right horizontal radius. final double trRadiusX; - - /// The top-right vertical radius. final double trRadiusY; - - /// The top-right [Radius]. Radius get trRadius => Radius.elliptical(trRadiusX, trRadiusY); - - /// The bottom-right horizontal radius. final double brRadiusX; - - /// The bottom-right vertical radius. final double brRadiusY; - - /// The bottom-right [Radius]. Radius get brRadius => Radius.elliptical(brRadiusX, brRadiusY); - - /// The bottom-left horizontal radius. final double blRadiusX; - - /// The bottom-left vertical radius. final double blRadiusY; - - /// If radii is equal for all corners. // webOnly final bool webOnlyUniformRadii; - - /// The bottom-left [Radius]. Radius get blRadius => Radius.elliptical(blRadiusX, blRadiusY); - - /// A rounded rectangle with all the values set to zero. static const RRect zero = RRect._raw(); - /// Returns a new [RRect] translated by the given offset. RRect shift(Offset offset) { return RRect._raw( left: left + offset.dx, @@ -1281,8 +586,6 @@ class RRect { ); } - /// Returns a new [RRect] with edges and radii moved outwards by the given - /// delta. RRect inflate(double delta) { return RRect._raw( left: left - delta, @@ -1300,22 +603,10 @@ class RRect { ); } - /// Returns a new [RRect] with edges and radii moved inwards by the given delta. RRect deflate(double delta) => inflate(-delta); - - /// The distance between the left and right edges of this rectangle. double get width => right - left; - - /// The distance between the top and bottom edges of this rectangle. double get height => bottom - top; - - /// The bounding box of this rounded rectangle (the rectangle with no rounded corners). Rect get outerRect => Rect.fromLTRB(left, top, right, bottom); - - /// The non-rounded rectangle that is constrained by the smaller of the two - /// diagonals, with each diagonal traveling through the middle of the curve - /// corners. The middle of a corner is the intersection of the curve with its - /// respective quadrant bisector. Rect get safeInnerRect { const double kInsetFactor = 0.29289321881; // 1-cos(pi/4) @@ -1332,12 +623,6 @@ class RRect { ); } - /// The rectangle that would be formed using the axis-aligned intersection of - /// the sides of the rectangle, i.e., the rectangle formed from the - /// inner-most centers of the ellipses that form the corners. This is the - /// intersection of the [wideMiddleRect] and the [tallMiddleRect]. If any of - /// the intersections are void, the resulting [Rect] will have negative width - /// or height. Rect get middleRect { final double leftRadius = math.max(blRadiusX, tlRadiusX); final double topRadius = math.max(tlRadiusY, trRadiusY); @@ -1351,10 +636,6 @@ class RRect { ); } - /// The biggest rectangle that is entirely inside the rounded rectangle and - /// has the full width of the rounded rectangle. If the rounded rectangle does - /// not have an axis-aligned intersection of its left and right side, the - /// resulting [Rect] will have negative width or height. Rect get wideMiddleRect { final double topRadius = math.max(tlRadiusY, trRadiusY); final double bottomRadius = math.max(brRadiusY, blRadiusY); @@ -1366,10 +647,6 @@ class RRect { ); } - /// The biggest rectangle that is entirely inside the rounded rectangle and - /// has the full height of the rounded rectangle. If the rounded rectangle - /// does not have an axis-aligned intersection of its top and bottom side, the - /// resulting [Rect] will have negative width or height. Rect get tallMiddleRect { final double leftRadius = math.max(blRadiusX, tlRadiusX); final double rightRadius = math.max(trRadiusX, brRadiusX); @@ -1381,23 +658,15 @@ class RRect { ); } - /// Whether this rounded rectangle encloses a non-zero area. - /// Negative areas are considered empty. bool get isEmpty => left >= right || top >= bottom; - - /// Whether all coordinates of this rounded rectangle are finite. bool get isFinite => left.isFinite && top.isFinite && right.isFinite && bottom.isFinite; - - /// Whether this rounded rectangle is a simple rectangle with zero - /// corner radii. bool get isRect { - return (tlRadiusX == 0.0 || tlRadiusY == 0.0) && - (trRadiusX == 0.0 || trRadiusY == 0.0) && - (blRadiusX == 0.0 || blRadiusY == 0.0) && - (brRadiusX == 0.0 || brRadiusY == 0.0); + return (tlRadiusX == 0.0 || tlRadiusY == 0.0) + && (trRadiusX == 0.0 || trRadiusY == 0.0) + && (blRadiusX == 0.0 || blRadiusY == 0.0) + && (brRadiusX == 0.0 || brRadiusY == 0.0); } - /// Whether this rounded rectangle has a side with no straight section. bool get isStadium { return tlRadius == trRadius && trRadius == brRadius @@ -1405,7 +674,6 @@ class RRect { && (width <= 2.0 * tlRadiusX || height <= 2.0 * tlRadiusY); } - /// Whether this rounded rectangle has no side with a straight section. bool get isEllipse { return tlRadius == trRadius && trRadius == brRadius @@ -1414,24 +682,12 @@ class RRect { && height <= 2.0 * tlRadiusY; } - /// Whether this rounded rectangle would draw as a circle. bool get isCircle => width == height && isEllipse; - - /// The lesser of the magnitudes of the [width] and the [height] of this - /// rounded rectangle. double get shortestSide => math.min(width.abs(), height.abs()); - - /// The greater of the magnitudes of the [width] and the [height] of this - /// rounded rectangle. double get longestSide => math.max(width.abs(), height.abs()); - - /// Whether any of the dimensions are `NaN`. bool get hasNaN => left.isNaN || top.isNaN || right.isNaN || bottom.isNaN || trRadiusX.isNaN || trRadiusY.isNaN || tlRadiusX.isNaN || tlRadiusY.isNaN || brRadiusX.isNaN || brRadiusY.isNaN || blRadiusX.isNaN || blRadiusY.isNaN; - - /// The offset to the point halfway between the left and right and the top and - /// bottom edges of this rectangle. Offset get center => Offset(left + width / 2.0, top + height / 2.0); // Returns the minimum between min and scale to which radius1 and radius2 @@ -1443,15 +699,6 @@ class RRect { return min; } - /// Scales all radii so that on each side their sum will not exceed the size - /// of the width/height. - /// - /// Skia already handles RRects with radii that are too large in this way. - /// Therefore, this method is only needed for RRect use cases that require - /// the appropriately scaled radii values. - /// - /// See the [Skia scaling implementation](https://github.com/google/skia/blob/master/src/core/SkRRect.cpp) - /// for more details. RRect scaleRadii() { double scale = 1.0; scale = _getMin(scale, blRadiusY, tlRadiusY, height); @@ -1492,13 +739,6 @@ class RRect { ); } - /// Whether the point specified by the given offset (which is assumed to be - /// relative to the origin) lies inside the rounded rectangle. - /// - /// This method may allocate (and cache) a copy of the object with normalized - /// radii the first time it is called on a particular [RRect] instance. When - /// using this method, prefer to reuse existing [RRect]s rather than - /// recreating the object each time. bool contains(Offset point) { if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) return false; // outside bounding box @@ -1547,21 +787,6 @@ class RRect { return true; } - /// Linearly interpolate between two rounded rectangles. - /// - /// If either is null, this function substitutes [RRect.zero] instead. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static RRect? lerp(RRect? a, RRect? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -1667,37 +892,9 @@ class RRect { ')'; } } - -/// A transform consisting of a translation, a rotation, and a uniform scale. -/// -/// Used by [Canvas.drawAtlas]. This is a more efficient way to represent these -/// simple transformations than a full matrix. // Modeled after Skia's SkRSXform. + class RSTransform { - /// Creates an RSTransform. - /// - /// An [RSTransform] expresses the combination of a translation, a rotation - /// around a particular point, and a scale factor. - /// - /// The first argument, `scos`, is the cosine of the rotation, multiplied by - /// the scale factor. - /// - /// The second argument, `ssin`, is the sine of the rotation, multiplied by - /// that same scale factor. - /// - /// The third argument is the x coordinate of the translation, minus the - /// `scos` argument multiplied by the x-coordinate of the rotation point, plus - /// the `ssin` argument multiplied by the y-coordinate of the rotation point. - /// - /// The fourth argument is the y coordinate of the translation, minus the `ssin` - /// argument multiplied by the x-coordinate of the rotation point, minus the - /// `scos` argument multiplied by the y-coordinate of the rotation point. - /// - /// The [RSTransform.fromComponents] method may be a simpler way to - /// construct these values. However, if there is a way to factor out the - /// computations of the sine and cosine of the rotation so that they can be - /// reused over multiple calls to this constructor, it may be more efficient - /// to directly use this constructor instead. RSTransform(double scos, double ssin, double tx, double ty) { _value ..[0] = scos @@ -1705,33 +902,13 @@ class RSTransform { ..[2] = tx ..[3] = ty; } - - /// Creates an RSTransform from its individual components. - /// - /// The `rotation` parameter gives the rotation in radians. - /// - /// The `scale` parameter describes the uniform scale factor. - /// - /// The `anchorX` and `anchorY` parameters give the coordinate of the point - /// around which to rotate. - /// - /// The `translateX` and `translateY` parameters give the coordinate of the - /// offset by which to translate. - /// - /// This constructor computes the arguments of the [new RSTransform] - /// constructor and then defers to that constructor to actually create the - /// object. If many [RSTransform] objects are being created and there is a way - /// to factor out the computations of the sine and cosine of the rotation - /// (which are computed each time this constructor is called) and reuse them - /// over multiple [RSTransform] objects, it may be more efficient to directly - /// use the more direct [new RSTransform] constructor instead. factory RSTransform.fromComponents({ required double rotation, required double scale, required double anchorX, required double anchorY, required double translateX, - required double translateY + required double translateY, }) { final double scos = math.cos(rotation) * scale; final double ssin = math.sin(rotation) * scale; @@ -1741,20 +918,8 @@ class RSTransform { } final Float32List _value = Float32List(4); - - /// The cosine of the rotation multiplied by the scale factor. double get scos => _value[0]; - - /// The sine of the rotation multiplied by that same scale factor. double get ssin => _value[1]; - - /// The x coordinate of the translation, minus [scos] multiplied by the - /// x-coordinate of the rotation point, plus [ssin] multiplied by the - /// y-coordinate of the rotation point. double get tx => _value[2]; - - /// The y coordinate of the translation, minus [ssin] multiplied by the - /// x-coordinate of the rotation point, minus [scos] multiplied by the - /// y-coordinate of the rotation point. double get ty => _value[3]; } diff --git a/lib/web_ui/lib/src/ui/hash_codes.dart b/lib/web_ui/lib/src/ui/hash_codes.dart index e91fb1cc5f46c..58efa17c69588 100644 --- a/lib/web_ui/lib/src/ui/hash_codes.dart +++ b/lib/web_ui/lib/src/ui/hash_codes.dart @@ -5,10 +5,13 @@ // @dart = 2.10 part of ui; -class _HashEnd { const _HashEnd(); } +class _HashEnd { + const _HashEnd(); +} + const _HashEnd _hashEnd = _HashEnd(); -/// Jenkins hash function, optimized for small integers. +// Jenkins hash function, optimized for small integers. // // Borrowed from the dart sdk: sdk/lib/math/jenkins_smi_hash.dart. class _Jenkins { @@ -28,28 +31,28 @@ class _Jenkins { } } -/// Combine up to twenty objects' hash codes into one value. -/// -/// If you only need to handle one object's hash code, then just refer to its -/// [Object.hashCode] getter directly. -/// -/// If you need to combine an arbitrary number of objects from a [List] or other -/// [Iterable], use [hashList]. The output of [hashList] can be used as one of -/// the arguments to this function. -/// -/// For example: -/// -/// ```dart -/// int hashCode => hashValues(foo, bar, hashList(quux), baz); -/// ``` int hashValues( - Object? arg01, Object? arg02, [ Object? arg03 = _hashEnd, - Object? arg04 = _hashEnd, Object? arg05 = _hashEnd, Object? arg06 = _hashEnd, - Object? arg07 = _hashEnd, Object? arg08 = _hashEnd, Object? arg09 = _hashEnd, - Object? arg10 = _hashEnd, Object? arg11 = _hashEnd, Object? arg12 = _hashEnd, - Object? arg13 = _hashEnd, Object? arg14 = _hashEnd, Object? arg15 = _hashEnd, - Object? arg16 = _hashEnd, Object? arg17 = _hashEnd, Object? arg18 = _hashEnd, - Object? arg19 = _hashEnd, Object? arg20 = _hashEnd ]) { + Object? arg01, + Object? arg02, [ + Object? arg03 = _hashEnd, + Object? arg04 = _hashEnd, + Object? arg05 = _hashEnd, + Object? arg06 = _hashEnd, + Object? arg07 = _hashEnd, + Object? arg08 = _hashEnd, + Object? arg09 = _hashEnd, + Object? arg10 = _hashEnd, + Object? arg11 = _hashEnd, + Object? arg12 = _hashEnd, + Object? arg13 = _hashEnd, + Object? arg14 = _hashEnd, + Object? arg15 = _hashEnd, + Object? arg16 = _hashEnd, + Object? arg17 = _hashEnd, + Object? arg18 = _hashEnd, + Object? arg19 = _hashEnd, + Object? arg20 = _hashEnd, +]) { int result = 0; result = _Jenkins.combine(result, arg01); result = _Jenkins.combine(result, arg02); @@ -111,9 +114,6 @@ int hashValues( return _Jenkins.finish(result); } -/// Combine the [Object.hashCode] values of an arbitrary number of objects from -/// an [Iterable] into one value. This function will return the same value if -/// given null as if given an empty list. int hashList(Iterable? arguments) { int result = 0; if (arguments != null) { diff --git a/lib/web_ui/lib/src/ui/initialization.dart b/lib/web_ui/lib/src/ui/initialization.dart index 6865da8121a2f..a7b06b3586def 100644 --- a/lib/web_ui/lib/src/ui/initialization.dart +++ b/lib/web_ui/lib/src/ui/initialization.dart @@ -5,7 +5,6 @@ // @dart = 2.10 part of ui; -/// Initializes the platform. Future webOnlyInitializePlatform({ engine.AssetManager? assetManager, }) { @@ -51,11 +50,6 @@ engine.FontCollection? _fontCollection; bool _webOnlyIsInitialized = false; bool get webOnlyIsInitialized => _webOnlyIsInitialized; - -/// Specifies that the platform should use the given [AssetManager] to load -/// assets. -/// -/// The given asset manager is used to initialize the font collection. Future webOnlySetAssetManager(engine.AssetManager assetManager) async { assert(assetManager != null, 'Cannot set assetManager to null'); // ignore: unnecessary_null_comparison if (assetManager == _assetManager) { @@ -71,7 +65,6 @@ Future webOnlySetAssetManager(engine.AssetManager assetManager) async { _fontCollection!.clear(); } - if (_assetManager != null) { if (engine.experimentalUseSkia) { await engine.skiaFontCollection.registerFonts(_assetManager!); @@ -85,29 +78,16 @@ Future webOnlySetAssetManager(engine.AssetManager assetManager) async { } } -/// Flag that shows whether the Flutter Testing Behavior is enabled. -/// -/// This flag can be used to decide if the code is running from a Flutter Test -/// such as a Widget test. -/// -/// For example in these tests we use a predictable-size font which makes widget -/// tests less flaky. -bool get debugEmulateFlutterTesterEnvironment => - _debugEmulateFlutterTesterEnvironment; +bool get debugEmulateFlutterTesterEnvironment => _debugEmulateFlutterTesterEnvironment; set debugEmulateFlutterTesterEnvironment(bool value) { _debugEmulateFlutterTesterEnvironment = value; if (_debugEmulateFlutterTesterEnvironment) { const Size logicalSize = Size(800.0, 600.0); - engine.window.webOnlyDebugPhysicalSizeOverride = - logicalSize * window.devicePixelRatio; + engine.window.webOnlyDebugPhysicalSizeOverride = logicalSize * window.devicePixelRatio; } } bool _debugEmulateFlutterTesterEnvironment = false; - -/// This class handles downloading assets over the network. engine.AssetManager get webOnlyAssetManager => _assetManager!; - -/// A collection of fonts that may be used by the platform. engine.FontCollection get webOnlyFontCollection => _fontCollection!; diff --git a/lib/web_ui/lib/src/ui/lerp.dart b/lib/web_ui/lib/src/ui/lerp.dart index 5cd4c8ac1a672..8287cbd4ee1cd 100644 --- a/lib/web_ui/lib/src/ui/lerp.dart +++ b/lib/web_ui/lib/src/ui/lerp.dart @@ -5,7 +5,6 @@ // @dart = 2.10 part of ui; -/// Linearly interpolate between two numbers. double? lerpDouble(num? a, num? b, double t) { if (a == null && b == null) { return null; @@ -23,7 +22,6 @@ double _lerpInt(int a, int b, double t) { return a + (b - a) * t; } -/// Same as [num.clamp] but specialized for [int]. int _clampInt(int value, int min, int max) { assert(min <= max); if (value < min) { diff --git a/lib/web_ui/lib/src/ui/natives.dart b/lib/web_ui/lib/src/ui/natives.dart index 4763db34e58a6..48bd8912ca80e 100644 --- a/lib/web_ui/lib/src/ui/natives.dart +++ b/lib/web_ui/lib/src/ui/natives.dart @@ -19,27 +19,12 @@ class _Logger { static void _printString(String? s) { print(s); } + static void _printDebugString(String? s) { html.window.console.error(s!); } } -/// Returns runtime Dart compilation trace as a UTF-8 encoded memory buffer. -/// -/// The buffer contains a list of symbols compiled by the Dart JIT at runtime up to the point -/// when this function was called. This list can be saved to a text file and passed to tools -/// such as `flutter build` or Dart `gen_snapshot` in order to precompile this code offline. -/// -/// The list has one symbol per line of the following format: `,,\n`. -/// Here are some examples: -/// -/// ``` -/// dart:core,Duration,get:inMilliseconds -/// package:flutter/src/widgets/binding.dart,::,runApp -/// file:///.../my_app.dart,::,main -/// ``` -/// -/// This function is only effective in debug and dynamic modes, and will throw in AOT mode. List saveCompilationTrace() { throw UnimplementedError(); } diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 6e8ee2975f963..06fc4f445a0fc 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -8,8 +8,7 @@ part of ui; // ignore: unused_element, Used in Shader assert. bool _offsetIsValid(Offset offset) { assert(offset != null, 'Offset argument was null.'); // ignore: unnecessary_null_comparison - assert(!offset.dx.isNaN && !offset.dy.isNaN, - 'Offset argument contained a NaN value.'); + assert(!offset.dx.isNaN && !offset.dy.isNaN, 'Offset argument contained a NaN value.'); return true; } @@ -23,12 +22,10 @@ bool _matrix4IsValid(Float32List matrix4) { void _validateColorStops(List colors, List? colorStops) { if (colorStops == null) { if (colors.length != 2) - throw ArgumentError( - '"colors" must have length 2 if "colorStops" is omitted.'); + throw ArgumentError('"colors" must have length 2 if "colorStops" is omitted.'); } else { if (colors.length != colorStops.length) - throw ArgumentError( - '"colors" and "colorStops" arguments must have equal length.'); + throw ArgumentError('"colors" and "colorStops" arguments must have equal length.'); } } @@ -36,92 +33,43 @@ Color _scaleAlpha(Color a, double factor) { return a.withAlpha(_clampInt((a.alpha * factor).round(), 0, 255)); } -/// An immutable 32 bit color value in ARGB class Color { - /// Construct a color from the lower 32 bits of an int. - /// - /// Bits 24-31 are the alpha value. - /// Bits 16-23 are the red value. - /// Bits 8-15 are the green value. - /// Bits 0-7 are the blue value. const Color(int value) : this.value = value & 0xFFFFFFFF; - - /// Construct a color from the lower 8 bits of four integers. const Color.fromARGB(int a, int r, int g, int b) : value = (((a & 0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | ((b & 0xff) << 0)) & 0xFFFFFFFF; - - /// Create a color from red, green, blue, and opacity, similar to `rgba()` in CSS. - /// - /// * `r` is [red], from 0 to 255. - /// * `g` is [green], from 0 to 255. - /// * `b` is [blue], from 0 to 255. - /// * `opacity` is alpha channel of this color as a double, with 0.0 being - /// transparent and 1.0 being fully opaque. - /// - /// Out of range values are brought into range using modulo 255. - /// - /// See also [fromARGB], which takes the opacity as an integer value. const Color.fromRGBO(int r, int g, int b, double opacity) : value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | ((b & 0xff) << 0)) & 0xFFFFFFFF; - - /// A 32 bit value representing this color. - /// - /// Bits 24-31 are the alpha value. - /// Bits 16-23 are the red value. - /// Bits 8-15 are the green value. - /// Bits 0-7 are the blue value. final int value; - - /// The alpha channel of this color in an 8 bit value. int get alpha => (0xff000000 & value) >> 24; - - /// The alpha channel of this color as a double. double get opacity => alpha / 0xFF; - - /// The red channel of this color in an 8 bit value. int get red => (0x00ff0000 & value) >> 16; - - /// The green channel of this color in an 8 bit value. int get green => (0x0000ff00 & value) >> 8; - - /// The blue channel of this color in an 8 bit value. int get blue => (0x000000ff & value) >> 0; - - /// Returns a new color that matches this color with the alpha channel - /// replaced with a (which ranges from 0 to 255). Color withAlpha(int a) { return Color.fromARGB(a, red, green, blue); } - /// Returns a new color that matches this color with the alpha channel - /// replaced with the given opacity (which ranges from 0.0 to 1.0). Color withOpacity(double opacity) { assert(opacity >= 0.0 && opacity <= 1.0); return withAlpha((255.0 * opacity).round()); } - /// Returns a new color that matches this color with the red channel replaced - /// with r. Color withRed(int r) { return Color.fromARGB(alpha, r, green, blue); } - /// Returns a new color that matches this color with the green channel - /// replaced with g. Color withGreen(int g) { return Color.fromARGB(alpha, red, g, blue); } - /// Returns a new color that matches this color with the blue channel replaced - /// with b. Color withBlue(int b) { return Color.fromARGB(alpha, red, green, b); } @@ -134,12 +82,6 @@ class Color { return math.pow((component + 0.055) / 1.055, 2.4) as double; } - /// Returns a brightness value between 0 for darkest and 1 for lightest. - /// - /// Represents the relative luminance of the color. This value is - /// computationally expensive to calculate. - /// - /// See . double computeLuminance() { // See final double R = _linearizeColorComponent(red / 0xFF); @@ -148,28 +90,6 @@ class Color { return 0.2126 * R + 0.7152 * G + 0.0722 * B; } - /// Linearly interpolate between two colors. - /// - /// This is intended to be fast but as a result may be ugly. Consider - /// [HSVColor] or writing custom logic for interpolating colors. - /// - /// If either color is null, this function linearly interpolates from a - /// transparent instance of the other color. This is usually preferable to - /// interpolating from [material.Colors.transparent] (`const - /// Color(0x00000000)`), which is specifically transparent _black_. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). Each channel - /// will be clamped to the range 0 to 255. - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Color? lerp(Color? a, Color? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -192,14 +112,6 @@ class Color { } } - /// Combine the foreground color as a transparent color over top - /// of a background color, and return the resulting combined color. - /// - /// This uses standard alpha blending ("SRC over DST") rules to produce a - /// blended color from two colors. This can be used as a performance - /// enhancement when trying to avoid needless alpha blending compositing - /// operations for two things that are solid colors with the same shape, but - /// overlay each other: instead, just paint one with the combined color. static Color alphaBlend(Color foreground, Color background) { final int alpha = foreground.alpha; if (alpha == 0x00) { @@ -230,9 +142,6 @@ class Color { } } - /// Returns an alpha value representative of the provided [opacity] value. - /// - /// The [opacity] value may not be null. static int getAlphaFromOpacity(double opacity) { assert(opacity != null); // ignore: unnecessary_null_comparison return (opacity.clamp(0.0, 1.0) * 255).round(); @@ -246,8 +155,7 @@ class Color { if (other.runtimeType != runtimeType) { return false; } - return other is Color - && other.value == value; + return other is Color && other.value == value; } @override @@ -259,836 +167,110 @@ class Color { } } -/// Styles to use for line endings. -/// -/// See [Paint.strokeCap]. enum StrokeCap { - /// Begin and end contours with a flat edge and no extension. butt, - - /// Begin and end contours with a semi-circle extension. round, - - /// Begin and end contours with a half square extension. This is - /// similar to extending each contour by half the stroke width (as - /// given by [Paint.strokeWidth]). square, } -/// Styles to use for line segment joins. -/// -/// This only affects line joins for polygons drawn by [Canvas.drawPath] and -/// rectangles, not points drawn as lines with [Canvas.drawPoints]. -/// -/// See also: -/// -/// * [Paint.strokeJoin] and [Paint.strokeMiterLimit] for how this value is -/// used. -/// * [StrokeCap] for the different kinds of line endings. // These enum values must be kept in sync with SkPaint::Join. enum StrokeJoin { - /// Joins between line segments form sharp corners. - /// - /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4} - /// - /// The center of the line segment is colored in the diagram above to - /// highlight the join, but in normal usage the join is the same color as the - /// line. - /// - /// See also: - /// - /// * [Paint.strokeJoin], used to set the line segment join style to this - /// value. - /// * [Paint.strokeMiterLimit], used to define when a miter is drawn instead - /// of a bevel when the join is set to this value. miter, - - /// Joins between line segments are semi-circular. - /// - /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_join.mp4} - /// - /// The center of the line segment is colored in the diagram above to - /// highlight the join, but in normal usage the join is the same color as the - /// line. - /// - /// See also: - /// - /// * [Paint.strokeJoin], used to set the line segment join style to this - /// value. round, - - /// Joins between line segments connect the corners of the butt ends of the - /// line segments to give a beveled appearance. - /// - /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/bevel_join.mp4} - /// - /// The center of the line segment is colored in the diagram above to - /// highlight the join, but in normal usage the join is the same color as the - /// line. - /// - /// See also: - /// - /// * [Paint.strokeJoin], used to set the line segment join style to this - /// value. bevel, } -/// Strategies for painting shapes and paths on a canvas. -/// -/// See [Paint.style]. enum PaintingStyle { - /// Apply the [Paint] to the inside of the shape. For example, when - /// applied to the [Paint.drawCircle] call, this results in a disc - /// of the given size being painted. fill, - - /// Apply the [Paint] to the edge of the shape. For example, when - /// applied to the [Paint.drawCircle] call, this results is a hoop - /// of the given size being painted. The line drawn on the edge will - /// be the width given by the [Paint.strokeWidth] property. stroke, } -/// Algorithms to use when painting on the canvas. -/// -/// When drawing a shape or image onto a canvas, different algorithms can be -/// used to blend the pixels. The different values of [BlendMode] specify -/// different such algorithms. -/// -/// Each algorithm has two inputs, the _source_, which is the image being drawn, -/// and the _destination_, which is the image into which the source image is -/// being composited. The destination is often thought of as the _background_. -/// The source and destination both have four color channels, the red, green, -/// blue, and alpha channels. These are typically represented as numbers in the -/// range 0.0 to 1.0. The output of the algorithm also has these same four -/// channels, with values computed from the source and destination. -/// -/// The documentation of each value below describes how the algorithm works. In -/// each case, an image shows the output of blending a source image with a -/// destination image. In the images below, the destination is represented by an -/// image with horizontal lines and an opaque landscape photograph, and the -/// source is represented by an image with vertical lines (the same lines but -/// rotated) and a bird clip-art image. The [src] mode shows only the source -/// image, and the [dst] mode shows only the destination image. In the -/// documentation below, the transparency is illustrated by a checkerboard -/// pattern. The [clear] mode drops both the source and destination, resulting -/// in an output that is entirely transparent (illustrated by a solid -/// checkerboard pattern). -/// -/// The horizontal and vertical bars in these images show the red, green, and -/// blue channels with varying opacity levels, then all three color channels -/// together with those same varying opacity levels, then all three color -/// channels set to zero with those varying opacity levels, then two bars -/// showing a red/green/blue repeating gradient, the first with full opacity and -/// the second with partial opacity, and finally a bar with the three color -/// channels set to zero but the opacity varying in a repeating gradient. -/// -/// ## Application to the [Canvas] API -/// -/// When using [Canvas.saveLayer] and [Canvas.restore], the blend mode of the -/// [Paint] given to the [Canvas.saveLayer] will be applied when -/// [Canvas.restore] is called. Each call to [Canvas.saveLayer] introduces a new -/// layer onto which shapes and images are painted; when [Canvas.restore] is -/// called, that layer is then composited onto the parent layer, with the source -/// being the most-recently-drawn shapes and images, and the destination being -/// the parent layer. (For the first [Canvas.saveLayer] call, the parent layer -/// is the canvas itself.) -/// -/// See also: -/// -/// * [Paint.blendMode], which uses [BlendMode] to define the compositing -/// strategy. enum BlendMode { // This list comes from Skia's SkXfermode.h and the values (order) should be // kept in sync. // See: https://skia.org/user/api/skpaint#SkXfermode - - /// Drop both the source and destination images, leaving nothing. - /// - /// This corresponds to the "clear" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_clear.png) clear, - - /// Drop the destination image, only paint the source image. - /// - /// Conceptually, the destination is first cleared, then the source image is - /// painted. - /// - /// This corresponds to the "Copy" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_src.png) src, - - /// Drop the source image, only paint the destination image. - /// - /// Conceptually, the source image is discarded, leaving the destination - /// untouched. - /// - /// This corresponds to the "Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dst.png) dst, - - /// Composite the source image over the destination image. - /// - /// This is the default value. It represents the most intuitive case, where - /// shapes are painted on top of what is below, with transparent areas showing - /// the destination layer. - /// - /// This corresponds to the "Source over Destination" Porter-Duff operator, - /// also known as the Painter's Algorithm. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcOver.png) srcOver, - - /// Composite the source image under the destination image. - /// - /// This is the opposite of [srcOver]. - /// - /// This corresponds to the "Destination over Source" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstOver.png) - /// - /// This is useful when the source image should have been painted before the - /// destination image, but could not be. dstOver, - - /// Show the source image, but only where the two images overlap. The - /// destination image is not rendered, it is treated merely as a mask. The - /// color channels of the destination are ignored, only the opacity has an - /// effect. - /// - /// To show the destination image instead, consider [dstIn]. - /// - /// To reverse the semantic of the mask (only showing the source where the - /// destination is absent, rather than where it is present), consider - /// [srcOut]. - /// - /// This corresponds to the "Source in Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcIn.png) srcIn, - - /// Show the destination image, but only where the two images overlap. The - /// source image is not rendered, it is treated merely as a mask. The color - /// channels of the source are ignored, only the opacity has an effect. - /// - /// To show the source image instead, consider [srcIn]. - /// - /// To reverse the semantic of the mask (only showing the source where the - /// destination is present, rather than where it is absent), consider - /// [dstOut]. - /// - /// This corresponds to the "Destination in Source" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstIn.png) dstIn, - - /// Show the source image, but only where the two images do not overlap. The - /// destination image is not rendered, it is treated merely as a mask. The color - /// channels of the destination are ignored, only the opacity has an effect. - /// - /// To show the destination image instead, consider [dstOut]. - /// - /// To reverse the semantic of the mask (only showing the source where the - /// destination is present, rather than where it is absent), consider [srcIn]. - /// - /// This corresponds to the "Source out Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcOut.png) srcOut, - - /// Show the destination image, but only where the two images do not overlap. - /// The source image is not rendered, it is treated merely as a mask. The - /// color channels of the source are ignored, only the opacity has an effect. - /// - /// To show the source image instead, consider [srcOut]. - /// - /// To reverse the semantic of the mask (only showing the destination where - /// the source is present, rather than where it is absent), consider [dstIn]. - /// - /// This corresponds to the "Destination out Source" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstOut.png) dstOut, - - /// Composite the source image over the destination image, but only where it - /// overlaps the destination. - /// - /// This corresponds to the "Source atop Destination" Porter-Duff operator. - /// - /// This is essentially the [srcOver] operator, but with the output's opacity - /// channel being set to that of the destination image instead of being a - /// combination of both image's opacity channels. - /// - /// For a variant with the destination on top instead of the source, see - /// [dstATop]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcATop.png) srcATop, - - /// Composite the destination image over the source image, but only where it - /// overlaps the source. - /// - /// This corresponds to the "Destination atop Source" Porter-Duff operator. - /// - /// This is essentially the [dstOver] operator, but with the output's opacity - /// channel being set to that of the source image instead of being a - /// combination of both image's opacity channels. - /// - /// For a variant with the source on top instead of the destination, see - /// [srcATop]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstATop.png) dstATop, - - /// Apply a bitwise `xor` operator to the source and destination images. This - /// leaves transparency where they would overlap. - /// - /// This corresponds to the "Source xor Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_xor.png) xor, - - /// Sum the components of the source and destination images. - /// - /// Transparency in a pixel of one of the images reduces the contribution of - /// that image to the corresponding output pixel, as if the color of that - /// pixel in that image was darker. - /// - /// This corresponds to the "Source plus Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_plus.png) plus, - - /// Multiply the color components of the source and destination images. - /// - /// This can only result in the same or darker colors (multiplying by white, - /// 1.0, results in no change; multiplying by black, 0.0, results in black). - /// - /// When compositing two opaque images, this has similar effect to overlapping - /// two transparencies on a projector. - /// - /// For a variant that also multiplies the alpha channel, consider [multiply]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_modulate.png) - /// - /// See also: - /// - /// * [screen], which does a similar computation but inverted. - /// * [overlay], which combines [modulate] and [screen] to favor the - /// destination image. - /// * [hardLight], which combines [modulate] and [screen] to favor the - /// source image. modulate, // Following blend modes are defined in the CSS Compositing standard. - - /// Multiply the inverse of the components of the source and destination - /// images, and inverse the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// This is essentially the same as [modulate] blend mode, but with the values - /// of the colors inverted before the multiplication and the result being - /// inverted back before rendering. - /// - /// This can only result in the same or lighter colors (multiplying by black, - /// 1.0, results in no change; multiplying by white, 0.0, results in white). - /// Similarly, in the alpha channel, it can only result in more opaque colors. - /// - /// This has similar effect to two projectors displaying their images on the - /// same screen simultaneously. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_screen.png) - /// - /// See also: - /// - /// * [modulate], which does a similar computation but without inverting the - /// values. - /// * [overlay], which combines [modulate] and [screen] to favor the - /// destination image. - /// * [hardLight], which combines [modulate] and [screen] to favor the - /// source image. screen, // The last coeff mode. - - /// Multiply the components of the source and destination images after - /// adjusting them to favor the destination. - /// - /// Specifically, if the destination value is smaller, this multiplies it with - /// the source value, whereas is the source value is smaller, it multiplies - /// the inverse of the source value with the inverse of the destination value, - /// then inverts the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_overlay.png) - /// - /// See also: - /// - /// * [modulate], which always multiplies the values. - /// * [screen], which always multiplies the inverses of the values. - /// * [hardLight], which is similar to [overlay] but favors the source image - /// instead of the destination image. overlay, - - /// Composite the source and destination image by choosing the lowest value - /// from each color channel. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_darken.png) darken, - - /// Composite the source and destination image by choosing the highest value - /// from each color channel. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_lighten.png) lighten, - - /// Divide the destination by the inverse of the source. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_colorDodge.png) colorDodge, - - /// Divide the inverse of the destination by the source, and inverse the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_colorBurn.png) colorBurn, - - /// Multiply the components of the source and destination images after - /// adjusting them to favor the source. - /// - /// Specifically, if the source value is smaller, this multiplies it with the - /// destination value, whereas is the destination value is smaller, it - /// multiplies the inverse of the destination value with the inverse of the - /// source value, then inverts the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hardLight.png) - /// - /// See also: - /// - /// * [modulate], which always multiplies the values. - /// * [screen], which always multiplies the inverses of the values. - /// * [overlay], which is similar to [hardLight] but favors the destination - /// image instead of the source image. hardLight, - - /// Use [colorDodge] for source values below 0.5 and [colorBurn] for source - /// values above 0.5. - /// - /// This results in a similar but softer effect than [overlay]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_softLight.png) - /// - /// See also: - /// - /// * [color], which is a more subtle tinting effect. softLight, - - /// Subtract the smaller value from the bigger value for each channel. - /// - /// Compositing black has no effect; compositing white inverts the colors of - /// the other image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// The effect is similar to [exclusion] but harsher. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_difference.png) difference, - - /// Subtract double the product of the two images from the sum of the two - /// images. - /// - /// Compositing black has no effect; compositing white inverts the colors of - /// the other image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// The effect is similar to [difference] but softer. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_exclusion.png) exclusion, - - /// Multiply the components of the source and destination images, including - /// the alpha channel. - /// - /// This can only result in the same or darker colors (multiplying by white, - /// 1.0, results in no change; multiplying by black, 0.0, results in black). - /// - /// Since the alpha channel is also multiplied, a fully-transparent pixel - /// (opacity 0.0) in one image results in a fully transparent pixel in the - /// output. This is similar to [dstIn], but with the colors combined. - /// - /// For a variant that multiplies the colors but does not multiply the alpha - /// channel, consider [modulate]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_multiply.png) multiply, // The last separable mode. - - /// Take the hue of the source image, and the saturation and luminosity of the - /// destination image. - /// - /// The effect is to tint the destination image with the source image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their hue from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hue.png) - /// - /// See also: - /// - /// * [color], which is a similar but stronger effect as it also applies the - /// saturation of the source image. - /// * [HSVColor], which allows colors to be expressed using Hue rather than - /// the red/green/blue channels of [Color]. hue, - - /// Take the saturation of the source image, and the hue and luminosity of the - /// destination image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their saturation from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hue.png) - /// - /// See also: - /// - /// * [color], which also applies the hue of the source image. - /// * [luminosity], which applies the luminosity of the source image to the - /// destination. saturation, - - /// Take the hue and saturation of the source image, and the luminosity of the - /// destination image. - /// - /// The effect is to tint the destination image with the source image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their hue and saturation from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_color.png) - /// - /// See also: - /// - /// * [hue], which is a similar but weaker effect. - /// * [softLight], which is a similar tinting effect but also tints white. - /// * [saturation], which only applies the saturation of the source image. color, - - /// Take the luminosity of the source image, and the hue and saturation of the - /// destination image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their luminosity from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_luminosity.png) - /// - /// See also: - /// - /// * [saturation], which applies the saturation of the source image to the - /// destination. - /// * [ImageFilter.blur], which can be used with [BackdropFilter] for a - /// related effect. luminosity, } -/// Different ways to clip a widget's content. enum Clip { - /// No clip at all. - /// - /// This is the default option for most widgets: if the content does not - /// overflow the widget boundary, don't pay any performance cost for clipping. - /// - /// If the content does overflow, please explicitly specify the following - /// [Clip] options: - /// * [hardEdge], which is the fastest clipping, but with lower fidelity. - /// * [antiAlias], which is a little slower than [hardEdge], but with smoothed edges. - /// * [antiAliasWithSaveLayer], which is much slower than [antiAlias], and should - /// rarely be used. none, - - /// Clip, but do not apply anti-aliasing. - /// - /// This mode enables clipping, but curves and non-axis-aligned straight lines will be - /// jagged as no effort is made to anti-alias. - /// - /// Faster than other clipping modes, but slower than [none]. - /// - /// This is a reasonable choice when clipping is needed, if the container is an axis- - /// aligned rectangle or an axis-aligned rounded rectangle with very small corner radii. - /// - /// See also: - /// - /// * [antiAlias], which is more reasonable when clipping is needed and the shape is not - /// an axis-aligned rectangle. hardEdge, - - /// Clip with anti-aliasing. - /// - /// This mode has anti-aliased clipping edges to achieve a smoother look. - /// - /// It' s much faster than [antiAliasWithSaveLayer], but slower than [hardEdge]. - /// - /// This will be the common case when dealing with circles and arcs. - /// - /// Different from [hardEdge] and [antiAliasWithSaveLayer], this clipping may have - /// bleeding edge artifacts. - /// (See https://fiddle.skia.org/c/21cb4c2b2515996b537f36e7819288ae for an example.) - /// - /// See also: - /// - /// * [hardEdge], which is a little faster, but with lower fidelity. - /// * [antiAliasWithSaveLayer], which is much slower, but can avoid the - /// bleeding edges if there's no other way. - /// * [Paint.isAntiAlias], which is the anti-aliasing switch for general draw operations. antiAlias, - - /// Clip with anti-aliasing and saveLayer immediately following the clip. - /// - /// This mode not only clips with anti-aliasing, but also allocates an offscreen - /// buffer. All subsequent paints are carried out on that buffer before finally - /// being clipped and composited back. - /// - /// This is very slow. It has no bleeding edge artifacts (that [antiAlias] has) - /// but it changes the semantics as an offscreen buffer is now introduced. - /// (See https://github.com/flutter/flutter/issues/18057#issuecomment-394197336 - /// for a difference between paint without saveLayer and paint with saveLayer.) - /// - /// This will be only rarely needed. One case where you might need this is if - /// you have an image overlaid on a very different background color. In these - /// cases, consider whether you can avoid overlaying multiple colors in one - /// spot (e.g. by having the background color only present where the image is - /// absent). If you can, [antiAlias] would be fine and much faster. - /// - /// See also: - /// - /// * [antiAlias], which is much faster, and has similar clipping results. antiAliasWithSaveLayer, } -/// A description of the style to use when drawing on a [Canvas]. -/// -/// Most APIs on [Canvas] take a [Paint] object to describe the style -/// to use for that operation. abstract class Paint { - /// Constructs an empty [Paint] object with all fields initialized to - /// their defaults. - factory Paint() => - engine.experimentalUseSkia ? engine.CkPaint() : engine.SurfacePaint(); - - /// Whether to dither the output when drawing images. - /// - /// If false, the default value, dithering will be enabled when the input - /// color depth is higher than the output color depth. For example, - /// drawing an RGB8 image onto an RGB565 canvas. - /// - /// This value also controls dithering of [shader]s, which can make - /// gradients appear smoother. - /// - /// Whether or not dithering affects the output is implementation defined. - /// Some implementations may choose to ignore this completely, if they're - /// unable to control dithering. - /// - /// To ensure that dithering is consistently enabled for your entire - /// application, set this to true before invoking any drawing related code. + factory Paint() => engine.experimentalUseSkia ? engine.CkPaint() : engine.SurfacePaint(); static bool enableDithering = false; - - /// A blend mode to apply when a shape is drawn or a layer is composited. - /// - /// The source colors are from the shape being drawn (e.g. from - /// [Canvas.drawPath]) or layer being composited (the graphics that were drawn - /// between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying - /// the [colorFilter], if any. - /// - /// The destination colors are from the background onto which the shape or - /// layer is being composited. - /// - /// Defaults to [BlendMode.srcOver]. - /// - /// See also: - /// - /// * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite - /// the layer when [restore] is called. - /// * [BlendMode], which discusses the user of [saveLayer] with [blendMode]. BlendMode get blendMode; set blendMode(BlendMode value); - - /// Whether to paint inside shapes, the edges of shapes, or both. - /// - /// If null, defaults to [PaintingStyle.fill]. PaintingStyle get style; set style(PaintingStyle value); - - /// How wide to make edges drawn when [style] is set to - /// [PaintingStyle.stroke] or [PaintingStyle.strokeAndFill]. The - /// width is given in logical pixels measured in the direction - /// orthogonal to the direction of the path. - /// - /// The values null and 0.0 correspond to a hairline width. double get strokeWidth; set strokeWidth(double value); - - /// The kind of finish to place on the end of lines drawn when - /// [style] is set to [PaintingStyle.stroke] or - /// [PaintingStyle.strokeAndFill]. - /// - /// If null, defaults to [StrokeCap.butt], i.e. no caps. StrokeCap get strokeCap; set strokeCap(StrokeCap value); - - /// The kind of finish to use for line segment joins. - /// [style] is set to [PaintingStyle.stroke] or - /// [PaintingStyle.strokeAndFill]. Only applies to drawPath not drawPoints. - /// - /// If null, defaults to [StrokeCap.butt], i.e. no caps. StrokeJoin get strokeJoin; set strokeJoin(StrokeJoin value); - - /// Whether to apply anti-aliasing to lines and images drawn on the - /// canvas. - /// - /// Defaults to true. The value null is treated as false. bool get isAntiAlias; set isAntiAlias(bool value); Color get color; set color(Color value); - - /// Whether the colors of the image are inverted when drawn. - /// - /// Inverting the colors of an image applies a new color filter that will - /// be composed with any user provided color filters. This is primarily - /// used for implementing smart invert on iOS. bool get invertColors; set invertColors(bool value); - - /// The shader to use when stroking or filling a shape. - /// - /// When this is null, the [color] is used instead. - /// - /// See also: - /// - /// * [Gradient], a shader that paints a color gradient. - /// * [ImageShader], a shader that tiles an [Image]. - /// * [colorFilter], which overrides [shader]. - /// * [color], which is used if [shader] and [colorFilter] are null. Shader? get shader; set shader(Shader? value); - - /// A mask filter (for example, a blur) to apply to a shape after it has been - /// drawn but before it has been composited into the image. - /// - /// See [MaskFilter] for details. MaskFilter? get maskFilter; set maskFilter(MaskFilter? value); - - /// Controls the performance vs quality trade-off to use when applying - /// filters, such as [maskFilter], or when drawing images, as with - /// [Canvas.drawImageRect] or [Canvas.drawImageNine]. - /// - /// Defaults to [FilterQuality.none]. // TODO(ianh): verify that the image drawing methods actually respect this FilterQuality get filterQuality; set filterQuality(FilterQuality value); - - /// A color filter to apply when a shape is drawn or when a layer is - /// composited. - /// - /// See [ColorFilter] for details. - /// - /// When a shape is being drawn, [colorFilter] overrides [color] and [shader]. ColorFilter? get colorFilter; set colorFilter(ColorFilter? value); double get strokeMiterLimit; set strokeMiterLimit(double value); - - /// The [ImageFilter] to use when drawing raster images. - /// - /// For example, to blur an image using [Canvas.drawImage], apply an - /// [ImageFilter.blur]: - /// - /// ```dart - /// import 'dart:ui' as ui; - /// - /// ui.Image image; - /// - /// void paint(Canvas canvas, Size size) { - /// canvas.drawImage( - /// image, - /// Offset.zero, - /// Paint()..imageFilter = ui.ImageFilter.blur(sigmaX: .5, sigmaY: .5), - /// ); - /// } - /// ``` - /// - /// See also: - /// - /// * [MaskFilter], which is used for drawing geometry. ImageFilter? get imageFilter; set imageFilter(ImageFilter? value); } -/// Base class for objects such as [Gradient] and [ImageShader] which -/// correspond to shaders as used by [Paint.shader]. abstract class Shader { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. Shader._(); } -/// A shader (as used by [Paint.shader]) that renders a color gradient. -/// -/// There are several types of gradients, represented by the various -/// constructors on this class. abstract class Gradient extends Shader { - /// Creates a linear gradient from `from` to `to`. - /// - /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 - /// that specifies where `color[i]` begins in the gradient. If `colorStops` is - /// not provided, then only two stops, at 0.0 and 1.0, are implied (and - /// `color` must therefore only have two entries). - /// - /// The behavior before `from` and after `to` is described by the `tileMode` - /// argument. For details, see the [TileMode] enum. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png) - /// - /// If `from`, `to`, `colors`, or `tileMode` are null, or if `colors` or - /// `colorStops` contain null values, this constructor will throw a - /// [NoSuchMethodError]. factory Gradient.linear( Offset from, Offset to, @@ -1096,38 +278,9 @@ abstract class Gradient extends Shader { List? colorStops, TileMode tileMode = TileMode.clamp, Float64List? matrix4, - ]) => - engine.GradientLinear(from, to, colors, colorStops, tileMode, matrix4); - - /// Creates a radial gradient centered at `center` that ends at `radius` - /// distance from the center. - /// - /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 - /// that specifies where `color[i]` begins in the gradient. If `colorStops` is - /// not provided, then only two stops, at 0.0 and 1.0, are implied (and - /// `color` must therefore only have two entries). - /// - /// The behavior before and after the radius is described by the `tileMode` - /// argument. For details, see the [TileMode] enum. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png) - /// - /// If `center`, `radius`, `colors`, or `tileMode` are null, or if `colors` or - /// `colorStops` contain null values, this constructor will throw a - /// [NoSuchMethodError]. - /// - /// If `matrix4` is provided, the gradient fill will be transformed by the - /// specified 4x4 matrix relative to the local coordinate system. `matrix4` - /// must be a column-major matrix packed into a list of 16 values. - /// - /// If `focal` is provided and not equal to `center` and `focalRadius` is - /// provided and not equal to 0.0, the generated shader will be a two point - /// conical radial gradient, with `focal` being the center of the focal - /// circle and `focalRadius` being the radius of that circle. If `focal` is - /// provided and not equal to `center`, at least one of the two offsets must - /// not be equal to [Offset.zero]. + ]) => engine.experimentalUseSkia + ? engine.CkGradientLinear(from, to, colors, colorStops, tileMode, matrix4) + : engine.GradientLinear(from, to, colors, colorStops, tileMode, matrix4); factory Gradient.radial( Offset center, double radius, @@ -1143,229 +296,61 @@ abstract class Gradient extends Shader { // If focal == center and the focal radius is 0.0, it's still a regular radial gradient final Float32List? matrix32 = matrix4 != null ? engine.toMatrix32(matrix4) : null; if (focal == null || (focal == center && focalRadius == 0.0)) { - return engine.GradientRadial( - center, radius, colors, colorStops, tileMode, matrix32); + return engine.experimentalUseSkia + ? engine.CkGradientRadial(center, radius, colors, colorStops, tileMode, matrix32) + : engine.GradientRadial(center, radius, colors, colorStops, tileMode, matrix32); } else { assert(center != Offset.zero || focal != Offset.zero); // will result in exception(s) in Skia side - return engine.GradientConical(focal, focalRadius, center, radius, colors, - colorStops, tileMode, matrix32); + return engine.experimentalUseSkia + ? engine.CkGradientConical( + focal, focalRadius, center, radius, colors, colorStops, tileMode, matrix32) + : engine.GradientConical( + focal, focalRadius, center, radius, colors, colorStops, tileMode, matrix32); } } - - /// Creates a sweep gradient centered at `center` that starts at `startAngle` - /// and ends at `endAngle`. - /// - /// `startAngle` and `endAngle` should be provided in radians, with zero - /// radians being the horizontal line to the right of the `center` and with - /// positive angles going clockwise around the `center`. - /// - /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 - /// that specifies where `color[i]` begins in the gradient. If `colorStops` is - /// not provided, then only two stops, at 0.0 and 1.0, are implied (and - /// `color` must therefore only have two entries). - /// - /// The behavior before `startAngle` and after `endAngle` is described by the - /// `tileMode` argument. For details, see the [TileMode] enum. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_sweep.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_sweep.png) - /// - /// If `center`, `colors`, `tileMode`, `startAngle`, or `endAngle` are null, - /// or if `colors` or `colorStops` contain null values, this constructor will - /// throw a [NoSuchMethodError]. - /// - /// If `matrix4` is provided, the gradient fill will be transformed by the - /// specified 4x4 matrix relative to the local coordinate system. `matrix4` - /// must be a column-major matrix packed into a list of 16 values. factory Gradient.sweep( Offset center, List colors, [ List? colorStops, TileMode tileMode = TileMode.clamp, - double startAngle/*?*/ = 0.0, - double endAngle/*!*/ = math.pi * 2, + double startAngle = 0.0, + double endAngle = math.pi * 2, Float64List? matrix4, - ]) => - engine.GradientSweep( - center, colors, colorStops, tileMode, startAngle, endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null); + ]) => engine.experimentalUseSkia + ? engine.CkGradientSweep(center, colors, colorStops, tileMode, startAngle, + endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null) + : engine.GradientSweep(center, colors, colorStops, tileMode, startAngle, + endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null); } -/// Opaque handle to raw decoded image data (pixels). -/// -/// To obtain an [Image] object, use [instantiateImageCodec]. -/// -/// To draw an [Image], use one of the methods on the [Canvas] class, such as -/// [Canvas.drawImage]. abstract class Image { - /// The number of image pixels along the image's horizontal axis. int get width; - - /// The number of image pixels along the image's vertical axis. int get height; - - /// Converts the [Image] object into a byte array. - /// - /// The [format] argument specifies the format in which the bytes will be - /// returned. - /// - /// Returns a future that completes with the binary image data or an error - /// if encoding fails. - Future toByteData( - {ImageByteFormat format = ImageByteFormat.rawRgba}); - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. + Future toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}); void dispose(); @override String toString() => '[$width\u00D7$height]'; } -/// A description of a color filter to apply when drawing a shape or compositing -/// a layer with a particular [Paint]. A color filter is a function that takes -/// two colors, and outputs one color. When applied during compositing, it is -/// independently applied to each pixel of the layer being drawn before the -/// entire layer is merged with the destination. -/// -/// Instances of this class are used with [Paint.colorFilter] on [Paint] -/// objects. abstract class ColorFilter { - /// Creates a color filter that applies the blend mode given as the second - /// argument. The source color is the one given as the first argument, and the - /// destination color is the one from the layer being composited. - /// - /// The output of this filter is then composited into the background according - /// to the [Paint.blendMode], using the output of this filter as the source - /// and the background as the destination. - const factory ColorFilter.mode(Color color, BlendMode blendMode) = - engine.EngineColorFilter.mode; - - /// Construct a color filter that transforms a color by a 4x5 matrix. - /// - /// Every pixel's color value, repsented as an `[R, G, B, A]`, is matrix - /// multiplied to create a new color: - /// - /// ``` - /// | R' | | a00 a01 a02 a03 a04 | | R | - /// | G' | = | a10 a11 a22 a33 a44 | * | G | - /// | B' | | a20 a21 a22 a33 a44 | | B | - /// | A' | | a30 a31 a22 a33 a44 | | A | - /// ``` - /// - /// The matrix is in row-major order and the translation column is specified - /// in unnormalized, 0...255, space. For example, the identity matrix is: - /// - /// ``` - /// const ColorMatrix identity = ColorFilter.matrix([ - /// 1, 0, 0, 0, 0, - /// 0, 1, 0, 0, 0, - /// 0, 0, 1, 0, 0, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - /// - /// ## Examples - /// - /// An inversion color matrix: - /// - /// ``` - /// const ColorFilter invert = ColorFilter.matrix([ - /// -1, 0, 0, 0, 255, - /// 0, -1, 0, 0, 255, - /// 0, 0, -1, 0, 255, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - /// - /// A sepia-toned color matrix (values based on the [Filter Effects Spec](https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent)): - /// - /// ``` - /// const ColorFilter sepia = ColorFilter.matrix([ - /// 0.393, 0.769, 0.189, 0, 0, - /// 0.349, 0.686, 0.168, 0, 0, - /// 0.272, 0.534, 0.131, 0, 0, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - /// - /// A greyscale color filter (values based on the [Filter Effects Spec](https://www.w3.org/TR/filter-effects-1/#grayscaleEquivalent)): - /// - /// ``` - /// const ColorFilter greyscale = ColorFilter.matrix([ - /// 0.2126, 0.7152, 0.0722, 0, 0, - /// 0.2126, 0.7152, 0.0722, 0, 0, - /// 0.2126, 0.7152, 0.0722, 0, 0, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - const factory ColorFilter.matrix(List matrix) = - engine.EngineColorFilter.matrix; - - /// Construct a color filter that applies the sRGB gamma curve to the RGB - /// channels. - const factory ColorFilter.linearToSrgbGamma() = - engine.EngineColorFilter.linearToSrgbGamma; - - /// Creates a color filter that applies the inverse of the sRGB gamma curve - /// to the RGB channels. - const factory ColorFilter.srgbToLinearGamma() = - engine.EngineColorFilter.srgbToLinearGamma; - - List webOnlySerializeToCssPaint() { - throw UnsupportedError('ColorFilter for CSS paint not yet supported'); - } + const factory ColorFilter.mode(Color color, BlendMode blendMode) = engine.EngineColorFilter.mode; + const factory ColorFilter.matrix(List matrix) = engine.EngineColorFilter.matrix; + const factory ColorFilter.linearToSrgbGamma() = engine.EngineColorFilter.linearToSrgbGamma; + const factory ColorFilter.srgbToLinearGamma() = engine.EngineColorFilter.srgbToLinearGamma; } -/// Styles to use for blurs in [MaskFilter] objects. // These enum values must be kept in sync with SkBlurStyle. enum BlurStyle { // These mirror SkBlurStyle and must be kept in sync. - - /// Fuzzy inside and outside. This is useful for painting shadows that are - /// offset from the shape that ostensibly is casting the shadow. normal, - - /// Solid inside, fuzzy outside. This corresponds to drawing the shape, and - /// additionally drawing the blur. This can make objects appear brighter, - /// maybe even as if they were fluorescent. solid, - - /// Nothing inside, fuzzy outside. This is useful for painting shadows for - /// partially transparent shapes, when they are painted separately but without - /// an offset, so that the shadow doesn't paint below the shape. outer, - - /// Fuzzy inside, nothing outside. This can make shapes appear to be lit from - /// within. inner, } -/// A mask filter to apply to shapes as they are painted. A mask filter is a -/// function that takes a bitmap of color pixels, and returns another bitmap of -/// color pixels. -/// -/// Instances of this class are used with [Paint.maskFilter] on [Paint] objects. class MaskFilter { - /// Creates a mask filter that takes the shape being drawn and blurs it. - /// - /// This is commonly used to approximate shadows. - /// - /// The `style` argument controls the kind of effect to draw; see [BlurStyle]. - /// - /// The `sigma` argument controls the size of the effect. It is the standard - /// deviation of the Gaussian blur to apply. The value must be greater than - /// zero. The sigma corresponds to very roughly half the radius of the effect - /// in pixels. - /// - /// A blur is an expensive operation and should therefore be used sparingly. - /// - /// The arguments must not be null. - /// - /// See also: - /// - /// * [Canvas.drawShadow], which is a more efficient way to draw shadows. const MaskFilter.blur( this._style, this._sigma, @@ -1374,11 +359,7 @@ class MaskFilter { final BlurStyle _style; final double _sigma; - - /// On the web returns the value of sigma passed to [MaskFilter.blur]. double get webOnlySigma => _sigma; - - /// On the web returns the value of `style` passed to [MaskFilter.blur]. BlurStyle get webOnlyBlurStyle => _style; @override @@ -1391,52 +372,20 @@ class MaskFilter { @override int get hashCode => hashValues(_style, _sigma); - List webOnlySerializeToCssPaint() { - return [_style.index, _sigma]; - } - @override String toString() => 'MaskFilter.blur($_style, ${_sigma.toStringAsFixed(1)})'; } -/// Quality levels for image filters. -/// -/// See [Paint.filterQuality]. enum FilterQuality { // This list comes from Skia's SkFilterQuality.h and the values (order) should // be kept in sync. - - /// Fastest possible filtering, albeit also the lowest quality. - /// - /// Typically this implies nearest-neighbour filtering. none, - - /// Better quality than [none], faster than [medium]. - /// - /// Typically this implies bilinear interpolation. low, - - /// Better quality than [low], faster than [high]. - /// - /// Typically this implies a combination of bilinear interpolation and - /// pyramidal parametric prefiltering (mipmaps). medium, - - /// Best possible quality filtering, albeit also the slowest. - /// - /// Typically this implies bicubic interpolation or better. high, } -/// A filter operation to apply to a raster image. -/// -/// See also: -/// -/// * [BackdropFilter], a widget that applies [ImageFilter] to its rendering. -/// * [SceneBuilder.pushBackdropFilter], which is the low-level API for using -/// this class. class ImageFilter { - /// Creates an image filter that applies a Gaussian blur. factory ImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) { if (engine.experimentalUseSkia) { return engine.CkImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY); @@ -1444,128 +393,46 @@ class ImageFilter { return engine.EngineImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY); } - ImageFilter.matrix(Float64List matrix4, - {FilterQuality filterQuality = FilterQuality.low}) { + ImageFilter.matrix(Float64List matrix4, {FilterQuality filterQuality = FilterQuality.low}) { // TODO(flutter_web): add implementation. - throw UnimplementedError( - 'ImageFilter.matrix not implemented for web platform.'); + throw UnimplementedError('ImageFilter.matrix not implemented for web platform.'); // if (matrix4.length != 16) // throw ArgumentError('"matrix4" must have 16 entries.'); } } -/// The format in which image bytes should be returned when using -/// [Image.toByteData]. enum ImageByteFormat { - /// Raw RGBA format. - /// - /// Unencoded bytes, in RGBA row-primary form, 8 bits per channel. rawRgba, - - /// Raw unmodified format. - /// - /// Unencoded bytes, in the image's existing format. For example, a grayscale - /// image may use a single 8-bit channel for each pixel. rawUnmodified, - - /// PNG format. - /// - /// A loss-less compression format for images. This format is well suited for - /// images with hard edges, such as screenshots or sprites, and images with - /// text. Transparency is supported. The PNG format supports images up to - /// 2,147,483,647 pixels in either dimension, though in practice available - /// memory provides a more immediate limitation on maximum image size. - /// - /// PNG images normally use the `.png` file extension and the `image/png` MIME - /// type. - /// - /// See also: - /// - /// * , the Wikipedia page on PNG. - /// * , the PNG standard. png, } -/// The format of pixel data given to [decodeImageFromPixels]. enum PixelFormat { - /// Each pixel is 32 bits, with the highest 8 bits encoding red, the next 8 - /// bits encoding green, the next 8 bits encoding blue, and the lowest 8 bits - /// encoding alpha. rgba8888, - - /// Each pixel is 32 bits, with the highest 8 bits encoding blue, the next 8 - /// bits encoding green, the next 8 bits encoding red, and the lowest 8 bits - /// encoding alpha. bgra8888, } -/// Callback signature for [decodeImageFromList]. typedef ImageDecoderCallback = void Function(Image result); -/// Information for a single frame of an animation. -/// -/// To obtain an instance of the [FrameInfo] interface, see -/// [Codec.getNextFrame]. abstract class FrameInfo { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. - /// - /// To obtain an instance of the [FrameInfo] interface, see - /// [Codec.getNextFrame]. FrameInfo._(); - - /// The duration this frame should be shown. Duration get duration => Duration(milliseconds: _durationMillis); int get _durationMillis => 0; - - /// The [Image] object for this frame. Image get image; } -/// A handle to an image codec. class Codec { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. - /// - /// To obtain an instance of the [Codec] interface, see - /// [instantiateImageCodec]. Codec._(); - - /// Number of frames in this image. int get frameCount => 0; - - /// Number of times to repeat the animation. - /// - /// * 0 when the animation should be played once. - /// * -1 for infinity repetitions. int get repetitionCount => 0; - - /// Fetches the next animation frame. - /// - /// Wraps back to the first frame after returning the last frame. - /// - /// The returned future can complete with an error if the decoding has failed. Future getNextFrame() { return engine.futurize(_getNextFrame); } - /// Returns an error message on failure, null on success. String? _getNextFrame(engine.Callback callback) => null; - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. void dispose() {} } -/// Instantiates an image codec [Codec] object. -/// -/// [list] is the binary image data (e.g a PNG or GIF binary data). -/// The data can be for either static or animated images. -/// -/// The following image formats are supported: {@macro flutter.dart:ui.imageFormats} -/// -/// The returned future can complete with an error if the image decoding has -/// failed. Future instantiateImageCodec( Uint8List list, { int? targetWidth, @@ -1577,9 +444,6 @@ Future instantiateImageCodec( _instantiateImageCodec(list, callback)); } -/// Instantiates a [Codec] object for an image binary data. -/// -/// Returns an error message if the instantiation has failed, null otherwise. String? _instantiateImageCodec( Uint8List list, engine.Callback callback, { @@ -1610,35 +474,29 @@ Future webOnlyInstantiateImageCodecFromUrl(Uri uri, } String? _instantiateImageCodecFromUrl( - Uri uri, - engine.WebOnlyImageCodecChunkCallback? chunkCallback, - engine.Callback callback) { - callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); - return null; + Uri uri, + engine.WebOnlyImageCodecChunkCallback? chunkCallback, + engine.Callback callback, +) { + if (engine.experimentalUseSkia) { + engine.skiaInstantiateWebImageCodec(uri.toString(), callback, chunkCallback); + return null; + } else { + callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); + return null; + } } -/// Loads a single image frame from a byte array into an [Image] object. -/// -/// This is a convenience wrapper around [instantiateImageCodec]. -/// Prefer using [instantiateImageCodec] which also supports multi frame images. void decodeImageFromList(Uint8List list, ImageDecoderCallback callback) { _decodeImageFromListAsync(list, callback); } -Future _decodeImageFromListAsync( - Uint8List list, ImageDecoderCallback callback) async { +Future _decodeImageFromListAsync(Uint8List list, ImageDecoderCallback callback) async { final Codec codec = await instantiateImageCodec(list); final FrameInfo frameInfo = await codec.getNextFrame(); callback(frameInfo.image); } -/// Convert an array of pixel values into an [Image] object. -/// -/// [pixels] is the pixel data in the encoding described by [format]. -/// -/// [rowBytes] is the number of bytes consumed by each row of pixels in the -/// data buffer. If unspecified, it defaults to [width] multipled by the -/// number of bytes per pixel in the provided [format]. void decodeImageFromPixels( Uint8List pixels, int width, @@ -1650,95 +508,50 @@ void decodeImageFromPixels( int? targetHeight, bool allowUpscaling = true, }) { - final Future codecFuture = _futurize( - (engine.Callback callback) { - return _instantiateImageCodec( - pixels, - callback, - width: width, - height: height, - format: format, - rowBytes: rowBytes, - ); + final Future codecFuture = _futurize((engine.Callback callback) { + return _instantiateImageCodec( + pixels, + callback, + width: width, + height: height, + format: format, + rowBytes: rowBytes, + ); }); codecFuture .then((Codec codec) => codec.getNextFrame()) .then((FrameInfo frameInfo) => callback(frameInfo.image)); } -/// A single shadow. -/// -/// Multiple shadows are stacked together in a [TextStyle]. class Shadow { - /// Construct a shadow. - /// - /// The default shadow is a black shadow with zero offset and zero blur. - /// Default shadows should be completely covered by the casting element, - /// and not be visble. - /// - /// Transparency should be adjusted through the [color] alpha. - /// - /// Shadow order matters due to compositing multiple translucent objects not - /// being commutative. const Shadow({ this.color = const Color(_kColorDefault), this.offset = Offset.zero, this.blurRadius = 0.0, - }) : assert(color != null, 'Text shadow color was null.'), // ignore: unnecessary_null_comparison - assert(offset != null, 'Text shadow offset was null.'), // ignore: unnecessary_null_comparison + }) : assert(color != null, + 'Text shadow color was null.'), // ignore: unnecessary_null_comparison + assert(offset != null, + 'Text shadow offset was null.'), // ignore: unnecessary_null_comparison assert(blurRadius >= 0.0, 'Text shadow blur radius should be non-negative.'); static const int _kColorDefault = 0xFF000000; - - /// Color that the shadow will be drawn with. - /// - /// The shadows are shapes composited directly over the base canvas, and do not - /// represent optical occlusion. final Color color; - - /// The displacement of the shadow from the casting element. - /// - /// Positive x/y offsets will shift the shadow to the right and down, while - /// negative offsets shift the shadow to the left and up. The offsets are - /// relative to the position of the element that is casting it. final Offset offset; - - /// The standard deviation of the Gaussian to convolve with the shadow's shape. final double blurRadius; - - /// Converts a blur radius in pixels to sigmas. - /// - /// See the sigma argument to [MaskFilter.blur]. - /// // See SkBlurMask::ConvertRadiusToSigma(). // static double convertRadiusToSigma(double radius) { return radius * 0.57735 + 0.5; } - /// The [blurRadius] in sigmas instead of logical pixels. - /// - /// See the sigma argument to [MaskFilter.blur]. double get blurSigma => convertRadiusToSigma(blurRadius); - - /// Create the [Paint] object that corresponds to this shadow description. - /// - /// The [offset] is not represented in the [Paint] object. - /// To honor this as well, the shape should be translated by [offset] before - /// being filled using this [Paint]. - /// - /// This class does not provide a way to disable shadows to avoid inconsistencies - /// in shadow blur rendering, primarily as a method of reducing test flakiness. - /// [toPaint] should be overriden in subclasses to provide this functionality. Paint toPaint() { return Paint() ..color = color ..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma); } - /// Returns a new shadow with its [offset] and [blurRadius] scaled by the given - /// factor. Shadow scale(double factor) { return Shadow( color: color, @@ -1747,25 +560,6 @@ class Shadow { ); } - /// Linearly interpolate between two shadows. - /// - /// If either shadow is null, this function linearly interpolates from a - /// a shadow that matches the other shadow in color but has a zero - /// offset and a zero blurRadius. - /// - /// {@template dart.ui.shadow.lerp} - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. - /// {@endtemplate} static Shadow? lerp(Shadow? a, Shadow? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -1787,11 +581,6 @@ class Shadow { } } - /// Linearly interpolate between two lists of shadows. - /// - /// If the lists differ in length, excess items are lerped with null. - /// - /// {@macro dart.ui.shadow.lerp} static List? lerpList(List? a, List? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (a == null && b == null) { @@ -1816,10 +605,10 @@ class Shadow { if (identical(this, other)) { return true; } - return other is Shadow - && other.color == color - && other.offset == offset - && other.blurRadius == blurRadius; + return other is Shadow && + other.color == color && + other.offset == offset && + other.blurRadius == blurRadius; } @override @@ -1829,34 +618,17 @@ class Shadow { String toString() => 'TextShadow($color, $offset, $blurRadius)'; } - -/// A shader (as used by [Paint.shader]) that tiles an image. class ImageShader extends Shader { - /// Creates an image-tiling shader. The first argument specifies the image to - /// tile. The second and third arguments specify the [TileMode] for the x - /// direction and y direction respectively. The fourth argument gives the - /// matrix to apply to the effect. All the arguments are required and must not - /// be null. - factory ImageShader( - Image image, - TileMode tmx, - TileMode tmy, - Float64List matrix4) { + factory ImageShader(Image image, TileMode tmx, TileMode tmy, Float64List matrix4) { if (engine.experimentalUseSkia) { - return engine.EngineImageShader(image, tmx, tmy, matrix4); + return engine.CkImageShader(image, tmx, tmy, matrix4); } - throw UnsupportedError( - 'ImageShader not implemented for web platform.'); + throw UnsupportedError('ImageShader not implemented for web platform.'); } } - -/// A handle to a read-only byte buffer that is managed by the engine. class ImmutableBuffer { ImmutableBuffer._(this.length); - - /// Creates a copy of the data from a [Uint8List] suitable for internal use - /// in the engine. static Future fromUint8List(Uint8List list) async { final ImmutableBuffer instance = ImmutableBuffer._(list.length); instance._list = list; @@ -1864,37 +636,22 @@ class ImmutableBuffer { } Uint8List? _list; - - /// The length, in bytes, of the underlying data. final int length; - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. void dispose() => _list = null; } -/// A descriptor of data that can be turned into an [Image] via a [Codec]. -/// -/// Use this class to determine the height, width, and byte size of image data -/// before decoding it. class ImageDescriptor { - ImageDescriptor._() : _width = null, _height = null, _rowBytes = null, _format = null; - - /// Creates an image descriptor from encoded data in a supported format. + ImageDescriptor._() + : _width = null, + _height = null, + _rowBytes = null, + _format = null; static Future encoded(ImmutableBuffer buffer) async { final ImageDescriptor descriptor = ImageDescriptor._(); descriptor._data = buffer._list; return descriptor; } - /// Creates an image descriptor from raw image pixels. - /// - /// The `pixels` parameter is the pixel data in the encoding described by - /// `format`. - /// - /// The `rowBytes` parameter is the number of bytes consumed by each row of - /// pixels in the data buffer. If unspecified, it defaults to `width` multiplied - /// by the number of bytes per pixel in the provided `format`. // Not async because there's no expensive work to do here. ImageDescriptor.raw( ImmutableBuffer buffer, { @@ -1902,7 +659,10 @@ class ImageDescriptor { required int height, int? rowBytes, required PixelFormat pixelFormat, - }) : _width = width, _height = height, _rowBytes = rowBytes, _format = pixelFormat { + }) : _width = width, + _height = height, + _rowBytes = rowBytes, + _format = pixelFormat { _data = buffer._list; } @@ -1913,24 +673,14 @@ class ImageDescriptor { final PixelFormat? _format; Never _throw(String parameter) { - throw UnsupportedError('ImageDescriptor.$parameter is not supported on web.'); + throw UnsupportedError('ImageDescriptor.$parameter is not supported on web.'); } - /// The width, in pixels, of the image. int get width => _width ?? _throw('width'); - - /// The height, in pixels, of the image. int get height => _height ?? _throw('height'); - - /// The number of bytes per pixel in the image. - int get bytesPerPixel => throw UnsupportedError('ImageDescriptor.bytesPerPixel is not supported on web.'); - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. + int get bytesPerPixel => + throw UnsupportedError('ImageDescriptor.bytesPerPixel is not supported on web.'); void dispose() => _data = null; - - /// Creates a [Codec] object which is suitable for decoding the data in the - /// buffer to an [Image]. Future instantiateCodec({int? targetWidth, int? targetHeight}) { if (_data == null) { throw StateError('Object is disposed'); @@ -1943,16 +693,15 @@ class ImageDescriptor { allowUpscaling: false, ); } - return _futurize( - (engine.Callback callback) { - return _instantiateImageCodec( - _data!, - callback, - width: _width, - height: _height, - format: _format, - rowBytes: _rowBytes, - ); - }); + return _futurize((engine.Callback callback) { + return _instantiateImageCodec( + _data!, + callback, + width: _width, + height: _height, + format: _format, + rowBytes: _rowBytes, + ); + }); } } diff --git a/lib/web_ui/lib/src/ui/path.dart b/lib/web_ui/lib/src/ui/path.dart index 1514d2432f409..02d4e40d73484 100644 --- a/lib/web_ui/lib/src/ui/path.dart +++ b/lib/web_ui/lib/src/ui/path.dart @@ -5,25 +5,7 @@ // @dart = 2.10 part of ui; -/// A complex, one-dimensional subset of a plane. -/// -/// A path consists of a number of subpaths, and a _current point_. -/// -/// Subpaths consist of segments of various types, such as lines, -/// arcs, or beziers. Subpaths can be open or closed, and can -/// self-intersect. -/// -/// Closed subpaths enclose a (possibly discontiguous) region of the -/// plane based on the current [fillType]. -/// -/// The _current point_ is initially at the origin. After each -/// operation adding a segment to a subpath, the current point is -/// updated to the end of that segment. -/// -/// Paths can be drawn on canvases using [Canvas.drawPath], and can -/// used to create clip regions using [Canvas.clipPath]. abstract class Path { - /// Create a new empty [Path] object. factory Path() { if (engine.experimentalUseSkia) { return engine.CkPath(); @@ -31,11 +13,6 @@ abstract class Path { return engine.SurfacePath(); } } - - /// Creates a copy of another [Path]. - /// - /// This copy is fast and does not require additional memory unless either - /// the `source` path or the path returned by this constructor are modified. factory Path.from(Path source) { if (engine.experimentalUseSkia) { return engine.CkPath.from(source as engine.CkPath); @@ -43,231 +20,47 @@ abstract class Path { return engine.SurfacePath.from(source as engine.SurfacePath); } } - - /// Determines how the interior of this path is calculated. - /// - /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. PathFillType get fillType; set fillType(PathFillType value); - - /// Starts a new subpath at the given coordinate. void moveTo(double x, double y); - - /// Starts a new subpath at the given offset from the current point. void relativeMoveTo(double dx, double dy); - - /// Adds a straight line segment from the current point to the given - /// point. void lineTo(double x, double y); - - /// Adds a straight line segment from the current point to the point - /// at the given offset from the current point. void relativeLineTo(double dx, double dy); - - /// Adds a quadratic bezier segment that curves from the current - /// point to the given point (x2,y2), using the control point - /// (x1,y1). void quadraticBezierTo(double x1, double y1, double x2, double y2); - - /// Adds a quadratic bezier segment that curves from the current - /// point to the point at the offset (x2,y2) from the current point, - /// using the control point at the offset (x1,y1) from the current - /// point. void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2); - - /// Adds a cubic bezier segment that curves from the current point - /// to the given point (x3,y3), using the control points (x1,y1) and - /// (x2,y2). - void cubicTo( - double x1, double y1, double x2, double y2, double x3, double y3); - - /// Adds a cubic bezier segment that curves from the current point - /// to the point at the offset (x3,y3) from the current point, using - /// the control points at the offsets (x1,y1) and (x2,y2) from the - /// current point. - void relativeCubicTo( - double x1, double y1, double x2, double y2, double x3, double y3); - - /// Adds a bezier segment that curves from the current point to the - /// given point (x2,y2), using the control points (x1,y1) and the - /// weight w. If the weight is greater than 1, then the curve is a - /// hyperbola; if the weight equals 1, it's a parabola; and if it is - /// less than 1, it is an ellipse. + void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3); + void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3); void conicTo(double x1, double y1, double x2, double y2, double w); - - /// Adds a bezier segment that curves from the current point to the - /// point at the offset (x2,y2) from the current point, using the - /// control point at the offset (x1,y1) from the current point and - /// the weight w. If the weight is greater than 1, then the curve is - /// a hyperbola; if the weight equals 1, it's a parabola; and if it - /// is less than 1, it is an ellipse. void relativeConicTo(double x1, double y1, double x2, double y2, double w); - - /// If the `forceMoveTo` argument is false, adds a straight line - /// segment and an arc segment. - /// - /// If the `forceMoveTo` argument is true, starts a new subpath - /// consisting of an arc segment. - /// - /// In either case, the arc segment consists of the arc that follows - /// the edge of the oval bounded by the given rectangle, from - /// startAngle radians around the oval up to startAngle + sweepAngle - /// radians around the oval, with zero radians being the point on - /// the right hand side of the oval that crosses the horizontal line - /// that intersects the center of the rectangle and with positive - /// angles going clockwise around the oval. - /// - /// The line segment added if `forceMoveTo` is false starts at the - /// current point and ends at the start of the arc. - void arcTo( - Rect rect, double startAngle, double sweepAngle, bool forceMoveTo); - - /// Appends up to four conic curves weighted to describe an oval of `radius` - /// and rotated by `rotation`. - /// - /// The first curve begins from the last point in the path and the last ends - /// at `arcEnd`. The curves follow a path in a direction determined by - /// `clockwise` and `largeArc` in such a way that the sweep angle - /// is always less than 360 degrees. - /// - /// A simple line is appended if either either radii are zero or the last - /// point in the path is `arcEnd`. The radii are scaled to fit the last path - /// point if both are greater than zero but too small to describe an arc. - /// - /// See Conversion from endpoint to center parametrization described in - /// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter - /// as reference for implementation. + void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo); void arcToPoint( - Offset arcEnd, { - Radius radius = Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }); - - /// Appends up to four conic curves weighted to describe an oval of `radius` - /// and rotated by `rotation`. - /// - /// The last path point is described by (px, py). - /// - /// The first curve begins from the last point in the path and the last ends - /// at `arcEndDelta.dx + px` and `arcEndDelta.dy + py`. The curves follow a - /// path in a direction determined by `clockwise` and `largeArc` - /// in such a way that the sweep angle is always less than 360 degrees. - /// - /// A simple line is appended if either either radii are zero, or, both - /// `arcEndDelta.dx` and `arcEndDelta.dy` are zero. The radii are scaled to - /// fit the last path point if both are greater than zero but too small to - /// describe an arc. + Offset arcEnd, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }); void relativeArcToPoint( - Offset arcEndDelta, { - Radius radius = Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }); - - /// Adds a new subpath that consists of four lines that outline the - /// given rectangle. + Offset arcEndDelta, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }); void addRect(Rect rect); - - /// Adds a new subpath that consists of a curve that forms the - /// ellipse that fills the given rectangle. - /// - /// To add a circle, pass an appropriate rectangle as `oval`. - /// [Rect.fromCircle] can be used to easily describe the circle's center - /// [Offset] and radius. void addOval(Rect oval); - - /// Adds a new subpath with one arc segment that consists of the arc - /// that follows the edge of the oval bounded by the given - /// rectangle, from startAngle radians around the oval up to - /// startAngle + sweepAngle radians around the oval, with zero - /// radians being the point on the right hand side of the oval that - /// crosses the horizontal line that intersects the center of the - /// rectangle and with positive angles going clockwise around the - /// oval. void addArc(Rect oval, double startAngle, double sweepAngle); - - /// Adds a new subpath with a sequence of line segments that connect the given - /// points. - /// - /// If `close` is true, a final line segment will be added that connects the - /// last point to the first point. - /// - /// The `points` argument is interpreted as offsets from the origin. void addPolygon(List points, bool close); - - /// Adds a new subpath that consists of the straight lines and - /// curves needed to form the rounded rectangle described by the - /// argument. void addRRect(RRect rrect); - - /// Adds a new subpath that consists of the given `path` offset by the given - /// `offset`. - /// - /// If `matrix4` is specified, the path will be transformed by this matrix - /// after the matrix is translated by the given offset. The matrix is a 4x4 - /// matrix stored in column major order. void addPath(Path path, Offset offset, {Float64List? matrix4}); - - /// Adds the given path to this path by extending the current segment of this - /// path with the first segment of the given path. - /// - /// If `matrix4` is specified, the path will be transformed by this matrix - /// after the matrix is translated by the given `offset`. The matrix is a 4x4 - /// matrix stored in column major order. void extendWithPath(Path path, Offset offset, {Float64List? matrix4}); - - /// Closes the last subpath, as if a straight line had been drawn - /// from the current point to the first point of the subpath. void close(); - - /// Clears the [Path] object of all subpaths, returning it to the - /// same state it had when it was created. The _current point_ is - /// reset to the origin. void reset(); - - /// Tests to see if the given point is within the path. (That is, whether the - /// point would be in the visible portion of the path if the path was used - /// with [Canvas.clipPath].) - /// - /// The `point` argument is interpreted as an offset from the origin. - /// - /// Returns true if the point is in the path, and false otherwise. bool contains(Offset point); - - /// Returns a copy of the path with all the segments of every - /// subpath translated by the given offset. Path shift(Offset offset); - - /// Returns a copy of the path with all the segments of every - /// sub path transformed by the given matrix. Path transform(Float64List matrix4); - - /// Computes the bounding rectangle for this path. - /// - /// A path containing only axis-aligned points on the same straight line will - /// have no area, and therefore `Rect.isEmpty` will return true for such a - /// path. Consider checking `rect.width + rect.height > 0.0` instead, or - /// using the [computeMetrics] API to check the path length. - /// - /// For many more elaborate paths, the bounds may be inaccurate. For example, - /// when a path contains a circle, the points used to compute the bounds are - /// the circle's implied control points, which form a square around the - /// circle; if the circle has a transformation applied using [transform] then - /// that square is rotated, and the (axis-aligned, non-rotated) bounding box - /// therefore ends up grossly overestimating the actual area covered by the - /// circle. // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds Rect getBounds(); - - /// Combines the two paths according to the manner specified by the given - /// `operation`. - /// - /// The resulting path will be constructed from non-overlapping contours. The - /// curve order is reduced where possible so that cubics may be turned into - /// quadratics, and quadratics maybe turned into lines. static Path combine(PathOperation operation, Path path1, Path path2) { assert(path1 != null); // ignore: unnecessary_null_comparison assert(path2 != null); // ignore: unnecessary_null_comparison @@ -277,9 +70,5 @@ abstract class Path { throw UnimplementedError(); } - /// Creates a [PathMetrics] object for this path. - /// - /// If `forceClosed` is set to true, the contours of the path will be measured - /// as if they had been closed, even if they were not explicitly closed. PathMetrics computeMetrics({bool forceClosed = false}); } diff --git a/lib/web_ui/lib/src/ui/path_metrics.dart b/lib/web_ui/lib/src/ui/path_metrics.dart index b65e2b9288fa2..65f07f881f1a2 100644 --- a/lib/web_ui/lib/src/ui/path_metrics.dart +++ b/lib/web_ui/lib/src/ui/path_metrics.dart @@ -5,28 +5,11 @@ // @dart = 2.10 part of ui; -/// An iterable collection of [PathMetric] objects describing a [Path]. -/// -/// A [PathMetrics] object is created by using the [Path.computeMetrics] method, -/// and represents the path as it stood at the time of the call. Subsequent -/// modifications of the path do not affect the [PathMetrics] object. -/// -/// Each path metric corresponds to a segment, or contour, of a path. -/// -/// For example, a path consisting of a [Path.lineTo], a [Path.moveTo], and -/// another [Path.lineTo] will contain two contours and thus be represented by -/// two [PathMetric] objects. -/// -/// This iterable does not memoize. Callers who need to traverse the list -/// multiple times, or who need to randomly access elements of the list, should -/// use [toList] on this object. abstract class PathMetrics extends collection.IterableBase { @override Iterator get iterator; } -/// Used by [PathMetrics] to track iteration from one segment of a path to the -/// next for measurement. abstract class PathMetricIterator implements Iterator { @override PathMetric get current; @@ -35,114 +18,23 @@ abstract class PathMetricIterator implements Iterator { bool moveNext(); } -/// Utilities for measuring a [Path] and extracting sub-paths. -/// -/// Iterate over the object returned by [Path.computeMetrics] to obtain -/// [PathMetric] objects. Callers that want to randomly access elements or -/// iterate multiple times should use `path.computeMetrics().toList()`, since -/// [PathMetrics] does not memoize. -/// -/// Once created, the metrics are only valid for the path as it was specified -/// when [Path.computeMetrics] was called. If additional contours are added or -/// any contours are updated, the metrics need to be recomputed. Previously -/// created metrics will still refer to a snapshot of the path at the time they -/// were computed, rather than to the actual metrics for the new mutations to -/// the path. -/// -/// Implementation is based on -/// https://github.com/google/skia/blob/master/src/core/SkContourMeasure.cpp -/// to maintain consistency with native platforms. abstract class PathMetric { - /// Return the total length of the current contour. double get length; - - /// The zero-based index of the contour. - /// - /// [Path] objects are made up of zero or more contours. The first contour is - /// created once a drawing command (e.g. [Path.lineTo]) is issued. A - /// [Path.moveTo] command after a drawing command may create a new contour, - /// although it may not if optimizations are applied that determine the move - /// command did not actually result in moving the pen. - /// - /// This property is only valid with reference to its original iterator and - /// the contours of the path at the time the path's metrics were computed. If - /// additional contours were added or existing contours updated, this metric - /// will be invalid for the current state of the path. int get contourIndex; - - /// Computes the position of hte current contour at the given offset, and the - /// angle of the path at that point. - /// - /// For example, calling this method with a distance of 1.41 for a line from - /// 0.0,0.0 to 2.0,2.0 would give a point 1.0,1.0 and the angle 45 degrees - /// (but in radians). - /// - /// Returns null if the contour has zero [length]. - /// - /// The distance is clamped to the [length] of the current contour. Tangent? getTangentForOffset(double distance); - - /// Given a start and stop distance, return the intervening segment(s). - /// - /// `start` and `end` are pinned to legal values (0..[length]) - /// Returns null if the segment is 0 length or `start` > `stop`. - /// Begin the segment with a moveTo if `startWithMoveTo` is true. Path? extractPath(double start, double end, {bool startWithMoveTo = true}); - - /// Whether the contour is closed. - /// - /// Returns true if the contour ends with a call to [Path.close] (which may - /// have been implied when using [Path.addRect]) or if `forceClosed` was - /// specified as true in the call to [Path.computeMetrics]. Returns false - /// otherwise. bool get isClosed; } -/// The geometric description of a tangent: the angle at a point. -/// -/// See also: -/// * [PathMetric.getTangentForOffset], which returns the tangent of an offset -/// along a path. class Tangent { - /// Creates a [Tangent] with the given values. - /// - /// The arguments must not be null. const Tangent(this.position, this.vector) : assert(position != null), // ignore: unnecessary_null_comparison assert(vector != null); // ignore: unnecessary_null_comparison - - /// Creates a [Tangent] based on the angle rather than the vector. - /// - /// The [vector] is computed to be the unit vector at the given angle, - /// interpreted as clockwise radians from the x axis. factory Tangent.fromAngle(Offset position, double angle) { return Tangent(position, Offset(math.cos(angle), math.sin(angle))); } - - /// Position of the tangent. - /// - /// When used with [PathMetric.getTangentForOffset], this represents the - /// precise position that the given offset along the path corresponds to. final Offset position; - - /// The vector of the curve at [position]. - /// - /// When used with [PathMetric.getTangentForOffset], this is the vector of the - /// curve that is at the given offset along the path (i.e. the direction of - /// the curve at [position]). final Offset vector; - - /// The direction of the curve at [position]. - /// - /// When used with [PathMetric.getTangentForOffset], this is the angle of the - /// curve that is the given offset along the path (i.e. the direction of the - /// curve at [position]). - /// - /// This value is in radians, with 0.0 meaning pointing along the x axis in - /// the positive x-axis direction, positive numbers pointing downward toward - /// the negative y-axis, i.e. in a clockwise direction, and negative numbers - /// pointing upward toward the positive y-axis, i.e. in a counter-clockwise - /// direction. // flip the sign to be consistent with [Path.arcTo]'s `sweepAngle` double get angle => -math.atan2(vector.dy, vector.dx); } diff --git a/lib/web_ui/lib/src/ui/pointer.dart b/lib/web_ui/lib/src/ui/pointer.dart index e2f351cafedb5..0ad384b4d1209 100644 --- a/lib/web_ui/lib/src/ui/pointer.dart +++ b/lib/web_ui/lib/src/ui/pointer.dart @@ -5,71 +5,31 @@ // @dart = 2.10 part of ui; -/// How the pointer has changed since the last report. enum PointerChange { - /// The input from the pointer is no longer directed towards this receiver. cancel, - - /// The device has started tracking the pointer. - /// - /// For example, the pointer might be hovering above the device, having not yet - /// made contact with the surface of the device. add, - - /// The device is no longer tracking the pointer. - /// - /// For example, the pointer might have drifted out of the device's hover - /// detection range or might have been disconnected from the system entirely. remove, - - /// The pointer has moved with respect to the device while not in contact with - /// the device. hover, - - /// The pointer has made contact with the device. down, - - /// The pointer has moved with respect to the device while in contact with the - /// device. move, - - /// The pointer has stopped making contact with the device. up, } -/// The kind of pointer device. enum PointerDeviceKind { - /// A touch-based pointer device. touch, - - /// A mouse-based pointer device. mouse, - - /// A pointer device with a stylus. stylus, - - /// A pointer device with a stylus that has been inverted. invertedStylus, - - /// An unknown pointer device. unknown } -/// The kind of [PointerDeviceKind.signal]. enum PointerSignalKind { - /// The event is not associated with a pointer signal. none, - - /// A pointer-generated scroll (e.g., mouse wheel or trackpad scroll). scroll, - - /// An unknown pointer signal kind. unknown } -/// Information about the state of a pointer. class PointerData { - /// Creates an object that represents the state of a pointer. const PointerData({ this.embedderId = 0, this.timeStamp = Duration.zero, @@ -101,214 +61,74 @@ class PointerData { this.scrollDeltaX = 0.0, this.scrollDeltaY = 0.0, }); - - /// Unique identifier that ties the [PointerEvent] to embedder event created it. - /// - /// No two pointer events can have the same [embedderId]. This is different from - /// [pointerIdentifier] - used for hit-testing, whereas [embedderId] is used to - /// identify the platform event. final int embedderId; - - /// Time of event dispatch, relative to an arbitrary timeline. final Duration timeStamp; - - /// How the pointer has changed since the last report. final PointerChange change; - - /// The kind of input device for which the event was generated. final PointerDeviceKind kind; - - /// The kind of signal for a pointer signal event. final PointerSignalKind? signalKind; - - /// Unique identifier for the pointing device, reused across interactions. final int device; - - /// Unique identifier for the pointer. - /// - /// This field changes for each new pointer down event. Framework uses this - /// identifier to determine hit test result. final int pointerIdentifier; - - /// X coordinate of the position of the pointer, in physical pixels in the - /// global coordinate space. final double physicalX; - - /// Y coordinate of the position of the pointer, in physical pixels in the - /// global coordinate space. final double physicalY; - - /// The distance of pointer movement on X coordinate in physical pixels. final double physicalDeltaX; - - /// The distance of pointer movement on Y coordinate in physical pixels. final double physicalDeltaY; - - /// Bit field using the *Button constants (primaryMouseButton, - /// secondaryStylusButton, etc). For example, if this has the value 6 and the - /// [kind] is [PointerDeviceKind.invertedStylus], then this indicates an - /// upside-down stylus with both its primary and secondary buttons pressed. final int buttons; - - /// Set if an application from a different security domain is in any way - /// obscuring this application's window. (Aspirational; not currently - /// implemented.) final bool obscured; - - /// Set if this pointer data was synthesized by pointer data packet converter. - /// pointer data packet converter will synthesize additional pointer datas if - /// the input sequence of pointer data is illegal. - /// - /// For example, a down pointer data will be synthesized if the converter receives - /// a move pointer data while the pointer is not previously down. final bool synthesized; - - /// The pressure of the touch as a number ranging from 0.0, indicating a touch - /// with no discernible pressure, to 1.0, indicating a touch with "normal" - /// pressure, and possibly beyond, indicating a stronger touch. For devices - /// that do not detect pressure (e.g. mice), returns 1.0. final double pressure; - - /// The minimum value that [pressure] can return for this pointer. For devices - /// that do not detect pressure (e.g. mice), returns 1.0. This will always be - /// a number less than or equal to 1.0. final double pressureMin; - - /// The maximum value that [pressure] can return for this pointer. For devices - /// that do not detect pressure (e.g. mice), returns 1.0. This will always be - /// a greater than or equal to 1.0. final double pressureMax; - - /// The distance of the detected object from the input surface (e.g. the - /// distance of a stylus or finger from a touch screen), in arbitrary units on - /// an arbitrary (not necessarily linear) scale. If the pointer is down, this - /// is 0.0 by definition. final double distance; - - /// The maximum value that a distance can return for this pointer. If this - /// input device cannot detect "hover touch" input events, then this will be - /// 0.0. final double distanceMax; - - /// The area of the screen being pressed, scaled to a value between 0 and 1. - /// The value of size can be used to determine fat touch events. This value - /// is only set on Android, and is a device specific approximation within - /// the range of detectable values. So, for example, the value of 0.1 could - /// mean a touch with the tip of the finger, 0.2 a touch with full finger, - /// and 0.3 the full palm. final double size; - - /// The radius of the contact ellipse along the major axis, in logical pixels. final double radiusMajor; - - /// The radius of the contact ellipse along the minor axis, in logical pixels. final double radiusMinor; - - /// The minimum value that could be reported for radiusMajor and radiusMinor - /// for this pointer, in logical pixels. final double radiusMin; - - /// The minimum value that could be reported for radiusMajor and radiusMinor - /// for this pointer, in logical pixels. final double radiusMax; - - /// For PointerDeviceKind.touch events: - /// - /// The angle of the contact ellipse, in radius in the range: - /// - /// -pi/2 < orientation <= pi/2 - /// - /// ...giving the angle of the major axis of the ellipse with the y-axis - /// (negative angles indicating an orientation along the top-left / - /// bottom-right diagonal, positive angles indicating an orientation along the - /// top-right / bottom-left diagonal, and zero indicating an orientation - /// parallel with the y-axis). - /// - /// For PointerDeviceKind.stylus and PointerDeviceKind.invertedStylus events: - /// - /// The angle of the stylus, in radians in the range: - /// - /// -pi < orientation <= pi - /// - /// ...giving the angle of the axis of the stylus projected onto the input - /// surface, relative to the positive y-axis of that surface (thus 0.0 - /// indicates the stylus, if projected onto that surface, would go from the - /// contact point vertically up in the positive y-axis direction, pi would - /// indicate that the stylus would go down in the negative y-axis direction; - /// pi/4 would indicate that the stylus goes up and to the right, -pi/2 would - /// indicate that the stylus goes to the left, etc). final double orientation; - - /// For PointerDeviceKind.stylus and PointerDeviceKind.invertedStylus events: - /// - /// The angle of the stylus, in radians in the range: - /// - /// 0 <= tilt <= pi/2 - /// - /// ...giving the angle of the axis of the stylus, relative to the axis - /// perpendicular to the input surface (thus 0.0 indicates the stylus is - /// orthogonal to the plane of the input surface, while pi/2 indicates that - /// the stylus is flat on that surface). final double tilt; - - /// Opaque platform-specific data associated with the event. final int platformData; - - /// For events with signalKind of PointerSignalKind.scroll: - /// - /// The amount to scroll in the x direction, in physical pixels. final double scrollDeltaX; - - /// For events with signalKind of PointerSignalKind.scroll: - /// - /// The amount to scroll in the y direction, in physical pixels. final double scrollDeltaY; @override String toString() => 'PointerData(x: $physicalX, y: $physicalY)'; - - /// Returns a complete textual description of the information in this object. String toStringFull() { return '$runtimeType(' - 'embedderId: $embedderId, ' - 'timeStamp: $timeStamp, ' - 'change: $change, ' - 'kind: $kind, ' - 'signalKind: $signalKind, ' - 'device: $device, ' - 'pointerIdentifier: $pointerIdentifier, ' - 'physicalX: $physicalX, ' - 'physicalY: $physicalY, ' - 'physicalDeltaX: $physicalDeltaX, ' - 'physicalDeltaY: $physicalDeltaY, ' - 'buttons: $buttons, ' - 'synthesized: $synthesized, ' - 'pressure: $pressure, ' - 'pressureMin: $pressureMin, ' - 'pressureMax: $pressureMax, ' - 'distance: $distance, ' - 'distanceMax: $distanceMax, ' - 'size: $size, ' - 'radiusMajor: $radiusMajor, ' - 'radiusMinor: $radiusMinor, ' - 'radiusMin: $radiusMin, ' - 'radiusMax: $radiusMax, ' - 'orientation: $orientation, ' - 'tilt: $tilt, ' - 'platformData: $platformData, ' - 'scrollDeltaX: $scrollDeltaX, ' - 'scrollDeltaY: $scrollDeltaY' + 'embedderId: $embedderId, ' + 'timeStamp: $timeStamp, ' + 'change: $change, ' + 'kind: $kind, ' + 'signalKind: $signalKind, ' + 'device: $device, ' + 'pointerIdentifier: $pointerIdentifier, ' + 'physicalX: $physicalX, ' + 'physicalY: $physicalY, ' + 'physicalDeltaX: $physicalDeltaX, ' + 'physicalDeltaY: $physicalDeltaY, ' + 'buttons: $buttons, ' + 'synthesized: $synthesized, ' + 'pressure: $pressure, ' + 'pressureMin: $pressureMin, ' + 'pressureMax: $pressureMax, ' + 'distance: $distance, ' + 'distanceMax: $distanceMax, ' + 'size: $size, ' + 'radiusMajor: $radiusMajor, ' + 'radiusMinor: $radiusMinor, ' + 'radiusMin: $radiusMin, ' + 'radiusMax: $radiusMax, ' + 'orientation: $orientation, ' + 'tilt: $tilt, ' + 'platformData: $platformData, ' + 'scrollDeltaX: $scrollDeltaX, ' + 'scrollDeltaY: $scrollDeltaY' ')'; } } -/// A sequence of reports about the state of pointers. class PointerDataPacket { - /// Creates a packet of pointer data reports. - const PointerDataPacket({ this.data = const [] }) : assert(data != null); // ignore: unnecessary_null_comparison - - /// Data about the individual pointers in this packet. - /// - /// This list might contain multiple pieces of data about the same pointer. + const PointerDataPacket({this.data = const []}) + : assert(data != null); // ignore: unnecessary_null_comparison final List data; } diff --git a/lib/web_ui/lib/src/ui/semantics.dart b/lib/web_ui/lib/src/ui/semantics.dart index a0fde9a5b7916..24b0acc4f318c 100644 --- a/lib/web_ui/lib/src/ui/semantics.dart +++ b/lib/web_ui/lib/src/ui/semantics.dart @@ -5,8 +5,6 @@ // @dart = 2.10 part of ui; -/// The possible actions that can be conveyed from the operating system -/// accessibility APIs to a semantics node. class SemanticsAction { const SemanticsAction._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison @@ -31,172 +29,34 @@ class SemanticsAction { static const int _kDismissIndex = 1 << 18; static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; - - /// The numerical value for this action. - /// - /// Each action has one bit set in this bit field. final int index; - - /// The equivalent of a user briefly tapping the screen with the finger - /// without moving it. static const SemanticsAction tap = SemanticsAction._(_kTapIndex); - - /// The equivalent of a user pressing and holding the screen with the finger - /// for a few seconds without moving it. static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex); - - /// The equivalent of a user moving their finger across the screen from right - /// to left. - /// - /// This action should be recognized by controls that are horizontally - /// scrollable. - static const SemanticsAction scrollLeft = - SemanticsAction._(_kScrollLeftIndex); - - /// The equivalent of a user moving their finger across the screen from left - /// to right. - /// - /// This action should be recognized by controls that are horizontally - /// scrollable. - static const SemanticsAction scrollRight = - SemanticsAction._(_kScrollRightIndex); - - /// The equivalent of a user moving their finger across the screen from - /// bottom to top. - /// - /// This action should be recognized by controls that are vertically - /// scrollable. + static const SemanticsAction scrollLeft = SemanticsAction._(_kScrollLeftIndex); + static const SemanticsAction scrollRight = SemanticsAction._(_kScrollRightIndex); static const SemanticsAction scrollUp = SemanticsAction._(_kScrollUpIndex); - - /// The equivalent of a user moving their finger across the screen from top - /// to bottom. - /// - /// This action should be recognized by controls that are vertically - /// scrollable. - static const SemanticsAction scrollDown = - SemanticsAction._(_kScrollDownIndex); - - /// A request to increase the value represented by the semantics node. - /// - /// For example, this action might be recognized by a slider control. + static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex); static const SemanticsAction increase = SemanticsAction._(_kIncreaseIndex); - - /// A request to decrease the value represented by the semantics node. - /// - /// For example, this action might be recognized by a slider control. static const SemanticsAction decrease = SemanticsAction._(_kDecreaseIndex); - - /// A request to fully show the semantics node on screen. - /// - /// For example, this action might be send to a node in a scrollable list that - /// is partially off screen to bring it on screen. - static const SemanticsAction showOnScreen = - SemanticsAction._(_kShowOnScreenIndex); - - /// Move the cursor forward by one character. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. + static const SemanticsAction showOnScreen = SemanticsAction._(_kShowOnScreenIndex); static const SemanticsAction moveCursorForwardByCharacter = SemanticsAction._(_kMoveCursorForwardByCharacterIndex); - - /// Move the cursor backward by one character. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. static const SemanticsAction moveCursorBackwardByCharacter = SemanticsAction._(_kMoveCursorBackwardByCharacterIndex); - - /// Set the text selection to the given range. - /// - /// The provided argument is a Map which includes the keys `base` - /// and `extent` indicating where the selection within the `value` of the - /// semantics node should start and where it should end. Values for both - /// keys can range from 0 to length of `value` (inclusive). - /// - /// Setting `base` and `extent` to the same value will move the cursor to - /// that position (without selecting anything). - static const SemanticsAction setSelection = - SemanticsAction._(_kSetSelectionIndex); - - /// Copy the current selection to the clipboard. + static const SemanticsAction setSelection = SemanticsAction._(_kSetSelectionIndex); static const SemanticsAction copy = SemanticsAction._(_kCopyIndex); - - /// Cut the current selection and place it in the clipboard. static const SemanticsAction cut = SemanticsAction._(_kCutIndex); - - /// Paste the current content of the clipboard. static const SemanticsAction paste = SemanticsAction._(_kPasteIndex); - - /// Indicates that the nodes has gained accessibility focus. - /// - /// This handler is invoked when the node annotated with this handler gains - /// the accessibility focus. The accessibility focus is the - /// green (on Android with TalkBack) or black (on iOS with VoiceOver) - /// rectangle shown on screen to indicate what element an accessibility - /// user is currently interacting with. - /// - /// The accessibility focus is different from the input focus. The input focus - /// is usually held by the element that currently responds to keyboard inputs. - /// Accessibility focus and input focus can be held by two different nodes! static const SemanticsAction didGainAccessibilityFocus = SemanticsAction._(_kDidGainAccessibilityFocusIndex); - - /// Indicates that the nodes has lost accessibility focus. - /// - /// This handler is invoked when the node annotated with this handler - /// loses the accessibility focus. The accessibility focus is - /// the green (on Android with TalkBack) or black (on iOS with VoiceOver) - /// rectangle shown on screen to indicate what element an accessibility - /// user is currently interacting with. - /// - /// The accessibility focus is different from the input focus. The input focus - /// is usually held by the element that currently responds to keyboard inputs. - /// Accessibility focus and input focus can be held by two different nodes! static const SemanticsAction didLoseAccessibilityFocus = SemanticsAction._(_kDidLoseAccessibilityFocusIndex); - - /// Indicates that the user has invoked a custom accessibility action. - /// - /// This handler is added automatically whenever a custom accessibility - /// action is added to a semantics node. static const SemanticsAction customAction = SemanticsAction._(_kCustomAction); - - /// A request that the node should be dismissed. - /// - /// A [Snackbar], for example, may have a dismiss action to indicate to the - /// user that it can be removed after it is no longer relevant. On Android, - /// (with TalkBack) special hint text is spoken when focusing the node and - /// a custom action is availible in the local context menu. On iOS, - /// (with VoiceOver) users can perform a standard gesture to dismiss it. static const SemanticsAction dismiss = SemanticsAction._(_kDismissIndex); - - /// Move the cursor forward by one word. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. static const SemanticsAction moveCursorForwardByWord = SemanticsAction._(_kMoveCursorForwardByWordIndex); - - /// Move the cursor backward by one word. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex); - - /// The possible semantics actions. - /// - /// The map's key is the [index] of the action and the value is the action - /// itself. static const Map values = { _kTapIndex: tap, _kLongPressIndex: longPress, @@ -272,7 +132,6 @@ class SemanticsAction { } } -/// A Boolean value that can be associated with a semantics node. class SemanticsFlag { static const int _kHasCheckedStateIndex = 1 << 0; static const int _kIsCheckedIndex = 1 << 1; @@ -299,233 +158,30 @@ class SemanticsFlag { static const int _kIsLinkIndex = 1 << 22; const SemanticsFlag._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison - - /// The numerical value for this flag. - /// - /// Each flag has one bit set in this bit field. final int index; - - /// The semantics node has the quality of either being "checked" or "unchecked". - /// - /// This flag is mutually exclusive with [hasToggledState]. - /// - /// For example, a checkbox or a radio button widget has checked state. - /// - /// See also: - /// - /// * [SemanticsFlag.isChecked], which controls whether the node is "checked" or "unchecked". - static const SemanticsFlag hasCheckedState = - SemanticsFlag._(_kHasCheckedStateIndex); - - /// Whether a semantics node that [hasCheckedState] is checked. - /// - /// If true, the semantics node is "checked". If false, the semantics node is - /// "unchecked". - /// - /// For example, if a checkbox has a visible checkmark, [isChecked] is true. - /// - /// See also: - /// - /// * [SemanticsFlag.hasCheckedState], which enables a checked state. + static const SemanticsFlag hasCheckedState = SemanticsFlag._(_kHasCheckedStateIndex); static const SemanticsFlag isChecked = SemanticsFlag._(_kIsCheckedIndex); - - /// Whether a semantics node is selected. - /// - /// If true, the semantics node is "selected". If false, the semantics node is - /// "unselected". - /// - /// For example, the active tab in a tab bar has [isSelected] set to true. static const SemanticsFlag isSelected = SemanticsFlag._(_kIsSelectedIndex); - - /// Whether the semantic node represents a button. - /// - /// Platforms has special handling for buttons, for example Android's TalkBack - /// and iOS's VoiceOver provides an additional hint when the focused object is - /// a button. static const SemanticsFlag isButton = SemanticsFlag._(_kIsButtonIndex); - - /// Whether the semantic node represents a link. - /// - /// Platforms have special handling for links, for example, iOS's VoiceOver - /// provides an additional hint when the focused object is a link. static const SemanticsFlag isLink = SemanticsFlag._(_kIsLinkIndex); - - /// Whether the semantic node represents a text field. - /// - /// Text fields are announced as such and allow text input via accessibility - /// affordances. static const SemanticsFlag isTextField = SemanticsFlag._(_kIsTextFieldIndex); - - /// Whether the semantic node is read only. - /// - /// Only applicable when [isTextField] is true. static const SemanticsFlag isReadOnly = SemanticsFlag._(_kIsReadOnlyIndex); - - /// Whether the semantic node is able to hold the user's focus. - /// - /// The focused element is usually the current receiver of keyboard inputs. static const SemanticsFlag isFocusable = SemanticsFlag._(_kIsFocusableIndex); - - /// Whether the semantic node currently holds the user's focus. - /// - /// The focused element is usually the current receiver of keyboard inputs. static const SemanticsFlag isFocused = SemanticsFlag._(_kIsFocusedIndex); - - /// The semantics node has the quality of either being "enabled" or - /// "disabled". - /// - /// For example, a button can be enabled or disabled and therefore has an - /// "enabled" state. Static text is usually neither enabled nor disabled and - /// therefore does not have an "enabled" state. - static const SemanticsFlag hasEnabledState = - SemanticsFlag._(_kHasEnabledStateIndex); - - /// Whether a semantic node that [hasEnabledState] is currently enabled. - /// - /// A disabled element does not respond to user interaction. For example, a - /// button that currently does not respond to user interaction should be - /// marked as disabled. + static const SemanticsFlag hasEnabledState = SemanticsFlag._(_kHasEnabledStateIndex); static const SemanticsFlag isEnabled = SemanticsFlag._(_kIsEnabledIndex); - - /// Whether a semantic node is in a mutually exclusive group. - /// - /// For example, a radio button is in a mutually exclusive group because - /// only one radio button in that group can be marked as [isChecked]. - static const SemanticsFlag isInMutuallyExclusiveGroup = - SemanticsFlag._(_kIsInMutuallyExclusiveGroupIndex); - - /// Whether a semantic node is a header that divides content into sections. - /// - /// For example, headers can be used to divide a list of alphabetically - /// sorted words into the sections A, B, C, etc. as can be found in many - /// address book applications. + static const SemanticsFlag isInMutuallyExclusiveGroup = SemanticsFlag._(_kIsInMutuallyExclusiveGroupIndex); static const SemanticsFlag isHeader = SemanticsFlag._(_kIsHeaderIndex); - - /// Whether the value of the semantics node is obscured. - /// - /// This is usually used for text fields to indicate that its content - /// is a password or contains other sensitive information. static const SemanticsFlag isObscured = SemanticsFlag._(_kIsObscuredIndex); - - /// Whether the semantics node is the root of a subtree for which a route name - /// should be announced. - /// - /// When a node with this flag is removed from the semantics tree, the - /// framework will select the last in depth-first, paint order node with this - /// flag. When a node with this flag is added to the semantics tree, it is - /// selected automatically, unless there were multiple nodes with this flag - /// added. In this case, the last added node in depth-first, paint order - /// will be selected. - /// - /// From this selected node, the framework will search in depth-first, paint - /// order for the first node with a [namesRoute] flag and a non-null, - /// non-empty label. The [namesRoute] and [scopesRoute] flags may be on the - /// same node. The label of the found node will be announced as an edge - /// transition. If no non-empty, non-null label is found then: - /// - /// * VoiceOver will make a chime announcement. - /// * TalkBack will make no announcement - /// - /// Semantic nodes annotated with this flag are generally not a11y focusable. - /// - /// This is used in widgets such as Routes, Drawers, and Dialogs to - /// communicate significant changes in the visible screen. static const SemanticsFlag scopesRoute = SemanticsFlag._(_kScopesRouteIndex); - - /// Whether the semantics node label is the name of a visually distinct - /// route. - /// - /// This is used by certain widgets like Drawers and Dialogs, to indicate - /// that the node's semantic label can be used to announce an edge triggered - /// semantics update. - /// - /// Semantic nodes annotated with this flag will still recieve a11y focus. - /// - /// Updating this label within the same active route subtree will not cause - /// additional announcements. static const SemanticsFlag namesRoute = SemanticsFlag._(_kNamesRouteIndex); - - /// Whether the semantics node is considered hidden. - /// - /// Hidden elements are currently not visible on screen. They may be covered - /// by other elements or positioned outside of the visible area of a viewport. - /// - /// Hidden elements cannot gain accessibility focus though regular touch. The - /// only way they can be focused is by moving the focus to them via linear - /// navigation. - /// - /// Platforms are free to completely ignore hidden elements and new platforms - /// are encouraged to do so. - /// - /// Instead of marking an element as hidden it should usually be excluded from - /// the semantics tree altogether. Hidden elements are only included in the - /// semantics tree to work around platform limitations and they are mainly - /// used to implement accessibility scrolling on iOS. static const SemanticsFlag isHidden = SemanticsFlag._(_kIsHiddenIndex); - - /// Whether the semantics node represents an image. - /// - /// Both TalkBack and VoiceOver will inform the user the semantics node - /// represents an image. static const SemanticsFlag isImage = SemanticsFlag._(_kIsImageIndex); - - /// Whether the semantics node is a live region. - /// - /// A live region indicates that updates to semantics node are important. - /// Platforms may use this information to make polite announcements to the - /// user to inform them of updates to this node. - /// - /// An example of a live region is a [SnackBar] widget. On Android, A live - /// region causes a polite announcement to be generated automatically, even - /// if the user does not have focus of the widget. - static const SemanticsFlag isLiveRegion = - SemanticsFlag._(_kIsLiveRegionIndex); - - /// The semantics node has the quality of either being "on" or "off". - /// - /// This flag is mutually exclusive with [hasCheckedState]. - /// - /// For example, a switch has toggled state. - /// - /// See also: - /// - /// * [SemanticsFlag.isToggled], which controls whether the node is "on" or "off". - static const SemanticsFlag hasToggledState = - SemanticsFlag._(_kHasToggledStateIndex); - - /// If true, the semantics node is "on". If false, the semantics node is - /// "off". - /// - /// For example, if a switch is in the on position, [isToggled] is true. - /// - /// See also: - /// - /// * [SemanticsFlag.hasToggledState], which enables a toggled state. + static const SemanticsFlag isLiveRegion = SemanticsFlag._(_kIsLiveRegionIndex); + static const SemanticsFlag hasToggledState = SemanticsFlag._(_kHasToggledStateIndex); static const SemanticsFlag isToggled = SemanticsFlag._(_kIsToggledIndex); - - /// Whether the platform can scroll the semantics node when the user attempts - /// to move focus to an offscreen child. - /// - /// For example, a [ListView] widget has implicit scrolling so that users can - /// easily move to the next visible set of children. A [TabBar] widget does - /// not have implicit scrolling, so that users can navigate into the tab - /// body when reaching the end of the tab bar. - static const SemanticsFlag hasImplicitScrolling = - SemanticsFlag._(_kHasImplicitScrollingIndex); - - /// Whether the value of the semantics node is coming from a multi-line text - /// field. - /// - /// This isused for text fields to distinguish single-line text field from - /// multi-line ones. + static const SemanticsFlag hasImplicitScrolling = SemanticsFlag._(_kHasImplicitScrollingIndex); static const SemanticsFlag isMultiline = SemanticsFlag._(_kIsMultilineIndex); - - /// The possible semantics flags. - /// - /// The map's key is the [index] of the flag and the value is the flag itself. - /// The possible semantics flags. - /// - /// The map's key is the [index] of the flag and the value is the flag itself. static const Map values = { _kHasCheckedStateIndex: hasCheckedState, _kIsCheckedIndex: isChecked, @@ -607,65 +263,10 @@ class SemanticsFlag { } } -/// An object that creates [SemanticsUpdate] objects. -/// -/// Once created, the [SemanticsUpdate] objects can be passed to -/// [Window.updateSemantics] to update the semantics conveyed to the user. class SemanticsUpdateBuilder { - /// Creates an empty [SemanticsUpdateBuilder] object. SemanticsUpdateBuilder(); - final List _nodeUpdates = - []; - - /// Update the information associated with the node with the given `id`. - /// - /// The semantics nodes form a tree, with the root of the tree always having - /// an id of zero. The `childrenInTraversalOrder` and `childrenInHitTestOrder` - /// are the ids of the nodes that are immediate children of this node. The - /// former enumerates children in traversal order, and the latter enumerates - /// the same children in the hit test order. The two lists must have the same - /// length and contain the same ids. They may only differ in the order the - /// ids are listed in. For more information about different child orders, see - /// [DebugSemanticsDumpOrder]. - /// - /// The system retains the nodes that are currently reachable from the root. - /// A given update need not contain information for nodes that do not change - /// in the update. If a node is not reachable from the root after an update, - /// the node will be discarded from the tree. - /// - /// The `flags` are a bit field of [SemanticsFlag]s that apply to this node. - /// - /// The `actions` are a bit field of [SemanticsAction]s that can be undertaken - /// by this node. If the user wishes to undertake one of these actions on this - /// node, the [Window.onSemanticsAction] will be called with `id` and one of - /// the possible [SemanticsAction]s. Because the semantics tree is maintained - /// asynchronously, the [Window.onSemanticsAction] callback might be called - /// with an action that is no longer possible. - /// - /// The `label` is a string that describes this node. The `value` property - /// describes the current value of the node as a string. The `increasedValue` - /// string will become the `value` string after a [SemanticsAction.increase] - /// action is performed. The `decreasedValue` string will become the `value` - /// string after a [SemanticsAction.decrease] action is performed. The `hint` - /// string describes what result an action performed on this node has. The - /// reading direction of all these strings is given by `textDirection`. - /// - /// The fields 'textSelectionBase' and 'textSelectionExtent' describe the - /// currently selected text within `value`. - /// - /// For scrollable nodes `scrollPosition` describes the current scroll - /// position in logical pixel. `scrollExtentMax` and `scrollExtentMin` - /// describe the maximum and minimum in-rage values that `scrollPosition` can - /// be. Both or either may be infinity to indicate unbound scrolling. The - /// value for `scrollPosition` can (temporarily) be outside this range, for - /// example during an overscroll. - /// - /// The `rect` is the region occupied by this node in its own coordinate - /// system. - /// - /// The `transform` is a matrix that maps this node's coordinate system into - /// its parent's coordinate system. + final List _nodeUpdates = []; void updateNode({ required int id, required int flags, @@ -726,16 +327,14 @@ class SemanticsUpdateBuilder { )); } - void updateCustomAction( - {required int id, String? label, String? hint, int overrideId = -1}) { + void updateCustomAction({ + required int id, + String? label, + String? hint, + int overrideId = -1, + }) { // TODO(yjbanov): implement. } - - /// Creates a [SemanticsUpdate] object that encapsulates the updates recorded - /// by this object. - /// - /// The returned object can be passed to [Window.updateSemantics] to actually - /// update the semantics retained by the system. SemanticsUpdate build() { return SemanticsUpdate._( nodeUpdates: _nodeUpdates, @@ -743,23 +342,8 @@ class SemanticsUpdateBuilder { } } -/// An opaque object representing a batch of semantics updates. -/// -/// To create a SemanticsUpdate object, use a [SemanticsUpdateBuilder]. -/// -/// Semantics updates can be applied to the system's retained semantics tree -/// using the [Window.updateSemantics] method. abstract class SemanticsUpdate { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. - /// - /// To create a SemanticsUpdate object, use a [SemanticsUpdateBuilder]. factory SemanticsUpdate._({List? nodeUpdates}) = engine.SemanticsUpdate; - - /// Releases the resources used by this semantics update. - /// - /// After calling this function, the semantics update is cannot be used - /// further. void dispose(); } diff --git a/lib/web_ui/lib/src/ui/test_embedding.dart b/lib/web_ui/lib/src/ui/test_embedding.dart index 955dbfe53c0db..7007ebf4420c2 100644 --- a/lib/web_ui/lib/src/ui/test_embedding.dart +++ b/lib/web_ui/lib/src/ui/test_embedding.dart @@ -7,30 +7,22 @@ // @dart = 2.10 part of ui; -/// Used to track when the platform is initialized. This ensures the test fonts -/// are available. Future? _testPlatformInitializedFuture; -/// If the platform is already initialized (by a previous test), then run the test -/// body immediately. Otherwise, initialize the platform then run the test. -Future ensureTestPlatformInitializedThenRunTest( - dynamic Function() body) { +Future ensureTestPlatformInitializedThenRunTest(dynamic Function() body) { if (_testPlatformInitializedFuture == null) { debugEmulateFlutterTesterEnvironment = true; // Initializing the platform will ensure that the test font is loaded. - _testPlatformInitializedFuture = webOnlyInitializePlatform( - assetManager: engine.WebOnlyMockAssetManager()); + _testPlatformInitializedFuture = + webOnlyInitializePlatform(assetManager: engine.WebOnlyMockAssetManager()); } return _testPlatformInitializedFuture!.then((_) => body()); } -/// Used to track when the platform is initialized. This ensures the test fonts -/// are available. // TODO(yjbanov): can we make this late non-null? See https://github.com/dart-lang/sdk/issues/42214 Future? _platformInitializedFuture; -/// Initializes domRenderer with specific devicePixelRatio and physicalSize. Future webOnlyInitializeTestDomRenderer({double devicePixelRatio = 3.0}) { // Force-initialize DomRenderer so it doesn't overwrite test pixel ratio. engine.domRenderer; diff --git a/lib/web_ui/lib/src/ui/text.dart b/lib/web_ui/lib/src/ui/text.dart index 58698d862a6d4..054ac36a84ba4 100644 --- a/lib/web_ui/lib/src/ui/text.dart +++ b/lib/web_ui/lib/src/ui/text.dart @@ -6,99 +6,34 @@ // @dart = 2.10 part of ui; -/// Whether to slant the glyphs in the font enum FontStyle { - /// Use the upright glyphs normal, - - /// Use glyphs designed for slanting italic, } -/// Where to vertically align the placeholder relative to the surrounding text. -/// -/// Used by [ParagraphBuilder.addPlaceholder]. enum PlaceholderAlignment { - /// Match the baseline of the placeholder with the baseline. - /// - /// The [TextBaseline] to use must be specified and non-null when using this - /// alignment mode. baseline, - - /// Align the bottom edge of the placeholder with the baseline such that the - /// placeholder sits on top of the baseline. - /// - /// The [TextBaseline] to use must be specified and non-null when using this - /// alignment mode. aboveBaseline, - - /// Align the top edge of the placeholder with the baseline specified - /// such that the placeholder hangs below the baseline. - /// - /// The [TextBaseline] to use must be specified and non-null when using this - /// alignment mode. belowBaseline, - - /// Align the top edge of the placeholder with the top edge of the font. - /// - /// When the placeholder is very tall, the extra space will hang from - /// the top and extend through the bottom of the line. top, - - /// Align the bottom edge of the placeholder with the top edge of the font. - /// - /// When the placeholder is very tall, the extra space will rise from the - /// bottom and extend through the top of the line. bottom, - - /// Align the middle of the placeholder with the middle of the text. - /// - /// When the placeholder is very tall, the extra space will grow equally - /// from the top and bottom of the line. middle, } -/// The thickness of the glyphs used to draw the text class FontWeight { const FontWeight._(this.index); - - /// The encoded integer value of this font weight. final int index; - - /// Thin, the least thick static const FontWeight w100 = FontWeight._(0); - - /// Extra-light static const FontWeight w200 = FontWeight._(1); - - /// Light static const FontWeight w300 = FontWeight._(2); - - /// Normal / regular / plain static const FontWeight w400 = FontWeight._(3); - - /// Medium static const FontWeight w500 = FontWeight._(4); - - /// Semi-bold static const FontWeight w600 = FontWeight._(5); - - /// Bold static const FontWeight w700 = FontWeight._(6); - - /// Extra-bold static const FontWeight w800 = FontWeight._(7); - - /// Black, the most thick static const FontWeight w900 = FontWeight._(8); - - /// The default font weight. static const FontWeight normal = w400; - - /// A commonly used font weight that is heavier than normal. static const FontWeight bold = w700; - - /// A list of all the font weights. static const List values = [ w100, w200, @@ -110,27 +45,6 @@ class FontWeight { w800, w900 ]; - - /// Linearly interpolates between two font weights. - /// - /// Rather than using fractional weights, the interpolation rounds to the - /// nearest weight. - /// - /// Any null values for `a` or `b` are interpreted as equivalent to [normal] - /// (also known as [w400]). - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). The result - /// is clamped to the range [w100]–[w900]. - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static FontWeight? lerp(FontWeight? a, FontWeight? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (a == null && b == null) @@ -154,129 +68,35 @@ class FontWeight { } } -/// A feature tag and value that affect the selection of glyphs in a font. class FontFeature { - /// Creates a [FontFeature] object, which can be added to a [TextStyle] to - /// change how the engine selects glyphs when rendering text. - /// - /// `feature` is the four-character tag that identifies the feature. - /// These tags are specified by font formats such as OpenType. - /// - /// `value` is the value that the feature will be set to. The behavior - /// of the value depends on the specific feature. Many features are - /// flags whose value can be 1 (when enabled) or 0 (when disabled). - /// - /// See const FontFeature(this.feature, [this.value = 1]) : assert(feature != null), // ignore: unnecessary_null_comparison assert(feature.length == 4), assert(value != null), // ignore: unnecessary_null_comparison assert(value >= 0); - - /// Create a [FontFeature] object that enables the feature with the given tag. const FontFeature.enable(String feature) : this(feature, 1); - - /// Create a [FontFeature] object that disables the feature with the given tag. const FontFeature.disable(String feature) : this(feature, 0); - - /// Randomize the alternate forms used in text. - /// - /// For example, this can be used with suitably-prepared handwriting fonts to - /// vary the forms used for each character, so that, for instance, the word - /// "cross-section" would be rendered with two different "c"s, two different "o"s, - /// and three different "s"s. - /// - /// See also: - /// - /// * const FontFeature.randomize() : feature = 'rand', value = 1; - - /// Select a stylistic set. - /// - /// Fonts may have up to 20 stylistic sets, numbered 1 through 20. - /// - /// See also: - /// - /// * factory FontFeature.stylisticSet(int value) { assert(value >= 1); assert(value <= 20); return FontFeature('ss${value.toString().padLeft(2, "0")}'); } - - /// Use the slashed zero. - /// - /// Some fonts contain both a circular zero and a zero with a slash. This - /// enables the use of the latter form. - /// - /// This is overridden by [FontFeature.oldstyleFigures]. - /// - /// See also: - /// - /// * const FontFeature.slashedZero() : feature = 'zero', value = 1; - - /// Use oldstyle figures. - /// - /// Some fonts have variants of the figures (e.g. the digit 9) that, when - /// this feature is enabled, render with descenders under the baseline instead - /// of being entirely above the baseline. - /// - /// This overrides [FontFeature.slashedZero]. - /// - /// See also: - /// - /// * const FontFeature.oldstyleFigures() : feature = 'onum', value = 1; - - /// Use proportional (varying width) figures. - /// - /// For fonts that have both proportional and tabular (monospace) figures, - /// this enables the proportional figures. - /// - /// This is mutually exclusive with [FontFeature.tabularFigures]. - /// - /// The default behavior varies from font to font. - /// - /// See also: - /// - /// * const FontFeature.proportionalFigures() : feature = 'pnum', value = 1; - - /// Use tabular (monospace) figures. - /// - /// For fonts that have both proportional (varying width) and tabular figures, - /// this enables the tabular figures. - /// - /// This is mutually exclusive with [FontFeature.proportionalFigures]. - /// - /// The default behavior varies from font to font. - /// - /// See also: - /// - /// * const FontFeature.tabularFigures() : feature = 'tnum', value = 1; - - /// The tag that identifies the effect of this feature. Must consist of 4 - /// ASCII characters (typically lowercase letters). - /// - /// See final String feature; - - /// The value assigned to this feature. - /// - /// Must be a positive integer. Many features are Boolean values that accept - /// values of either 0 (feature is disabled) or 1 (feature is enabled). final int value; @override @@ -299,53 +119,23 @@ class FontFeature { String toString() => 'FontFeature($feature, $value)'; } -/// Whether and how to align text horizontally. // The order of this enum must match the order of the values in RenderStyleConstants.h's ETextAlign. enum TextAlign { - /// Align the text on the left edge of the container. left, - - /// Align the text on the right edge of the container. right, - - /// Align the text in the center of the container. center, - - /// Stretch lines of text that end with a soft line break to fill the width of - /// the container. - /// - /// Lines that end with hard line breaks are aligned towards the [start] edge. justify, - - /// Align the text on the leading edge of the container. - /// - /// For left-to-right text ([TextDirection.ltr]), this is the left edge. - /// - /// For right-to-left text ([TextDirection.rtl]), this is the right edge. start, - - /// Align the text on the trailing edge of the container. - /// - /// For left-to-right text ([TextDirection.ltr]), this is the right edge. - /// - /// For right-to-left text ([TextDirection.rtl]), this is the left edge. end, } -/// A horizontal line used for aligning text. enum TextBaseline { - /// The horizontal line used to align the bottom of glyphs for alphabetic characters. alphabetic, - - /// The horizontal line used to align ideographic characters. ideographic, } -/// A linear decoration to draw near the text. class TextDecoration { const TextDecoration._(this._mask); - - /// Creates a decoration that paints the union of all the given decorations. factory TextDecoration.combine(List decorations) { int mask = 0; for (TextDecoration decoration in decorations) { @@ -355,22 +145,13 @@ class TextDecoration { } final int _mask; - - /// Whether this decoration will paint at least as much decoration as the given decoration. bool contains(TextDecoration other) { return (_mask | other._mask) == _mask; } - /// Do not draw a decoration static const TextDecoration none = TextDecoration._(0x0); - - /// Draw a line underneath each line of text static const TextDecoration underline = TextDecoration._(0x1); - - /// Draw a line above each line of text static const TextDecoration overline = TextDecoration._(0x2); - - /// Draw a line through each line of text static const TextDecoration lineThrough = TextDecoration._(0x4); @override @@ -404,81 +185,24 @@ class TextDecoration { } } -/// The style in which to draw a text decoration enum TextDecorationStyle { - /// Draw a solid line solid, - - /// Draw two lines double, - - /// Draw a dotted line dotted, - - /// Draw a dashed line dashed, - - /// Draw a sinusoidal line wavy } -/// Defines how the paragraph will apply [TextStyle.height] the ascent of the -/// first line and descent of the last line. -/// -/// The boolean value represents whether the [TextStyle.height] modifier will -/// be applied to the corresponding metric. By default, all properties are true, -/// and [TextStyle.height] is applied as normal. When set to false, the font's -/// default ascent will be used. class TextHeightBehavior { - - /// Creates a new TextHeightBehavior object. - /// - /// * applyHeightToFirstAscent: When true, the [TextStyle.height] modifier - /// will be applied to the ascent of the first line. When false, the font's - /// default ascent will be used. - /// * applyHeightToLastDescent: When true, the [TextStyle.height] modifier - /// will be applied to the descent of the last line. When false, the font's - /// default descent will be used. - /// - /// All properties default to true (height modifications applied as normal). const TextHeightBehavior({ this.applyHeightToFirstAscent = true, this.applyHeightToLastDescent = true, }); - - /// Creates a new TextHeightBehavior object from an encoded form. - /// - /// See [encode] for the creation of the encoded form. const TextHeightBehavior.fromEncoded(int encoded) : applyHeightToFirstAscent = (encoded & 0x1) == 0, applyHeightToLastDescent = (encoded & 0x2) == 0; - - - /// Whether to apply the [TextStyle.height] modifier to the ascent of the first - /// line in the paragraph. - /// - /// When true, the [TextStyle.height] modifier will be applied to to the ascent - /// of the first line. When false, the font's default ascent will be used and - /// the [TextStyle.height] will have no effect on the ascent of the first line. - /// - /// This property only has effect if a non-null [TextStyle.height] is specified. - /// - /// Defaults to true (height modifications applied as normal). final bool applyHeightToFirstAscent; - - /// Whether to apply the [TextStyle.height] modifier to the descent of the last - /// line in the paragraph. - /// - /// When true, the [TextStyle.height] modifier will be applied to to the descent - /// of the last line. When false, the font's default descent will be used and - /// the [TextStyle.height] will have no effect on the descent of the last line. - /// - /// This property only has effect if a non-null [TextStyle.height] is specified. - /// - /// Defaults to true (height modifications applied as normal). final bool applyHeightToLastDescent; - - /// Returns an encoded int representation of this object. int encode() { return (applyHeightToFirstAscent ? 0 : 1 << 0) | (applyHeightToLastDescent ? 0 : 1 << 1); } @@ -486,7 +210,7 @@ class TextHeightBehavior { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) - return false; + return false; return other is TextHeightBehavior && other.applyHeightToFirstAscent == applyHeightToFirstAscent && other.applyHeightToLastDescent == applyHeightToLastDescent; @@ -509,33 +233,7 @@ class TextHeightBehavior { } } -/// An opaque object that determines the size, position, and rendering of text. abstract class TextStyle { - /// Creates a new TextStyle object. - /// - /// * `color`: The color to use when painting the text. If this is specified, `foreground` must be null. - /// * `decoration`: The decorations to paint near the text (e.g., an underline). - /// * `decorationColor`: The color in which to paint the text decorations. - /// * `decorationStyle`: The style in which to paint the text decorations (e.g., dashed). - /// * `fontWeight`: The typeface thickness to use when painting the text (e.g., bold). - /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., italics). - /// * `fontFamily`: The name of the font to use when painting the text (e.g., Roboto). If a `fontFamilyFallback` is - /// provided and `fontFamily` is not, then the first font family in `fontFamilyFallback` will take the postion of - /// the preferred font family. When a higher priority font cannot be found or does not contain a glyph, a lower - /// priority font will be used. - /// * `fontFamilyFallback`: An ordered list of the names of the fonts to fallback on when a glyph cannot - /// be found in a higher priority font. When the `fontFamily` is null, the first font family in this list - /// is used as the preferred font. Internally, the 'fontFamily` is concatenated to the front of this list. - /// When no font family is provided through 'fontFamilyFallback' (null or empty) or `fontFamily`, then the - /// platform default font will be used. - /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting the text. - /// * `letterSpacing`: The amount of space (in logical pixels) to add between each letter. - /// * `wordSpacing`: The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). - /// * `textBaseline`: The common baseline that should be aligned between this text span and its parent text span, or, for the root text spans, with the line box. - /// * `height`: The height of this text span, as a multiple of the font size. - /// * `locale`: The locale used to select region-specific glyphs. - /// * `background`: The paint drawn as a background for the text. - /// * `foreground`: The paint used to draw the text. If this is specified, `color` must be null. factory TextStyle({ Color? color, TextDecoration? decoration, @@ -559,109 +257,54 @@ abstract class TextStyle { }) { if (engine.experimentalUseSkia) { return engine.CkTextStyle( - color: color, - decoration: decoration, - decorationColor: decorationColor, - decorationStyle: decorationStyle, - decorationThickness: decorationThickness, - fontWeight: fontWeight, - fontStyle: fontStyle, - textBaseline: textBaseline, - fontFamily: fontFamily, - fontFamilyFallback: fontFamilyFallback, - fontSize: fontSize, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - height: height, - locale: locale, - background: background as engine.CkPaint?, - foreground: foreground as engine.CkPaint?, - shadows: shadows, - fontFeatures: fontFeatures, + color: color, + decoration: decoration, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + fontWeight: fontWeight, + fontStyle: fontStyle, + textBaseline: textBaseline, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + fontSize: fontSize, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + height: height, + locale: locale, + background: background as engine.CkPaint?, + foreground: foreground as engine.CkPaint?, + shadows: shadows, + fontFeatures: fontFeatures, ); } else { return engine.EngineTextStyle( - color: color, - decoration: decoration, - decorationColor: decorationColor, - decorationStyle: decorationStyle, - decorationThickness: decorationThickness, - fontWeight: fontWeight, - fontStyle: fontStyle, - textBaseline: textBaseline, - fontFamily: fontFamily, - fontFamilyFallback: fontFamilyFallback, - fontSize: fontSize, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - height: height, - locale: locale, - background: background, - foreground: foreground, - shadows: shadows, - fontFeatures: fontFeatures, + color: color, + decoration: decoration, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + fontWeight: fontWeight, + fontStyle: fontStyle, + textBaseline: textBaseline, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + fontSize: fontSize, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + height: height, + locale: locale, + background: background, + foreground: foreground, + shadows: shadows, + fontFeatures: fontFeatures, ); } } } -/// An opaque object that determines the configuration used by -/// [ParagraphBuilder] to position lines within a [Paragraph] of text. abstract class ParagraphStyle { - /// Creates a new ParagraphStyle object. - /// - /// * `textAlign`: The alignment of the text within the lines of the - /// paragraph. If the last line is ellipsized (see `ellipsis` below), the - /// alignment is applied to that line after it has been truncated but before - /// the ellipsis has been added. // See: https://github.com/flutter/flutter/issues/9819 - /// - /// * `textDirection`: The directionality of the text, left-to-right (e.g. - /// Norwegian) or right-to-left (e.g. Hebrew). This controls the overall - /// directionality of the paragraph, as well as the meaning of - /// [TextAlign.start] and [TextAlign.end] in the `textAlign` field. - /// - /// * `maxLines`: The maximum number of lines painted. Lines beyond this - /// number are silently dropped. For example, if `maxLines` is 1, then only - /// one line is rendered. If `maxLines` is null, but `ellipsis` is not null, - /// then lines after the first one that overflows the width constraints are - /// dropped. The width constraints are those set in the - /// [ParagraphConstraints] object passed to the [Paragraph.layout] method. - /// - /// * `fontFamily`: The name of the font to use when painting the text (e.g., - /// Roboto). - /// - /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting - /// the text. - /// - /// * `height`: The minimum height of the line boxes, as a multiple of the - /// font size. The lines of the paragraph will be at least - /// `(height + leading) * fontSize` tall when fontSize - /// is not null. When fontSize is null, there is no minimum line height. Tall - /// glyphs due to baseline alignment or large [TextStyle.fontSize] may cause - /// the actual line height after layout to be taller than specified here. - /// [fontSize] must be provided for this property to take effect. - /// - /// * `fontWeight`: The typeface thickness to use when painting the text - /// (e.g., bold). - /// - /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., - /// italics). - /// - /// * `strutStyle`: The properties of the strut. Strut defines a set of minimum - /// vertical line height related metrics and can be used to obtain more - /// advanced line spacing behavior. - /// - /// * `ellipsis`: String used to ellipsize overflowing text. If `maxLines` is - /// not null, then the `ellipsis`, if any, is applied to the last rendered - /// line, if that line overflows the width constraints. If `maxLines` is - /// null, then the `ellipsis` is applied to the first line that overflows - /// the width constraints, and subsequent lines are dropped. The width - /// constraints are those set in the [ParagraphConstraints] object passed to - /// the [Paragraph.layout] method. The empty string and the null value are - /// considered equivalent and turn off this behavior. - /// - /// * `locale`: The locale used to select region-specific glyphs. factory ParagraphStyle({ TextAlign? textAlign, TextDirection? textDirection, @@ -711,41 +354,6 @@ abstract class ParagraphStyle { } abstract class StrutStyle { - /// Creates a new StrutStyle object. - /// - /// * `fontFamily`: The name of the font to use when painting the text (e.g., - /// Roboto). - /// - /// * `fontFamilyFallback`: An ordered list of font family names that will be searched for when - /// the font in `fontFamily` cannot be found. - /// - /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting - /// the text. - /// - /// * `lineHeight`: The minimum height of the line boxes, as a multiple of the - /// font size. The lines of the paragraph will be at least - /// `(lineHeight + leading) * fontSize` tall when fontSize - /// is not null. When fontSize is null, there is no minimum line height. Tall - /// glyphs due to baseline alignment or large [TextStyle.fontSize] may cause - /// the actual line height after layout to be taller than specified here. - /// [fontSize] must be provided for this property to take effect. - /// - /// * `leading`: The minimum amount of leading between lines as a multiple of - /// the font size. [fontSize] must be provided for this property to take effect. - /// - /// * `fontWeight`: The typeface thickness to use when painting the text - /// (e.g., bold). - /// - /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., - /// italics). - /// - /// * `forceStrutHeight`: When true, the paragraph will force all lines to be exactly - /// `(lineHeight + leading) * fontSize` tall from baseline to baseline. - /// [TextStyle] is no longer able to influence the line height, and any tall - /// glyphs may overlap with lines above. If a [fontFamily] is specified, the - /// total ascent of the first line will be the min of the `Ascent + half-leading` - /// of the [fontFamily] and `(lineHeight + leading) * fontSize`. Otherwise, it - /// will be determined by the Ascent + half-leading of the first text. factory StrutStyle({ String? fontFamily, List? fontFamilyFallback, @@ -758,104 +366,13 @@ abstract class StrutStyle { }) = engine.EngineStrutStyle; } -/// A direction in which text flows. -/// -/// Some languages are written from the left to the right (for example, English, -/// Tamil, or Chinese), while others are written from the right to the left (for -/// example Aramaic, Hebrew, or Urdu). Some are also written in a mixture, for -/// example Arabic is mostly written right-to-left, with numerals written -/// left-to-right. -/// -/// The text direction must be provided to APIs that render text or lay out -/// boxes horizontally, so that they can determine which direction to start in: -/// either right-to-left, [TextDirection.rtl]; or left-to-right, -/// [TextDirection.ltr]. -/// -/// ## Design discussion -/// -/// Flutter is designed to address the needs of applications written in any of -/// the world's currently-used languages, whether they use a right-to-left or -/// left-to-right writing direction. Flutter does not support other writing -/// modes, such as vertical text or boustrophedon text, as these are rarely used -/// in computer programs. -/// -/// It is common when developing user interface frameworks to pick a default -/// text direction — typically left-to-right, the direction most familiar to the -/// engineers working on the framework — because this simplifies the development -/// of applications on the platform. Unfortunately, this frequently results in -/// the platform having unexpected left-to-right biases or assumptions, as -/// engineers will typically miss places where they need to support -/// right-to-left text. This then results in bugs that only manifest in -/// right-to-left environments. -/// -/// In an effort to minimize the extent to which Flutter experiences this -/// category of issues, the lowest levels of the Flutter framework do not have a -/// default text reading direction. Any time a reading direction is necessary, -/// for example when text is to be displayed, or when a -/// writing-direction-dependent value is to be interpreted, the reading -/// direction must be explicitly specified. Where possible, such as in `switch` -/// statements, the right-to-left case is listed first, to avoid the impression -/// that it is an afterthought. -/// -/// At the higher levels (specifically starting at the widgets library), an -/// ambient [Directionality] is introduced, which provides a default. Thus, for -/// instance, a [Text] widget in the scope of a [MaterialApp] widget does not -/// need to be given an explicit writing direction. The [Directionality.of] -/// static method can be used to obtain the ambient text direction for a -/// particular [BuildContext]. -/// -/// ### Known left-to-right biases in Flutter -/// -/// Despite the design intent described above, certain left-to-right biases have -/// nonetheless crept into Flutter's design. These include: -/// -/// * The [Canvas] origin is at the top left, and the x-axis increases in a -/// left-to-right direction. -/// -/// * The default localization in the widgets and material libraries is -/// American English, which is left-to-right. -/// -/// ### Visual properties vs directional properties -/// -/// Many classes in the Flutter framework are offered in two versions, a -/// visually-oriented variant, and a text-direction-dependent variant. For -/// example, [EdgeInsets] is described in terms of top, left, right, and bottom, -/// while [EdgeInsetsDirectional] is described in terms of top, start, end, and -/// bottom, where start and end correspond to right and left in right-to-left -/// text and left and right in left-to-right text. -/// -/// There are distinct use cases for each of these variants. -/// -/// Text-direction-dependent variants are useful when developing user interfaces -/// that should "flip" with the text direction. For example, a paragraph of text -/// in English will typically be left-aligned and a quote will be indented from -/// the left, while in Arabic it will be right-aligned and indented from the -/// right. Both of these cases are described by the direction-dependent -/// [TextAlign.start] and [EdgeInsetsDirectional.start]. -/// -/// In contrast, the visual variants are useful when the text direction is known -/// and not affected by the reading direction. For example, an application -/// giving driving directions might show a "turn left" arrow on the left and a -/// "turn right" arrow on the right — and would do so whether the application -/// was localized to French (left-to-right) or Hebrew (right-to-left). -/// -/// In practice, it is also expected that many developers will only be -/// targeting one language, and in that case it may be simpler to think in -/// visual terms. // The order of this enum must match the order of the values in TextDirection.h's TextDirection. enum TextDirection { - /// The text flows from right to left (e.g. Arabic, Hebrew). rtl, - - /// The text flows from left to right (e.g., English, French). ltr, } -/// A rectangle enclosing a run of text. -/// -/// This is similar to [Rect] but includes an inherent [TextDirection]. class TextBox { - /// Creates an object that describes a box containing text. const TextBox.fromLTRBD( this.left, this.top, @@ -863,43 +380,16 @@ class TextBox { this.bottom, this.direction, ); - - /// The left edge of the text box, irrespective of direction. - /// - /// To get the leading edge (which may depend on the [direction]), consider [start]. final double left; - - /// The top edge of the text box. final double top; - - /// The right edge of the text box, irrespective of direction. - /// - /// To get the trailing edge (which may depend on the [direction]), consider [end]. final double right; - - /// The bottom edge of the text box. final double bottom; - - /// The direction in which text inside this box flows. final TextDirection direction; - - /// Returns a rect of the same size as this box. Rect toRect() => Rect.fromLTRB(left, top, right, bottom); - - /// The [left] edge of the box for left-to-right text; the [right] edge of the box for right-to-left text. - /// - /// See also: - /// - /// * [direction], which specifies the text direction. double get start { return (direction == TextDirection.ltr) ? left : right; } - /// The [right] edge of the box for left-to-right text; the [left] edge of the box for right-to-left text. - /// - /// See also: - /// - /// * [direction], which specifies the text direction. double get end { return (direction == TextDirection.ltr) ? right : left; } @@ -929,95 +419,18 @@ class TextBox { } } -/// A way to disambiguate a [TextPosition] when its offset could match two -/// different locations in the rendered string. -/// -/// For example, at an offset where the rendered text wraps, there are two -/// visual positions that the offset could represent: one prior to the line -/// break (at the end of the first line) and one after the line break (at the -/// start of the second line). A text affinity disambiguates between these two -/// cases. -/// -/// This affects only line breaks caused by wrapping, not explicit newline -/// characters. For newline characters, the position is fully specified by the -/// offset alone, and there is no ambiguity. -/// -/// [TextAffinity] also affects bidirectional text at the interface between LTR -/// and RTL text. Consider the following string, where the lowercase letters -/// will be displayed as LTR and the uppercase letters RTL: "helloHELLO". When -/// rendered, the string would appear visually as "helloOLLEH". An offset of 5 -/// would be ambiguous without a corresponding [TextAffinity]. Looking at the -/// string in code, the offset represents the position just after the "o" and -/// just before the "H". When rendered, this offset could be either in the -/// middle of the string to the right of the "o" or at the end of the string to -/// the right of the "H". enum TextAffinity { - /// The position has affinity for the upstream side of the text position, i.e. - /// in the direction of the beginning of the string. - /// - /// In the example of an offset at the place where text is wrapping, upstream - /// indicates the end of the first line. - /// - /// In the bidirectional text example "helloHELLO", an offset of 5 with - /// [TextAffinity] upstream would appear in the middle of the rendered text, - /// just to the right of the "o". See the definition of [TextAffinity] for the - /// full example. upstream, - - /// The position has affinity for the downstream side of the text position, - /// i.e. in the direction of the end of the string. - /// - /// In the example of an offset at the place where text is wrapping, - /// downstream indicates the beginning of the second line. - /// - /// In the bidirectional text example "helloHELLO", an offset of 5 with - /// [TextAffinity] downstream would appear at the end of the rendered text, - /// just to the right of the "H". See the definition of [TextAffinity] for the - /// full example. downstream, } -/// A position in a string of text. -/// -/// A TextPosition can be used to locate a position in a string in code (using -/// the [offset] property), and it can also be used to locate the same position -/// visually in a rendered string of text (using [offset] and, when needed to -/// resolve ambiguity, [affinity]). -/// -/// The location of an offset in a rendered string is ambiguous in two cases. -/// One happens when rendered text is forced to wrap. In this case, the offset -/// where the wrap occurs could visually appear either at the end of the first -/// line or the beginning of the second line. The second way is with -/// bidirectional text. An offset at the interface between two different text -/// directions could have one of two locations in the rendered text. -/// -/// See the documentation for [TextAffinity] for more information on how -/// TextAffinity disambiguates situations like these. class TextPosition { - /// Creates an object representing a particular position in a string. - /// - /// The arguments must not be null (so the [offset] argument is required). const TextPosition({ required this.offset, this.affinity = TextAffinity.downstream, }) : assert(offset != null), // ignore: unnecessary_null_comparison assert(affinity != null); // ignore: unnecessary_null_comparison - - /// The index of the character that immediately follows the position in the - /// string representation of the text. - /// - /// For example, given the string `'Hello'`, offset 0 represents the cursor - /// being before the `H`, while offset 5 represents the cursor being just - /// after the `o`. final int offset; - - /// Disambiguates cases where the position in the string given by [offset] - /// could represent two different visual positions in the rendered text. For - /// example, this can happen when text is forced to wrap, or when one string - /// of text is rendered with multiple text directions. - /// - /// See the documentation for [TextAffinity] for more information on how - /// TextAffinity disambiguates situations like these. final TextAffinity affinity; @override @@ -1039,64 +452,32 @@ class TextPosition { } } -/// A range of characters in a string of text. class TextRange { - /// Creates a text range. - /// - /// The [start] and [end] arguments must not be null. Both the [start] and - /// [end] must either be greater than or equal to zero or both exactly -1. - /// - /// Instead of creating an empty text range, consider using the [empty] - /// constant. const TextRange({ required this.start, required this.end, }) : assert(start != null && start >= -1), // ignore: unnecessary_null_comparison assert(end != null && end >= -1); // ignore: unnecessary_null_comparison - - /// A text range that starts and ends at offset. - /// - /// The [offset] argument must be non-null and greater than or equal to -1. const TextRange.collapsed(int offset) : assert(offset != null && offset >= -1), // ignore: unnecessary_null_comparison start = offset, end = offset; - - /// A text range that contains nothing and is not in the text. static const TextRange empty = TextRange(start: -1, end: -1); - - /// The index of the first character in the range. - /// - /// If [start] and [end] are both -1, the text range is empty. final int start; - - /// The next index after the characters in this range. - /// - /// If [start] and [end] are both -1, the text range is empty. final int end; - - /// Whether this range represents a valid position in the text. bool get isValid => start >= 0 && end >= 0; - - /// Whether this range is empty (but still potentially placed inside the text). bool get isCollapsed => start == end; - - /// Whether the start of this range precedes the end. bool get isNormalized => end >= start; - - /// The text before this range. String textBefore(String text) { assert(isNormalized); return text.substring(0, start); } - /// The text after this range. String textAfter(String text) { assert(isNormalized); return text.substring(end); } - /// The text inside this range. String textInside(String text) { assert(isNormalized); return text.substring(start, end); @@ -1122,37 +503,10 @@ class TextRange { String toString() => 'TextRange(start: $start, end: $end)'; } -/// Layout constraints for [Paragraph] objects. -/// -/// Instances of this class are typically used with [Paragraph.layout]. -/// -/// The only constraint that can be specified is the [width]. See the discussion -/// at [width] for more details. class ParagraphConstraints { - /// Creates constraints for laying out a pargraph. - /// - /// The [width] argument must not be null. const ParagraphConstraints({ required this.width, }) : assert(width != null); // ignore: unnecessary_null_comparison - - /// The width the paragraph should use whey computing the positions of glyphs. - /// - /// If possible, the paragraph will select a soft line break prior to reaching - /// this width. If no soft line break is available, the paragraph will select - /// a hard line break prior to reaching this width. If that would force a line - /// break without any characters having been placed (i.e. if the next - /// character to be laid out does not fit within the given width constraint) - /// then the next character is allowed to overflow the width constraint and a - /// forced line break is placed after it (even if an explicit line break - /// follows). - /// - /// The width influences how ellipses are applied. See the discussion at [new - /// ParagraphStyle] for more details. - /// - /// This width is also used to position glyphs according to the [TextAlign] - /// alignment described in the [ParagraphStyle] used when building the - /// [Paragraph] with a [ParagraphBuilder]. final double width; @override @@ -1171,72 +525,19 @@ class ParagraphConstraints { String toString() => '$runtimeType(width: $width)'; } -/// Defines various ways to vertically bound the boxes returned by -/// [Paragraph.getBoxesForRange]. enum BoxHeightStyle { - /// Provide tight bounding boxes that fit heights per run. This style may result - /// in uneven bounding boxes that do not nicely connect with adjacent boxes. tight, - - /// The height of the boxes will be the maximum height of all runs in the - /// line. All boxes in the same line will be the same height. This does not - /// guarantee that the boxes will cover the entire vertical height of the line - /// when there is additional line spacing. - /// - /// See [BoxHeightStyle.includeLineSpacingTop], [BoxHeightStyle.includeLineSpacingMiddle], - /// and [BoxHeightStyle.includeLineSpacingBottom] for styles that will cover - /// the entire line. max, - - /// Extends the top and bottom edge of the bounds to fully cover any line - /// spacing. - /// - /// The top and bottom of each box will cover half of the - /// space above and half of the space below the line. - /// - /// {@template flutter.dart:ui.boxHeightStyle.includeLineSpacing} - /// The top edge of each line should be the same as the bottom edge - /// of the line above. There should be no gaps in vertical coverage given any - /// amount of line spacing. Line spacing is not included above the first line - /// and below the last line due to no additional space present there. - /// {@endtemplate} includeLineSpacingMiddle, - - /// Extends the top edge of the bounds to fully cover any line spacing. - /// - /// The line spacing will be added to the top of the box. - /// - /// {@macro flutter.dart:ui.boxHeightStyle.includeLineSpacing} includeLineSpacingTop, - - /// Extends the bottom edge of the bounds to fully cover any line spacing. - /// - /// The line spacing will be added to the bottom of the box. - /// - /// {@macro flutter.dart:ui.boxHeightStyle.includeLineSpacing} includeLineSpacingBottom, - - /// Calculate box heights based on the metrics of this paragraph's [StrutStyle]. - /// - /// Boxes based on the strut will have consistent heights throughout the - /// entire paragraph. The top edge of each line will align with the bottom - /// edge of the previous line. It is possible for glyphs to extend outside - /// these boxes. strut, } -/// Defines various ways to horizontally bound the boxes returned by -/// [Paragraph.getBoxesForRange]. enum BoxWidthStyle { // Provide tight bounding boxes that fit widths to the runs of each line // independently. tight, - - /// Adds up to two additional boxes as needed at the beginning and/or end - /// of each line so that the widths of the boxes in line are the same width - /// as the widest line in the paragraph. The additional boxes on each line - /// are only added when the relevant box at the relevant edge of that line - /// does not span the maximum width of the paragraph. max, } @@ -1252,237 +553,38 @@ abstract class LineMetrics { required double baseline, required int lineNumber, }) = engine.EngineLineMetrics; - - /// {@template dart.ui.LineMetrics.hardBreak} - /// True if this line ends with an explicit line break (e.g. '\n') or is the end - /// of the paragraph. False otherwise. - /// {@endtemplate} bool get hardBreak; - - /// {@template dart.ui.LineMetrics.ascent} - /// The rise from the [baseline] as calculated from the font and style for this line. - /// - /// This is the final computed ascent and can be impacted by the strut, height, scaling, - /// as well as outlying runs that are very tall. - /// - /// The [ascent] is provided as a positive value, even though it is typically defined - /// in fonts as negative. This is to ensure the signage of operations with these - /// metrics directly reflects the intended signage of the value. For example, - /// the y coordinate of the top edge of the line is `baseline - ascent`. - /// {@endtemplate} double get ascent; - - /// {@template dart.ui.LineMetrics.descent} - /// The drop from the [baseline] as calculated from the font and style for this line. - /// - /// This is the final computed ascent and can be impacted by the strut, height, scaling, - /// as well as outlying runs that are very tall. - /// - /// The y coordinate of the bottom edge of the line is `baseline + descent`. - /// {@endtemplate} double get descent; - - /// {@template dart.ui.LineMetrics.unscaledAscent} - /// The rise from the [baseline] as calculated from the font and style for this line - /// ignoring the [TextStyle.height]. - /// - /// The [unscaledAscent] is provided as a positive value, even though it is typically - /// defined in fonts as negative. This is to ensure the signage of operations with - /// these metrics directly reflects the intended signage of the value. - /// {@endtemplate} double get unscaledAscent; - - /// {@template dart.ui.LineMetrics.height} - /// Total height of the line from the top edge to the bottom edge. - /// - /// This is equivalent to `round(ascent + descent)`. This value is provided - /// separately due to rounding causing sub-pixel differences from the unrounded - /// values. - /// {@endtemplate} double get height; - - /// {@template dart.ui.LineMetrics.width} - /// Width of the line from the left edge of the leftmost glyph to the right - /// edge of the rightmost glyph. - /// - /// This is not the same as the width of the pargraph. - /// - /// See also: - /// - /// * [Paragraph.width], the max width passed in during layout. - /// * [Paragraph.longestLine], the width of the longest line in the paragraph. - /// {@endtemplate} double get width; - - /// {@template dart.ui.LineMetrics.left} - /// The x coordinate of left edge of the line. - /// - /// The right edge can be obtained with `left + width`. - /// {@endtemplate} double get left; - - /// {@template dart.ui.LineMetrics.baseline} - /// The y coordinate of the baseline for this line from the top of the paragraph. - /// - /// The bottom edge of the paragraph up to and including this line may be obtained - /// through `baseline + descent`. - /// {@endtemplate} double get baseline; - - /// {@template dart.ui.LineMetrics.lineNumber} - /// The number of this line in the overall paragraph, with the first line being - /// index zero. - /// - /// For example, the first line is line 0, second line is line 1. - /// {@endtemplate} int get lineNumber; } -/// A paragraph of text. -/// -/// A paragraph retains the size and position of each glyph in the text and can -/// be efficiently resized and painted. -/// -/// To create a [Paragraph] object, use a [ParagraphBuilder]. -/// -/// Paragraphs can be displayed on a [Canvas] using the [Canvas.drawParagraph] -/// method. abstract class Paragraph { - /// The amount of horizontal space this paragraph occupies. - /// - /// Valid only after [layout] has been called. double get width; - - /// The amount of vertical space this paragraph occupies. - /// - /// Valid only after [layout] has been called. double get height; - - /// The distance from the left edge of the leftmost glyph to the right edge of - /// the rightmost glyph in the paragraph. - /// - /// Valid only after [layout] has been called. double get longestLine; - - /// {@template dart.ui.paragraph.minIntrinsicWidth} - /// The minimum width that this paragraph could be without failing to paint - /// its contents within itself. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get minIntrinsicWidth; - - /// {@template dart.ui.paragraph.maxIntrinsicWidth} - /// Returns the smallest width beyond which increasing the width never - /// decreases the height. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get maxIntrinsicWidth; - - /// {@template dart.ui.paragraph.alphabeticBaseline} - /// The distance from the top of the paragraph to the alphabetic - /// baseline of the first line, in logical pixels. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get alphabeticBaseline; - - /// {@template dart.ui.paragraph.ideographicBaseline} - /// The distance from the top of the paragraph to the ideographic - /// baseline of the first line, in logical pixels. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get ideographicBaseline; - - /// True if there is more vertical content, but the text was truncated, either - /// because we reached `maxLines` lines of text or because the `maxLines` was - /// null, `ellipsis` was not null, and one of the lines exceeded the width - /// constraint. - /// - /// See the discussion of the `maxLines` and `ellipsis` arguments at [new - /// ParagraphStyle]. bool get didExceedMaxLines; - - /// Computes the size and position of each glyph in the paragraph. - /// - /// The [ParagraphConstraints] control how wide the text is allowed to be. void layout(ParagraphConstraints constraints); - - /// Returns a list of text boxes that enclose the given text range. - /// - /// The [boxHeightStyle] and [boxWidthStyle] parameters allow customization - /// of how the boxes are bound vertically and horizontally. Both style - /// parameters default to the tight option, which will provide close-fitting - /// boxes and will not account for any line spacing. - /// - /// The [boxHeightStyle] and [boxWidthStyle] parameters must not be null. - /// - /// See [BoxHeightStyle] and [BoxWidthStyle] for full descriptions of each option. List getBoxesForRange(int start, int end, {BoxHeightStyle boxHeightStyle = BoxHeightStyle.tight, BoxWidthStyle boxWidthStyle = BoxWidthStyle.tight}); - - /// Returns the text position closest to the given offset. - /// - /// It does so by performing a binary search to find where the tap occurred - /// within the text. TextPosition getPositionForOffset(Offset offset); - - /// Returns the [TextRange] of the word at the given [TextPosition]. - /// - /// Characters not part of a word, such as spaces, symbols, and punctuation, - /// have word breaks on both sides. In such cases, this method will return - /// [offset, offset+1]. Word boundaries are defined more precisely in Unicode - /// Standard Annex #29 http://www.unicode.org/reports/tr29/#Word_Boundaries TextRange getWordBoundary(TextPosition position); - - /// Returns the [TextRange] of the line at the given [TextPosition]. - /// - /// The newline (if any) is returned as part of the range. - /// - /// Not valid until after layout. - /// - /// This can potentially be expensive, since it needs to compute the line - /// metrics, so use it sparingly. TextRange getLineBoundary(TextPosition position); - - /// Returns a list of text boxes that enclose all placeholders in the paragraph. - /// - /// The order of the boxes are in the same order as passed in through [addPlaceholder]. - /// - /// Coordinates of the [TextBox] are relative to the upper-left corner of the paragraph, - /// where positive y values indicate down. List getBoxesForPlaceholders(); - - /// Returns the full list of [LineMetrics] that describe in detail the various - /// metrics of each laid out line. - /// - /// Not valid until after layout. - /// - /// This can potentially return a large amount of data, so it is not recommended - /// to repeatedly call this. Instead, cache the results. List computeLineMetrics(); } -/// Builds a [Paragraph] containing text with the given styling information. -/// -/// To set the paragraph's alignment, truncation, and ellipsising behavior, pass -/// an appropriately-configured [ParagraphStyle] object to the [new -/// ParagraphBuilder] constructor. -/// -/// Then, call combinations of [pushStyle], [addText], and [pop] to add styled -/// text to the object. -/// -/// Finally, call [build] to obtain the constructed [Paragraph] object. After -/// this point, the builder is no longer usable. -/// -/// After constructing a [Paragraph], call [Paragraph.layout] on it and then -/// paint it with [Canvas.drawParagraph]. abstract class ParagraphBuilder { - /// Creates a [ParagraphBuilder] object, which is used to create a - /// [Paragraph]. factory ParagraphBuilder(ParagraphStyle style) { if (engine.experimentalUseSkia) { return engine.CkParagraphBuilder(style); @@ -1490,81 +592,12 @@ abstract class ParagraphBuilder { return engine.EngineParagraphBuilder(style as engine.EngineParagraphStyle); } } - - /// Applies the given style to the added text until [pop] is called. - /// - /// See [pop] for details. void pushStyle(TextStyle style); - - /// Ends the effect of the most recent call to [pushStyle]. - /// - /// Internally, the paragraph builder maintains a stack of text styles. Text - /// added to the paragraph is affected by all the styles in the stack. Calling - /// [pop] removes the topmost style in the stack, leaving the remaining styles - /// in effect. void pop(); - - /// Adds the given text to the paragraph. - /// - /// The text will be styled according to the current stack of text styles. void addText(String text); - - /// Applies the given paragraph style and returns a [Paragraph] containing the - /// added text and associated styling. - /// - /// After calling this function, the paragraph builder object is invalid and - /// cannot be used further. Paragraph build(); - - /// The number of placeholders currently in the paragraph. int get placeholderCount; - - /// The scales of the placeholders in the paragraph. List get placeholderScales; - - /// Adds an inline placeholder space to the paragraph. - /// - /// The paragraph will contain a rectangular space with no text of the dimensions - /// specified. - /// - /// The `width` and `height` parameters specify the size of the placeholder rectangle. - /// - /// The `alignment` parameter specifies how the placeholder rectangle will be vertically - /// aligned with the surrounding text. When [PlaceholderAlignment.baseline], - /// [PlaceholderAlignment.aboveBaseline], and [PlaceholderAlignment.belowBaseline] - /// alignment modes are used, the baseline needs to be set with the `baseline`. - /// When using [PlaceholderAlignment.baseline], `baselineOffset` indicates the distance - /// of the baseline down from the top of of the rectangle. The default `baselineOffset` - /// is the `height`. - /// - /// Examples: - /// - /// * For a 30x50 placeholder with the bottom edge aligned with the bottom of the text, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.bottom);` - /// * For a 30x50 placeholder that is vertically centered around the text, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.middle);`. - /// * For a 30x50 placeholder that sits completely on top of the alphabetic baseline, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.aboveBaseline, baseline: TextBaseline.alphabetic)`. - /// * For a 30x50 placeholder with 40 pixels above and 10 pixels below the alphabetic baseline, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic, baselineOffset: 40)`. - /// - /// Lines are permitted to break around each placeholder. - /// - /// Decorations will be drawn based on the font defined in the most recently - /// pushed [TextStyle]. The decorations are drawn as if unicode text were present - /// in the placeholder space, and will draw the same regardless of the height and - /// alignment of the placeholder. To hide or manually adjust decorations to fit, - /// a text style with the desired decoration behavior should be pushed before - /// adding a placeholder. - /// - /// Any decorations drawn through a placeholder will exist on the same canvas/layer - /// as the text. This means any content drawn on top of the space reserved by - /// the placeholder will be drawn over the decoration, possibly obscuring the - /// decoration. - /// - /// Placeholders are represented by a unicode 0xFFFC "object replacement character" - /// in the text buffer. For each placeholder, one object replacement character is - /// added on to the text buffer. void addPlaceholder( double width, double height, @@ -1575,15 +608,10 @@ abstract class ParagraphBuilder { }); } -/// Loads a font from a buffer and makes it available for rendering text. -/// -/// * `list`: A list of bytes containing the font file. -/// * `fontFamily`: The family name used to identify the font in text styles. -/// If this is not provided, then the family name will be extracted from the font file. Future loadFontFromList(Uint8List list, {String? fontFamily}) { if (engine.experimentalUseSkia) { return engine.skiaFontCollection.loadFontFromList(list, fontFamily: fontFamily).then( - (_) => engine.sendFontChangeMessage() + (_) => engine.sendFontChangeMessage() ); } else { return _fontCollection!.loadFontFromList(list, fontFamily: fontFamily!).then( diff --git a/lib/web_ui/lib/src/ui/tile_mode.dart b/lib/web_ui/lib/src/ui/tile_mode.dart index 9ce9ddf3c21fc..567b9a551573b 100644 --- a/lib/web_ui/lib/src/ui/tile_mode.dart +++ b/lib/web_ui/lib/src/ui/tile_mode.dart @@ -5,53 +5,9 @@ // @dart = 2.10 part of ui; -/// Defines what happens at the edge of the gradient. -/// -/// A gradient is defined along a finite inner area. In the case of a linear -/// gradient, it's between the parallel lines that are orthogonal to the line -/// drawn between two points. In the case of radial gradients, it's the disc -/// that covers the circle centered on a particular point up to a given radius. -/// -/// This enum is used to define how the gradient should paint the regions -/// outside that defined inner area. -/// -/// See also: -/// -/// * [painting.Gradient], the superclass for [LinearGradient] and -/// [RadialGradient], as used by [BoxDecoration] et al, which works in -/// relative coordinates and can create a [Shader] representing the gradient -/// for a particular [Rect] on demand. -/// * [dart:ui.Gradient], the low-level class used when dealing with the -/// [Paint.shader] property directly, with its [new Gradient.linear] and [new -/// Gradient.radial] constructors. // These enum values must be kept in sync with SkShader::TileMode. enum TileMode { - /// Edge is clamped to the final color. - /// - /// The gradient will paint the all the regions outside the inner area with - /// the color of the point closest to that region. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png) clamp, - - /// Edge is repeated from first color to last. - /// - /// This is as if the stop points from 0.0 to 1.0 were then repeated from 1.0 - /// to 2.0, 2.0 to 3.0, and so forth (and for linear gradients, similarly from - /// -1.0 to 0.0, -2.0 to -1.0, etc). - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png) repeated, - - /// Edge is mirrored from last color to first. - /// - /// This is as if the stop points from 0.0 to 1.0 were then repeated backwards - /// from 2.0 to 1.0, then forwards from 2.0 to 3.0, then backwards again from - /// 4.0 to 3.0, and so forth (and for linear gradients, similarly from in the - /// negative direction). - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png) mirror, } diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index fd9e667401ae2..fa58ff01e5537 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -6,127 +6,35 @@ // @dart = 2.10 part of ui; -/// Signature of callbacks that have no arguments and return no data. typedef VoidCallback = void Function(); - -/// Signature for frame-related callbacks from the scheduler. -/// -/// The `timeStamp` is the number of milliseconds since the beginning of the -/// scheduler's epoch. Use timeStamp to determine how far to advance animation -/// timelines so that all the animations in the system are synchronized to a -/// common time base. typedef FrameCallback = void Function(Duration duration); - -/// Signature for [Window.onReportTimings]. typedef TimingsCallback = void Function(List timings); - -/// Signature for [Window.onPointerDataPacket]. typedef PointerDataPacketCallback = void Function(PointerDataPacket packet); - -/// Signature for [Window.onSemanticsAction]. -typedef SemanticsActionCallback = void Function( - int id, SemanticsAction action, ByteData? args); - -/// Signature for responses to platform messages. -/// -/// Used as a parameter to [Window.sendPlatformMessage] and -/// [Window.onPlatformMessage]. +typedef SemanticsActionCallback = void Function(int id, SemanticsAction action, ByteData? args); typedef PlatformMessageResponseCallback = void Function(ByteData? data); - -/// Signature for [Window.onPlatformMessage]. typedef PlatformMessageCallback = void Function( String name, ByteData? data, PlatformMessageResponseCallback? callback); -/// States that an application can be in. -/// -/// The values below describe notifications from the operating system. -/// Applications should not expect to always receive all possible -/// notifications. For example, if the users pulls out the battery from the -/// device, no notification will be sent before the application is suddenly -/// terminated, along with the rest of the operating system. -/// -/// See also: -/// -/// * [WidgetsBindingObserver], for a mechanism to observe the lifecycle state -/// from the widgets layer. enum AppLifecycleState { - /// The application is visible and responding to user input. resumed, - - /// The application is in an inactive state and is not receiving user input. - /// - /// On iOS, this state corresponds to an app or the Flutter host view running - /// in the foreground inactive state. Apps transition to this state when in - /// a phone call, responding to a TouchID request, when entering the app - /// switcher or the control center, or when the UIViewController hosting the - /// Flutter app is transitioning. - /// - /// On Android, this corresponds to an app or the Flutter host view running - /// in the foreground inactive state. Apps transition to this state when - /// another activity is focused, such as a split-screen app, a phone call, - /// a picture-in-picture app, a system dialog, or another window. - /// - /// Apps in this state should assume that they may be [paused] at any time. inactive, - - /// The application is not currently visible to the user, not responding to - /// user input, and running in the background. - /// - /// When the application is in this state, the engine will not call the - /// [Window.onBeginFrame] and [Window.onDrawFrame] callbacks. paused, - - /// The application is detached from view. - /// - /// When the application is in this state, the engine is running without - /// a platform UI. detached, } -/// A representation of distances for each of the four edges of a rectangle, -/// used to encode the view insets and padding that applications should place -/// around their user interface, as exposed by [Window.viewInsets] and -/// [Window.padding]. View insets and padding are preferably read via -/// [MediaQuery.of]. -/// -/// For the engine implementation of this class see the [engine.WindowPadding]. -/// -/// For a generic class that represents distances around a rectangle, see the -/// [EdgeInsets] class. -/// -/// See also: -/// -/// * [WidgetsBindingObserver], for a widgets layer mechanism to receive -/// notifications when the padding changes. -/// * [MediaQuery.of], for the preferred mechanism for accessing these values. -/// * [Scaffold], which automatically applies the padding in material design -/// applications. abstract class WindowPadding { - const factory WindowPadding._( - {required double left, - required double top, - required double right, - required double bottom}) = engine.WindowPadding; + const factory WindowPadding._({ + required double left, + required double top, + required double right, + required double bottom, + }) = engine.WindowPadding; - /// The distance from the left edge to the first unpadded pixel, in physical - /// pixels. double get left; - - /// The distance from the top edge to the first unpadded pixel, in physical - /// pixels. double get top; - - /// The distance from the right edge to the first unpadded pixel, in physical - /// pixels. double get right; - - /// The distance from the bottom edge to the first unpadded pixel, in physical - /// pixels. double get bottom; - - /// A window padding that has zeros for each edge. - static const WindowPadding zero = - WindowPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0); + static const WindowPadding zero = WindowPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0); @override String toString() { @@ -134,79 +42,13 @@ abstract class WindowPadding { } } -/// An identifier used to select a user's language and formatting preferences. -/// -/// This represents a [Unicode Language -/// Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier) -/// (i.e. without Locale extensions), except variants are not supported. -/// -/// Locales are canonicalized according to the "preferred value" entries in the -/// [IANA Language Subtag -/// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry). -/// For example, `const Locale('he')` and `const Locale('iw')` are equal and -/// both have the [languageCode] `he`, because `iw` is a deprecated language -/// subtag that was replaced by the subtag `he`. -/// -/// See also: -/// -/// * [Window.locale], which specifies the system's currently selected -/// [Locale]. class Locale { - /// Creates a new Locale object. The first argument is the - /// primary language subtag, the second is the region (also - /// referred to as 'country') subtag. - /// - /// For example: - /// - /// ```dart - /// const Locale swissFrench = const Locale('fr', 'CH'); - /// const Locale canadianFrench = const Locale('fr', 'CA'); - /// ``` - /// - /// The primary language subtag must not be null. The region subtag is - /// optional. When there is no region/country subtag, the parameter should - /// be omitted or passed `null` instead of an empty-string. - /// - /// The subtag values are _case sensitive_ and must be one of the valid - /// subtags according to CLDR supplemental data: - /// [language](http://unicode.org/cldr/latest/common/validity/language.xml), - /// [region](http://unicode.org/cldr/latest/common/validity/region.xml). The - /// primary language subtag must be at least two and at most eight lowercase - /// letters, but not four letters. The region region subtag must be two - /// uppercase letters or three digits. See the [Unicode Language - /// Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier) - /// specification. - /// - /// Validity is not checked by default, but some methods may throw away - /// invalid data. - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which also allows a [scriptCode] to be - /// specified. const Locale( this._languageCode, [ this._countryCode, ]) : assert(_languageCode != null), // ignore: unnecessary_null_comparison assert(_languageCode != ''), scriptCode = null; - - /// Creates a new Locale object. - /// - /// The keyword arguments specify the subtags of the Locale. - /// - /// The subtag values are _case sensitive_ and must be valid subtags according - /// to CLDR supplemental data: - /// [language](http://unicode.org/cldr/latest/common/validity/language.xml), - /// [script](http://unicode.org/cldr/latest/common/validity/script.xml) and - /// [region](http://unicode.org/cldr/latest/common/validity/region.xml) for - /// each of languageCode, scriptCode and countryCode respectively. - /// - /// The [countryCode] subtag is optional. When there is no country subtag, - /// the parameter should be omitted or passed `null` instead of an empty-string. - /// - /// Validity is not checked by default, but some methods may throw away - /// invalid data. const Locale.fromSubtags({ String languageCode = 'und', this.scriptCode, @@ -217,30 +59,6 @@ class Locale { assert(scriptCode != ''), assert(countryCode != ''), _countryCode = countryCode; - - /// The primary language subtag for the locale. - /// - /// This must not be null. It may be 'und', representing 'undefined'. - /// - /// This is expected to be string registered in the [IANA Language Subtag - /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) - /// with the type "language". The string specified must match the case of the - /// string in the registry. - /// - /// Language subtags that are deprecated in the registry and have a preferred - /// code are changed to their preferred code. For example, `const - /// Locale('he')` and `const Locale('iw')` are equal, and both have the - /// [languageCode] `he`, because `iw` is a deprecated language subtag that was - /// replaced by the subtag `he`. - /// - /// This must be a valid Unicode Language subtag as listed in [Unicode CLDR - /// supplemental - /// data](http://unicode.org/cldr/latest/common/validity/language.xml). - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which describes the conventions for creating - /// [Locale] objects. String get languageCode => _deprecatedLanguageSubtagMap[_languageCode] ?? _languageCode; final String _languageCode; @@ -326,40 +144,7 @@ class Locale { 'yos': 'zom', // Yos; deprecated 2013-09-10 'yuu': 'yug', // Yugh; deprecated 2014-02-28 }; - - /// The script subtag for the locale. - /// - /// This may be null, indicating that there is no specified script subtag. - /// - /// This must be a valid Unicode Language Identifier script subtag as listed - /// in [Unicode CLDR supplemental - /// data](http://unicode.org/cldr/latest/common/validity/script.xml). - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which describes the conventions for creating - /// [Locale] objects. final String? scriptCode; - - /// The region subtag for the locale. - /// - /// This may be null, indicating that there is no specified region subtag. - /// - /// This is expected to be string registered in the [IANA Language Subtag - /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) - /// with the type "region". The string specified must match the case of the - /// string in the registry. - /// - /// Region subtags that are deprecated in the registry and have a preferred - /// code are changed to their preferred code. For example, `const Locale('de', - /// 'DE')` and `const Locale('de', 'DD')` are equal, and both have the - /// [countryCode] `DE`, because `DD` is a deprecated language subtag that was - /// replaced by the subtag `DE`. - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which describes the conventions for creating - /// [Locale] objects. String? get countryCode => _deprecatedRegionSubtagMap[_countryCode] ?? _countryCode; final String? _countryCode; @@ -406,443 +191,65 @@ class Locale { } } -/// The most basic interface to the host operating system's user interface. -/// -/// There is a single Window instance in the system, which you can -/// obtain from the [window] property. abstract class Window { - /// The number of device pixels for each logical pixel. This number might not - /// be a power of two. Indeed, it might not even be an integer. For example, - /// the Nexus 6 has a device pixel ratio of 3.5. - /// - /// Device pixels are also referred to as physical pixels. Logical pixels are - /// also referred to as device-independent or resolution-independent pixels. - /// - /// By definition, there are roughly 38 logical pixels per centimeter, or - /// about 96 logical pixels per inch, of the physical display. The value - /// returned by [devicePixelRatio] is ultimately obtained either from the - /// hardware itself, the device drivers, or a hard-coded value stored in the - /// operating system or firmware, and may be inaccurate, sometimes by a - /// significant margin. - /// - /// The Flutter framework operates in logical pixels, so it is rarely - /// necessary to directly deal with this property. - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. double get devicePixelRatio; - - /// The dimensions of the rectangle into which the application will be drawn, - /// in physical pixels. - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// At startup, the size of the application window may not be known before - /// Dart code runs. If this value is observed early in the application - /// lifecycle, it may report [Size.zero]. - /// - /// This value does not take into account any on-screen keyboards or other - /// system UI. The [padding] and [viewInsets] properties provide a view into - /// how much of each side of the application may be obscured by system UI. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. Size get physicalSize; - - /// The physical depth is the maximum elevation that the Window allows. - /// - /// Physical layers drawn at or above this elevation will have their elevation - /// clamped to this value. This can happen if the physical layer itself has - /// an elevation larger than available depth, or if some ancestor of the layer - /// causes it to have a cumulative elevation that is larger than the available - /// depth. - /// - /// The default value is [double.maxFinite], which is used for platforms that - /// do not specify a maximum elevation. This property is currently on expected - /// to be set to a non-default value on Fuchsia. - double get physicalDepth; - - /// The number of physical pixels on each side of the display rectangle into - /// which the application can render, but over which the operating system - /// will likely place system UI, such as the keyboard, that fully obscures - /// any content. - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. - /// * [MediaQuery.of], a simpler mechanism for the same. - /// * [Scaffold], which automatically applies the view insets in material - /// design applications. WindowPadding get viewInsets => WindowPadding.zero; WindowPadding get viewPadding => WindowPadding.zero; WindowPadding get systemGestureInsets => WindowPadding.zero; - - /// The number of physical pixels on each side of the display rectangle into - /// which the application can render, but which may be partially obscured by - /// system UI (such as the system notification area), or or physical - /// intrusions in the display (e.g. overscan regions on television screens or - /// phone sensor housings). - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. - /// * [MediaQuery.of], a simpler mechanism for the same. - /// * [Scaffold], which automatically applies the padding in material design - /// applications. WindowPadding get padding => WindowPadding.zero; - - /// The system-reported text scale. - /// - /// This establishes the text scaling factor to use when rendering text, - /// according to the user's platform preferences. - /// - /// The [onTextScaleFactorChanged] callback is called whenever this value - /// changes. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. double get textScaleFactor => _textScaleFactor; double _textScaleFactor = 1.0; - - /// The setting indicating whether time should always be shown in the 24-hour - /// format. - /// - /// This option is used by [showTimePicker]. bool get alwaysUse24HourFormat => _alwaysUse24HourFormat; bool _alwaysUse24HourFormat = false; - - /// A callback that is invoked whenever [textScaleFactor] changes value. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this callback is invoked. VoidCallback? get onTextScaleFactorChanged; set onTextScaleFactorChanged(VoidCallback? callback); - - /// The setting indicating the current brightness mode of the host platform. Brightness get platformBrightness; - - /// A callback that is invoked whenever [platformBrightness] changes value. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this callback is invoked. VoidCallback? get onPlatformBrightnessChanged; set onPlatformBrightnessChanged(VoidCallback? callback); - - /// A callback that is invoked whenever the [devicePixelRatio], - /// [physicalSize], [padding], or [viewInsets] values change, for example - /// when the device is rotated or when the application is resized (e.g. when - /// showing applications side-by-side on Android). - /// - /// The engine invokes this callback in the same zone in which the callback - /// was set. - /// - /// The framework registers with this callback and updates the layout - /// appropriately. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// register for notifications when this is called. - /// * [MediaQuery.of], a simpler mechanism for the same. VoidCallback? get onMetricsChanged; set onMetricsChanged(VoidCallback? callback); - - /// The system-reported default locale of the device. - /// - /// This establishes the language and formatting conventions that application - /// should, if possible, use to render their user interface. - /// - /// This is the first locale selected by the user and is the user's - /// primary locale (the locale the device UI is displayed in) - /// - /// This is equivalent to `locales.first` and will provide an empty non-null locale - /// if the [locales] list has not been set or is empty. Locale? get locale; - - /// The full system-reported supported locales of the device. - /// - /// This establishes the language and formatting conventions that application - /// should, if possible, use to render their user interface. - /// - /// The list is ordered in order of priority, with lower-indexed locales being - /// preferred over higher-indexed ones. The first element is the primary [locale]. - /// - /// The [onLocaleChanged] callback is called whenever this value changes. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. List? get locales; - - /// Performs the platform-native locale resolution. - /// - /// Each platform may return different results. - /// - /// If the platform fails to resolve a locale, then this will return null. - /// - /// This method returns synchronously and is a direct call to - /// platform specific APIs without invoking method channels. Locale? computePlatformResolvedLocale(List supportedLocales) { // TODO(garyq): Implement on web. return null; } - /// A callback that is invoked whenever [locale] changes value. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this callback is invoked. VoidCallback? get onLocaleChanged; set onLocaleChanged(VoidCallback? callback); - - /// Requests that, at the next appropriate opportunity, the [onBeginFrame] - /// and [onDrawFrame] callbacks be invoked. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. void scheduleFrame(); - - /// A callback that is invoked to notify the application that it is an - /// appropriate time to provide a scene using the [SceneBuilder] API and the - /// [render] method. When possible, this is driven by the hardware VSync - /// signal. This is only called if [scheduleFrame] has been called since the - /// last time this callback was invoked. - /// - /// The [onDrawFrame] callback is invoked immediately after [onBeginFrame], - /// after draining any microtasks (e.g. completions of any [Future]s) queued - /// by the [onBeginFrame] handler. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. - /// * [RendererBinding], the Flutter framework class which manages layout and - /// painting. FrameCallback? get onBeginFrame; set onBeginFrame(FrameCallback? callback); - - /// A callback that is invoked to report the [FrameTiming] of recently - /// rasterized frames. - /// - /// This can be used to see if the application has missed frames (through - /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high - /// latencies (through [FrameTiming.totalSpan]). - /// - /// Unlike [Timeline], the timing information here is available in the release - /// mode (additional to the profile and the debug mode). Hence this can be - /// used to monitor the application's performance in the wild. - /// - /// The callback may not be immediately triggered after each frame. Instead, - /// it tries to batch frames together and send all their timings at once to - /// decrease the overhead (as this is available in the release mode). The - /// timing of any frame will be sent within about 1 second even if there are - /// no later frames to batch. TimingsCallback? get onReportTimings; set onReportTimings(TimingsCallback? callback); - - /// A callback that is invoked for each frame after [onBeginFrame] has - /// completed and after the microtask queue has been drained. This can be - /// used to implement a second phase of frame rendering that happens - /// after any deferred work queued by the [onBeginFrame] phase. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. - /// * [RendererBinding], the Flutter framework class which manages layout and - /// painting. VoidCallback? get onDrawFrame; set onDrawFrame(VoidCallback? callback); - - /// A callback that is invoked when pointer data is available. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [GestureBinding], the Flutter framework class which manages pointer - /// events. PointerDataPacketCallback? get onPointerDataPacket; set onPointerDataPacket(PointerDataPacketCallback? callback); - - /// The route or path that the embedder requested when the application was - /// launched. - /// - /// This will be the string "`/`" if no particular route was requested. - /// - /// ## Android - /// - /// On Android, calling - /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `createFlutterView` method in your `FlutterActivity` - /// subclass is a suitable time to set the value. The application's - /// `AndroidManifest.xml` file must also be updated to have a suitable - /// [``](https://developer.android.com/guide/topics/manifest/intent-filter-element.html). - /// - /// ## iOS - /// - /// On iOS, calling - /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `application:didFinishLaunchingWithOptions:` method is a - /// suitable time to set this value. - /// - /// See also: - /// - /// * [Navigator], a widget that handles routing. - /// * [SystemChannels.navigation], which handles subsequent navigation - /// requests from the embedder. String get defaultRouteName; - - /// Whether the user has requested that [updateSemantics] be called when - /// the semantic contents of window changes. - /// - /// The [onSemanticsEnabledChanged] callback is called whenever this value - /// changes. - /// - /// This defaults to `true` on the Web because we may never receive a signal - /// that an assistive technology is turned on. - bool get semanticsEnabled => - engine.EngineSemanticsOwner.instance.semanticsEnabled; - - /// A callback that is invoked when the value of [semanticsEnabled] changes. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. + bool get semanticsEnabled => engine.EngineSemanticsOwner.instance.semanticsEnabled; VoidCallback? get onSemanticsEnabledChanged; set onSemanticsEnabledChanged(VoidCallback? callback); - - /// A callback that is invoked whenever the user requests an action to be - /// performed. - /// - /// This callback is used when the user expresses the action they wish to - /// perform based on the semantics supplied by [updateSemantics]. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. SemanticsActionCallback? get onSemanticsAction; set onSemanticsAction(SemanticsActionCallback? callback); - - /// A callback that is invoked when the value of [accessibilityFlags] changes. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. VoidCallback? get onAccessibilityFeaturesChanged; set onAccessibilityFeaturesChanged(VoidCallback? callback); - - /// Called whenever this window receives a message from a platform-specific - /// plugin. - /// - /// The `name` parameter determines which plugin sent the message. The `data` - /// parameter is the payload and is typically UTF-8 encoded JSON but can be - /// arbitrary data. - /// - /// Message handlers must call the function given in the `callback` parameter. - /// If the handler does not need to respond, the handler should pass null to - /// the callback. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. PlatformMessageCallback? get onPlatformMessage; set onPlatformMessage(PlatformMessageCallback? callback); - - /// Change the retained semantics data about this window. - /// - /// If [semanticsEnabled] is true, the user has requested that this funciton - /// be called whenever the semantic content of this window changes. - /// - /// In either case, this function disposes the given update, which means the - /// semantics update cannot be used further. void updateSemantics(SemanticsUpdate update) { engine.EngineSemanticsOwner.instance.updateSemantics(update); } - /// Sends a message to a platform-specific plugin. - /// - /// The `name` parameter determines which plugin receives the message. The - /// `data` parameter contains the message payload and is typically UTF-8 - /// encoded JSON but can be arbitrary data. If the plugin replies to the - /// message, `callback` will be called with the response. - /// - /// The framework invokes [callback] in the same zone in which this method - /// was called. void sendPlatformMessage( String name, ByteData? data, PlatformMessageResponseCallback? callback, ); - - /// Additional accessibility features that may be enabled by the platform. AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; AccessibilityFeatures _accessibilityFeatures = AccessibilityFeatures._(0); - - /// Updates the application's rendering on the GPU with the newly provided - /// [Scene]. This function must be called within the scope of the - /// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function - /// is called a second time during a single [onBeginFrame]/[onDrawFrame] - /// callback sequence or called outside the scope of those callbacks, the call - /// will be ignored. - /// - /// To record graphical operations, first create a [PictureRecorder], then - /// construct a [Canvas], passing that [PictureRecorder] to its constructor. - /// After issuing all the graphical operations, call the - /// [PictureRecorder.endRecording] function on the [PictureRecorder] to obtain - /// the final [Picture] that represents the issued graphical operations. - /// - /// Next, create a [SceneBuilder], and add the [Picture] to it using - /// [SceneBuilder.addPicture]. With the [SceneBuilder.build] method you can - /// then obtain a [Scene] object, which you can display to the user via this - /// [render] function. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. - /// * [RendererBinding], the Flutter framework class which manages layout and - /// painting. void render(Scene scene); String get initialLifecycleState => 'AppLifecycleState.resumed'; @@ -852,11 +259,6 @@ abstract class Window { ByteData? getPersistentIsolateData() => null; } -/// Additional accessibility features that may be enabled by the platform. -/// -/// It is not possible to enable these settings from Flutter, instead they are -/// used by the platform to indicate that additional accessibility features are -/// enabled. class AccessibilityFeatures { const AccessibilityFeatures._(this._index); @@ -869,33 +271,11 @@ class AccessibilityFeatures { // A bitfield which represents each enabled feature. final int _index; - - /// Whether there is a running accessibility service which is changing the - /// interaction model of the device. - /// - /// For example, TalkBack on Android and VoiceOver on iOS enable this flag. bool get accessibleNavigation => _kAccessibleNavigation & _index != 0; - - /// The platform is inverting the colors of the application. bool get invertColors => _kInvertColorsIndex & _index != 0; - - /// The platform is requesting that animations be disabled or simplified. bool get disableAnimations => _kDisableAnimationsIndex & _index != 0; - - /// The platform is requesting that text be rendered at a bold font weight. - /// - /// Only supported on iOS. bool get boldText => _kBoldTextIndex & _index != 0; - - /// The platform is requesting that certain animations be simplified and - /// parallax effects removed. - /// - /// Only supported on iOS. bool get reduceMotion => _kReduceMotionIndex & _index != 0; - - /// The platform is requesting that UI be rendered with darker colors. - /// - /// Only supported on iOS. bool get highContrast => _kHighContrastIndex & _index != 0; @override @@ -935,18 +315,8 @@ class AccessibilityFeatures { int get hashCode => _index.hashCode; } -/// Describes the contrast of a theme or color palette. enum Brightness { - /// The color is dark and will require a light text color to achieve readable - /// contrast. - /// - /// For example, the color might be dark grey, requiring white text. dark, - - /// The color is light and will require a dark text color to achieve readable - /// contrast. - /// - /// For example, the color might be bright white, requiring black text. light, } @@ -1001,95 +371,41 @@ class IsolateNameServer { } } -/// Various important time points in the lifetime of a frame. -/// -/// [FrameTiming] records a timestamp of each phase for performance analysis. enum FramePhase { - /// When the UI thread starts building a frame. - /// - /// See also [FrameTiming.buildDuration]. + vsyncStart, buildStart, - - /// When the UI thread finishes building a frame. - /// - /// See also [FrameTiming.buildDuration]. buildFinish, - - /// When the raster thread starts rasterizing a frame. - /// - /// See also [FrameTiming.rasterDuration]. rasterStart, - - /// When the raster thread finishes rasterizing a frame. - /// - /// See also [FrameTiming.rasterDuration]. rasterFinish, } -/// Time-related performance metrics of a frame. -/// -/// See [Window.onReportTimings] for how to get this. -/// -/// The metrics in debug mode (`flutter run` without any flags) may be very -/// different from those in profile and release modes due to the debug overhead. -/// Therefore it's recommended to only monitor and analyze performance metrics -/// in profile and release modes. class FrameTiming { - /// Construct [FrameTiming] with raw timestamps in microseconds. - /// - /// List [timestamps] must have the same number of elements as - /// [FramePhase.values]. - /// - /// This constructor is usually only called by the Flutter engine, or a test. - /// To get the [FrameTiming] of your app, see [Window.onReportTimings]. - FrameTiming(List timestamps) + factory FrameTiming({ + required int vsyncStart, + required int buildStart, + required int buildFinish, + required int rasterStart, + required int rasterFinish, + }) { + return FrameTiming._([ + vsyncStart, + buildStart, + buildFinish, + rasterStart, + rasterFinish + ]); + } + FrameTiming._(List timestamps) : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; - /// This is a raw timestamp in microseconds from some epoch. The epoch in all - /// [FrameTiming] is the same, but it may not match [DateTime]'s epoch. int timestampInMicroseconds(FramePhase phase) => _timestamps[phase.index]; - Duration _rawDuration(FramePhase phase) => - Duration(microseconds: _timestamps[phase.index]); - - /// The duration to build the frame on the UI thread. - /// - /// The build starts approximately when [Window.onBeginFrame] is called. The - /// [Duration] in the [Window.onBeginFrame] callback is exactly the - /// `Duration(microseconds: timestampInMicroseconds(FramePhase.buildStart))`. - /// - /// The build finishes when [Window.render] is called. - /// - /// {@template dart.ui.FrameTiming.fps_smoothness_milliseconds} - /// To ensure smooth animations of X fps, this should not exceed 1000/X - /// milliseconds. - /// {@endtemplate} - /// {@template dart.ui.FrameTiming.fps_milliseconds} - /// That's about 16ms for 60fps, and 8ms for 120fps. - /// {@endtemplate} - Duration get buildDuration => - _rawDuration(FramePhase.buildFinish) - - _rawDuration(FramePhase.buildStart); - - /// The duration to rasterize the frame on the raster thread. - /// - /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds} - /// {@macro dart.ui.FrameTiming.fps_milliseconds} - Duration get rasterDuration => - _rawDuration(FramePhase.rasterFinish) - - _rawDuration(FramePhase.rasterStart); - - /// The timespan between build start and raster finish. - /// - /// To achieve the lowest latency on an X fps display, this should not exceed - /// 1000/X milliseconds. - /// {@macro dart.ui.FrameTiming.fps_milliseconds} - /// - /// See also [buildDuration] and [rasterDuration]. - Duration get totalSpan => - _rawDuration(FramePhase.rasterFinish) - - _rawDuration(FramePhase.buildStart); + Duration _rawDuration(FramePhase phase) => Duration(microseconds: _timestamps[phase.index]); + Duration get buildDuration => _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart); + Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart); + Duration get vsyncOverhead => _rawDuration(FramePhase.buildStart) - _rawDuration(FramePhase.vsyncStart); + Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.vsyncStart); final List _timestamps; // in microseconds @@ -1097,11 +413,8 @@ class FrameTiming { @override String toString() { - return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, totalSpan: ${_formatMS(totalSpan)})'; + return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, vsyncOverhead: ${_formatMS(vsyncOverhead)}, totalSpan: ${_formatMS(totalSpan)})'; } } -/// The [Window] singleton. This object exposes the size of the display, the -/// core scheduler API, the input event callback, the graphics drawing API, and -/// other such core services. Window get window => engine.window; diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 34a4ee540ec85..6defcc3037efe 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: dev_dependencies: analyzer: 0.39.15 + archive: 2.0.13 http: 0.12.1 image: 2.1.13 js: 0.6.1+1 diff --git a/lib/web_ui/test/alarm_clock_test.dart b/lib/web_ui/test/alarm_clock_test.dart index 3a90f955b4db5..b56d0dc27d798 100644 --- a/lib/web_ui/test/alarm_clock_test.dart +++ b/lib/web_ui/test/alarm_clock_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:quiver/testing/async.dart'; import 'package:quiver/time.dart'; @@ -10,6 +11,10 @@ import 'package:quiver/time.dart'; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group(AlarmClock, () { _alarmClockTests(); }); diff --git a/lib/web_ui/test/canvas_test.dart b/lib/web_ui/test/canvas_test.dart index 7ce7143150451..53635e0e3ccee 100644 --- a/lib/web_ui/test/canvas_test.dart +++ b/lib/web_ui/test/canvas_test.dart @@ -6,11 +6,16 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'mock_engine_canvas.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUpAll(() { WebExperiments.ensureInitialized(); }); @@ -26,7 +31,6 @@ void main() { test(description, () { testFn(BitmapCanvas(canvasSize)); testFn(DomCanvas()); - testFn(HoudiniCanvas(canvasSize)); testFn(mockCanvas = MockEngineCanvas()); if (whenDone != null) { whenDone(); diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 4787834f19899..8770120ac712c 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -5,14 +5,20 @@ // @dart = 2.6 import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import 'common.dart'; +import 'test_data.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('CanvasKit API', () { setUpAll(() async { await ui.webOnlyInitializePlatform(); @@ -266,7 +272,14 @@ void _imageTests() { expect(frame.height(), 1); expect(nonAnimated.decodeNextFrame(), -1); - expect(frame.makeShader(canvasKit.TileMode.Repeat, canvasKit.TileMode.Mirror), isNotNull); + expect( + frame.makeShader( + canvasKit.TileMode.Repeat, + canvasKit.TileMode.Mirror, + toSkMatrixFromFloat32(Matrix4.identity().storage), + ), + isNotNull, + ); }); test('MakeAnimatedImageFromEncoded makes an animated image', () { @@ -602,7 +615,7 @@ void _pathTests() { }); test('arcTo', () { - path.arcTo( + path.arcToOval( SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4), 5, 40, @@ -611,7 +624,7 @@ void _pathTests() { }); test('overloaded arcTo (used for arcToPoint)', () { - (path as SkPathArcToPointOverload).arcTo( + path.arcToRotated( 1, 2, 3, @@ -1175,26 +1188,29 @@ void _canvasTests() { 20, ); }); -} -final Uint8List kTransparentImage = Uint8List.fromList([ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, - 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, - 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, - 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, - 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, -]); - -/// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue -/// frames). The GIF animates forever, and each frame has a 100ms delay. -final Uint8List kAnimatedGif = Uint8List.fromList( [ - 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xa1, 0x03, 0x00, - 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x21, - 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, - 0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c, - 0x01, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x54, 0x01, 0x00, 0x21, - 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b, -]); + test('toImage.toByteData', () async { + final SkPictureRecorder otherRecorder = SkPictureRecorder(); + final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + )); + otherCanvas.drawRect( + SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + ), + SkPaint(), + ); + final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture(), null); + final CkImage image = await picture.toImage(1, 1); + final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + expect(rawData, isNotNull); + final ByteData pngData = await image.toByteData(format: ui.ImageByteFormat.png); + expect(pngData, isNotNull); + }); +} diff --git a/lib/web_ui/test/canvaskit/image_test.dart b/lib/web_ui/test/canvaskit/image_test.dart new file mode 100644 index 0000000000000..d8e3995da43d3 --- /dev/null +++ b/lib/web_ui/test/canvaskit/image_test.dart @@ -0,0 +1,45 @@ +// 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. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; +import 'test_data.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit image', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); + + test('CkAnimatedImage can be explicitly disposed of', () { + final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage); + expect(image.box.isDeleted, false); + image.dispose(); + expect(image.box.isDeleted, true); + image.dispose(); + expect(image.box.isDeleted, true); + }); + + test('CkImage can be explicitly disposed of', () { + final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame(); + final CkImage image = CkImage(skImage); + expect(image.box.isDeleted, false); + image.dispose(); + expect(image.box.isDeleted, true); + image.dispose(); + expect(image.box.isDeleted, true); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} diff --git a/lib/web_ui/test/canvaskit/path_metrics_test.dart b/lib/web_ui/test/canvaskit/path_metrics_test.dart index 62bde94a8678b..eece12c15b572 100644 --- a/lib/web_ui/test/canvaskit/path_metrics_test.dart +++ b/lib/web_ui/test/canvaskit/path_metrics_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -11,6 +12,10 @@ import 'package:ui/ui.dart' as ui; import 'common.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Path Metrics', () { setUpAll(() async { await ui.webOnlyInitializePlatform(); diff --git a/lib/web_ui/test/canvaskit/shader_test.dart b/lib/web_ui/test/canvaskit/shader_test.dart new file mode 100644 index 0000000000000..dfa3f2dd12b04 --- /dev/null +++ b/lib/web_ui/test/canvaskit/shader_test.dart @@ -0,0 +1,81 @@ +// 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. + +// @dart = 2.6 +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; +import 'test_data.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit shaders', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); + + test('Sweep gradient', () { + final CkGradientSweep gradient = ui.Gradient.sweep( + ui.Offset.zero, + testColors, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Linear gradient', () { + final CkGradientLinear gradient = ui.Gradient.linear( + ui.Offset.zero, + const ui.Offset(0, 1), + testColors, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Radial gradient', () { + final CkGradientRadial gradient = ui.Gradient.radial( + ui.Offset.zero, + 10, + testColors, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Conical gradient', () { + final CkGradientConical gradient = ui.Gradient.radial( + ui.Offset.zero, + 10, + testColors, + null, + ui.TileMode.clamp, + null, + const ui.Offset(10, 10), + 40, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Image shader', () { + final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame(); + final CkImage image = CkImage(skImage); + final CkImageShader imageShader = ui.ImageShader( + image, + ui.TileMode.clamp, + ui.TileMode.repeated, + Float64List.fromList(Matrix4.diagonal3Values(1, 2, 3).storage), + ); + expect(imageShader, isA()); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} + +const List testColors = [ui.Color(0xFFFFFF00), ui.Color(0xFFFFFFFF)]; diff --git a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart index d38a447eaa554..08e0cb3d55837 100644 --- a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart +++ b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'package:mockito/mockito.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; @@ -13,6 +14,10 @@ import 'package:ui/src/engine.dart'; import 'common.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('skia_objects_cache', () { _tests(); // TODO: https://github.com/flutter/flutter/issues/60040 @@ -21,12 +26,22 @@ void main() { void _tests() { SkiaObjects.maximumCacheSize = 4; + bool originalBrowserSupportsFinalizationRegistry; setUpAll(() async { await ui.webOnlyInitializePlatform(); + + // Pretend the browser does not support FinalizationRegistry so we can test the + // resurrection logic. + originalBrowserSupportsFinalizationRegistry = browserSupportsFinalizationRegistry; + browserSupportsFinalizationRegistry = false; + }); + + tearDownAll(() { + browserSupportsFinalizationRegistry = originalBrowserSupportsFinalizationRegistry; }); - group(ResurrectableSkiaObject, () { + group(ManagedSkiaObject, () { test('implements create, cache, delete, resurrect, delete lifecycle', () { int addPostFrameCallbackCount = 0; @@ -152,7 +167,7 @@ class TestOneShotSkiaObject extends OneShotSkiaObject { } } -class TestSkiaObject extends ResurrectableSkiaObject { +class TestSkiaObject extends ManagedSkiaObject { int createDefaultCount = 0; int resurrectCount = 0; int deleteCount = 0; diff --git a/lib/web_ui/test/canvaskit/test_data.dart b/lib/web_ui/test/canvaskit/test_data.dart new file mode 100644 index 0000000000000..9ce6313d714ae --- /dev/null +++ b/lib/web_ui/test/canvaskit/test_data.dart @@ -0,0 +1,29 @@ +// 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. + +// @dart = 2.10 + +import 'dart:typed_data'; + +final Uint8List kTransparentImage = Uint8List.fromList([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, + 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, +]); + +/// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue +/// frames). The GIF animates forever, and each frame has a 100ms delay. +final Uint8List kAnimatedGif = Uint8List.fromList( [ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xa1, 0x03, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x21, + 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, + 0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c, + 0x01, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x54, 0x01, 0x00, 0x21, + 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b, +]); diff --git a/lib/web_ui/test/canvaskit/vertices_test.dart b/lib/web_ui/test/canvaskit/vertices_test.dart new file mode 100644 index 0000000000000..0f0263a04eb7b --- /dev/null +++ b/lib/web_ui/test/canvaskit/vertices_test.dart @@ -0,0 +1,62 @@ +// 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. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('Vertices', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); + + test('can be constructed, drawn, and deleted', () { + final CkVertices vertices = _testVertices(); + expect(vertices, isA()); + expect(vertices.createDefault(), isNotNull); + expect(vertices.resurrect(), isNotNull); + + final recorder = CkPictureRecorder(); + final canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 100, 100)); + canvas.drawVertices( + vertices, + ui.BlendMode.srcOver, + ui.Paint(), + ); + vertices.delete(); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} + +ui.Vertices _testVertices() { + return ui.Vertices( + ui.VertexMode.triangles, + [ + ui.Offset(0, 0), + ui.Offset(10, 10), + ui.Offset(0, 20), + ], + textureCoordinates: [ + ui.Offset(0, 0), + ui.Offset(10, 10), + ui.Offset(0, 20), + ], + colors: [ + ui.Color.fromRGBO(255, 0, 0, 1.0), + ui.Color.fromRGBO(0, 255, 0, 1.0), + ui.Color.fromRGBO(0, 0, 255, 1.0), + ], + indices: [0, 1, 2], + ); +} diff --git a/lib/web_ui/test/clipboard_test.dart b/lib/web_ui/test/clipboard_test.dart index eb67e5beff575..e0c526176e831 100644 --- a/lib/web_ui/test/clipboard_test.dart +++ b/lib/web_ui/test/clipboard_test.dart @@ -7,11 +7,16 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:mockito/mockito.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -Future main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('message handler', () { const String testText = 'test text'; diff --git a/lib/web_ui/test/color_test.dart b/lib/web_ui/test/color_test.dart index 8d53b77f73659..d48ea0bef83cb 100644 --- a/lib/web_ui/test/color_test.dart +++ b/lib/web_ui/test/color_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'package:ui/ui.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +void main() { + internalBootstrapBrowserTest(() => testMain); +} + class NotAColor extends Color { const NotAColor(int value) : super(value); } -void main() { +void testMain() { test('color accessors should work', () { const Color foo = Color(0x12345678); expect(foo.alpha, equals(0x12)); diff --git a/lib/web_ui/test/dom_renderer_test.dart b/lib/web_ui/test/dom_renderer_test.dart index ca9d6804a47c8..514c54d4160bc 100644 --- a/lib/web_ui/test/dom_renderer_test.dart +++ b/lib/web_ui/test/dom_renderer_test.dart @@ -5,10 +5,15 @@ // @dart = 2.6 import 'dart:html' as html; -import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('creating elements works', () { final DomRenderer renderer = DomRenderer(); final html.Element element = renderer.createElement('div'); diff --git a/lib/web_ui/test/engine/frame_reference_test.dart b/lib/web_ui/test/engine/frame_reference_test.dart index 6ccd91c57b207..0658f8258ba7c 100644 --- a/lib/web_ui/test/engine/frame_reference_test.dart +++ b/lib/web_ui/test/engine/frame_reference_test.dart @@ -4,9 +4,14 @@ // @dart = 2.6 import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('CrossFrameCache', () { test('Reuse returns no object when cache empty', () { final CrossFrameCache cache = CrossFrameCache(); diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index 58d536aff4785..4d621117a633c 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -10,6 +10,7 @@ import 'dart:async'; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -29,6 +30,10 @@ const MethodCodec codec = JSONMethodCodec(); void emptyCallback(ByteData date) {} void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$BrowserHistory', () { final PlatformMessagesSpy spy = PlatformMessagesSpy(); diff --git a/lib/web_ui/test/engine/image/html_image_codec_test.dart b/lib/web_ui/test/engine/image/html_image_codec_test.dart index 38cd5602d32f4..d29df24ff44f7 100644 --- a/lib/web_ui/test/engine/image/html_image_codec_test.dart +++ b/lib/web_ui/test/engine/image/html_image_codec_test.dart @@ -3,11 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -Future main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('HtmCodec', () { test('loads sample image', () async { @@ -19,7 +24,27 @@ Future main() async { test('provides image loading progress', () async { StringBuffer buffer = new StringBuffer(); final HtmlCodec codec = HtmlCodec('sample_image1.png', - chunkCallback: (int loaded, int total) { + chunkCallback: (int loaded, int total) { + buffer.write('$loaded/$total,'); + }); + await codec.getNextFrame(); + expect(buffer.toString(), '0/100,100/100,'); + }); + }); + + group('ImageCodecUrl', () { + test('loads sample image from web', () async { + final Uri uri = Uri.base.resolve('sample_image1.png'); + final HtmlCodec codec = await ui.webOnlyInstantiateImageCodecFromUrl(uri); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + expect(frameInfo.image, isNotNull); + expect(frameInfo.image.width, 100); + }); + test('provides image loading progress from web', () async { + final Uri uri = Uri.base.resolve('sample_image1.png'); + StringBuffer buffer = new StringBuffer(); + final HtmlCodec codec = await ui.webOnlyInstantiateImageCodecFromUrl(uri, + chunkCallback: (int loaded, int total) { buffer.write('$loaded/$total,'); }); await codec.getNextFrame(); diff --git a/lib/web_ui/test/engine/navigation_test.dart b/lib/web_ui/test/engine/navigation_test.dart index 8266fd7ec9904..d4ac941d3de59 100644 --- a/lib/web_ui/test/engine/navigation_test.dart +++ b/lib/web_ui/test/engine/navigation_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart' as engine; @@ -15,6 +16,10 @@ const engine.MethodCodec codec = engine.JSONMethodCodec(); void emptyCallback(ByteData date) {} void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { engine.window.locationStrategy = _strategy = engine.TestLocationStrategy(); }); diff --git a/lib/web_ui/test/engine/path_metrics_test.dart b/lib/web_ui/test/engine/path_metrics_test.dart index f4181f3917a49..a8938f913d99f 100644 --- a/lib/web_ui/test/engine/path_metrics_test.dart +++ b/lib/web_ui/test/engine/path_metrics_test.dart @@ -4,14 +4,19 @@ import 'dart:math' as math; -import 'package:ui/ui.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; import '../matchers.dart'; const double kTolerance = 0.001; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('PathMetric length', () { test('empty path', () { Path path = Path(); diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 5f6ac793c38ff..091cde33fc52f 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -6,11 +6,11 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; - const int _kNoButtonChange = -1; const PointerSupportDetector _defaultSupportDetector = PointerSupportDetector(); @@ -40,6 +40,10 @@ bool get isIosSafari => (browserEngine == BrowserEngine.webkit && operatingSystem == OperatingSystem.iOs); void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { html.Element glassPane = domRenderer.glassPaneElement; setUp(() { diff --git a/lib/web_ui/test/engine/profiler_test.dart b/lib/web_ui/test/engine/profiler_test.dart index 0314ee9c8ef6a..49f11242c860d 100644 --- a/lib/web_ui/test/engine/profiler_test.dart +++ b/lib/web_ui/test/engine/profiler_test.dart @@ -6,11 +6,16 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { Profiler.isBenchmarkMode = true; Profiler.ensureInitialized(); diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart index d997f811b862c..7bd1a9810eef4 100644 --- a/lib/web_ui/test/engine/recording_canvas_test.dart +++ b/lib/web_ui/test/engine/recording_canvas_test.dart @@ -3,13 +3,18 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../mock_engine_canvas.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { RecordingCanvas underTest; MockEngineCanvas mockCanvas; final Rect screenRect = Rect.largest; diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index 767d4742ad0ea..6eb3366668752 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -6,8 +6,9 @@ import 'dart:async' show Future; import 'dart:html'; -import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; const MessageCodec codec = StandardMessageCodec(); const String testMessage = 'This is an tooltip.'; @@ -16,6 +17,10 @@ const Map testInput = { }; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { AccessibilityAnnouncements accessibilityAnnouncements; group('$AccessibilityAnnouncements', () { diff --git a/lib/web_ui/test/engine/semantics/semantics_helper_test.dart b/lib/web_ui/test/engine/semantics/semantics_helper_test.dart index cd3e0e9e561d9..56fb691d117a1 100644 --- a/lib/web_ui/test/engine/semantics/semantics_helper_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_helper_test.dart @@ -5,11 +5,15 @@ // @dart = 2.6 import 'dart:html' as html; -import 'package:ui/src/engine.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$DesktopSemanticsEnabler', () { DesktopSemanticsEnabler desktopSemanticsEnabler; html.Element _placeholder; diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 016c94f0c5f7c..d3b0e52eb0180 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -12,6 +12,7 @@ import 'dart:typed_data'; import 'package:mockito/mockito.dart'; import 'package:quiver/testing/async.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -24,6 +25,10 @@ DateTime _testTime = DateTime(2018, 12, 17); EngineSemanticsOwner semantics() => EngineSemanticsOwner.instance; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { EngineSemanticsOwner.debugResetSemantics(); }); diff --git a/lib/web_ui/test/engine/services/serialization_test.dart b/lib/web_ui/test/engine/services/serialization_test.dart index f0f0b58fd4900..e7eb72b7f73c9 100644 --- a/lib/web_ui/test/engine/services/serialization_test.dart +++ b/lib/web_ui/test/engine/services/serialization_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:typed_data'; +import 'package:test/test.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Write and read buffer round-trip', () { test('of single byte', () { final WriteBuffer write = WriteBuffer(); diff --git a/lib/web_ui/test/engine/surface/path/path_iterator_test.dart b/lib/web_ui/test/engine/surface/path/path_iterator_test.dart new file mode 100644 index 0000000000000..3e4c21e029930 --- /dev/null +++ b/lib/web_ui/test/engine/surface/path/path_iterator_test.dart @@ -0,0 +1,90 @@ +// 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:typed_data'; + +import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + final Float32List points = Float32List(PathIterator.kMaxBufferSize); + + group('PathIterator', () { + test('Should return done verb for empty path', () { + final SurfacePath path = SurfacePath(); + final PathIterator iter = PathIterator(path.pathRef, false); + expect(iter.peek(), SPath.kDoneVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should return done when moveTo is last instruction', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + final PathIterator iter = PathIterator(path.pathRef, false); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should return lineTo', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + final PathIterator iter = PathIterator(path.pathRef, false); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kMoveVerb); + expect(points[0], 10); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 20); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should return extra lineTo if iteration is closed', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + final PathIterator iter = PathIterator(path.pathRef, true); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kMoveVerb); + expect(points[0], 10); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 20); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 10); + expect(iter.next(points), SPath.kCloseVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should not return extra lineTo if last point is starting point', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + path.lineTo(10, 10); + final PathIterator iter = PathIterator(path.pathRef, true); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kMoveVerb); + expect(points[0], 10); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 20); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 10); + expect(iter.next(points), SPath.kCloseVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('peek should return lineTo if iteration is closed', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + final PathIterator iter = PathIterator(path.pathRef, true); + expect(iter.next(points), SPath.kMoveVerb); + expect(iter.next(points), SPath.kLineVerb); + expect(iter.peek(), SPath.kLineVerb); + }); + }); +} diff --git a/lib/web_ui/test/path_winding_test.dart b/lib/web_ui/test/engine/surface/path/path_winding_test.dart similarity index 99% rename from lib/web_ui/test/path_winding_test.dart rename to lib/web_ui/test/engine/surface/path/path_winding_test.dart index 905fc6c8f153f..32133e648285b 100644 --- a/lib/web_ui/test/path_winding_test.dart +++ b/lib/web_ui/test/engine/surface/path/path_winding_test.dart @@ -5,12 +5,17 @@ // @dart = 2.10 import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; -/// Test winding and convexity of a path. void main() { + internalBootstrapBrowserTest(() => testMain); +} + +/// Test winding and convexity of a path. +void testMain() { group('Convexity', () { test('Empty path should be convex', () { final SurfacePath path = SurfacePath(); 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 f01864fde9f0a..da2bdb275215e 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -8,14 +8,21 @@ import 'dart:async'; import 'dart:html' as html; import 'dart:js_util' as js_util; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' hide window; -import 'package:test/test.dart'; + import '../../matchers.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUpAll(() async { await webOnlyInitializeEngine(); }); @@ -56,7 +63,8 @@ void main() { testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) { return sceneBuilder.pushClipRRect( RRect.fromLTRBR(10, 20, 30, 40, const Radius.circular(3)), - oldLayer: oldLayer); + oldLayer: oldLayer, + clipBehavior: Clip.none); }, () { return ''' diff --git a/lib/web_ui/test/engine/surface/surface_test.dart b/lib/web_ui/test/engine/surface/surface_test.dart index 9da82f026821e..d7dbe9c827afd 100644 --- a/lib/web_ui/test/engine/surface/surface_test.dart +++ b/lib/web_ui/test/engine/surface/surface_test.dart @@ -8,9 +8,14 @@ import 'dart:html' as html; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Surface', () { setUp(() { SurfaceSceneBuilder.debugForgetFrameScene(); diff --git a/lib/web_ui/test/engine/ulps_test.dart b/lib/web_ui/test/engine/ulps_test.dart index 9d993e92eb262..d0a26c45faad7 100644 --- a/lib/web_ui/test/engine/ulps_test.dart +++ b/lib/web_ui/test/engine/ulps_test.dart @@ -3,10 +3,15 @@ // found in the LICENSE file. import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Float Int conversions', (){ test('Should convert signbit to 2\'s compliment', () { expect(signBitTo2sCompliment(0), 0); diff --git a/lib/web_ui/test/engine/util_test.dart b/lib/web_ui/test/engine/util_test.dart index 51d58c2b11810..9ff47eb2b3e14 100644 --- a/lib/web_ui/test/engine/util_test.dart +++ b/lib/web_ui/test/engine/util_test.dart @@ -5,9 +5,9 @@ // @dart = 2.6 import 'dart:typed_data'; -import 'package:ui/src/engine.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; final Float32List identityTransform = Matrix4.identity().storage; final Float32List xTranslation = (Matrix4.identity()..translate(10)).storage; @@ -17,6 +17,10 @@ final Float32List scaleAndTranslate2d = (Matrix4.identity()..scale(2, 3, 1)..tra final Float32List rotation2d = (Matrix4.identity()..rotateZ(0.2)).storage; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('transformKindOf and isIdentityFloat32ListTransform identify matrix kind', () { expect(transformKindOf(identityTransform), TransformKind.identity); expect(isIdentityFloat32ListTransform(identityTransform), isTrue); diff --git a/lib/web_ui/test/engine/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart index 7f06f9639641b..c0f0a4f7f05f2 100644 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -6,12 +6,17 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; const bool _defaultUseCanvasText = true; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { WebExperiments.ensureInitialized(); }); diff --git a/lib/web_ui/test/engine/window_test.dart b/lib/web_ui/test/engine/window_test.dart index d73fcfdf94e94..ae97dceb8678b 100644 --- a/lib/web_ui/test/engine/window_test.dart +++ b/lib/web_ui/test/engine/window_test.dart @@ -7,11 +7,16 @@ import 'dart:async'; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('onTextScaleFactorChanged preserves the zone', () { final Zone innerZone = Zone.current.fork(); diff --git a/lib/web_ui/test/geometry_test.dart b/lib/web_ui/test/geometry_test.dart index ba2272a3a387c..07ad7601d8d5d 100644 --- a/lib/web_ui/test/geometry_test.dart +++ b/lib/web_ui/test/geometry_test.dart @@ -6,11 +6,16 @@ import 'dart:math' as math show sqrt; import 'dart:math' show pi; -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; + void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('Offset.direction', () { expect(const Offset(0.0, 0.0).direction, 0.0); expect(const Offset(0.0, 1.0).direction, pi / 2.0); diff --git a/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart b/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart index 5b0cc417e1397..797b1cf6b34d1 100644 --- a/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart @@ -5,15 +5,20 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; final Rect region = Rect.fromLTWH(0, 0, 500, 500); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { setUp(() async { debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart index 88e85ab9c95da..888a3f4f1e27a 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 400, 600); BitmapCanvas canvas; @@ -107,4 +112,4 @@ void paintArc(BitmapCanvas canvas, Offset offset, ..strokeWidth = 2 ..color = Color(0x61000000) // black38 ..style = PaintingStyle.stroke); -} \ No newline at end of file +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart index 8407e48766b33..39cfcb4b27feb 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart @@ -6,13 +6,17 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; +void main() { + internalBootstrapBrowserTest(() => testMain); +} -void main() async { +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart index d01620921c1d7..1436e934a59fa 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart' as engine; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart index 927bf2a300fbd..226b8c8a61095 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart @@ -5,14 +5,19 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart' as engine; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; +void main() { + internalBootstrapBrowserTest(() => testMain); +} + /// Tests context save/restore. -void main() async { +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart index 81f1163a4431e..16efb34e3fe78 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart @@ -7,15 +7,20 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; import 'scuba.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); @@ -374,6 +379,28 @@ void main() async { sceneElement.remove(); } }); + + // Regression test for https://github.com/flutter/flutter/issues/61691 + // + // The bug in bitmap_canvas.dart was that when we transformed and clipped + // the image we did not apply `transform-origin: 0 0 0` to the clipping + // element which resulted in an undesirable offset. + test('Paints clipped and transformed image', () async { + final Rect region = const Rect.fromLTRB(0, 0, 60, 70); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.translate(10, 10); + canvas.transform(Matrix4.rotationZ(0.4).storage); + canvas.clipPath(Path() + ..moveTo(10, 10) + ..lineTo(50, 10) + ..lineTo(50, 30) + ..lineTo(10, 30) + ..close() + ); + canvas.drawImage(createNineSliceImage(), Offset.zero, Paint()); + await _checkScreenshot(canvas, 'draw_clipped_and_transformed_image', region: region, + maxDiffRatePercent: 1.0); + }); } // 9 slice test image that has a shiny/glass look. diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart index de4c52cebb1d2..b70666711f5ba 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart @@ -5,15 +5,20 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; final Rect region = Rect.fromLTWH(0, 0, 500, 100); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { setUp(() async { debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart index 8688140b290f2..c3e6adaa0469c 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart @@ -6,14 +6,18 @@ import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; - import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 400, 600); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart index ce55498eba86a..a072d025974fd 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart @@ -6,15 +6,20 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; import 'scuba.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 500, 100); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart index ca30387f99319..962d930d8884f 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart index 215d51da5c29e..8447c26806d47 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 300, 300); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart index a808a57a28598..1f3184c67a097 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart @@ -7,13 +7,18 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { // Commit a recording canvas to a bitmap, and compare with the expected Future _checkScreenshot(RecordingCanvas rc, String fileName, ui.Rect screenRect, {bool write = false}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart index ea77b27ccd802..07d19e51dc902 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 150, 420); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart index 469cc2f88faf8..2bd62f258fa20 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart index 95d977993704c..1506419d20e74 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old scuba tester padding BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart index 0a85cc25f96aa..8ae75205bfb47 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 300, 300); BitmapCanvas canvas; @@ -68,4 +73,4 @@ void paintStrokeJoins(BitmapCanvas canvas) { end = end.translate(0, 20); } } -} \ No newline at end of file +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart index 447f9706030ec..4a81d2fe52665 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 300, 300); BitmapCanvas canvas; @@ -63,4 +68,4 @@ void paintSideBySideRects(BitmapCanvas canvas) { ..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Color(0x7fffff00)); -} \ No newline at end of file +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart new file mode 100644 index 0000000000000..3c8d8c64210f9 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -0,0 +1,49 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +void main() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Canvas to Picture', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = await _drawTestPictureWithCircle(region); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + //await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + }); +} + +Picture _drawTestPictureWithCircle(Rect region) { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00)); + return recorder.endRecording(); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart index 556705953bc23..e48001071a301 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 500, 500); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart index 2c270a0a0f938..04dce71d0b986 100644 --- a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart @@ -6,16 +6,21 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../../matchers.dart'; import 'package:web_engine_tester/golden_tester.dart'; final Rect region = Rect.fromLTWH(0, 0, 500, 100); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { setUp(() async { debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); @@ -133,7 +138,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, 0, 500, 100)); }, skip: '''TODO(https://github.com/flutter/flutter/issues/40395) Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500'''); @@ -147,7 +152,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, 0, 20, 20)); }); @@ -161,7 +166,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, Rect.zero); expect(picture.debugExactGlobalCullRect, Rect.zero); }); @@ -176,7 +181,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 60, 60)); }); @@ -195,7 +200,7 @@ void _testCullRectComputation() { builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 60, 60)); }); @@ -213,7 +218,7 @@ void _testCullRectComputation() { builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect( picture.debugExactGlobalCullRect, const Rect.fromLTRB(0, 70, 20, 100)); expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, -20, 20, 10)); @@ -244,7 +249,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_cull_rect_fills_layer_clip.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 70, 70)); }); @@ -274,7 +279,7 @@ void _testCullRectComputation() { 'compositing_cull_rect_intersects_clip_and_paint_bounds.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(50, 40, 70, 70)); }); @@ -305,7 +310,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_cull_rect_offset_inside_layer_clip.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(-15.0, -20.0, 15.0, 0.0)); }); @@ -335,7 +340,7 @@ void _testCullRectComputation() { builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, Rect.zero); expect(picture.debugExactGlobalCullRect, Rect.zero); }); @@ -378,7 +383,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_cull_rect_rotated.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect( picture.optimalLocalCullRect, within( @@ -510,7 +515,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_3d_rotate1.png', region: region); // ignore: unused_local_variable - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; // TODO(https://github.com/flutter/flutter/issues/40395): // Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500. // expect( diff --git a/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart b/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart index bcf69c155e212..9362cccebd3b9 100644 --- a/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding Future testPath(Path path, String scubaFileName) async { diff --git a/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart b/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart index 765c614ae4cfc..68a2136343791 100644 --- a/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart index cbeadad84a958..6eccec76d3234 100644 --- a/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart b/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart index cd3378b569896..fd9efc8bfb44d 100644 --- a/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; @@ -12,7 +13,11 @@ import 'scuba.dart'; typedef PaintTest = void Function(RecordingCanvas recordingCanvas); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { // Scuba doesn't give us viewport smaller than 472px wide. final EngineScubaTester scuba = await EngineScubaTester.initialize( viewportSize: const Size(600, 600), diff --git a/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart index 8695296ab9dbd..380036ba18dde 100644 --- a/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart @@ -5,14 +5,19 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../../matchers.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart b/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart index f657f36a5ae02..03d9f0c1ad2e7 100644 --- a/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding Future testPath(Path path, String scubaFileName, {Paint paint, double maxDiffRatePercent = null}) async { diff --git a/lib/web_ui/test/golden_tests/engine/path_transform_test.dart b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart index 789f0259eeee6..0f968ba72b056 100644 --- a/lib/web_ui/test/golden_tests/engine/path_transform_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart b/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart index af959cd4fe6c8..1c61fff196a09 100644 --- a/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Picture', () { test('toImage produces an image', () async { final EnginePictureRecorder recorder = ui.PictureRecorder(); diff --git a/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart index 4fb58ebc3df94..098233fe1cfb3 100644 --- a/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart index 618c7e9ac973e..4d69f8955ad6a 100644 --- a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart @@ -7,14 +7,19 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../../matchers.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/scuba.dart b/lib/web_ui/test/golden_tests/engine/scuba.dart index 821b9dddd0078..e735967a93bf5 100644 --- a/lib/web_ui/test/golden_tests/engine/scuba.dart +++ b/lib/web_ui/test/golden_tests/engine/scuba.dart @@ -91,7 +91,7 @@ typedef CanvasTest = FutureOr Function(EngineCanvas canvas); /// Runs the given test [body] with each type of canvas. void testEachCanvas(String description, CanvasTest body, - {double maxDiffRate, bool bSkipHoudini = false}) { + {double maxDiffRate}) { const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800); test('$description (bitmap)', () { try { @@ -123,18 +123,6 @@ void testEachCanvas(String description, CanvasTest body, TextMeasurementService.clearCache(); } }); - if (!bSkipHoudini) { - test('$description (houdini)', () { - try { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - WebExperiments.instance.useCanvasText = false; - return body(HoudiniCanvas(bounds)); - } finally { - WebExperiments.instance.useCanvasText = null; - TextMeasurementService.clearCache(); - } - }); - } } final ui.TextStyle _defaultTextStyle = ui.TextStyle( diff --git a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart index c6f98e4e5d91b..307cdb583caad 100644 --- a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart @@ -5,9 +5,10 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; @@ -15,7 +16,11 @@ import 'scuba.dart'; const Color _kShadowColor = Color.fromARGB(255, 0, 0, 0); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 550, 300); SurfaceSceneBuilder builder; diff --git a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart index 507be4fd33df9..dc243b2ab711d 100644 --- a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:async'; +import 'package:test/bootstrap/browser.dart'; import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; @@ -21,7 +22,11 @@ const String veryLong = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; const String longUnbreakable = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final EngineScubaTester scuba = await EngineScubaTester.initialize( viewportSize: const Size(800, 800), ); diff --git a/lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart b/lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart new file mode 100644 index 0000000000000..e89bc7ee69a7a --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart @@ -0,0 +1,79 @@ +// 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. + +// @dart = 2.6 +// import 'package:image/image.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart'; + +import 'scuba.dart'; + +typedef PaintTest = void Function(RecordingCanvas recordingCanvas); + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + final EngineScubaTester scuba = await EngineScubaTester.initialize( + viewportSize: const Size(600, 600), + ); + + setUpStableTestFonts(); + + testEachCanvas('draws paragraphs with placeholders', (EngineCanvas canvas) { + final Rect screenRect = const Rect.fromLTWH(0, 0, 600, 600); + final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect); + + Offset offset = Offset.zero; + for (PlaceholderAlignment alignment in PlaceholderAlignment.values) { + _paintTextWithPlaceholder(recordingCanvas, offset, alignment); + offset = offset.translate(0.0, 80.0); + } + recordingCanvas.endRecording(); + recordingCanvas.apply(canvas, screenRect); + return scuba.diffCanvasScreenshot(canvas, 'text_with_placeholders'); + }); +} + +const Color black = Color(0xFF000000); +const Color blue = Color(0xFF0000FF); +const Color red = Color(0xFFFF0000); + +const Size placeholderSize = Size(80.0, 50.0); + +void _paintTextWithPlaceholder( + RecordingCanvas canvas, + Offset offset, + PlaceholderAlignment alignment, +) { + // First let's draw the paragraph. + final Paragraph paragraph = _createParagraphWithPlaceholder(alignment); + canvas.drawParagraph(paragraph, offset); + + // Then fill the placeholders. + final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single; + canvas.drawRect( + placeholderBox.toRect().shift(offset), + Paint()..color = red, + ); +} + +Paragraph _createParagraphWithPlaceholder(PlaceholderAlignment alignment) { + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); + builder + .pushStyle(TextStyle(color: black, fontFamily: 'Roboto', fontSize: 14)); + builder.addText('Lorem ipsum'); + builder.addPlaceholder( + placeholderSize.width, + placeholderSize.height, + alignment, + baselineOffset: 40.0, + baseline: TextBaseline.alphabetic, + ); + builder.pushStyle(TextStyle(color: blue, fontFamily: 'Roboto', fontSize: 14)); + builder.addText('dolor sit amet, consectetur.'); + return builder.build()..layout(ParagraphConstraints(width: 200.0)); +} diff --git a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart index c8ec261e8b72a..5f31ca6ada828 100644 --- a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart @@ -3,12 +3,17 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; import 'scuba.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final EngineScubaTester scuba = await EngineScubaTester.initialize( viewportSize: const Size(800, 800), ); @@ -220,7 +225,7 @@ void main() async { testEachCanvas('draws text with a shadow', (EngineCanvas canvas) { drawTextWithShadow(canvas); return scuba.diffCanvasScreenshot(canvas, 'text_shadow', maxDiffRatePercent: 0.2); - }, bSkipHoudini: true); + }); testEachCanvas('Handles disabled strut style', (EngineCanvas canvas) { // Flutter uses [StrutStyle.disabled] for the [SelectableText] widget. This diff --git a/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart b/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart index 882bdb60e3f75..7a0cfc7c621e0 100644 --- a/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart +++ b/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:web_engine_tester/golden_tester.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('screenshot test reports failure', () async { html.document.body.innerHtml = 'Text that does not appear on the screenshot!'; await matchGoldenFile('__local__/smoke_test.png', region: Rect.fromLTWH(0, 0, 320, 200)); diff --git a/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart b/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart index 97d505511a709..563e3e92c7269 100644 --- a/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart +++ b/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart @@ -5,12 +5,17 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { debugEmulateFlutterTesterEnvironment = true; await webOnlyInitializePlatform(assetManager: WebOnlyMockAssetManager()); diff --git a/lib/web_ui/test/gradient_test.dart b/lib/web_ui/test/gradient_test.dart index a0df9ebdde051..b4335dd353228 100644 --- a/lib/web_ui/test/gradient_test.dart +++ b/lib/web_ui/test/gradient_test.dart @@ -3,11 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; + void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('Gradient.radial with no focal point', () { expect( Gradient.radial( diff --git a/lib/web_ui/test/hash_codes_test.dart b/lib/web_ui/test/hash_codes_test.dart index 7349dc7398edc..cca96671b711a 100644 --- a/lib/web_ui/test/hash_codes_test.dart +++ b/lib/web_ui/test/hash_codes_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; @@ -13,6 +14,10 @@ import 'package:ui/ui.dart'; const int _kBiggestExactJavaScriptInt = 9007199254740992; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('hashValues can hash lots of huge values effectively', () { expect( hashValues( diff --git a/lib/web_ui/test/keyboard_test.dart b/lib/web_ui/test/keyboard_test.dart index 3a819d6ffbf90..4b68cec6e2e13 100644 --- a/lib/web_ui/test/keyboard_test.dart +++ b/lib/web_ui/test/keyboard_test.dart @@ -8,12 +8,16 @@ import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:quiver/testing/async.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; - void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Keyboard', () { /// Used to save and restore [ui.window.onPlatformMessage] after each test. ui.PlatformMessageCallback savedCallback; diff --git a/lib/web_ui/test/locale_test.dart b/lib/web_ui/test/locale_test.dart index eb9331b465e59..316a42ae1101f 100644 --- a/lib/web_ui/test/locale_test.dart +++ b/lib/web_ui/test/locale_test.dart @@ -3,11 +3,15 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('Locale', () { const Null $null = null; expect(const Locale('en').toString(), 'en'); diff --git a/lib/web_ui/test/paragraph_builder_test.dart b/lib/web_ui/test/paragraph_builder_test.dart index 54a3bcf53b046..fe3041bc1539f 100644 --- a/lib/web_ui/test/paragraph_builder_test.dart +++ b/lib/web_ui/test/paragraph_builder_test.dart @@ -3,12 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; - void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUpAll(() { WebExperiments.ensureInitialized(); }); diff --git a/lib/web_ui/test/paragraph_test.dart b/lib/web_ui/test/paragraph_test.dart index 0bb402651fae4..df3f45edf2e6f 100644 --- a/lib/web_ui/test/paragraph_test.dart +++ b/lib/web_ui/test/paragraph_test.dart @@ -3,10 +3,11 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' hide window; -import 'package:test/test.dart'; void testEachMeasurement(String description, VoidCallback body, {bool skip}) { test('$description (dom measurement)', () async { @@ -31,7 +32,11 @@ void testEachMeasurement(String description, VoidCallback body, {bool skip}) { }, skip: skip); } -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await webOnlyInitializeTestDomRenderer(); // Ahem font uses a constant ideographic/alphabetic baseline ratio. @@ -801,6 +806,46 @@ void main() async { ); }); + testEachMeasurement('getBoxesForRange includes trailing spaces', () { + const String text = 'abcd abcde '; + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'Ahem', + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 10, + )); + builder.addText(text); + final Paragraph paragraph = builder.build(); + paragraph.layout(const ParagraphConstraints(width: double.infinity)); + expect( + paragraph.getBoxesForRange(0, text.length), + [ + TextBox.fromLTRBD(0.0, 0.0, 120.0, 10.0, TextDirection.ltr), + ], + ); + }); + + testEachMeasurement('getBoxesForRange multi-line includes trailing spaces', () { + const String text = 'abcd\nabcde \nabc'; + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'Ahem', + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 10, + )); + builder.addText(text); + final Paragraph paragraph = builder.build(); + paragraph.layout(const ParagraphConstraints(width: double.infinity)); + expect( + paragraph.getBoxesForRange(0, text.length), + [ + TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr), + TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), + TextBox.fromLTRBD(0.0, 20.0, 30.0, 30.0, TextDirection.ltr), + ], + ); + }); + test('longestLine', () { // [Paragraph.longestLine] is only supported by canvas-based measurement. WebExperiments.instance.useCanvasText = true; diff --git a/lib/web_ui/test/path_test.dart b/lib/web_ui/test/path_test.dart index 04461410cae5d..6f19c9cc8f439 100644 --- a/lib/web_ui/test/path_test.dart +++ b/lib/web_ui/test/path_test.dart @@ -7,6 +7,7 @@ import 'dart:js_util' as js_util; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; @@ -14,6 +15,10 @@ import 'package:ui/src/engine.dart'; import 'matchers.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Path', () { test('Should have no subpaths when created', () { final SurfacePath path = SurfacePath(); diff --git a/lib/web_ui/test/rect_test.dart b/lib/web_ui/test/rect_test.dart index e5719e711603f..a526b4590e9dc 100644 --- a/lib/web_ui/test/rect_test.dart +++ b/lib/web_ui/test/rect_test.dart @@ -3,11 +3,15 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('rect accessors', () { const Rect r = Rect.fromLTRB(1.0, 3.0, 5.0, 7.0); expect(r.left, equals(1.0)); diff --git a/lib/web_ui/test/rrect_test.dart b/lib/web_ui/test/rrect_test.dart index b9d1f59e07ae1..02943b8413adc 100644 --- a/lib/web_ui/test/rrect_test.dart +++ b/lib/web_ui/test/rrect_test.dart @@ -3,11 +3,15 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; +import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('RRect.contains()', () { final RRect rrect = RRect.fromRectAndCorners( const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), diff --git a/lib/web_ui/test/text/font_collection_test.dart b/lib/web_ui/test/text/font_collection_test.dart index ec5590f008a23..b6423378ef6b0 100644 --- a/lib/web_ui/test/text/font_collection_test.dart +++ b/lib/web_ui/test/text/font_collection_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:html' as html; -import 'package:ui/src/engine.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; + void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$FontManager', () { FontManager fontManager; const String _testFontUrl = 'packages/ui/assets/ahem.ttf'; diff --git a/lib/web_ui/test/text/font_loading_test.dart b/lib/web_ui/test/text/font_loading_test.dart index 5b34d9d9346ae..81ff90eb568b8 100644 --- a/lib/web_ui/test/text/font_loading_test.dart +++ b/lib/web_ui/test/text/font_loading_test.dart @@ -8,11 +8,16 @@ import 'dart:convert'; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -Future main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('loadFontFromList', () { const String _testFontUrl = 'packages/ui/assets/ahem.ttf'; diff --git a/lib/web_ui/test/text/line_breaker_test.dart b/lib/web_ui/test/text/line_breaker_test.dart index bfffabb769135..f536729faeeaa 100644 --- a/lib/web_ui/test/text/line_breaker_test.dart +++ b/lib/web_ui/test/text/line_breaker_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.10 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -11,6 +12,10 @@ import 'package:ui/ui.dart'; import 'line_breaker_test_data.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('nextLineBreak', () { test('Does not go beyond the ends of a string', () { expect(split('foo'), [ @@ -162,6 +167,62 @@ void main() { ]); }); + test('trailing spaces and new lines', () { + expect( + findBreaks('foo bar '), + [ + LineBreakResult(4, 4, 3, LineBreakType.opportunity), + LineBreakResult(9, 9, 7, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks('foo \nbar\nbaz \n'), + [ + LineBreakResult(6, 5, 3, LineBreakType.mandatory), + LineBreakResult(10, 9, 9, LineBreakType.mandatory), + LineBreakResult(17, 16, 13, LineBreakType.mandatory), + LineBreakResult(17, 17, 17, LineBreakType.endOfText), + ], + ); + }); + + test('leading spaces', () { + expect( + findBreaks(' foo'), + [ + LineBreakResult(1, 1, 0, LineBreakType.opportunity), + LineBreakResult(4, 4, 4, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks(' foo'), + [ + LineBreakResult(3, 3, 0, LineBreakType.opportunity), + LineBreakResult(6, 6, 6, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks(' foo bar'), + [ + LineBreakResult(2, 2, 0, LineBreakType.opportunity), + LineBreakResult(8, 8, 5, LineBreakType.opportunity), + LineBreakResult(11, 11, 11, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks(' \n foo'), + [ + LineBreakResult(3, 2, 0, LineBreakType.mandatory), + LineBreakResult(6, 6, 3, LineBreakType.opportunity), + LineBreakResult(9, 9, 9, LineBreakType.endOfText), + ], + ); + }); + test('comprehensive test', () { for (int t = 0; t < data.length; t++) { final TestCase testCase = data[t]; @@ -220,9 +281,7 @@ class Line { @override bool operator ==(Object other) { - return other is Line - && other.text == text - && other.breakType == breakType; + return other is Line && other.text == text && other.breakType == breakType; } String get escapedText { @@ -245,14 +304,22 @@ class Line { List split(String text) { final List lines = []; - int i = 0; - LineBreakType? breakType; - while (breakType != LineBreakType.endOfText) { - final LineBreakResult result = nextLineBreak(text, i); - lines.add(Line(text.substring(i, result.index), result.type)); - - i = result.index; - breakType = result.type; + int lastIndex = 0; + for (LineBreakResult brk in findBreaks(text)) { + lines.add(Line(text.substring(lastIndex, brk.index), brk.type)); + lastIndex = brk.index; } return lines; } + +List findBreaks(String text) { + final List breaks = []; + + LineBreakResult brk = nextLineBreak(text, 0); + breaks.add(brk); + while (brk.type != LineBreakType.endOfText) { + brk = nextLineBreak(text, brk.index); + breaks.add(brk); + } + return breaks; +} diff --git a/lib/web_ui/test/text/measurement_test.dart b/lib/web_ui/test/text/measurement_test.dart index 5e5e61d65b2e5..ca4258c89cc77 100644 --- a/lib/web_ui/test/text/measurement_test.dart +++ b/lib/web_ui/test/text/measurement_test.dart @@ -5,10 +5,12 @@ // @dart = 2.6 @TestOn('chrome || firefox') +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; final ui.ParagraphStyle ahemStyle = ui.ParagraphStyle( fontFamily: 'ahem', @@ -47,8 +49,11 @@ void testMeasurements(String description, MeasurementTestBody body, { skip: skipCanvas, ); } +void main() { + internalBootstrapBrowserTest(() => testMain); +} -void main() async { +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('$RulerManager', () { @@ -1157,5 +1162,6 @@ EngineLineMetrics line( lineNumber: lineNumber, left: left, endIndexWithoutNewlines: -1, + widthWithTrailingSpaces: width, ); } diff --git a/lib/web_ui/test/text/word_breaker_test.dart b/lib/web_ui/test/text/word_breaker_test.dart index 54ae7dd33c6cb..15966c9bce2ec 100644 --- a/lib/web_ui/test/text/word_breaker_test.dart +++ b/lib/web_ui/test/text/word_breaker_test.dart @@ -3,11 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$WordBreaker', () { test('Does not go beyond the ends of a string', () { expect(WordBreaker.prevBreakIndex('foo', 0), 0); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index dfe51f28e90a5..ec511cf4f98a2 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -3,14 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'dart:async'; import 'dart:html'; import 'dart:js_util' as js_util; import 'dart:typed_data'; -import 'package:ui/src/engine.dart' hide window; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart' hide window; + import 'matchers.dart'; import 'spy.dart'; @@ -28,22 +30,14 @@ String lastInputAction; final InputConfiguration singlelineConfig = InputConfiguration( inputType: EngineInputType.text, - obscureText: false, - inputAction: 'TextInputAction.done', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none'), ); final Map flutterSinglelineConfig = createFlutterConfig('text'); final InputConfiguration multilineConfig = InputConfiguration( - inputType: EngineInputType.multiline, - obscureText: false, - inputAction: 'TextInputAction.newline', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + inputType: EngineInputType.multiline, + inputAction: 'TextInputAction.newline', +); final Map flutterMultilineConfig = createFlutterConfig('multiline'); @@ -56,6 +50,10 @@ void trackInputAction(String inputAction) { } void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { tearDown(() { lastEditingState = null; lastInputAction = null; @@ -109,14 +107,23 @@ void main() { expect(document.activeElement, document.body); }); + test('Respects read-only config', () { + final InputConfiguration config = InputConfiguration(readOnly: true); + editingElement.enable( + config, + onChange: trackEditingState, + onAction: trackInputAction, + ); + expect(document.getElementsByTagName('input'), hasLength(1)); + final InputElement input = document.getElementsByTagName('input')[0]; + expect(editingElement.domElement, input); + expect(input.getAttribute('readonly'), 'readonly'); + + editingElement.disable(); + }); + test('Knows how to create password fields', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - inputAction: 'TextInputAction.done', - obscureText: true, - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(obscureText: true); editingElement.enable( config, onChange: trackEditingState, @@ -131,13 +138,7 @@ void main() { }); test('Knows to turn autocorrect off', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - inputAction: 'TextInputAction.done', - obscureText: false, - autocorrect: false, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(autocorrect: false); editingElement.enable( config, onChange: trackEditingState, @@ -152,13 +153,7 @@ void main() { }); test('Knows to turn autocorrect on', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - inputAction: 'TextInputAction.done', - obscureText: false, - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(autocorrect: true); editingElement.enable( config, onChange: trackEditingState, @@ -292,13 +287,7 @@ void main() { }); test('Triggers input action', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - obscureText: false, - inputAction: 'TextInputAction.done', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(inputAction: 'TextInputAction.done'); editingElement.enable( config, onChange: trackEditingState, @@ -320,12 +309,9 @@ void main() { test('Does not trigger input action in multi-line mode', () { final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.multiline, - obscureText: false, - inputAction: 'TextInputAction.done', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + inputType: EngineInputType.multiline, + inputAction: 'TextInputAction.done', + ); editingElement.enable( config, onChange: trackEditingState, @@ -719,6 +705,204 @@ void main() { skip: (browserEngine == BrowserEngine.webkit || browserEngine == BrowserEngine.edge)); + test('finishAutofillContext closes connection no autofill element', + () async { + final MethodCall setClient = MethodCall( + 'TextInput.setClient', [123, flutterSinglelineConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); + + // Editing shouldn't have started yet. + expect(document.activeElement, document.body); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', false); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + expect(spy.messages, hasLength(1)); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.onConnectionClosed'); + expect( + spy.messages[0].methodArguments, + [ + 123, // Client ID + ], + ); + spy.messages.clear(); + // Input element is removed from DOM. + expect(document.getElementsByTagName('input'), hasLength(0)); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + + test('finishAutofillContext removes form from DOM', () async { + // Create a configuration with an AutofillGroup of four text fields. + final Map flutterMultiAutofillElementConfig = + createFlutterConfig('text', + autofillHint: 'username', + autofillHintsForFields: [ + 'username', + 'email', + 'name', + 'telephoneNumber' + ]); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterMultiAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // Form is added to DOM. + expect(document.getElementsByTagName('form'), isNotEmpty); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + + // Confirm that [HybridTextEditing] didn't send any messages. + expect(spy.messages, isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); + + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', false); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + // Form element is removed from DOM. + expect(document.getElementsByTagName('form'), hasLength(0)); + expect(formsOnTheDom, hasLength(0)); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + + test('finishAutofillContext with save submits forms', () async { + // Create a configuration with an AutofillGroup of four text fields. + final Map flutterMultiAutofillElementConfig = + createFlutterConfig('text', + autofillHint: 'username', + autofillHintsForFields: [ + 'username', + 'email', + 'name', + 'telephoneNumber' + ]); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterMultiAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // Form is added to DOM. + expect(document.getElementsByTagName('form'), isNotEmpty); + FormElement formElement = document.getElementsByTagName('form')[0]; + final Completer submittedForm = Completer(); + formElement.addEventListener( + 'submit', (event) => submittedForm.complete(true)); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', true); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + // `submit` action is called on form. + await expectLater(await submittedForm.future, true); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + + test('forms submits for focused input', () async { + // Create a configuration with an AutofillGroup of four text fields. + final Map flutterMultiAutofillElementConfig = + createFlutterConfig('text', + autofillHint: 'username', + autofillHintsForFields: [ + 'username', + 'email', + 'name', + 'telephoneNumber' + ]); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterMultiAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // Form is added to DOM. + expect(document.getElementsByTagName('form'), isNotEmpty); + FormElement formElement = document.getElementsByTagName('form')[0]; + final Completer submittedForm = Completer(); + formElement.addEventListener( + 'submit', (event) => submittedForm.complete(true)); + + // Clear client is not called. The used requested context to be finalized. + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', true); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + // Connection is closed by the engine. + expect(spy.messages, hasLength(1)); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.onConnectionClosed'); + + // `submit` action is called on form. + await expectLater(await submittedForm.future, true); + // Form element is removed from DOM. + expect(document.getElementsByTagName('form'), hasLength(0)); + expect(formsOnTheDom, hasLength(0)); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + test('setClient, setEditingState, show, setClient', () { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); @@ -816,14 +1000,66 @@ void main() { textEditing.editingElement.domElement, 'abcd', 2, 3); final FormElement formElement = document.getElementsByTagName('form')[0]; - expect(formElement.childNodes, hasLength(1)); + // The form has one input element and one submit button. + expect(formElement.childNodes, hasLength(2)); const MethodCall clearClient = MethodCall('TextInput.clearClient'); sendFrameworkMessage(codec.encodeMethodCall(clearClient)); // Confirm that [HybridTextEditing] didn't send any messages. expect(spy.messages, isEmpty); - expect(document.getElementsByTagName('form'), isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); + }); + + test( + 'singleTextField Autofill setEditableSizeAndTransform preserves' + 'editing state', () { + // Create a configuration with focused element has autofil hint. + final Map flutterSingleAutofillElementConfig = + createFlutterConfig('text', autofillHint: 'username'); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterSingleAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // The second [setEditingState] should override the first one. + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + // The transform is changed. For example after a validation error, red + // line appeared under the input field. + final MethodCall setSizeAndTransform = + configureSetSizeAndTransformMethodCall(150, 50, + Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); + sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + + // Check the element still has focus. User can keep editing. + expect(document.activeElement, textEditing.editingElement.domElement); + + // Check the cursor location is the same. + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + + // Confirm that [HybridTextEditing] didn't send any messages. + expect(spy.messages, isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); }); test( @@ -859,14 +1095,17 @@ void main() { textEditing.editingElement.domElement, 'abcd', 2, 3); final FormElement formElement = document.getElementsByTagName('form')[0]; - expect(formElement.childNodes, hasLength(4)); + // The form has 4 input elements and one submit button. + expect(formElement.childNodes, hasLength(5)); const MethodCall clearClient = MethodCall('TextInput.clearClient'); sendFrameworkMessage(codec.encodeMethodCall(clearClient)); // Confirm that [HybridTextEditing] didn't send any messages. expect(spy.messages, isEmpty); - expect(document.getElementsByTagName('form'), isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); }); test('No capitilization: setClient, setEditingState, show', () { @@ -1228,7 +1467,8 @@ void main() { textEditing.editingElement.domElement, 'abcd', 2, 3); final FormElement formElement = document.getElementsByTagName('form')[0]; - expect(formElement.childNodes, hasLength(4)); + // The form has 4 input elements and one submit button. + expect(formElement.childNodes, hasLength(5)); // Autofill one of the form elements. InputElement element = formElement.childNodes.first; @@ -1450,13 +1690,17 @@ void main() { // And default behavior of keyboard event shouldn't have been prevented. expect(event.defaultPrevented, isFalse); }); + + tearDown(() { + clearForms(); + }); }); group('EngineAutofillForm', () { test('validate multi element form', () { final List fields = createFieldValues( ['username', 'password', 'newPassword'], - ['field1', 'fields2', 'field3']); + ['field1', 'field2', 'field3']); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( createAutofillInfo('username', 'field1'), fields); @@ -1467,14 +1711,19 @@ void main() { expect(autofillForm.items, hasLength(2)); expect(autofillForm.formElement, isNotNull); + expect(autofillForm.formIdentifier, 'field1*field2*field3'); + final FormElement form = autofillForm.formElement; - expect(form.childNodes, hasLength(2)); + // Note that we also add a submit button. Therefore the form element has + // 3 child nodes. + expect(form.childNodes, hasLength(3)); final InputElement firstElement = form.childNodes.first; // Autofill value is applied to the element. expect(firstElement.name, BrowserAutofillHints.instance.flutterToEngine('password')); - expect(firstElement.id, 'fields2'); + expect(firstElement.id, + BrowserAutofillHints.instance.flutterToEngine('password')); expect(firstElement.type, 'password'); if (browserEngine == BrowserEngine.firefox) { expect(firstElement.name, @@ -1495,7 +1744,20 @@ void main() { expect(css.backgroundColor, 'transparent'); }); - test('place remove form', () { + test('validate multi element form ids sorted for form id', () { + final List fields = createFieldValues( + ['username', 'password', 'newPassword'], + ['zzyyxx', 'aabbcc', 'jjkkll']); + final EngineAutofillForm autofillForm = + EngineAutofillForm.fromFrameworkMessage( + createAutofillInfo('username', 'field1'), fields); + + expect(autofillForm.formIdentifier, 'aabbcc*jjkkll*zzyyxx'); + }); + + test('place and store form', () { + expect(document.getElementsByTagName('form'), isEmpty); + final List fields = createFieldValues( ['username', 'password', 'newPassword'], ['field1', 'fields2', 'field3']); @@ -1506,16 +1768,18 @@ void main() { final InputElement testInputElement = InputElement(); autofillForm.placeForm(testInputElement); - // The focused element is appended to the form, + // The focused element is appended to the form, form also has the button + // so in total it shoould have 4 elements. final FormElement form = autofillForm.formElement; - expect(form.childNodes, hasLength(3)); + expect(form.childNodes, hasLength(4)); final FormElement formOnDom = document.getElementsByTagName('form')[0]; // Form is attached to the DOM. expect(form, equals(formOnDom)); - autofillForm.removeForm(); - expect(document.getElementsByTagName('form'), isEmpty); + autofillForm.storeForm(); + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); }); test('Validate single element form', () { @@ -1531,7 +1795,13 @@ void main() { expect(autofillForm.formElement, isNotNull); final FormElement form = autofillForm.formElement; - expect(form.childNodes, isEmpty); + // Submit button is added to the form. + expect(form.childNodes, isNotEmpty); + final InputElement inputElement = form.childNodes.first; + expect(inputElement.type, 'submit'); + + // The submit button should have class `submitBtn`. + expect(inputElement.className, 'submitBtn'); }); test('Return null if no focused element', () { @@ -1541,6 +1811,10 @@ void main() { expect(autofillForm, isNull); }); + + tearDown(() { + clearForms(); + }); }); group('AutofillInfo', () { @@ -1570,7 +1844,8 @@ void main() { // browsers. expect(testInputElement.name, BrowserAutofillHints.instance.flutterToEngine(testHint)); - expect(testInputElement.id, testId); + expect(testInputElement.id, + BrowserAutofillHints.instance.flutterToEngine(testHint)); expect(testInputElement.type, 'text'); if (browserEngine == BrowserEngine.firefox) { expect(testInputElement.name, @@ -1592,7 +1867,8 @@ void main() { // browsers. expect(testInputElement.name, BrowserAutofillHints.instance.flutterToEngine(testHint)); - expect(testInputElement.id, testId); + expect(testInputElement.id, + BrowserAutofillHints.instance.flutterToEngine(testHint)); expect(testInputElement.getAttribute('autocomplete'), BrowserAutofillHints.instance.flutterToEngine(testHint)); }); @@ -1608,7 +1884,8 @@ void main() { // browsers. expect(testInputElement.name, BrowserAutofillHints.instance.flutterToEngine(testPasswordHint)); - expect(testInputElement.id, testId); + expect(testInputElement.id, + BrowserAutofillHints.instance.flutterToEngine(testPasswordHint)); expect(testInputElement.type, 'password'); expect(testInputElement.getAttribute('autocomplete'), BrowserAutofillHints.instance.flutterToEngine(testPasswordHint)); @@ -1756,7 +2033,7 @@ MethodCall configureSetSizeAndTransformMethodCall( /// Will disable editing element which will also clean the backup DOM /// element from the page. void cleanTextEditingElement() { - if (editingElement.isEnabled) { + if (editingElement != null && editingElement.isEnabled) { // Clean up all the DOM elements and event listeners. editingElement.disable(); } @@ -1867,3 +2144,11 @@ Map createOneFieldValue(String hint, String uniqueId) => 'textCapitalization': 'TextCapitalization.none', 'autofill': createAutofillInfo(hint, uniqueId) }; + +/// In order to not leak test state, clean up the forms from dom if any remains. +void clearForms() { + while (document.getElementsByTagName('form').length > 0) { + document.getElementsByTagName('form').last.remove(); + } + formsOnTheDom.clear(); +} diff --git a/lib/web_ui/test/text_test.dart b/lib/web_ui/test/text_test.dart index 266d88d1eecae..35861b2eda6c0 100644 --- a/lib/web_ui/test/text_test.dart +++ b/lib/web_ui/test/text_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:html'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; @@ -12,7 +13,11 @@ import 'package:ui/src/engine.dart'; import 'matchers.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double baselineRatio = 1.1662499904632568; await webOnlyInitializeTestDomRenderer(); diff --git a/lib/web_ui/test/title_test.dart b/lib/web_ui/test/title_test.dart index c076e2acdd138..af366760e49b9 100644 --- a/lib/web_ui/test/title_test.dart +++ b/lib/web_ui/test/title_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:html'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { const MethodCodec codec = JSONMethodCodec(); group('Title', () { diff --git a/lib/web_ui/test/window_test.dart b/lib/web_ui/test/window_test.dart index 8924a06798d5f..44894d8bc8dfe 100644 --- a/lib/web_ui/test/window_test.dart +++ b/lib/web_ui/test/window_test.dart @@ -8,6 +8,7 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -16,6 +17,10 @@ const MethodCodec codec = JSONMethodCodec(); void emptyCallback(ByteData date) {} void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('window.defaultRouteName should not change', () { window.locationStrategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')); expect(window.defaultRouteName, '/initial'); diff --git a/runtime/BUILD.gn b/runtime/BUILD.gn index 9bd85d53d581f..58ddf06941a6d 100644 --- a/runtime/BUILD.gn +++ b/runtime/BUILD.gn @@ -35,7 +35,7 @@ group("libdart") { } } -source_set_maybe_fuchsia_legacy("runtime") { +source_set("runtime") { sources = [ "dart_isolate.cc", "dart_isolate.h", @@ -53,6 +53,8 @@ source_set_maybe_fuchsia_legacy("runtime") { "dart_vm_lifecycle.h", "embedder_resources.cc", "embedder_resources.h", + "platform_data.cc", + "platform_data.h", "ptrace_ios.cc", "ptrace_ios.h", "runtime_controller.cc", @@ -63,8 +65,6 @@ source_set_maybe_fuchsia_legacy("runtime") { "service_protocol.h", "skia_concurrent_executor.cc", "skia_concurrent_executor.h", - "window_data.cc", - "window_data.h", ] public_deps = [ "//third_party/rapidjson" ] @@ -75,8 +75,10 @@ source_set_maybe_fuchsia_legacy("runtime") { ":test_font", "//flutter/assets", "//flutter/common", + "//flutter/flow", "//flutter/fml", "//flutter/lib/io", + "//flutter/lib/ui", "//flutter/third_party/tonic", "//flutter/third_party/txt", "//third_party/dart/runtime:dart_api", @@ -91,11 +93,6 @@ source_set_maybe_fuchsia_legacy("runtime") { "//third_party/dart/runtime/observatory:embedded_observatory_archive", ] } - - deps_legacy_and_next = [ - "//flutter/flow:flow", - "//flutter/lib/ui:ui", - ] } if (enable_unittests) { @@ -103,7 +100,7 @@ if (enable_unittests) { dart_main = "fixtures/runtime_test.dart" } - source_set_maybe_fuchsia_legacy("runtime_unittests_common") { + executable("runtime_unittests") { testonly = true sources = [ @@ -117,40 +114,17 @@ if (enable_unittests) { public_deps = [ ":libdart", + ":runtime", ":runtime_fixtures", "//flutter/common", "//flutter/fml", "//flutter/lib/snapshot", "//flutter/testing", + "//flutter/testing:dart", + "//flutter/testing:fixture_test", "//flutter/third_party/tonic", "//third_party/dart/runtime/bin:elf_loader", "//third_party/skia", ] - - deps_legacy_and_next = [ - ":runtime", - "//flutter/testing:dart", - "//flutter/testing:fixture_test", - ] - } - - if (is_fuchsia) { - executable("runtime_unittests") { - testonly = true - - deps = [ ":runtime_unittests_common_fuchsia_legacy" ] - } - - executable("runtime_unittests_next") { - testonly = true - - deps = [ ":runtime_unittests_common" ] - } - } else { - executable("runtime_unittests") { - testonly = true - - deps = [ ":runtime_unittests_common" ] - } } } diff --git a/runtime/dart_isolate.cc b/runtime/dart_isolate.cc index 53459a9415b75..06ef693b4b46b 100644 --- a/runtime/dart_isolate.cc +++ b/runtime/dart_isolate.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/runtime/dart_isolate.h" @@ -44,7 +43,7 @@ class DartErrorString { } char** error() { return &str_; } const char* str() const { return str_; } - operator bool() const { return str_ != nullptr; } + explicit operator bool() const { return str_ != nullptr; } private: FML_DISALLOW_COPY_AND_ASSIGN(DartErrorString); @@ -57,7 +56,7 @@ std::weak_ptr DartIsolate::CreateRootIsolate( const Settings& settings, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - std::unique_ptr window, + std::unique_ptr platform_configuration, fml::WeakPtr snapshot_delegate, fml::WeakPtr io_manager, fml::RefPtr unref_queue, @@ -112,7 +111,8 @@ std::weak_ptr DartIsolate::CreateRootIsolate( std::shared_ptr* root_isolate_data = static_cast*>(Dart_IsolateData(vm_isolate)); - (*root_isolate_data)->SetWindow(std::move(window)); + (*root_isolate_data) + ->SetPlatformConfiguration(std::move(platform_configuration)); return (*root_isolate_data)->GetWeakIsolatePtr(); } @@ -139,7 +139,9 @@ DartIsolate::DartIsolate(const Settings& settings, settings.unhandled_exception_callback, DartVMRef::GetIsolateNameServer(), is_root_isolate), - disable_http_(settings.disable_http) { + may_insecurely_connect_to_all_domains_( + settings.may_insecurely_connect_to_all_domains), + domain_network_policy_(settings.domain_network_policy) { phase_ = Phase::Uninitialized; } @@ -263,7 +265,8 @@ bool DartIsolate::LoadLibraries() { tonic::DartState::Scope scope(this); - DartIO::InitForIsolate(disable_http_); + DartIO::InitForIsolate(may_insecurely_connect_to_all_domains_, + domain_network_policy_); DartUI::InitForIsolate(); @@ -384,7 +387,7 @@ bool DartIsolate::LoadKernel(std::shared_ptr mapping, if (GetIsolateGroupData().GetChildIsolatePreparer() == nullptr) { GetIsolateGroupData().SetChildIsolatePreparer( [buffers = kernel_buffers_](DartIsolate* isolate) { - for (unsigned long i = 0; i < buffers.size(); i++) { + for (uint64_t i = 0; i < buffers.size(); i++) { bool last_piece = i + 1 == buffers.size(); const std::shared_ptr& buffer = buffers.at(i); if (!isolate->PrepareForRunningFromKernel(buffer, last_piece)) { @@ -598,7 +601,7 @@ Dart_Isolate DartIsolate::DartCreateAndStartServiceIsolate( vm_data->GetSettings(), // settings vm_data->GetIsolateSnapshot(), // isolate snapshot null_task_runners, // task runners - nullptr, // window + nullptr, // platform_configuration {}, // snapshot delegate {}, // IO Manager {}, // Skia unref queue @@ -675,6 +678,10 @@ Dart_Isolate DartIsolate::DartIsolateGroupCreateCallback( ); } + if (!parent_isolate_data) { + return nullptr; + } + DartIsolateGroupData& parent_group_data = (*parent_isolate_data)->GetIsolateGroupData(); @@ -689,7 +696,8 @@ Dart_Isolate DartIsolate::DartIsolateGroupCreateCallback( parent_group_data.GetIsolateShutdownCallback()))); TaskRunners null_task_runners(advisory_script_uri, - /* platform= */ nullptr, /* raster= */ nullptr, + /* platform= */ nullptr, + /* raster= */ nullptr, /* ui= */ nullptr, /* io= */ nullptr); @@ -731,7 +739,8 @@ bool DartIsolate::DartIsolateInitializeCallback(void** child_callback_data, Dart_CurrentIsolateGroupData()); TaskRunners null_task_runners((*isolate_group_data)->GetAdvisoryScriptURI(), - /* platform= */ nullptr, /* raster= */ nullptr, + /* platform= */ nullptr, + /* raster= */ nullptr, /* ui= */ nullptr, /* io= */ nullptr); diff --git a/runtime/dart_isolate.h b/runtime/dart_isolate.h index 7f59aa7dc28d0..d3862d577ba73 100644 --- a/runtime/dart_isolate.h +++ b/runtime/dart_isolate.h @@ -16,7 +16,7 @@ #include "flutter/lib/ui/io_manager.h" #include "flutter/lib/ui/snapshot_delegate.h" #include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/runtime/dart_snapshot.h" #include "third_party/dart/runtime/include/dart_api.h" #include "third_party/tonic/dart_state.h" @@ -192,7 +192,7 @@ class DartIsolate : public UIDartState { const Settings& settings, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - std::unique_ptr window, + std::unique_ptr platform_configuration, fml::WeakPtr snapshot_delegate, fml::WeakPtr io_manager, fml::RefPtr skia_unref_queue, @@ -398,7 +398,8 @@ class DartIsolate : public UIDartState { std::vector> kernel_buffers_; std::vector> shutdown_callbacks_; fml::RefPtr message_handling_task_runner_; - const bool disable_http_; + const bool may_insecurely_connect_to_all_domains_; + std::string domain_network_policy_; DartIsolate(const Settings& settings, TaskRunners task_runners, diff --git a/runtime/dart_isolate_unittests.cc b/runtime/dart_isolate_unittests.cc index e751100f071b2..0e9e4cf59521c 100644 --- a/runtime/dart_isolate_unittests.cc +++ b/runtime/dart_isolate_unittests.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/fml/mapping.h" #include "flutter/fml/synchronization/count_down_latch.h" @@ -19,7 +18,19 @@ namespace flutter { namespace testing { -using DartIsolateTest = FixtureTest; +class DartIsolateTest : public FixtureTest { + public: + DartIsolateTest() {} + + void Wait() { latch_.Wait(); } + + void Signal() { latch_.Signal(); } + + private: + fml::AutoResetWaitableEvent latch_; + + FML_DISALLOW_COPY_AND_ASSIGN(DartIsolateTest); +}; TEST_F(DartIsolateTest, RootIsolateCreationAndShutdown) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); @@ -153,11 +164,10 @@ TEST_F(DartIsolateTest, CanRunDartCodeCodeSynchronously) { TEST_F(DartIsolateTest, CanRegisterNativeCallback) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); - fml::AutoResetWaitableEvent latch; AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { FML_LOG(ERROR) << "Hello from Dart!"; - latch.Signal(); + Signal(); }))); const auto settings = CreateSettingsForFixture(); auto vm_ref = DartVMRef::Create(settings); @@ -173,7 +183,7 @@ TEST_F(DartIsolateTest, CanRegisterNativeCallback) { "canRegisterNativeCallback", {}, GetFixturesPath()); ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - latch.Wait(); + Wait(); } TEST_F(DartIsolateTest, CanSaveCompilationTrace) { @@ -182,12 +192,11 @@ TEST_F(DartIsolateTest, CanSaveCompilationTrace) { GTEST_SKIP(); return; } - fml::AutoResetWaitableEvent latch; AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { ASSERT_TRUE(tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 0))); - latch.Signal(); + Signal(); }))); const auto settings = CreateSettingsForFixture(); @@ -205,31 +214,52 @@ TEST_F(DartIsolateTest, CanSaveCompilationTrace) { ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - latch.Wait(); + Wait(); } -TEST_F(DartIsolateTest, CanLaunchSecondaryIsolates) { - fml::CountDownLatch latch(3); - fml::AutoResetWaitableEvent child_shutdown_latch; - fml::AutoResetWaitableEvent root_isolate_shutdown_latch; +class DartSecondaryIsolateTest : public FixtureTest { + public: + DartSecondaryIsolateTest() : latch_(3) {} + + void LatchCountDown() { latch_.CountDown(); } + + void LatchWait() { latch_.Wait(); } + + void ChildShutdownSignal() { child_shutdown_latch_.Signal(); } + + void ChildShutdownWait() { child_shutdown_latch_.Wait(); } + + void RootIsolateShutdownSignal() { root_isolate_shutdown_latch_.Signal(); } + + bool RootIsolateIsSignaled() { + return root_isolate_shutdown_latch_.IsSignaledForTest(); + } + + private: + fml::CountDownLatch latch_; + fml::AutoResetWaitableEvent child_shutdown_latch_; + fml::AutoResetWaitableEvent root_isolate_shutdown_latch_; + + FML_DISALLOW_COPY_AND_ASSIGN(DartSecondaryIsolateTest); +}; + +TEST_F(DartSecondaryIsolateTest, CanLaunchSecondaryIsolates) { AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { - latch.CountDown(); + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { + LatchCountDown(); }))); AddNativeCallback( - "PassMessage", CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + "PassMessage", CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { auto message = tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 0)); ASSERT_EQ("Hello from code is secondary isolate.", message); - latch.CountDown(); + LatchCountDown(); }))); auto settings = CreateSettingsForFixture(); - settings.root_isolate_shutdown_callback = [&root_isolate_shutdown_latch]() { - root_isolate_shutdown_latch.Signal(); - }; - settings.isolate_shutdown_callback = [&child_shutdown_latch]() { - child_shutdown_latch.Signal(); + settings.root_isolate_shutdown_callback = [this]() { + RootIsolateShutdownSignal(); }; + settings.isolate_shutdown_callback = [this]() { ChildShutdownSignal(); }; auto vm_ref = DartVMRef::Create(settings); auto thread = CreateNewThread(); TaskRunners task_runners(GetCurrentTestName(), // @@ -243,19 +273,18 @@ TEST_F(DartIsolateTest, CanLaunchSecondaryIsolates) { GetFixturesPath()); ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - child_shutdown_latch.Wait(); // wait for child isolate to shutdown first - ASSERT_FALSE(root_isolate_shutdown_latch.IsSignaledForTest()); - latch.Wait(); // wait for last NotifyNative called by main isolate + ChildShutdownWait(); // wait for child isolate to shutdown first + ASSERT_FALSE(RootIsolateIsSignaled()); + LatchWait(); // wait for last NotifyNative called by main isolate // root isolate will be auto-shutdown } TEST_F(DartIsolateTest, CanRecieveArguments) { - fml::AutoResetWaitableEvent latch; AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { ASSERT_TRUE(tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 0))); - latch.Signal(); + Signal(); }))); const auto settings = CreateSettingsForFixture(); @@ -273,7 +302,7 @@ TEST_F(DartIsolateTest, CanRecieveArguments) { ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - latch.Wait(); + Wait(); } } // namespace testing diff --git a/runtime/dart_service_isolate.cc b/runtime/dart_service_isolate.cc index 9ad95767234da..9c1bbce2b016f 100644 --- a/runtime/dart_service_isolate.cc +++ b/runtime/dart_service_isolate.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/runtime/dart_service_isolate.h" @@ -202,6 +201,7 @@ bool DartServiceIsolate::Startup(std::string server_ip, result = Dart_SetField( library, Dart_NewStringFromCString("_enableServicePortFallback"), Dart_NewBoolean(enable_service_port_fallback)); + SHUTDOWN_ON_ERROR(result); return true; } diff --git a/runtime/dart_vm.cc b/runtime/dart_vm.cc index 47d2149dc4790..1824ea709ce97 100644 --- a/runtime/dart_vm.cc +++ b/runtime/dart_vm.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/runtime/dart_vm.h" @@ -129,13 +128,15 @@ bool DartFileModifiedCallback(const char* source_url, int64_t since_ms) { const char* path = source_url + kFileUriPrefixLength; struct stat info; - if (stat(path, &info) < 0) + if (stat(path, &info) < 0) { return true; + } // If st_mtime is zero, it's more likely that the file system doesn't support // mtime than that the file was actually modified in the 1970s. - if (!info.st_mtime) + if (!info.st_mtime) { return true; + } // It's very unclear what time bases we're with here. The Dart API doesn't // document the time base for since_ms. Reading the code, the value varies by @@ -383,8 +384,9 @@ DartVM::DartVM(std::shared_ptr vm_data, PushBackAll(&args, kDartTraceStreamsArgs, fml::size(kDartTraceStreamsArgs)); #endif - for (size_t i = 0; i < settings_.dart_flags.size(); i++) + for (size_t i = 0; i < settings_.dart_flags.size(); i++) { args.push_back(settings_.dart_flags[i].c_str()); + } char* flags_error = Dart_SetVMFlags(args.size(), args.data()); if (flags_error) { diff --git a/runtime/window_data.cc b/runtime/platform_data.cc similarity index 63% rename from runtime/window_data.cc rename to runtime/platform_data.cc index a92839d5e8a5d..15b9628599b47 100644 --- a/runtime/window_data.cc +++ b/runtime/platform_data.cc @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" namespace flutter { -WindowData::WindowData() = default; +PlatformData::PlatformData() = default; -WindowData::~WindowData() = default; +PlatformData::~PlatformData() = default; } // namespace flutter diff --git a/runtime/window_data.h b/runtime/platform_data.h similarity index 81% rename from runtime/window_data.h rename to runtime/platform_data.h index e234d2f558162..bb7fdd95fb6bb 100644 --- a/runtime/window_data.h +++ b/runtime/platform_data.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_RUNTIME_WINDOW_DATA_H_ -#define FLUTTER_RUNTIME_WINDOW_DATA_H_ +#ifndef FLUTTER_RUNTIME_PLATFORM_DATA_H_ +#define FLUTTER_RUNTIME_PLATFORM_DATA_H_ #include "flutter/lib/ui/window/viewport_metrics.h" @@ -23,12 +23,12 @@ namespace flutter { /// /// See also: /// -/// * flutter::Shell::Create, which takes a window_data to initialize the +/// * flutter::Shell::Create, which takes a platform_data to initialize the /// ui.Window attached to it. -struct WindowData { - WindowData(); +struct PlatformData { + PlatformData(); - ~WindowData(); + ~PlatformData(); ViewportMetrics viewport_metrics; std::string language_code; @@ -45,4 +45,4 @@ struct WindowData { } // namespace flutter -#endif // FLUTTER_RUNTIME_WINDOW_DATA_H_ +#endif // FLUTTER_RUNTIME_PLATFORM_DATA_H_ diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index aa557d2abf716..9c81d6a759cdb 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -8,12 +8,18 @@ #include "flutter/fml/trace_event.h" #include "flutter/lib/ui/compositing/scene.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" +#include "flutter/lib/ui/window/viewport_metrics.h" #include "flutter/lib/ui/window/window.h" #include "flutter/runtime/runtime_delegate.h" #include "third_party/tonic/dart_message_handler.h" namespace flutter { +RuntimeController::RuntimeController(RuntimeDelegate& client, + TaskRunners p_task_runners) + : client_(client), vm_(nullptr), task_runners_(p_task_runners) {} + RuntimeController::RuntimeController( RuntimeDelegate& p_client, DartVM* p_vm, @@ -26,7 +32,7 @@ RuntimeController::RuntimeController( std::string p_advisory_script_uri, std::string p_advisory_script_entrypoint, const std::function& idle_notification_callback, - const WindowData& p_window_data, + const PlatformData& p_platform_data, const fml::closure& p_isolate_create_callback, const fml::closure& p_isolate_shutdown_callback, std::shared_ptr p_persistent_isolate_data) @@ -41,7 +47,7 @@ RuntimeController::RuntimeController( advisory_script_uri_(p_advisory_script_uri), advisory_script_entrypoint_(p_advisory_script_entrypoint), idle_notification_callback_(idle_notification_callback), - window_data_(std::move(p_window_data)), + platform_data_(std::move(p_platform_data)), isolate_create_callback_(p_isolate_create_callback), isolate_shutdown_callback_(p_isolate_shutdown_callback), persistent_isolate_data_(std::move(p_persistent_isolate_data)) { @@ -49,20 +55,21 @@ RuntimeController::RuntimeController( // It will be run at a later point when the engine provides a run // configuration and then runs the isolate. auto strong_root_isolate = - DartIsolate::CreateRootIsolate(vm_->GetVMData()->GetSettings(), // - isolate_snapshot_, // - task_runners_, // - std::make_unique(this), // - snapshot_delegate_, // - io_manager_, // - unref_queue_, // - image_decoder_, // - p_advisory_script_uri, // - p_advisory_script_entrypoint, // - nullptr, // - isolate_create_callback_, // - isolate_shutdown_callback_ // - ) + DartIsolate::CreateRootIsolate( + vm_->GetVMData()->GetSettings(), // + isolate_snapshot_, // + task_runners_, // + std::make_unique(this), // + snapshot_delegate_, // + io_manager_, // + unref_queue_, // + image_decoder_, // + p_advisory_script_uri, // + p_advisory_script_entrypoint, // + nullptr, // + isolate_create_callback_, // + isolate_shutdown_callback_ // + ) .lock(); FML_CHECK(strong_root_isolate) << "Could not create root isolate."; @@ -74,9 +81,9 @@ RuntimeController::RuntimeController( root_isolate_return_code_ = {true, code}; }); - if (auto* window = GetWindowIfAvailable()) { + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { tonic::DartState::Scope scope(strong_root_isolate); - window->DidCreateIsolate(); + platform_configuration->DidCreateIsolate(); if (!FlushRuntimeStateToIsolate()) { FML_DLOG(ERROR) << "Could not setup initial isolate state."; } @@ -121,7 +128,7 @@ std::unique_ptr RuntimeController::Clone() const { advisory_script_uri_, // advisory_script_entrypoint_, // idle_notification_callback_, // - window_data_, // + platform_data_, // isolate_create_callback_, // isolate_shutdown_callback_, // persistent_isolate_data_ // @@ -129,30 +136,32 @@ std::unique_ptr RuntimeController::Clone() const { } bool RuntimeController::FlushRuntimeStateToIsolate() { - return SetViewportMetrics(window_data_.viewport_metrics) && - SetLocales(window_data_.locale_data) && - SetSemanticsEnabled(window_data_.semantics_enabled) && - SetAccessibilityFeatures(window_data_.accessibility_feature_flags_) && - SetUserSettingsData(window_data_.user_settings_data) && - SetLifecycleState(window_data_.lifecycle_state); + return SetViewportMetrics(platform_data_.viewport_metrics) && + SetLocales(platform_data_.locale_data) && + SetSemanticsEnabled(platform_data_.semantics_enabled) && + SetAccessibilityFeatures( + platform_data_.accessibility_feature_flags_) && + SetUserSettingsData(platform_data_.user_settings_data) && + SetLifecycleState(platform_data_.lifecycle_state); } bool RuntimeController::SetViewportMetrics(const ViewportMetrics& metrics) { - window_data_.viewport_metrics = metrics; + platform_data_.viewport_metrics = metrics; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateWindowMetrics(metrics); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->window()->UpdateWindowMetrics(metrics); return true; } + return false; } bool RuntimeController::SetLocales( const std::vector& locale_data) { - window_data_.locale_data = locale_data; + platform_data_.locale_data = locale_data; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateLocales(locale_data); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateLocales(locale_data); return true; } @@ -160,10 +169,11 @@ bool RuntimeController::SetLocales( } bool RuntimeController::SetUserSettingsData(const std::string& data) { - window_data_.user_settings_data = data; + platform_data_.user_settings_data = data; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateUserSettingsData(window_data_.user_settings_data); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateUserSettingsData( + platform_data_.user_settings_data); return true; } @@ -171,10 +181,11 @@ bool RuntimeController::SetUserSettingsData(const std::string& data) { } bool RuntimeController::SetLifecycleState(const std::string& data) { - window_data_.lifecycle_state = data; + platform_data_.lifecycle_state = data; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateLifecycleState(window_data_.lifecycle_state); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateLifecycleState( + platform_data_.lifecycle_state); return true; } @@ -182,10 +193,11 @@ bool RuntimeController::SetLifecycleState(const std::string& data) { } bool RuntimeController::SetSemanticsEnabled(bool enabled) { - window_data_.semantics_enabled = enabled; + platform_data_.semantics_enabled = enabled; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateSemanticsEnabled(window_data_.semantics_enabled); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateSemanticsEnabled( + platform_data_.semantics_enabled); return true; } @@ -193,10 +205,10 @@ bool RuntimeController::SetSemanticsEnabled(bool enabled) { } bool RuntimeController::SetAccessibilityFeatures(int32_t flags) { - window_data_.accessibility_feature_flags_ = flags; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateAccessibilityFeatures( - window_data_.accessibility_feature_flags_); + platform_data_.accessibility_feature_flags_ = flags; + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateAccessibilityFeatures( + platform_data_.accessibility_feature_flags_); return true; } @@ -204,22 +216,24 @@ bool RuntimeController::SetAccessibilityFeatures(int32_t flags) { } bool RuntimeController::BeginFrame(fml::TimePoint frame_time) { - if (auto* window = GetWindowIfAvailable()) { - window->BeginFrame(frame_time); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->BeginFrame(frame_time); return true; } + return false; } bool RuntimeController::ReportTimings(std::vector timings) { - if (auto* window = GetWindowIfAvailable()) { - window->ReportTimings(std::move(timings)); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->ReportTimings(std::move(timings)); return true; } + return false; } -bool RuntimeController::NotifyIdle(int64_t deadline) { +bool RuntimeController::NotifyIdle(int64_t deadline, size_t freed_hint) { std::shared_ptr root_isolate = root_isolate_.lock(); if (!root_isolate) { return false; @@ -227,6 +241,9 @@ bool RuntimeController::NotifyIdle(int64_t deadline) { tonic::DartState::Scope scope(root_isolate); + // Dart will use the freed hint at the next idle notification. Make sure to + // Update it with our latest value before calling NotifyIdle. + Dart_HintFreed(freed_hint); Dart_NotifyIdle(deadline); // Idle notifications being in isolate scope are part of the contract. @@ -239,23 +256,25 @@ bool RuntimeController::NotifyIdle(int64_t deadline) { bool RuntimeController::DispatchPlatformMessage( fml::RefPtr message) { - if (auto* window = GetWindowIfAvailable()) { + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { TRACE_EVENT1("flutter", "RuntimeController::DispatchPlatformMessage", "mode", "basic"); - window->DispatchPlatformMessage(std::move(message)); + platform_configuration->DispatchPlatformMessage(std::move(message)); return true; } + return false; } bool RuntimeController::DispatchPointerDataPacket( const PointerDataPacket& packet) { - if (auto* window = GetWindowIfAvailable()) { + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { TRACE_EVENT1("flutter", "RuntimeController::DispatchPointerDataPacket", "mode", "basic"); - window->DispatchPointerDataPacket(packet); + platform_configuration->window()->DispatchPointerDataPacket(packet); return true; } + return false; } @@ -264,69 +283,72 @@ bool RuntimeController::DispatchSemanticsAction(int32_t id, std::vector args) { TRACE_EVENT1("flutter", "RuntimeController::DispatchSemanticsAction", "mode", "basic"); - if (auto* window = GetWindowIfAvailable()) { - window->DispatchSemanticsAction(id, action, std::move(args)); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->DispatchSemanticsAction(id, action, + std::move(args)); return true; } + return false; } -Window* RuntimeController::GetWindowIfAvailable() { +PlatformConfiguration* +RuntimeController::GetPlatformConfigurationIfAvailable() { std::shared_ptr root_isolate = root_isolate_.lock(); - return root_isolate ? root_isolate->window() : nullptr; + return root_isolate ? root_isolate->platform_configuration() : nullptr; } -// |WindowClient| +// |PlatformConfigurationClient| std::string RuntimeController::DefaultRouteName() { return client_.DefaultRouteName(); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::ScheduleFrame() { client_.ScheduleFrame(); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::Render(Scene* scene) { client_.Render(scene->takeLayerTree()); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::UpdateSemantics(SemanticsUpdate* update) { - if (window_data_.semantics_enabled) { + if (platform_data_.semantics_enabled) { client_.UpdateSemantics(update->takeNodes(), update->takeActions()); } } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::HandlePlatformMessage( fml::RefPtr message) { client_.HandlePlatformMessage(std::move(message)); } -// |WindowClient| +// |PlatformConfigurationClient| FontCollection& RuntimeController::GetFontCollection() { return client_.GetFontCollection(); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) { client_.UpdateIsolateDescription(isolate_name, isolate_port); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::SetNeedsReportTimings(bool value) { client_.SetNeedsReportTimings(value); } -// |WindowClient| +// |PlatformConfigurationClient| std::shared_ptr RuntimeController::GetPersistentIsolateData() { return persistent_isolate_data_; } -// |WindowClient| +// |PlatformConfigurationClient| std::unique_ptr> RuntimeController::ComputePlatformResolvedLocale( const std::vector& supported_locale_data) { diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index ad89cbeae064b..e1b29f127a79f 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -14,10 +14,10 @@ #include "flutter/lib/ui/io_manager.h" #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/pointer_data_packet.h" -#include "flutter/lib/ui/window/window.h" #include "flutter/runtime/dart_vm.h" -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" #include "rapidjson/document.h" #include "rapidjson/stringbuffer.h" @@ -38,7 +38,7 @@ class Window; /// used by the engine to copy the currently accumulated window state so it can /// be referenced by the new runtime controller. /// -class RuntimeController final : public WindowClient { +class RuntimeController : public PlatformConfigurationClient { public: //---------------------------------------------------------------------------- /// @brief Creates a new instance of a runtime controller. This is @@ -90,7 +90,7 @@ class RuntimeController final : public WindowClient { /// code in isolate scope when the VM /// is about to be notified that the /// engine is going to be idle. - /// @param[in] window_data The window data (if exists). + /// @param[in] platform_data The window data (if exists). /// @param[in] isolate_create_callback The isolate create callback. This /// allows callers to run native code /// in isolate scope on the UI task @@ -117,12 +117,12 @@ class RuntimeController final : public WindowClient { std::string advisory_script_uri, std::string advisory_script_entrypoint, const std::function& idle_notification_callback, - const WindowData& window_data, + const PlatformData& platform_data, const fml::closure& isolate_create_callback, const fml::closure& isolate_shutdown_callback, std::shared_ptr persistent_isolate_data); - // |WindowClient| + // |PlatformConfigurationClient| ~RuntimeController() override; //---------------------------------------------------------------------------- @@ -136,11 +136,11 @@ class RuntimeController final : public WindowClient { std::unique_ptr Clone() const; //---------------------------------------------------------------------------- - /// @brief Forward the specified window metrics to the running isolate. + /// @brief Forward the specified viewport metrics to the running isolate. /// If the isolate is not running, these metrics will be saved and /// flushed to the isolate when it starts. /// - /// @param[in] metrics The metrics. + /// @param[in] metrics The viewport metrics. /// /// @return If the window metrics were forwarded to the running isolate. /// @@ -329,9 +329,12 @@ class RuntimeController final : public WindowClient { /// system's monotonic time. The clock can be accessed via /// `Dart_TimelineGetMicros`. /// + /// @param[in] freed_hint A hint of the number of bytes potentially freed + /// since the last call to NotifyIdle if a GC were run. + /// /// @return If the idle notification was forwarded to the running isolate. /// - bool NotifyIdle(int64_t deadline); + bool NotifyIdle(int64_t deadline, size_t freed_hint); //---------------------------------------------------------------------------- /// @brief Returns if the root isolate is running. The isolate must be @@ -340,7 +343,7 @@ class RuntimeController final : public WindowClient { /// /// @return True if root isolate running, False otherwise. /// - bool IsRootIsolateRunning() const; + virtual bool IsRootIsolateRunning() const; //---------------------------------------------------------------------------- /// @brief Dispatch the specified platform message to running root @@ -351,7 +354,7 @@ class RuntimeController final : public WindowClient { /// @return If the message was dispatched to the running root isolate. /// This may fail is an isolate is not running. /// - bool DispatchPlatformMessage(fml::RefPtr message); + virtual bool DispatchPlatformMessage(fml::RefPtr message); //---------------------------------------------------------------------------- /// @brief Dispatch the specified pointer data message to the running @@ -440,6 +443,10 @@ class RuntimeController final : public WindowClient { /// std::pair GetRootIsolateReturnCode(); + protected: + /// Constructor for Mocks. + RuntimeController(RuntimeDelegate& client, TaskRunners p_task_runners); + private: struct Locale { Locale(std::string language_code_, @@ -466,46 +473,46 @@ class RuntimeController final : public WindowClient { std::string advisory_script_uri_; std::string advisory_script_entrypoint_; std::function idle_notification_callback_; - WindowData window_data_; + PlatformData platform_data_; std::weak_ptr root_isolate_; std::pair root_isolate_return_code_ = {false, 0}; const fml::closure isolate_create_callback_; const fml::closure isolate_shutdown_callback_; std::shared_ptr persistent_isolate_data_; - Window* GetWindowIfAvailable(); + PlatformConfiguration* GetPlatformConfigurationIfAvailable(); bool FlushRuntimeStateToIsolate(); - // |WindowClient| + // |PlatformConfigurationClient| std::string DefaultRouteName() override; - // |WindowClient| + // |PlatformConfigurationClient| void ScheduleFrame() override; - // |WindowClient| + // |PlatformConfigurationClient| void Render(Scene* scene) override; - // |WindowClient| + // |PlatformConfigurationClient| void UpdateSemantics(SemanticsUpdate* update) override; - // |WindowClient| + // |PlatformConfigurationClient| void HandlePlatformMessage(fml::RefPtr message) override; - // |WindowClient| + // |PlatformConfigurationClient| FontCollection& GetFontCollection() override; - // |WindowClient| + // |PlatformConfigurationClient| void UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) override; - // |WindowClient| + // |PlatformConfigurationClient| void SetNeedsReportTimings(bool value) override; - // |WindowClient| + // |PlatformConfigurationClient| std::shared_ptr GetPersistentIsolateData() override; - // |WindowClient| + // |PlatformConfigurationClient| std::unique_ptr> ComputePlatformResolvedLocale( const std::vector& supported_locale_data) override; diff --git a/runtime/service_protocol.cc b/runtime/service_protocol.cc index 331e25e03fc73..ace3039f2f959 100644 --- a/runtime/service_protocol.cc +++ b/runtime/service_protocol.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #define RAPIDJSON_HAS_STDSTRING 1 @@ -36,6 +35,9 @@ const std::string_view ServiceProtocol::kGetDisplayRefreshRateExtensionName = "_flutter.getDisplayRefreshRate"; const std::string_view ServiceProtocol::kGetSkSLsExtensionName = "_flutter.getSkSLs"; +const std::string_view + ServiceProtocol::kEstimateRasterCacheMemoryExtensionName = + "_flutter.estimateRasterCacheMemory"; static constexpr std::string_view kViewIdPrefx = "_flutterView/"; static constexpr std::string_view kListViewsExtensionName = @@ -54,6 +56,7 @@ ServiceProtocol::ServiceProtocol() kSetAssetBundlePathExtensionName, kGetDisplayRefreshRateExtensionName, kGetSkSLsExtensionName, + kEstimateRasterCacheMemoryExtensionName, }), handlers_mutex_(fml::SharedMutex::Create()) {} @@ -76,8 +79,9 @@ void ServiceProtocol::SetHandlerDescription(Handler* handler, Handler::Description description) { fml::SharedLock lock(*handlers_mutex_); auto it = handlers_.find(handler); - if (it != handlers_.end()) + if (it != handlers_.end()) { it->second.Store(description); + } } void ServiceProtocol::ToggleHooks(bool set) { @@ -90,13 +94,13 @@ void ServiceProtocol::ToggleHooks(bool set) { } } -static void WriteServerErrorResponse(rapidjson::Document& document, +static void WriteServerErrorResponse(rapidjson::Document* document, const char* message) { - document.SetObject(); - document.AddMember("code", -32000, document.GetAllocator()); + document->SetObject(); + document->AddMember("code", -32000, document->GetAllocator()); rapidjson::Value message_value; - message_value.SetString(message, document.GetAllocator()); - document.AddMember("message", message_value, document.GetAllocator()); + message_value.SetString(message, document->GetAllocator()); + document->AddMember("message", message_value, document->GetAllocator()); } bool ServiceProtocol::HandleMessage(const char* method, @@ -123,7 +127,7 @@ bool ServiceProtocol::HandleMessage(const char* method, bool result = HandleMessage(std::string_view{method}, // params, // static_cast(user_data), // - document // + &document // ); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); @@ -141,7 +145,7 @@ bool ServiceProtocol::HandleMessage(const char* method, bool ServiceProtocol::HandleMessage(std::string_view method, const Handler::ServiceProtocolMap& params, ServiceProtocol* service_protocol, - rapidjson::Document& response) { + rapidjson::Document* response) { if (service_protocol == nullptr) { WriteServerErrorResponse(response, "Service protocol unavailable."); return false; @@ -154,7 +158,7 @@ bool ServiceProtocol::HandleMessage(std::string_view method, ServiceProtocol::Handler* handler, std::string_view method, const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& document) { + rapidjson::Document* document) { FML_DCHECK(handler); fml::AutoResetWaitableEvent latch; bool result = false; @@ -177,7 +181,7 @@ bool ServiceProtocol::HandleMessage(std::string_view method, bool ServiceProtocol::HandleMessage(std::string_view method, const Handler::ServiceProtocolMap& params, - rapidjson::Document& response) const { + rapidjson::Document* response) const { if (method == kListViewsExtensionName) { // So far, this is the only built-in method that does not forward to the // dynamic set of handlers. @@ -254,7 +258,7 @@ void ServiceProtocol::Handler::Description::Write( } bool ServiceProtocol::HandleListViewsMethod( - rapidjson::Document& response) const { + rapidjson::Document* response) const { fml::SharedLock lock(*handlers_mutex_); std::vector> descriptions; for (const auto& handler : handlers_) { @@ -262,11 +266,11 @@ bool ServiceProtocol::HandleListViewsMethod( handler.second.Load()); } - auto& allocator = response.GetAllocator(); + auto& allocator = response->GetAllocator(); // Construct the response objects. - response.SetObject(); - response.AddMember("type", "FlutterViewList", allocator); + response->SetObject(); + response->AddMember("type", "FlutterViewList", allocator); rapidjson::Value viewsList(rapidjson::Type::kArrayType); for (const auto& description : descriptions) { @@ -276,7 +280,7 @@ bool ServiceProtocol::HandleListViewsMethod( viewsList.PushBack(view, allocator); } - response.AddMember("views", viewsList, allocator); + response->AddMember("views", viewsList, allocator); return true; } diff --git a/runtime/service_protocol.h b/runtime/service_protocol.h index c66a836a2c52e..e809b7d833fc4 100644 --- a/runtime/service_protocol.h +++ b/runtime/service_protocol.h @@ -28,6 +28,7 @@ class ServiceProtocol { static const std::string_view kSetAssetBundlePathExtensionName; static const std::string_view kGetDisplayRefreshRateExtensionName; static const std::string_view kGetSkSLsExtensionName; + static const std::string_view kEstimateRasterCacheMemoryExtensionName; class Handler { public: @@ -56,7 +57,7 @@ class ServiceProtocol { virtual bool HandleServiceProtocolMessage( std::string_view method, // one if the extension names specified above. const ServiceProtocolMap& params, - rapidjson::Document& response) = 0; + rapidjson::Document* response) = 0; }; ServiceProtocol(); @@ -87,12 +88,12 @@ class ServiceProtocol { std::string_view method, const Handler::ServiceProtocolMap& params, ServiceProtocol* service_protocol, - rapidjson::Document& response); + rapidjson::Document* response); [[nodiscard]] bool HandleMessage(std::string_view method, const Handler::ServiceProtocolMap& params, - rapidjson::Document& response) const; + rapidjson::Document* response) const; - [[nodiscard]] bool HandleListViewsMethod(rapidjson::Document& response) const; + [[nodiscard]] bool HandleListViewsMethod(rapidjson::Document* response) const; FML_DISALLOW_COPY_AND_ASSIGN(ServiceProtocol); }; diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index 28ca87584f6db..62277b8b35e7c 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -58,7 +58,7 @@ template("dart_embedder_resources") { } } -source_set_maybe_fuchsia_legacy("common") { +source_set("common") { sources = [ "animator.cc", "animator.h", @@ -110,27 +110,17 @@ source_set_maybe_fuchsia_legacy("common") { deps = [ "//flutter/assets", "//flutter/common", + "//flutter/flow", "//flutter/fml", + "//flutter/lib/ui", + "//flutter/runtime", "//flutter/shell/profiling", "//third_party/dart/runtime:dart_api", "//third_party/skia", ] - - deps_legacy_and_next = [ - "//flutter/flow:flow", - "//flutter/lib/ui:ui", - "//flutter/runtime:runtime", - ] } template("shell_host_executable") { - common_dep = ":common" - if (defined(invoker.fuchsia_legacy)) { - if (invoker.fuchsia_legacy) { - common_dep += "_fuchsia_legacy" - } - } - executable(target_name) { testonly = true @@ -141,9 +131,9 @@ template("shell_host_executable") { forward_variables_from(invoker, "*") deps += [ + ":common", "//flutter/lib/snapshot", "//flutter/runtime:libdart", - common_dep, ] public_configs = [ "//flutter:export_dynamic_symbols" ] @@ -158,6 +148,14 @@ if (enable_unittests) { test_enable_metal = false } + config("test_enable_gl_config") { + defines = [ "SHELL_ENABLE_GL" ] + } + + config("test_enable_vulkan_config") { + defines = [ "SHELL_ENABLE_VULKAN" ] + } + shell_gpu_configuration("shell_unittests_gpu_configuration") { enable_software = test_enable_software enable_vulkan = test_enable_vulkan @@ -177,12 +175,13 @@ if (enable_unittests) { deps = [ ":shell_unittests_fixtures", "//flutter/benchmarking", + "//flutter/flow", "//flutter/testing:dart", "//flutter/testing:testing_lib", ] } - source_set_maybe_fuchsia_legacy("shell_test_fixture_sources") { + source_set("shell_test_fixture_sources") { testonly = true sources = [ @@ -197,18 +196,25 @@ if (enable_unittests) { ] public_deps = [ + "//flutter/flow", "//flutter/fml/dart", + "//flutter/runtime", + "//flutter/shell/common", "//flutter/testing", ] deps = [ + ":shell_unittests_gpu_configuration", "//flutter/assets", "//flutter/common", + "//flutter/lib/ui", + "//flutter/testing:dart", + "//flutter/testing:fixture_test", "//third_party/rapidjson", "//third_party/skia", ] - defines = [] + public_configs = [] # SwiftShader only supports x86/x64_64 if (target_cpu == "x86" || target_cpu == "x64") { @@ -218,9 +224,9 @@ if (enable_unittests) { "shell_test_platform_view_gl.h", ] - public_deps += [ "//flutter/testing:opengl" ] + public_configs += [ ":test_enable_gl_config" ] - defines += [ "SHELL_ENABLE_GL" ] + public_deps += [ "//flutter/testing:opengl" ] } } @@ -230,34 +236,22 @@ if (enable_unittests) { "shell_test_platform_view_vulkan.h", ] + public_configs += [ ":test_enable_vulkan_config" ] + public_deps += [ "//flutter/testing:vulkan", "//flutter/vulkan", ] - - defines += [ "SHELL_ENABLE_VULKAN" ] } - - public_deps_legacy_and_next = [ - "//flutter/shell/common:common", - "//flutter/flow:flow", - "//flutter/runtime:runtime", - ] - - deps_legacy_and_next = [ - ":shell_unittests_gpu_configuration", - "//flutter/lib/ui:ui", - "//flutter/testing:dart", - "//flutter/testing:fixture_test", - ] } - source_set_maybe_fuchsia_legacy("shell_unittests_common") { + shell_host_executable("shell_unittests") { testonly = true sources = [ "animator_unittests.cc", "canvas_spy_unittests.cc", + "engine_unittests.cc", "input_events_unittests.cc", "persistent_cache_unittests.cc", "pipeline_unittests.cc", @@ -266,35 +260,11 @@ if (enable_unittests) { ] deps = [ + ":shell_test_fixture_sources", + ":shell_unittests_fixtures", "//flutter/assets", "//flutter/shell/version", + "//third_party/googletest:gmock", ] - - public_deps_legacy_and_next = [ ":shell_test_fixture_sources" ] - } - - if (is_fuchsia) { - shell_host_executable("shell_unittests") { - deps = [ - ":shell_unittests_common_fuchsia_legacy", - ":shell_unittests_fixtures", - ] - - fuchsia_legacy = true - } - - shell_host_executable("shell_unittests_next") { - deps = [ - ":shell_unittests_common", - ":shell_unittests_fixtures", - ] - } - } else { - shell_host_executable("shell_unittests") { - deps = [ - ":shell_unittests_common", - ":shell_unittests_fixtures", - ] - } } } diff --git a/shell/common/animator.cc b/shell/common/animator.cc index 7e581c07ca6e5..3ca1a83788773 100644 --- a/shell/common/animator.cc +++ b/shell/common/animator.cc @@ -26,6 +26,7 @@ Animator::Animator(Delegate& delegate, task_runners_(std::move(task_runners)), waiter_(std::move(waiter)), last_frame_begin_time_(), + last_vsync_start_time_(), last_frame_target_time_(), dart_frame_deadline_(0), #if FLUTTER_SHELL_ENABLE_METAL @@ -98,7 +99,7 @@ static int64_t FxlToDartOrEarlier(fml::TimePoint time) { return (time - fxl_now).ToMicroseconds() + dart_now; } -void Animator::BeginFrame(fml::TimePoint frame_start_time, +void Animator::BeginFrame(fml::TimePoint vsync_start_time, fml::TimePoint frame_target_time) { TRACE_EVENT_ASYNC_END0("flutter", "Frame Request Pending", frame_number_++); @@ -133,7 +134,11 @@ void Animator::BeginFrame(fml::TimePoint frame_start_time, // to service potential frame. FML_DCHECK(producer_continuation_); - last_frame_begin_time_ = frame_start_time; + last_frame_begin_time_ = fml::TimePoint::Now(); + last_vsync_start_time_ = vsync_start_time; + fml::tracing::TraceEventAsyncComplete("flutter", "VsyncSchedulingOverhead", + last_vsync_start_time_, + last_frame_begin_time_); last_frame_target_time_ = frame_target_time; dart_frame_deadline_ = FxlToDartOrEarlier(frame_target_time); { @@ -178,11 +183,9 @@ void Animator::Render(std::unique_ptr layer_tree) { } last_layer_tree_size_ = layer_tree->frame_size(); - if (layer_tree) { - // Note the frame time for instrumentation. - layer_tree->RecordBuildTime(last_frame_begin_time_, - last_frame_target_time_); - } + // Note the frame time for instrumentation. + layer_tree->RecordBuildTime(last_vsync_start_time_, last_frame_begin_time_, + last_frame_target_time_); // Commit the pending continuation. bool result = producer_continuation_.Complete(std::move(layer_tree)); @@ -236,13 +239,13 @@ void Animator::RequestFrame(bool regenerate_layer_tree) { void Animator::AwaitVSync() { waiter_->AsyncWaitForVsync( - [self = weak_factory_.GetWeakPtr()](fml::TimePoint frame_start_time, + [self = weak_factory_.GetWeakPtr()](fml::TimePoint vsync_start_time, fml::TimePoint frame_target_time) { if (self) { if (self->CanReuseLastLayerTree()) { self->DrawLastLayerTree(); } else { - self->BeginFrame(frame_start_time, frame_target_time); + self->BeginFrame(vsync_start_time, frame_target_time); } } }); diff --git a/shell/common/animator.h b/shell/common/animator.h index 0bab57730015a..a6508fe24fa1a 100644 --- a/shell/common/animator.h +++ b/shell/common/animator.h @@ -98,6 +98,7 @@ class Animator final { std::shared_ptr waiter_; fml::TimePoint last_frame_begin_time_; + fml::TimePoint last_vsync_start_time_; fml::TimePoint last_frame_target_time_; int64_t dart_frame_deadline_; fml::RefPtr layer_tree_pipeline_; diff --git a/shell/common/animator_unittests.cc b/shell/common/animator_unittests.cc index e0d73499c0c01..322ea7e70275f 100644 --- a/shell/common/animator_unittests.cc +++ b/shell/common/animator_unittests.cc @@ -58,11 +58,7 @@ TEST_F(ShellTest, VSyncTargetTime) { std::move(create_vsync_waiter), ShellTestPlatformView::BackendType::kDefaultBackend, nullptr); }, - [](Shell& shell) { - return std::make_unique( - shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); ASSERT_TRUE(DartVMRef::IsInstanceRunning()); auto configuration = RunConfiguration::InferFromSettings(settings); diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 438a1945b9f50..dbbac6e583b15 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -35,30 +35,46 @@ static constexpr char kLocalizationChannel[] = "flutter/localization"; static constexpr char kSettingsChannel[] = "flutter/settings"; static constexpr char kIsolateChannel[] = "flutter/isolate"; +Engine::Engine( + Delegate& delegate, + const PointerDataDispatcherMaker& dispatcher_maker, + std::shared_ptr image_decoder_task_runner, + TaskRunners task_runners, + Settings settings, + std::unique_ptr animator, + fml::WeakPtr io_manager, + std::unique_ptr runtime_controller) + : delegate_(delegate), + settings_(std::move(settings)), + animator_(std::move(animator)), + runtime_controller_(std::move(runtime_controller)), + activity_running_(true), + have_surface_(false), + image_decoder_(task_runners, image_decoder_task_runner, io_manager), + task_runners_(std::move(task_runners)), + weak_factory_(this) { + pointer_data_dispatcher_ = dispatcher_maker(*this); +} + Engine::Engine(Delegate& delegate, const PointerDataDispatcherMaker& dispatcher_maker, DartVM& vm, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, std::unique_ptr animator, fml::WeakPtr io_manager, fml::RefPtr unref_queue, fml::WeakPtr snapshot_delegate) - : delegate_(delegate), - settings_(std::move(settings)), - animator_(std::move(animator)), - activity_running_(true), - have_surface_(false), - image_decoder_(task_runners, - vm.GetConcurrentWorkerTaskRunner(), - io_manager), - task_runners_(std::move(task_runners)), - weak_factory_(this) { - // Runtime controller is initialized here because it takes a reference to this - // object as its delegate. The delegate may be called in the constructor and - // we want to be fully initilazed by that point. + : Engine(delegate, + dispatcher_maker, + vm.GetConcurrentWorkerTaskRunner(), + task_runners, + settings, + std::move(animator), + io_manager, + nullptr) { runtime_controller_ = std::make_unique( *this, // runtime delegate &vm, // VM @@ -71,13 +87,11 @@ Engine::Engine(Delegate& delegate, settings_.advisory_script_uri, // advisory script uri settings_.advisory_script_entrypoint, // advisory script entrypoint settings_.idle_notification_callback, // idle notification callback - window_data, // window data + platform_data, // platform data settings_.isolate_create_callback, // isolate create callback settings_.isolate_shutdown_callback, // isolate shutdown callback settings_.persistent_isolate_data // persistent isolate data ); - - pointer_data_dispatcher_ = dispatcher_maker(*this); } Engine::~Engine() = default; @@ -234,11 +248,16 @@ void Engine::ReportTimings(std::vector timings) { runtime_controller_->ReportTimings(std::move(timings)); } +void Engine::HintFreed(size_t size) { + hint_freed_bytes_since_last_idle_ += size; +} + void Engine::NotifyIdle(int64_t deadline) { auto trace_event = std::to_string(deadline - Dart_TimelineGetMicros()); TRACE_EVENT1("flutter", "Engine::NotifyIdle", "deadline_now_delta", trace_event.c_str()); - runtime_controller_->NotifyIdle(deadline); + runtime_controller_->NotifyIdle(deadline, hint_freed_bytes_since_last_idle_); + hint_freed_bytes_since_last_idle_ = 0; } std::pair Engine::GetUIIsolateReturnCode() { @@ -276,7 +295,7 @@ void Engine::SetViewportMetrics(const ViewportMetrics& metrics) { bool dimensions_changed = viewport_metrics_.physical_height != metrics.physical_height || viewport_metrics_.physical_width != metrics.physical_width || - viewport_metrics_.physical_depth != metrics.physical_depth; + viewport_metrics_.device_pixel_ratio != metrics.device_pixel_ratio; viewport_metrics_ = metrics; runtime_controller_->SetViewportMetrics(viewport_metrics_); if (animator_) { @@ -302,7 +321,8 @@ void Engine::DispatchPlatformMessage(fml::RefPtr message) { } else if (channel == kSettingsChannel) { HandleSettingsPlatformMessage(message.get()); return; - } else if (channel == kNavigationChannel) { + } else if (!runtime_controller_->IsRootIsolateRunning() && + channel == kNavigationChannel) { // If there's no runtime_, we may still need to set the initial route. HandleNavigationPlatformMessage(std::move(message)); return; @@ -460,8 +480,7 @@ void Engine::Render(std::unique_ptr layer_tree) { // Ensure frame dimensions are sane. if (layer_tree->frame_size().isEmpty() || - layer_tree->frame_physical_depth() <= 0.0f || - layer_tree->frame_device_pixel_ratio() <= 0.0f) { + layer_tree->device_pixel_ratio() <= 0.0f) { return; } diff --git a/shell/common/engine.h b/shell/common/engine.h index d7e516617afc1..97165f42a69fa 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -248,6 +248,20 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { const std::vector& supported_locale_data) = 0; }; + //---------------------------------------------------------------------------- + /// @brief Creates an instance of the engine with a supplied + /// `RuntimeController`. Use the other constructor except for + /// tests. + /// + Engine(Delegate& delegate, + const PointerDataDispatcherMaker& dispatcher_maker, + std::shared_ptr image_decoder_task_runner, + TaskRunners task_runners, + Settings settings, + std::unique_ptr animator, + fml::WeakPtr io_manager, + std::unique_ptr runtime_controller); + //---------------------------------------------------------------------------- /// @brief Creates an instance of the engine. This is done by the Shell /// on the UI task runner. @@ -295,7 +309,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { DartVM& vm, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, std::unique_ptr animator, fml::WeakPtr io_manager, @@ -421,7 +435,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// one frame interval from this point, the Flutter application /// will jank. /// - /// If an root isolate is running, this method calls the + /// If a root isolate is running, this method calls the /// `::_beginFrame` method in `hooks.dart`. If a root isolate is /// not running, this call does nothing. /// @@ -451,6 +465,14 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// void BeginFrame(fml::TimePoint frame_time); + //---------------------------------------------------------------------------- + /// @brief Notifies the engine that native bytes might be freed if a + /// garbage collection ran now. + /// + /// @param[in] size The number of bytes freed. + /// + void HintFreed(size_t size); + //---------------------------------------------------------------------------- /// @brief Notifies the engine that the UI task runner is not expected to /// undertake a new frame workload till a specified timepoint. The @@ -701,9 +723,10 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { //---------------------------------------------------------------------------- /// @brief Notifies the engine that the embedder has expressed an opinion - /// about where the accessibility tree should be generated or not. - /// This call originates in the platform view and is forwarded to - /// the engine here on the UI task runner by the shell. + /// about whether the accessibility tree should be generated or + /// not. This call originates in the platform view and is + /// forwarded to the engine here on the UI task runner by the + /// shell. /// /// @param[in] enabled Whether the accessibility tree is enabled or /// disabled. @@ -755,6 +778,12 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// const std::string& GetLastEntrypointLibrary() const; + //---------------------------------------------------------------------------- + /// @brief Getter for the initial route. This can be set with a platform + /// message. + /// + const std::string& InitialRoute() const { return initial_route_; } + private: Engine::Delegate& delegate_; const Settings settings_; @@ -776,6 +805,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { FontCollection font_collection_; ImageDecoder image_decoder_; TaskRunners task_runners_; + size_t hint_freed_bytes_since_last_idle_ = 0; fml::WeakPtrFactory weak_factory_; // |RuntimeDelegate| diff --git a/shell/common/engine_unittests.cc b/shell/common/engine_unittests.cc new file mode 100644 index 0000000000000..54f6f00d7a8f6 --- /dev/null +++ b/shell/common/engine_unittests.cc @@ -0,0 +1,234 @@ +// 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. +// FLUTTER_NOLINT + +#include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/shell/common/engine.h" +#include "flutter/shell/common/thread_host.h" +#include "flutter/testing/testing.h" +#include "gmock/gmock.h" +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" + +///\note Deprecated MOCK_METHOD macros used until this issue is resolved: +// https://github.com/google/googletest/issues/2490 + +namespace flutter { + +namespace { +class MockDelegate : public Engine::Delegate { + MOCK_METHOD2(OnEngineUpdateSemantics, + void(SemanticsNodeUpdates, CustomAccessibilityActionUpdates)); + MOCK_METHOD1(OnEngineHandlePlatformMessage, + void(fml::RefPtr)); + MOCK_METHOD0(OnPreEngineRestart, void()); + MOCK_METHOD2(UpdateIsolateDescription, void(const std::string, int64_t)); + MOCK_METHOD1(SetNeedsReportTimings, void(bool)); + MOCK_METHOD1(ComputePlatformResolvedLocale, + std::unique_ptr>( + const std::vector&)); +}; + +class MockResponse : public PlatformMessageResponse { + public: + MOCK_METHOD1(Complete, void(std::unique_ptr data)); + MOCK_METHOD0(CompleteEmpty, void()); +}; + +class MockRuntimeDelegate : public RuntimeDelegate { + public: + MOCK_METHOD0(DefaultRouteName, std::string()); + MOCK_METHOD1(ScheduleFrame, void(bool)); + MOCK_METHOD1(Render, void(std::unique_ptr)); + MOCK_METHOD2(UpdateSemantics, + void(SemanticsNodeUpdates, CustomAccessibilityActionUpdates)); + MOCK_METHOD1(HandlePlatformMessage, void(fml::RefPtr)); + MOCK_METHOD0(GetFontCollection, FontCollection&()); + MOCK_METHOD2(UpdateIsolateDescription, void(const std::string, int64_t)); + MOCK_METHOD1(SetNeedsReportTimings, void(bool)); + MOCK_METHOD1(ComputePlatformResolvedLocale, + std::unique_ptr>( + const std::vector&)); +}; + +class MockRuntimeController : public RuntimeController { + public: + MockRuntimeController(RuntimeDelegate& client, TaskRunners p_task_runners) + : RuntimeController(client, p_task_runners) {} + MOCK_CONST_METHOD0(IsRootIsolateRunning, bool()); + MOCK_METHOD1(DispatchPlatformMessage, bool(fml::RefPtr)); +}; + +fml::RefPtr MakePlatformMessage( + const std::string& channel, + const std::map& values, + fml::RefPtr response) { + rapidjson::Document document; + auto& allocator = document.GetAllocator(); + document.SetObject(); + + for (const auto& pair : values) { + rapidjson::Value key(pair.first.c_str(), strlen(pair.first.c_str()), + allocator); + rapidjson::Value value(pair.second.c_str(), strlen(pair.second.c_str()), + allocator); + document.AddMember(key, value, allocator); + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + const uint8_t* data = reinterpret_cast(buffer.GetString()); + + fml::RefPtr message = fml::MakeRefCounted( + channel, std::vector(data, data + buffer.GetSize()), response); + return message; +} + +class EngineTest : public ::testing::Test { + public: + EngineTest() + : thread_host_("EngineTest", + ThreadHost::Type::Platform | ThreadHost::Type::IO | + ThreadHost::Type::UI | ThreadHost::Type::GPU), + task_runners_({ + "EngineTest", + thread_host_.platform_thread->GetTaskRunner(), // platform + thread_host_.raster_thread->GetTaskRunner(), // raster + thread_host_.ui_thread->GetTaskRunner(), // ui + thread_host_.io_thread->GetTaskRunner() // io + }) {} + + void PostUITaskSync(const std::function& function) { + fml::AutoResetWaitableEvent latch; + task_runners_.GetUITaskRunner()->PostTask([&] { + function(); + latch.Signal(); + }); + latch.Wait(); + } + + protected: + void SetUp() override { + dispatcher_maker_ = [](PointerDataDispatcher::Delegate&) { + return nullptr; + }; + } + + MockDelegate delegate_; + PointerDataDispatcherMaker dispatcher_maker_; + ThreadHost thread_host_; + TaskRunners task_runners_; + Settings settings_; + std::unique_ptr animator_; + fml::WeakPtr io_manager_; + std::unique_ptr runtime_controller_; + std::shared_ptr image_decoder_task_runner_; +}; +} // namespace + +TEST_F(EngineTest, Create) { + PostUITaskSync([this] { + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(runtime_controller_)); + EXPECT_TRUE(engine); + }); +} + +TEST_F(EngineTest, DispatchPlatformMessageUnknown) { + PostUITaskSync([this] { + MockRuntimeDelegate client; + auto mock_runtime_controller = + std::make_unique(client, task_runners_); + EXPECT_CALL(*mock_runtime_controller, IsRootIsolateRunning()) + .WillRepeatedly(::testing::Return(false)); + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(mock_runtime_controller)); + + fml::RefPtr response = + fml::MakeRefCounted(); + fml::RefPtr message = + fml::MakeRefCounted("foo", response); + engine->DispatchPlatformMessage(message); + }); +} + +TEST_F(EngineTest, DispatchPlatformMessageInitialRoute) { + PostUITaskSync([this] { + MockRuntimeDelegate client; + auto mock_runtime_controller = + std::make_unique(client, task_runners_); + EXPECT_CALL(*mock_runtime_controller, IsRootIsolateRunning()) + .WillRepeatedly(::testing::Return(false)); + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(mock_runtime_controller)); + + fml::RefPtr response = + fml::MakeRefCounted(); + std::map values{ + {"method", "setInitialRoute"}, + {"args", "test_initial_route"}, + }; + fml::RefPtr message = + MakePlatformMessage("flutter/navigation", values, response); + engine->DispatchPlatformMessage(message); + EXPECT_EQ(engine->InitialRoute(), "test_initial_route"); + }); +} + +TEST_F(EngineTest, DispatchPlatformMessageInitialRouteIgnored) { + PostUITaskSync([this] { + MockRuntimeDelegate client; + auto mock_runtime_controller = + std::make_unique(client, task_runners_); + EXPECT_CALL(*mock_runtime_controller, IsRootIsolateRunning()) + .WillRepeatedly(::testing::Return(true)); + EXPECT_CALL(*mock_runtime_controller, DispatchPlatformMessage(::testing::_)) + .WillRepeatedly(::testing::Return(true)); + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(mock_runtime_controller)); + + fml::RefPtr response = + fml::MakeRefCounted(); + std::map values{ + {"method", "setInitialRoute"}, + {"args", "test_initial_route"}, + }; + fml::RefPtr message = + MakePlatformMessage("flutter/navigation", values, response); + engine->DispatchPlatformMessage(message); + EXPECT_EQ(engine->InitialRoute(), ""); + }); +} + +} // namespace flutter diff --git a/shell/common/pointer_data_dispatcher.h b/shell/common/pointer_data_dispatcher.h index c222bee8d699e..208c8b0d2878e 100644 --- a/shell/common/pointer_data_dispatcher.h +++ b/shell/common/pointer_data_dispatcher.h @@ -23,7 +23,7 @@ class PointerDataDispatcher; /// delivered packets, and dispatches them in sync with the VSYNC signal. /// /// This object will be owned by the engine because it relies on the engine's -/// `Animator` (which owns `VsyncWaiter`) and `RuntomeController` to do the +/// `Animator` (which owns `VsyncWaiter`) and `RuntimeController` to do the /// filtering. This object is currently designed to be only called from the UI /// thread (no thread safety is guaranteed). /// diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index d43867876fd87..682d3a6d2672f 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -25,38 +25,17 @@ namespace flutter { // used within this interval. static constexpr std::chrono::milliseconds kSkiaCleanupExpiration(15000); -// TODO(dnfield): Remove this once internal embedders have caught up. -static Rasterizer::DummyDelegate dummy_delegate_; -Rasterizer::Rasterizer( - TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch) - : Rasterizer(dummy_delegate_, - std::move(task_runners), - std::move(compositor_context), - is_gpu_disabled_sync_switch) {} - -Rasterizer::Rasterizer( - Delegate& delegate, - TaskRunners task_runners, - std::shared_ptr is_gpu_disabled_sync_switch) +Rasterizer::Rasterizer(Delegate& delegate) : Rasterizer(delegate, - std::move(task_runners), - std::make_unique( - delegate.GetFrameBudget()), - is_gpu_disabled_sync_switch) {} + std::make_unique(delegate)) {} Rasterizer::Rasterizer( Delegate& delegate, - TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch) + std::unique_ptr compositor_context) : delegate_(delegate), - task_runners_(std::move(task_runners)), compositor_context_(std::move(compositor_context)), user_override_resource_cache_bytes_(false), - weak_factory_(this), - is_gpu_disabled_sync_switch_(is_gpu_disabled_sync_switch) { + weak_factory_(this) { FML_DCHECK(compositor_context_); } @@ -81,10 +60,12 @@ void Rasterizer::Setup(std::unique_ptr surface) { #if !defined(OS_FUCHSIA) // TODO(sanjayc77): https://github.com/flutter/flutter/issues/53179. Add // support for raster thread merger for Fuchsia. - if (surface_->GetExternalViewEmbedder()) { + if (surface_->GetExternalViewEmbedder() && + surface_->GetExternalViewEmbedder()->SupportsDynamicThreadMerging()) { const auto platform_id = - task_runners_.GetPlatformTaskRunner()->GetTaskQueueId(); - const auto gpu_id = task_runners_.GetRasterTaskRunner()->GetTaskQueueId(); + delegate_.GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId(); + const auto gpu_id = + delegate_.GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(); raster_thread_merger_ = fml::MakeRefCounted(platform_id, gpu_id); } @@ -95,19 +76,25 @@ void Rasterizer::Teardown() { compositor_context_->OnGrContextDestroyed(); surface_.reset(); last_layer_tree_.reset(); + if (raster_thread_merger_.get() != nullptr && + raster_thread_merger_.get()->IsMerged()) { + raster_thread_merger_->UnMergeNow(); + } } void Rasterizer::NotifyLowMemoryWarning() const { if (!surface_) { - FML_DLOG(INFO) << "Rasterizer::PurgeCaches called with no surface."; + FML_DLOG(INFO) + << "Rasterizer::NotifyLowMemoryWarning called with no surface."; return; } auto context = surface_->GetContext(); if (!context) { - FML_DLOG(INFO) << "Rasterizer::PurgeCaches called with no GrContext."; + FML_DLOG(INFO) + << "Rasterizer::NotifyLowMemoryWarning called with no GrContext."; return; } - context->freeGpuResources(); + context->performDeferredCleanup(std::chrono::milliseconds(0)); } flutter::TextureRegistry* Rasterizer::GetTextureRegistry() { @@ -132,7 +119,9 @@ void Rasterizer::Draw(fml::RefPtr> pipeline) { // we yield and let this frame be serviced on the right thread. return; } - FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(delegate_.GetTaskRunners() + .GetRasterTaskRunner() + ->RunsTasksOnCurrentThread()); RasterStatus raster_status = RasterStatus::kFailed; Pipeline::Consumer consumer = @@ -166,7 +155,7 @@ void Rasterizer::Draw(fml::RefPtr> pipeline) { // between successive tries. switch (consume_result) { case PipelineConsumeResult::MoreAvailable: { - task_runners_.GetRasterTaskRunner()->PostTask( + delegate_.GetTaskRunners().GetRasterTaskRunner()->PostTask( [weak_this = weak_factory_.GetWeakPtr(), pipeline]() { if (weak_this) { weak_this->Draw(pipeline); @@ -224,7 +213,7 @@ sk_sp Rasterizer::DoMakeRasterSnapshot( sk_sp surface = SkSurface::MakeRaster(image_info); result = DrawSnapshot(surface, draw_callback); } else { - is_gpu_disabled_sync_switch_->Execute( + delegate_.GetIsGpuDisabledSyncSwitch()->Execute( fml::SyncSwitch::Handlers() .SetIfTrue([&] { sk_sp surface = SkSurface::MakeRaster(image_info); @@ -280,7 +269,9 @@ sk_sp Rasterizer::ConvertToRasterImage(sk_sp image) { RasterStatus Rasterizer::DoDraw( std::unique_ptr layer_tree) { - FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(delegate_.GetTaskRunners() + .GetRasterTaskRunner() + ->RunsTasksOnCurrentThread()); if (!layer_tree || !surface_) { return RasterStatus::kFailed; @@ -290,6 +281,7 @@ RasterStatus Rasterizer::DoDraw( #if !defined(OS_FUCHSIA) const fml::TimePoint frame_target_time = layer_tree->target_time(); #endif + timing.Set(FrameTiming::kVsyncStart, layer_tree->vsync_start()); timing.Set(FrameTiming::kBuildStart, layer_tree->build_start()); timing.Set(FrameTiming::kBuildFinish, layer_tree->build_finish()); timing.Set(FrameTiming::kRasterStart, fml::TimePoint::Now()); @@ -670,6 +662,24 @@ std::optional Rasterizer::GetResourceCacheMaxBytes() const { return std::nullopt; } +bool Rasterizer::EnsureThreadsAreMerged() { + if (surface_ == nullptr || raster_thread_merger_.get() == nullptr) { + return false; + } + fml::TaskRunner::RunNowOrPostTask( + delegate_.GetTaskRunners().GetRasterTaskRunner(), + [weak_this = weak_factory_.GetWeakPtr(), + thread_merger = raster_thread_merger_]() { + if (weak_this->surface_ == nullptr) { + return; + } + thread_merger->MergeWithLease(10); + }); + raster_thread_merger_->WaitUntilMerged(); + FML_DCHECK(raster_thread_merger_->IsMerged()); + return true; +} + Rasterizer::Screenshot::Screenshot() {} Rasterizer::Screenshot::Screenshot(sk_sp p_data, SkISize p_size) diff --git a/shell/common/rasterizer.h b/shell/common/rasterizer.h index 68701d0c0a75b..963a0998db465 100644 --- a/shell/common/rasterizer.h +++ b/shell/common/rasterizer.h @@ -50,7 +50,7 @@ class Rasterizer final : public SnapshotDelegate { /// are made on the GPU task runner. Any delegate must ensure that /// they can handle the threading implications. /// - class Delegate { + class Delegate : public CompositorContext::Delegate { public: //-------------------------------------------------------------------------- /// @brief Notifies the delegate that a frame has been rendered. The @@ -74,41 +74,18 @@ class Rasterizer final : public SnapshotDelegate { /// Target time for the latest frame. See also `Shell::OnAnimatorBeginFrame` /// for when this time gets updated. virtual fml::TimePoint GetLatestFrameTargetTime() const = 0; - }; - // TODO(dnfield): remove once embedders have caught up. - class DummyDelegate : public Delegate { - void OnFrameRasterized(const FrameTiming&) override {} - fml::Milliseconds GetFrameBudget() override { - return fml::kDefaultFrameBudget; - } - // Returning a time in the past so we don't add additional trace - // events when exceeding the frame budget for other embedders. - fml::TimePoint GetLatestFrameTargetTime() const override { - return fml::TimePoint::FromEpochDelta(fml::TimeDelta::Zero()); - } - }; + /// Task runners used by the shell. + virtual const TaskRunners& GetTaskRunners() const = 0; - //---------------------------------------------------------------------------- - /// @brief Creates a new instance of a rasterizer. Rasterizers may only - /// be created on the GPU task runner. Rasterizers are currently - /// only created by the shell. Usually, the shell also sets itself - /// up as the rasterizer delegate. But, this constructor sets up a - /// dummy rasterizer delegate. - /// - // TODO(chinmaygarde): The rasterizer does not use the task runners for - // anything other than thread checks. Remove the same as an argument. - /// - /// @param[in] task_runners The task runners used by the shell. - /// @param[in] compositor_context The compositor context used to hold all - /// the GPU state used by the rasterizer. - /// @param[in] is_gpu_disabled_sync_switch - /// A `SyncSwitch` for handling disabling of the GPU (typically happens - /// when an app is backgrounded) - /// - Rasterizer(TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch); + /// Accessor for the shell's GPU sync switch, which determines whether GPU + /// operations are allowed on the current thread. + /// + /// For example, on some platforms when the application is backgrounded it + /// is critical that GPU operations are not processed. + virtual std::shared_ptr GetIsGpuDisabledSyncSwitch() + const = 0; + }; //---------------------------------------------------------------------------- /// @brief Creates a new instance of a rasterizer. Rasterizers may only @@ -116,18 +93,9 @@ class Rasterizer final : public SnapshotDelegate { /// only created by the shell (which also sets itself up as the /// rasterizer delegate). /// - // TODO(chinmaygarde): The rasterizer does not use the task runners for - // anything other than thread checks. Remove the same as an argument. - /// /// @param[in] delegate The rasterizer delegate. - /// @param[in] task_runners The task runners used by the shell. - /// @param[in] is_gpu_disabled_sync_switch - /// A `SyncSwitch` for handling disabling of the GPU (typically happens - /// when an app is backgrounded) /// - Rasterizer(Delegate& delegate, - TaskRunners task_runners, - std::shared_ptr is_gpu_disabled_sync_switch); + Rasterizer(Delegate& delegate); //---------------------------------------------------------------------------- /// @brief Creates a new instance of a rasterizer. Rasterizers may only @@ -135,21 +103,12 @@ class Rasterizer final : public SnapshotDelegate { /// only created by the shell (which also sets itself up as the /// rasterizer delegate). /// - // TODO(chinmaygarde): The rasterizer does not use the task runners for - // anything other than thread checks. Remove the same as an argument. - /// /// @param[in] delegate The rasterizer delegate. - /// @param[in] task_runners The task runners used by the shell. /// @param[in] compositor_context The compositor context used to hold all /// the GPU state used by the rasterizer. - /// @param[in] is_gpu_disabled_sync_switch - /// A `SyncSwitch` for handling disabling of the GPU (typically happens - /// when an app is backgrounded) /// Rasterizer(Delegate& delegate, - TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch); + std::unique_ptr compositor_context); //---------------------------------------------------------------------------- /// @brief Destroys the rasterizer. This must happen on the GPU task @@ -430,9 +389,24 @@ class Rasterizer final : public SnapshotDelegate { /// std::optional GetResourceCacheMaxBytes() const; + //---------------------------------------------------------------------------- + /// @brief Makes sure the raster task runner and the platform task runner + /// are merged. + /// + /// @attention If raster and platform task runners are not the same or not + /// merged, this method will try to merge the task runners, + /// blocking the current thread until the 2 task runners are + /// merged. + /// + /// @return `true` if raster and platform task runners are the same. + /// `true` if/when raster and platform task runners are merged. + /// `false` if the surface or the |RasterThreadMerger| has not + /// been initialized. + /// + bool EnsureThreadsAreMerged(); + private: Delegate& delegate_; - TaskRunners task_runners_; std::unique_ptr surface_; std::unique_ptr compositor_context_; // This is the last successfully rasterized layer tree. @@ -446,7 +420,6 @@ class Rasterizer final : public SnapshotDelegate { std::optional max_cache_bytes_; fml::TaskRunnerAffineWeakPtrFactory weak_factory_; fml::RefPtr raster_thread_merger_; - std::shared_ptr is_gpu_disabled_sync_switch_; // |SnapshotDelegate| sk_sp MakeRasterSnapshot(sk_sp picture, diff --git a/shell/common/serialization_callbacks.cc b/shell/common/serialization_callbacks.cc index b482f5fab9f42..bd334b7bf80e5 100644 --- a/shell/common/serialization_callbacks.cc +++ b/shell/common/serialization_callbacks.cc @@ -11,11 +11,11 @@ namespace flutter { sk_sp SerializeTypefaceWithoutData(SkTypeface* typeface, void* ctx) { - return typeface->serialize(SkTypeface::SerializeBehavior::kDoIncludeData); + return typeface->serialize(SkTypeface::SerializeBehavior::kDontIncludeData); } sk_sp SerializeTypefaceWithData(SkTypeface* typeface, void* ctx) { - return typeface->serialize(SkTypeface::SerializeBehavior::kDontIncludeData); + return typeface->serialize(SkTypeface::SerializeBehavior::kDoIncludeData); } struct ImageMetaData { diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 9137ebcd945a8..f4651184c0e08 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -43,7 +43,7 @@ constexpr char kFontChange[] = "fontsChange"; std::unique_ptr Shell::CreateShellOnPlatformThread( DartVMRef vm, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const Shell::CreateCallback& on_create_platform_view, @@ -134,7 +134,7 @@ std::unique_ptr Shell::CreateShellOnPlatformThread( fml::MakeCopyable([&engine_promise, // shell = shell.get(), // &dispatcher_maker, // - &window_data, // + &platform_data, // isolate_snapshot = std::move(isolate_snapshot), // vsync_waiter = std::move(vsync_waiter), // &weak_io_manager_future, // @@ -155,7 +155,7 @@ std::unique_ptr Shell::CreateShellOnPlatformThread( *shell->GetDartVM(), // std::move(isolate_snapshot), // task_runners, // - window_data, // + platform_data, // shell->GetSettings(), // std::move(animator), // weak_io_manager_future.get(), // @@ -242,17 +242,17 @@ std::unique_ptr Shell::Create( Settings settings, const Shell::CreateCallback& on_create_platform_view, const Shell::CreateCallback& on_create_rasterizer) { - return Shell::Create(std::move(task_runners), // - WindowData{/* default window data */}, // - std::move(settings), // - std::move(on_create_platform_view), // - std::move(on_create_rasterizer) // + return Shell::Create(std::move(task_runners), // + PlatformData{/* default platform data */}, // + std::move(settings), // + std::move(on_create_platform_view), // + std::move(on_create_rasterizer) // ); } std::unique_ptr Shell::Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, Shell::CreateCallback on_create_platform_view, Shell::CreateCallback on_create_rasterizer) { @@ -267,7 +267,7 @@ std::unique_ptr Shell::Create( auto vm_data = vm->GetVMData(); return Shell::Create(std::move(task_runners), // - std::move(window_data), // + std::move(platform_data), // std::move(settings), // vm_data->GetIsolateSnapshot(), // isolate snapshot on_create_platform_view, // @@ -278,7 +278,7 @@ std::unique_ptr Shell::Create( std::unique_ptr Shell::Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const Shell::CreateCallback& on_create_platform_view, @@ -302,7 +302,7 @@ std::unique_ptr Shell::Create( vm = std::move(vm), // &shell, // task_runners = std::move(task_runners), // - window_data, // + platform_data, // settings, // isolate_snapshot = std::move(isolate_snapshot), // on_create_platform_view, // @@ -310,7 +310,7 @@ std::unique_ptr Shell::Create( ]() mutable { shell = CreateShellOnPlatformThread(std::move(vm), std::move(task_runners), // - window_data, // + platform_data, // settings, // std::move(isolate_snapshot), // on_create_platform_view, // @@ -375,6 +375,11 @@ Shell::Shell(DartVMRef vm, TaskRunners task_runners, Settings settings) task_runners_.GetIOTaskRunner(), std::bind(&Shell::OnServiceProtocolGetSkSLs, this, std::placeholders::_1, std::placeholders::_2)}; + service_protocol_handlers_ + [ServiceProtocol::kEstimateRasterCacheMemoryExtensionName] = { + task_runners_.GetRasterTaskRunner(), + std::bind(&Shell::OnServiceProtocolEstimateRasterCacheMemory, this, + std::placeholders::_1, std::placeholders::_2)}; } Shell::~Shell() { @@ -731,47 +736,21 @@ void Shell::OnPlatformViewDestroyed() { fml::TaskRunner::RunNowOrPostTask(io_task_runner, io_task); }; - // The normal flow executed by this method is that the platform thread is - // starting the sequence and waiting on the latch. Later the UI thread posts - // raster_task to the raster thread triggers signaling the latch(on the IO - // thread). If the raster and the platform threads are the same this results - // in a deadlock as the raster_task will never be posted to the plaform/raster - // thread that is blocked on a latch. To avoid the described deadlock, if the - // raster and the platform threads are the same, should_post_raster_task will - // be false, and then instead of posting a task to the raster thread, the ui - // thread just signals the latch and the platform/raster thread follows with - // executing raster_task. - bool should_post_raster_task = task_runners_.GetRasterTaskRunner() != - task_runners_.GetPlatformTaskRunner(); - - auto ui_task = [engine = engine_->GetWeakPtr(), - raster_task_runner = task_runners_.GetRasterTaskRunner(), - raster_task, should_post_raster_task, &latch]() { + auto ui_task = [engine = engine_->GetWeakPtr(), &latch]() { if (engine) { engine->OnOutputSurfaceDestroyed(); } - // Step 1: Next, tell the raster thread that its rasterizer should suspend - // access to the underlying surface. - if (should_post_raster_task) { - fml::TaskRunner::RunNowOrPostTask(raster_task_runner, raster_task); - } else { - // See comment on should_post_raster_task, in this case we just unblock - // the platform thread. - latch.Signal(); - } + latch.Signal(); }; // Step 0: Post a task onto the UI thread to tell the engine that its output // surface is about to go away. fml::TaskRunner::RunNowOrPostTask(task_runners_.GetUITaskRunner(), ui_task); latch.Wait(); - if (!should_post_raster_task) { - // See comment on should_post_raster_task, in this case the raster_task - // wasn't executed, and we just run it here as the platform thread - // is the raster thread. - raster_task(); - latch.Wait(); - } + rasterizer_->EnsureThreadsAreMerged(); + fml::TaskRunner::RunNowOrPostTask(task_runners_.GetRasterTaskRunner(), + raster_task); + latch.Wait(); } // |PlatformView::Delegate| @@ -1200,6 +1179,16 @@ void Shell::OnFrameRasterized(const FrameTiming& timing) { } } +void Shell::OnCompositorEndFrame(size_t freed_hint) { + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + task_runners_.GetUITaskRunner()->PostTask( + [engine = weak_engine_, freed_hint = freed_hint]() { + if (engine) { + engine->HintFreed(freed_hint); + } + }); +} + fml::Milliseconds Shell::GetFrameBudget() { if (display_refresh_rate_ > 0) { return fml::RefreshRateToFrameBudget(display_refresh_rate_.load()); @@ -1230,7 +1219,7 @@ fml::RefPtr Shell::GetServiceProtocolHandlerTaskRunner( bool Shell::HandleServiceProtocolMessage( std::string_view method, // one if the extension names specified above. const ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { auto found = service_protocol_handlers_.find(method); if (found != service_protocol_handlers_.end()) { return found->second.second(params, response); @@ -1247,44 +1236,44 @@ ServiceProtocol::Handler::Description Shell::GetServiceProtocolDescription() }; } -static void ServiceProtocolParameterError(rapidjson::Document& response, +static void ServiceProtocolParameterError(rapidjson::Document* response, std::string error_details) { - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); const int64_t kInvalidParams = -32602; - response.AddMember("code", kInvalidParams, allocator); - response.AddMember("message", "Invalid params", allocator); + response->AddMember("code", kInvalidParams, allocator); + response->AddMember("message", "Invalid params", allocator); { rapidjson::Value details(rapidjson::kObjectType); details.AddMember("details", error_details, allocator); - response.AddMember("data", details, allocator); + response->AddMember("data", details, allocator); } } -static void ServiceProtocolFailureError(rapidjson::Document& response, +static void ServiceProtocolFailureError(rapidjson::Document* response, std::string message) { - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); const int64_t kJsonServerError = -32000; - response.AddMember("code", kJsonServerError, allocator); - response.AddMember("message", message, allocator); + response->AddMember("code", kJsonServerError, allocator); + response->AddMember("message", message, allocator); } // Service protocol handler bool Shell::OnServiceProtocolScreenshot( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); auto screenshot = rasterizer_->ScreenshotLastLayerTree( Rasterizer::ScreenshotType::CompressedImage, true); if (screenshot.data) { - response.SetObject(); - auto& allocator = response.GetAllocator(); - response.AddMember("type", "Screenshot", allocator); + response->SetObject(); + auto& allocator = response->GetAllocator(); + response->AddMember("type", "Screenshot", allocator); rapidjson::Value image; image.SetString(static_cast(screenshot.data->data()), screenshot.data->size(), allocator); - response.AddMember("screenshot", image, allocator); + response->AddMember("screenshot", image, allocator); return true; } ServiceProtocolFailureError(response, "Could not capture image screenshot."); @@ -1294,18 +1283,18 @@ bool Shell::OnServiceProtocolScreenshot( // Service protocol handler bool Shell::OnServiceProtocolScreenshotSKP( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); auto screenshot = rasterizer_->ScreenshotLastLayerTree( Rasterizer::ScreenshotType::SkiaPicture, true); if (screenshot.data) { - response.SetObject(); - auto& allocator = response.GetAllocator(); - response.AddMember("type", "ScreenshotSkp", allocator); + response->SetObject(); + auto& allocator = response->GetAllocator(); + response->AddMember("type", "ScreenshotSkp", allocator); rapidjson::Value skp; skp.SetString(static_cast(screenshot.data->data()), screenshot.data->size(), allocator); - response.AddMember("skp", skp, allocator); + response->AddMember("skp", skp, allocator); return true; } ServiceProtocolFailureError(response, "Could not capture SKP screenshot."); @@ -1315,7 +1304,7 @@ bool Shell::OnServiceProtocolScreenshotSKP( // Service protocol handler bool Shell::OnServiceProtocolRunInView( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); if (params.count("mainScript") == 0) { @@ -1351,14 +1340,14 @@ bool Shell::OnServiceProtocolRunInView( std::make_unique(fml::OpenDirectory( asset_directory_path.c_str(), false, fml::FilePermission::kRead))); - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); if (engine_->Restart(std::move(configuration))) { - response.AddMember("type", "Success", allocator); + response->AddMember("type", "Success", allocator); auto new_description = GetServiceProtocolDescription(); rapidjson::Value view(rapidjson::kObjectType); new_description.Write(this, view, allocator); - response.AddMember("view", view, allocator); + response->AddMember("view", view, allocator); return true; } else { FML_DLOG(ERROR) << "Could not run configuration in engine."; @@ -1374,7 +1363,7 @@ bool Shell::OnServiceProtocolRunInView( // Service protocol handler bool Shell::OnServiceProtocolFlushUIThreadTasks( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); // This API should not be invoked by production code. // It can potentially starve the service isolate if the main isolate pauses @@ -1382,28 +1371,28 @@ bool Shell::OnServiceProtocolFlushUIThreadTasks( // // It should be invoked from the VM Service and and blocks it until UI thread // tasks are processed. - response.SetObject(); - response.AddMember("type", "Success", response.GetAllocator()); + response->SetObject(); + response->AddMember("type", "Success", response->GetAllocator()); return true; } bool Shell::OnServiceProtocolGetDisplayRefreshRate( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); - response.SetObject(); - response.AddMember("type", "DisplayRefreshRate", response.GetAllocator()); - response.AddMember("fps", engine_->GetDisplayRefreshRate(), - response.GetAllocator()); + response->SetObject(); + response->AddMember("type", "DisplayRefreshRate", response->GetAllocator()); + response->AddMember("fps", engine_->GetDisplayRefreshRate(), + response->GetAllocator()); return true; } bool Shell::OnServiceProtocolGetSkSLs( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetIOTaskRunner()->RunsTasksOnCurrentThread()); - response.SetObject(); - response.AddMember("type", "GetSkSLs", response.GetAllocator()); + response->SetObject(); + response->AddMember("type", "GetSkSLs", response->GetAllocator()); rapidjson::Value shaders_json(rapidjson::kObjectType); PersistentCache* persistent_cache = PersistentCache::GetCacheForProcess(); @@ -1415,19 +1404,36 @@ bool Shell::OnServiceProtocolGetSkSLs( char* b64_char = static_cast(b64_data->writable_data()); SkBase64::Encode(sksl.second->data(), sksl.second->size(), b64_char); b64_char[b64_size] = 0; // make it null terminated for printing - rapidjson::Value shader_value(b64_char, response.GetAllocator()); + rapidjson::Value shader_value(b64_char, response->GetAllocator()); rapidjson::Value shader_key(PersistentCache::SkKeyToFilePath(*sksl.first), - response.GetAllocator()); - shaders_json.AddMember(shader_key, shader_value, response.GetAllocator()); + response->GetAllocator()); + shaders_json.AddMember(shader_key, shader_value, response->GetAllocator()); } - response.AddMember("SkSLs", shaders_json, response.GetAllocator()); + response->AddMember("SkSLs", shaders_json, response->GetAllocator()); + return true; +} + +bool Shell::OnServiceProtocolEstimateRasterCacheMemory( + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document* response) { + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + const auto& raster_cache = rasterizer_->compositor_context()->raster_cache(); + response->SetObject(); + response->AddMember("type", "EstimateRasterCacheMemory", + response->GetAllocator()); + response->AddMember("layerBytes", + raster_cache.EstimateLayerCacheByteSize(), + response->GetAllocator()); + response->AddMember("pictureBytes", + raster_cache.EstimatePictureCacheByteSize(), + response->GetAllocator()); return true; } // Service protocol handler bool Shell::OnServiceProtocolSetAssetBundlePath( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); if (params.count("assetDirectory") == 0) { @@ -1436,8 +1442,8 @@ bool Shell::OnServiceProtocolSetAssetBundlePath( return false; } - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); auto asset_manager = std::make_shared(); @@ -1446,11 +1452,11 @@ bool Shell::OnServiceProtocolSetAssetBundlePath( fml::FilePermission::kRead))); if (engine_->UpdateAssetManager(std::move(asset_manager))) { - response.AddMember("type", "Success", allocator); + response->AddMember("type", "Success", allocator); auto new_description = GetServiceProtocolDescription(); rapidjson::Value view(rapidjson::kObjectType); new_description.Write(this, view, allocator); - response.AddMember("view", view, allocator); + response->AddMember("view", view, allocator); return true; } else { FML_DLOG(ERROR) << "Could not update asset directory."; diff --git a/shell/common/shell.h b/shell/common/shell.h index fd1a30d3f4e35..aa9b64ae1ee32 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -12,6 +12,7 @@ #include "flutter/common/settings.h" #include "flutter/common/task_runners.h" +#include "flutter/flow/compositor_context.h" #include "flutter/flow/surface.h" #include "flutter/flow/texture.h" #include "flutter/fml/closure.h" @@ -138,7 +139,7 @@ class Shell final : public PlatformView::Delegate, /// the Dart VM. /// /// @param[in] task_runners The task runners - /// @param[in] window_data The default data for setting up + /// @param[in] platform_data The default data for setting up /// ui.Window that attached to this /// intance. /// @param[in] settings The settings @@ -161,7 +162,7 @@ class Shell final : public PlatformView::Delegate, /// static std::unique_ptr Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, CreateCallback on_create_platform_view, CreateCallback on_create_rasterizer); @@ -176,7 +177,7 @@ class Shell final : public PlatformView::Delegate, /// requires the specification of a running VM instance. /// /// @param[in] task_runners The task runners - /// @param[in] window_data The default data for setting up + /// @param[in] platform_data The default data for setting up /// ui.Window that attached to this /// intance. /// @param[in] settings The settings @@ -203,7 +204,7 @@ class Shell final : public PlatformView::Delegate, /// static std::unique_ptr Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const CreateCallback& on_create_platform_view, @@ -248,7 +249,7 @@ class Shell final : public PlatformView::Delegate, /// /// @return The task runners current in use by the shell. /// - const TaskRunners& GetTaskRunners() const; + const TaskRunners& GetTaskRunners() const override; //---------------------------------------------------------------------------- /// @brief Rasterizers may only be accessed on the GPU task runner. @@ -352,7 +353,7 @@ class Shell final : public PlatformView::Delegate, //---------------------------------------------------------------------------- /// @brief Accessor for the disable GPU SyncSwitch - std::shared_ptr GetIsGpuDisabledSyncSwitch() const; + std::shared_ptr GetIsGpuDisabledSyncSwitch() const override; //---------------------------------------------------------------------------- /// @brief Get a pointer to the Dart VM used by this running shell @@ -365,7 +366,7 @@ class Shell final : public PlatformView::Delegate, private: using ServiceProtocolHandler = std::function; + rapidjson::Document*)>; const TaskRunners task_runners_; const Settings settings_; @@ -426,7 +427,7 @@ class Shell final : public PlatformView::Delegate, static std::unique_ptr CreateShellOnPlatformThread( DartVMRef vm, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const Shell::CreateCallback& on_create_platform_view, @@ -528,10 +529,14 @@ class Shell final : public PlatformView::Delegate, void OnFrameRasterized(const FrameTiming&) override; // |Rasterizer::Delegate| - fml::Milliseconds GetFrameBudget() override; + fml::TimePoint GetLatestFrameTargetTime() const override; // |Rasterizer::Delegate| - fml::TimePoint GetLatestFrameTargetTime() const override; + // |CompositorContext::Delegate| + fml::Milliseconds GetFrameBudget() override; + + // |CompositorContext::Delegate| + void OnCompositorEndFrame(size_t freed_hint) override; // |ServiceProtocol::Handler| fml::RefPtr GetServiceProtocolHandlerTaskRunner( @@ -541,7 +546,7 @@ class Shell final : public PlatformView::Delegate, bool HandleServiceProtocolMessage( std::string_view method, // one if the extension names specified above. const ServiceProtocolMap& params, - rapidjson::Document& response) override; + rapidjson::Document* response) override; // |ServiceProtocol::Handler| ServiceProtocol::Handler::Description GetServiceProtocolDescription() @@ -550,39 +555,44 @@ class Shell final : public PlatformView::Delegate, // Service protocol handler bool OnServiceProtocolScreenshot( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolScreenshotSKP( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolRunInView( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolFlushUIThreadTasks( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolSetAssetBundlePath( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolGetDisplayRefreshRate( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler // // The returned SkSLs are base64 encoded. Decode before storing them to files. bool OnServiceProtocolGetSkSLs( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); + + // Service protocol handler + bool OnServiceProtocolEstimateRasterCacheMemory( + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document* response); fml::WeakPtrFactory weak_factory_; diff --git a/shell/common/shell_benchmarks.cc b/shell/common/shell_benchmarks.cc index 7808130659959..26e4dc9f08d6d 100644 --- a/shell/common/shell_benchmarks.cc +++ b/shell/common/shell_benchmarks.cc @@ -58,11 +58,7 @@ static void StartupAndShutdownShell(benchmark::State& state, [](Shell& shell) { return std::make_unique(shell, shell.GetTaskRunners()); }, - [](Shell& shell) { - return std::make_unique( - shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); } FML_CHECK(shell); diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index c8ebf642e056f..e80652e33c059 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -49,6 +49,16 @@ void ShellTest::PlatformViewNotifyCreated(Shell* shell) { latch.Wait(); } +void ShellTest::PlatformViewNotifyDestroyed(Shell* shell) { + fml::AutoResetWaitableEvent latch; + fml::TaskRunner::RunNowOrPostTask( + shell->GetTaskRunners().GetPlatformTaskRunner(), [shell, &latch]() { + shell->GetPlatformView()->NotifyDestroyed(); + latch.Signal(); + }); + latch.Wait(); +} + void ShellTest::RunEngine(Shell* shell, RunConfiguration configuration) { fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( @@ -96,14 +106,45 @@ void ShellTest::VSyncFlush(Shell* shell, bool& will_draw_new_frame) { latch.Wait(); } +void ShellTest::SetViewportMetrics(Shell* shell, double width, double height) { + flutter::ViewportMetrics viewport_metrics = { + 1, // device pixel ratio + width, // physical width + height, // physical height + 0, // padding top + 0, // padding right + 0, // padding bottom + 0, // padding left + 0, // view inset top + 0, // view inset right + 0, // view inset bottom + 0, // view inset left + 0, // gesture inset top + 0, // gesture inset right + 0, // gesture inset bottom + 0 // gesture inset left + }; + // Set viewport to nonempty, and call Animator::BeginFrame to make the layer + // tree pipeline nonempty. Without either of this, the layer tree below + // won't be rasterized. + fml::AutoResetWaitableEvent latch; + shell->GetTaskRunners().GetUITaskRunner()->PostTask( + [&latch, engine = shell->weak_engine_, viewport_metrics]() { + engine->SetViewportMetrics(std::move(viewport_metrics)); + const auto frame_begin_time = fml::TimePoint::Now(); + const auto frame_end_time = + frame_begin_time + fml::TimeDelta::FromSecondsF(1.0 / 60.0); + engine->animator_->BeginFrame(frame_begin_time, frame_end_time); + latch.Signal(); + }); + latch.Wait(); +} + void ShellTest::PumpOneFrame(Shell* shell, double width, double height, LayerTreeBuilder builder) { - PumpOneFrame(shell, - flutter::ViewportMetrics{1, width, height, flutter::kUnsetDepth, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - std::move(builder)); + PumpOneFrame(shell, {1.0, width, height}, std::move(builder)); } void ShellTest::PumpOneFrame(Shell* shell, @@ -132,7 +173,6 @@ void ShellTest::PumpOneFrame(Shell* shell, auto layer_tree = std::make_unique( SkISize::Make(viewport_metrics.physical_width, viewport_metrics.physical_height), - static_cast(viewport_metrics.physical_depth), static_cast(viewport_metrics.device_pixel_ratio)); SkMatrix identity; identity.setIdentity(); @@ -181,14 +221,17 @@ void ShellTest::OnServiceProtocol( ServiceProtocolEnum some_protocol, fml::RefPtr task_runner, const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { std::promise finished; fml::TaskRunner::RunNowOrPostTask( - task_runner, [shell, some_protocol, params, &response, &finished]() { + task_runner, [shell, some_protocol, params, response, &finished]() { switch (some_protocol) { case ServiceProtocolEnum::kGetSkSLs: shell->OnServiceProtocolGetSkSLs(params, response); break; + case ServiceProtocolEnum::kEstimateRasterCacheMemory: + shell->OnServiceProtocolEstimateRasterCacheMemory(params, response); + break; case ServiceProtocolEnum::kSetAssetBundlePath: shell->OnServiceProtocolSetAssetBundlePath(params, response); break; @@ -271,10 +314,7 @@ std::unique_ptr ShellTest::CreateShell( ShellTestPlatformView::BackendType::kDefaultBackend, shell_test_external_view_embedder); }, - [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); } void ShellTest::DestroyShell(std::unique_ptr shell) { DestroyShell(std::move(shell), GetTaskRunnersForFixture()); diff --git a/shell/common/shell_test.h b/shell/common/shell_test.h index 2b46881c3c68f..5b626289cd766 100644 --- a/shell/common/shell_test.h +++ b/shell/common/shell_test.h @@ -50,6 +50,8 @@ class ShellTest : public FixtureTest { static void PlatformViewNotifyCreated( Shell* shell); // This creates the surface + static void PlatformViewNotifyDestroyed( + Shell* shell); // This destroys the surface static void RunEngine(Shell* shell, RunConfiguration configuration); static void RestartEngine(Shell* shell, RunConfiguration configuration); @@ -57,6 +59,7 @@ class ShellTest : public FixtureTest { /// the `will_draw_new_frame` to true. static void VSyncFlush(Shell* shell, bool& will_draw_new_frame); + static void SetViewportMetrics(Shell* shell, double width, double height); /// Given the root layer, this callback builds the layer tree to be rasterized /// in PumpOneFrame. using LayerTreeBuilder = @@ -80,6 +83,7 @@ class ShellTest : public FixtureTest { enum ServiceProtocolEnum { kGetSkSLs, + kEstimateRasterCacheMemory, kSetAssetBundlePath, kRunInView, }; @@ -92,7 +96,7 @@ class ShellTest : public FixtureTest { ServiceProtocolEnum some_protocol, fml::RefPtr task_runner, const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); std::shared_ptr GetFontCollection(Shell* shell); diff --git a/shell/common/shell_test_external_view_embedder.cc b/shell/common/shell_test_external_view_embedder.cc index f3a5a0a37d27d..07557768a46b7 100644 --- a/shell/common/shell_test_external_view_embedder.cc +++ b/shell/common/shell_test_external_view_embedder.cc @@ -2,6 +2,25 @@ namespace flutter { +ShellTestExternalViewEmbedder::ShellTestExternalViewEmbedder( + const EndFrameCallBack& end_frame_call_back, + PostPrerollResult post_preroll_result, + bool support_thread_merging) + : end_frame_call_back_(end_frame_call_back), + post_preroll_result_(post_preroll_result), + support_thread_merging_(support_thread_merging) { + resubmit_once_ = false; +} + +void ShellTestExternalViewEmbedder::UpdatePostPrerollResult( + PostPrerollResult post_preroll_result) { + post_preroll_result_ = post_preroll_result; +} + +void ShellTestExternalViewEmbedder::SetResubmitOnce() { + resubmit_once_ = true; +} + // |ExternalViewEmbedder| void ShellTestExternalViewEmbedder::CancelFrame() {} @@ -21,6 +40,10 @@ void ShellTestExternalViewEmbedder::PrerollCompositeEmbeddedView( PostPrerollResult ShellTestExternalViewEmbedder::PostPrerollAction( fml::RefPtr raster_thread_merger) { FML_DCHECK(raster_thread_merger); + if (resubmit_once_) { + resubmit_once_ = false; + return PostPrerollResult::kResubmitFrame; + } return post_preroll_result_; } @@ -45,7 +68,7 @@ void ShellTestExternalViewEmbedder::SubmitFrame( void ShellTestExternalViewEmbedder::EndFrame( bool should_resubmit_frame, fml::RefPtr raster_thread_merger) { - end_frame_call_back_(should_resubmit_frame); + end_frame_call_back_(should_resubmit_frame, raster_thread_merger); } // |ExternalViewEmbedder| @@ -53,4 +76,8 @@ SkCanvas* ShellTestExternalViewEmbedder::GetRootCanvas() { return nullptr; } +bool ShellTestExternalViewEmbedder::SupportsDynamicThreadMerging() { + return support_thread_merging_; +} + } // namespace flutter diff --git a/shell/common/shell_test_external_view_embedder.h b/shell/common/shell_test_external_view_embedder.h index c67772df74f9a..6d90879c74e17 100644 --- a/shell/common/shell_test_external_view_embedder.h +++ b/shell/common/shell_test_external_view_embedder.h @@ -15,15 +15,23 @@ namespace flutter { /// class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { public: - using EndFrameCallBack = std::function; + using EndFrameCallBack = + std::function)>; ShellTestExternalViewEmbedder(const EndFrameCallBack& end_frame_call_back, - PostPrerollResult post_preroll_result) - : end_frame_call_back_(end_frame_call_back), - post_preroll_result_(post_preroll_result) {} + PostPrerollResult post_preroll_result, + bool support_thread_merging); ~ShellTestExternalViewEmbedder() = default; + // Updates the post preroll result so the |PostPrerollAction| after always + // returns the new `post_preroll_result`. + void UpdatePostPrerollResult(PostPrerollResult post_preroll_result); + + // Updates the post preroll result to `PostPrerollResult::kResubmitFrame` for + // only the next frame. + void SetResubmitOnce(); + private: // |ExternalViewEmbedder| void CancelFrame() override; @@ -62,8 +70,14 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { // |ExternalViewEmbedder| SkCanvas* GetRootCanvas() override; + // |ExternalViewEmbedder| + bool SupportsDynamicThreadMerging() override; + const EndFrameCallBack end_frame_call_back_; - const PostPrerollResult post_preroll_result_; + PostPrerollResult post_preroll_result_; + bool resubmit_once_; + + bool support_thread_merging_; FML_DISALLOW_COPY_AND_ASSIGN(ShellTestExternalViewEmbedder); }; diff --git a/shell/common/shell_test_platform_view_gl.cc b/shell/common/shell_test_platform_view_gl.cc index 0b7b1f588c71d..2adcbe6b5dfda 100644 --- a/shell/common/shell_test_platform_view_gl.cc +++ b/shell/common/shell_test_platform_view_gl.cc @@ -60,7 +60,7 @@ bool ShellTestPlatformViewGL::GLContextPresent() { } // |GPUSurfaceGLDelegate| -intptr_t ShellTestPlatformViewGL::GLContextFBO() const { +intptr_t ShellTestPlatformViewGL::GLContextFBO(GLFrameInfo frame_info) const { return gl_surface_.GetFramebuffer(); } diff --git a/shell/common/shell_test_platform_view_gl.h b/shell/common/shell_test_platform_view_gl.h index e33b84fed7814..5ca969531751d 100644 --- a/shell/common/shell_test_platform_view_gl.h +++ b/shell/common/shell_test_platform_view_gl.h @@ -56,7 +56,7 @@ class ShellTestPlatformViewGL : public ShellTestPlatformView, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| GLProcResolver GetGLProcResolver() const override; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index 5f82ee64b5b50..30e03d1818fc7 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -32,10 +32,14 @@ #include "flutter/shell/common/vsync_waiter_fallback.h" #include "flutter/shell/version/version.h" #include "flutter/testing/testing.h" -#include "rapidjson/writer.h" +#include "third_party/rapidjson/include/rapidjson/writer.h" #include "third_party/skia/include/core/SkPictureRecorder.h" #include "third_party/tonic/converter/dart_converter.h" +#ifdef SHELL_ENABLE_VULKAN +#include "flutter/vulkan/vulkan_application.h" // nogncheck +#endif + namespace flutter { namespace testing { @@ -63,6 +67,32 @@ static bool ValidateShell(Shell* shell) { return true; } +static bool RasterizerHasLayerTree(Shell* shell) { + fml::AutoResetWaitableEvent latch; + bool has_layer_tree = false; + fml::TaskRunner::RunNowOrPostTask( + shell->GetTaskRunners().GetRasterTaskRunner(), + [shell, &latch, &has_layer_tree]() { + has_layer_tree = shell->GetRasterizer()->GetLastLayerTree() != nullptr; + latch.Signal(); + }); + latch.Wait(); + return has_layer_tree; +} + +static void ValidateDestroyPlatformView(Shell* shell) { + ASSERT_TRUE(shell != nullptr); + ASSERT_TRUE(shell->IsSetup()); + + // To validate destroy platform view, we must ensure the rasterizer has a + // layer tree before the platform view is destroyed. + ASSERT_TRUE(RasterizerHasLayerTree(shell)); + + ShellTest::PlatformViewNotifyDestroyed(shell); + // Validate the layer tree is destroyed + ASSERT_FALSE(RasterizerHasLayerTree(shell)); +} + TEST_F(ShellTest, InitializeWithInvalidThreads) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); Settings settings = CreateSettingsForFixture(); @@ -145,10 +175,7 @@ TEST_F(ShellTest, }, ShellTestPlatformView::BackendType::kDefaultBackend, nullptr); }, - [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); ASSERT_TRUE(ValidateShell(shell.get())); ASSERT_TRUE(DartVMRef::IsInstanceRunning()); DestroyShell(std::move(shell), std::move(task_runners)); @@ -453,7 +480,6 @@ TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) { CREATE_NATIVE_ENTRY(nativeOnBeginFrame)); RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get()); // Check that timing is properly set. This implies that @@ -474,18 +500,70 @@ TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) { #if !defined(OS_FUCHSIA) // TODO(sanjayc77): https://github.com/flutter/flutter/issues/53179. Add // support for raster thread merger for Fuchsia. +TEST_F(ShellTest, ExternalEmbedderNoThreadMerger) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + bool end_frame_called = false; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + ASSERT_TRUE(raster_thread_merger.get() == nullptr); + ASSERT_FALSE(should_resubmit_frame); + end_frame_called = true; + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kResubmitFrame, false); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + + ASSERT_TRUE(end_frame_called); + + DestroyShell(std::move(shell)); +} + TEST_F(ShellTest, ExternalEmbedderEndFrameIsCalledWhenPostPrerollResultIsResubmit) { auto settings = CreateSettingsForFixture(); - fml::AutoResetWaitableEvent endFrameLatch; + fml::AutoResetWaitableEvent end_frame_latch; bool end_frame_called = false; - auto end_frame_callback = [&](bool should_resubmit_frame) { - ASSERT_TRUE(should_resubmit_frame); - end_frame_called = true; - endFrameLatch.Signal(); - }; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + ASSERT_TRUE(raster_thread_merger.get() != nullptr); + ASSERT_TRUE(should_resubmit_frame); + end_frame_called = true; + end_frame_latch.Signal(); + }; auto external_view_embedder = std::make_shared( - end_frame_callback, PostPrerollResult::kResubmitFrame); + end_frame_callback, PostPrerollResult::kResubmitFrame, true); auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), false, external_view_embedder); @@ -508,19 +586,323 @@ TEST_F(ShellTest, this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); auto picture_layer = std::make_shared( SkPoint::Make(10, 10), - flutter::SkiaGPUObject({sk_picture, queue}), false, false); + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); root->Add(picture_layer); }; PumpOneFrame(shell.get(), 100, 100, builder); - endFrameLatch.Wait(); + end_frame_latch.Wait(); ASSERT_TRUE(end_frame_called); DestroyShell(std::move(shell)); } + +TEST_F(ShellTest, OnPlatformViewDestroyAfterMergingThreads) { + const size_t ThreadMergingLease = 10; + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + if (should_resubmit_frame && !raster_thread_merger->IsMerged()) { + raster_thread_merger->MergeWithLease(ThreadMergingLease); + } + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + // Set resubmit once to trigger thread merging. + external_view_embedder->SetResubmitOnce(); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + // Pump one frame to trigger thread merging. + end_frame_latch.Wait(); + // Pump another frame to ensure threads are merged and a regular layer tree is + // submitted. + PumpOneFrame(shell.get(), 100, 100, builder); + // Threads are merged here. PlatformViewNotifyDestroy should be executed + // successfully. + ASSERT_TRUE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, OnPlatformViewDestroyWhenThreadsAreMerging) { + const size_t ThreadMergingLease = 10; + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + if (should_resubmit_frame && !raster_thread_merger->IsMerged()) { + raster_thread_merger->MergeWithLease(ThreadMergingLease); + } + end_frame_latch.Signal(); + }; + // Start with a regular layer tree with `PostPrerollResult::kSuccess` so we + // can later check if the rasterizer is tore down using + // |ValidateDestroyPlatformView| + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + // Pump one frame and threads aren't merged + end_frame_latch.Wait(); + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Pump a frame with `PostPrerollResult::kResubmitFrame` to start merging + // threads + external_view_embedder->SetResubmitOnce(); + PumpOneFrame(shell.get(), 100, 100, builder); + + // Now destroy the platform view immediately. + // Two things can happen here: + // 1. Threads haven't merged. 2. Threads has already merged. + // |Shell:OnPlatformViewDestroy| should be able to handle both cases. + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, + OnPlatformViewDestroyWithThreadMergerWhileThreadsAreUnmerged) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + + // Threads should not be merged. + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, OnPlatformViewDestroyWithoutRasterThreadMerger) { + auto settings = CreateSettingsForFixture(); + + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, nullptr); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + PumpOneFrame(shell.get(), 100, 100, builder); + + // Threads should not be merged. + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} #endif +TEST_F(ShellTest, OnPlatformViewDestroyWithStaticThreadMerging) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + ThreadHost thread_host( + "io.flutter.test." + GetCurrentTestName() + ".", + ThreadHost::Type::Platform | ThreadHost::Type::IO | ThreadHost::Type::UI); + TaskRunners task_runners( + "test", + thread_host.platform_thread->GetTaskRunner(), // platform + thread_host.platform_thread->GetTaskRunner(), // raster + thread_host.ui_thread->GetTaskRunner(), // ui + thread_host.io_thread->GetTaskRunner() // io + ); + auto shell = CreateShell(std::move(settings), std::move(task_runners), false, + external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + + ValidateDestroyPlatformView(shell.get()); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell), std::move(task_runners)); +} + TEST(SettingsTest, FrameTimingSetsAndGetsProperly) { // Ensure that all phases are in kPhases. ASSERT_EQ(sizeof(FrameTiming::kPhases), @@ -576,16 +958,23 @@ TEST_F(ShellTest, ReportTimingsIsCalledSoonerInNonReleaseMode) { DestroyShell(std::move(shell)); fml::TimePoint finish = fml::TimePoint::Now(); - fml::TimeDelta ellapsed = finish - start; + fml::TimeDelta elapsed = finish - start; #if FLUTTER_RELEASE // Our batch time is 1000ms. Hopefully the 800ms limit is relaxed enough to // make it not too flaky. - ASSERT_TRUE(ellapsed >= fml::TimeDelta::FromMilliseconds(800)); + ASSERT_TRUE(elapsed >= fml::TimeDelta::FromMilliseconds(800)); #else // Our batch time is 100ms. Hopefully the 500ms limit is relaxed enough to // make it not too flaky. - ASSERT_TRUE(ellapsed <= fml::TimeDelta::FromMilliseconds(500)); + // + // TODO(https://github.com/flutter/flutter/issues/64087): Fuchsia uses a + // 2000ms timeout to handle slowdowns in FEMU. +#if OS_FUCHSIA + ASSERT_TRUE(elapsed <= fml::TimeDelta::FromMilliseconds(2000)); +#else + ASSERT_TRUE(elapsed <= fml::TimeDelta::FromMilliseconds(500)); +#endif #endif } @@ -684,7 +1073,7 @@ TEST_F(ShellTest, WaitForFirstFrameZeroSizeFrame) { configuration.SetEntrypoint("emptyMain"); RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get(), {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + PumpOneFrame(shell.get(), {1.0, 0.0, 0.0}); fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::FromMilliseconds(1000)); ASSERT_FALSE(result.ok()); @@ -797,13 +1186,19 @@ TEST_F(ShellTest, SetResourceCacheSize) { RunEngine(shell.get(), std::move(configuration)); PumpOneFrame(shell.get()); + // The Vulkan and GL backends set different default values for the resource + // cache size. +#ifdef SHELL_ENABLE_VULKAN + EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), + vulkan::kGrCacheMaxByteSize); +#else EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), static_cast(24 * (1 << 20))); +#endif fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 400, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 400, 200}); }); PumpOneFrame(shell.get()); @@ -822,8 +1217,7 @@ TEST_F(ShellTest, SetResourceCacheSize) { fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 800, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 800, 400}); }); PumpOneFrame(shell.get()); @@ -841,8 +1235,7 @@ TEST_F(ShellTest, SetResourceCacheSizeEarly) { fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 400, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 400, 200}); }); PumpOneFrame(shell.get()); @@ -870,8 +1263,7 @@ TEST_F(ShellTest, SetResourceCacheSizeNotifiesDart) { fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 400, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 400, 200}); }); PumpOneFrame(shell.get()); @@ -1075,7 +1467,8 @@ TEST_F(ShellTest, Screenshot) { this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); auto picture_layer = std::make_shared( SkPoint::Make(10, 10), - flutter::SkiaGPUObject({sk_picture, queue}), false, false); + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); root->Add(picture_layer); }; @@ -1235,15 +1628,17 @@ TEST_F(ShellTest, CanDecompressImageFromAsset) { } TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) { - // Create 2 dummpy SkSL cache file IE (base32 encoding of A), II (base32 - // encoding of B) with content x and y. - fml::ScopedTemporaryDirectory temp_dir; - PersistentCache::SetCacheDirectoryPath(temp_dir.path()); + fml::ScopedTemporaryDirectory base_dir; + ASSERT_TRUE(base_dir.fd().is_valid()); + PersistentCache::SetCacheDirectoryPath(base_dir.path()); PersistentCache::ResetCacheForProcess(); - std::vector components = {"flutter_engine", - GetFlutterEngineVersion(), "skia", - GetSkiaVersion(), "sksl"}; - auto sksl_dir = fml::CreateDirectory(temp_dir.fd(), components, + + // Create 2 dummy SkSL cache file IE (base32 encoding of A), II (base32 + // encoding of B) with content x and y. + std::vector components = { + "flutter_engine", GetFlutterEngineVersion(), "skia", GetSkiaVersion(), + PersistentCache::kSkSLSubdirName}; + auto sksl_dir = fml::CreateDirectory(base_dir.fd(), components, fml::FilePermission::kReadWrite); const std::string x = "x"; const std::string y = "y"; @@ -1260,7 +1655,7 @@ TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) { rapidjson::Document document; OnServiceProtocol(shell.get(), ServiceProtocolEnum::kGetSkSLs, shell->GetTaskRunners().GetIOTaskRunner(), empty_params, - document); + &document); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); document.Accept(writer); @@ -1274,9 +1669,6 @@ TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) { (expected_json2 == buffer.GetString()); ASSERT_TRUE(json_is_expected) << buffer.GetString() << " is not equal to " << expected_json1 << " or " << expected_json2; - - // Cleanup files - fml::RemoveFilesInDirectory(temp_dir.fd()); } TEST_F(ShellTest, RasterizerScreenshot) { @@ -1342,5 +1734,91 @@ TEST_F(ShellTest, RasterizerMakeRasterSnapshot) { DestroyShell(std::move(shell), std::move(task_runners)); } +static sk_sp MakeSizedPicture(int width, int height) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, width, height)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, width, height), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + return recorder.finishRecordingAsPicture(); +} + +TEST_F(ShellTest, OnServiceProtocolEstimateRasterCacheMemoryWorks) { + Settings settings = CreateSettingsForFixture(); + std::unique_ptr shell = CreateShell(settings); + + // 1. Construct a picture and a picture layer to be raster cached. + sk_sp picture = MakeSizedPicture(10, 10); + fml::RefPtr queue = fml::MakeRefCounted( + GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(0, 0), + flutter::SkiaGPUObject({MakeSizedPicture(100, 100), queue}), + false, false, 0); + picture_layer->set_paint_bounds(SkRect::MakeWH(100, 100)); + + // 2. Rasterize the picture and the picture layer in the raster cache. + std::promise rasterized; + shell->GetTaskRunners().GetRasterTaskRunner()->PostTask( + [&shell, &rasterized, &picture, &picture_layer] { + auto* compositor_context = shell->GetRasterizer()->compositor_context(); + auto& raster_cache = compositor_context->raster_cache(); + // 2.1. Rasterize the picture. Call Draw multiple times to pass the + // access threshold (default to 3) so a cache can be generated. + SkCanvas dummy_canvas; + bool picture_cache_generated; + for (int i = 0; i < 4; i += 1) { + picture_cache_generated = + raster_cache.Prepare(nullptr, // GrDirectContext + picture.get(), SkMatrix::I(), + nullptr, // SkColorSpace + true, // isComplex + false // willChange + ); + raster_cache.Draw(*picture, dummy_canvas); + } + ASSERT_TRUE(picture_cache_generated); + + // 2.2. Rasterize the picture layer. + Stopwatch raster_time; + Stopwatch ui_time; + MutatorsStack mutators_stack; + TextureRegistry texture_registry; + PrerollContext preroll_context = { + nullptr, /* raster_cache */ + nullptr, /* gr_context */ + nullptr, /* external_view_embedder */ + mutators_stack, nullptr, /* color_space */ + kGiantRect, /* cull_rect */ + false, /* layer reads from surface */ + raster_time, ui_time, texture_registry, + false, /* checkerboard_offscreen_layers */ + 1.0f, /* frame_device_pixel_ratio */ + false, /* has_platform_view */ + }; + raster_cache.Prepare(&preroll_context, picture_layer.get(), + SkMatrix::I()); + rasterized.set_value(true); + }); + rasterized.get_future().wait(); + + // 3. Call the service protocol and check its output. + ServiceProtocol::Handler::ServiceProtocolMap empty_params; + rapidjson::Document document; + OnServiceProtocol( + shell.get(), ServiceProtocolEnum::kEstimateRasterCacheMemory, + shell->GetTaskRunners().GetRasterTaskRunner(), empty_params, &document); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + std::string expected_json = + "{\"type\":\"EstimateRasterCacheMemory\",\"layerBytes\":40000,\"picture" + "Bytes\":400}"; + std::string actual_json = buffer.GetString(); + ASSERT_EQ(actual_json, expected_json); + + DestroyShell(std::move(shell)); +} + } // namespace testing } // namespace flutter diff --git a/shell/common/skp_shader_warmup_unittests.cc b/shell/common/skp_shader_warmup_unittests.cc index 689f2d9322fcc..ec81003c8f04f 100644 --- a/shell/common/skp_shader_warmup_unittests.cc +++ b/shell/common/skp_shader_warmup_unittests.cc @@ -153,7 +153,8 @@ class SkpWarmupTest : public ShellTest { auto picture_layer = std::make_shared( SkPoint::Make(0, 0), SkiaGPUObject(picture, queue), /* is_complex */ false, - /* will_change */ false); + /* will_change */ false, + /* external_size */ 0); root->Add(picture_layer); }; PumpOneFrame(shell.get(), picture->cullRect().width(), @@ -235,7 +236,8 @@ TEST_F(SkpWarmupTest, Image) { auto picture_layer = std::make_shared( SkPoint::Make(0, 0), SkiaGPUObject(picture, queue), /* is_complex */ false, - /* will_change */ false); + /* will_change */ false, + /* external_size */ 0); root->Add(picture_layer); }; diff --git a/shell/common/switches.cc b/shell/common/switches.cc index e2f83f3f9c748..16494204ddf81 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -242,8 +242,11 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { } } - settings.disable_http = - command_line.HasOption(FlagForSwitch(Switch::DisableHttp)); + settings.may_insecurely_connect_to_all_domains = !command_line.HasOption( + FlagForSwitch(Switch::DisallowInsecureConnections)); + + command_line.GetOptionValue(FlagForSwitch(Switch::DomainNetworkPolicy), + &settings.domain_network_policy); // Disable need for authentication codes for VM service communication, if // specified. diff --git a/shell/common/switches.h b/shell/common/switches.h index 9b91355c33856..1110261f767c3 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -181,12 +181,15 @@ DEF_SWITCH(DisableDartAsserts, "disabled. This flag may be specified if the user wishes to run " "with assertions disabled in the debug product mode (i.e. with JIT " "or DBC).") -DEF_SWITCH(DisableHttp, - "disable-http", - "Dart VM has a master switch that can be set to disable insecure " - "HTTP and WebSocket protocols. Localhost or loopback addresses are " - "exempted. This flag can be specified if the embedder wants this " - "for a particular platform.") +DEF_SWITCH(DisallowInsecureConnections, + "disallow-insecure-connections", + "By default, dart:io allows all socket connections. If this switch " + "is set, all insecure connections are rejected.") +DEF_SWITCH(DomainNetworkPolicy, + "domain-network-policy", + "JSON encoded network policy per domain. This overrides the " + "DisallowInsecureConnections switch. Embedder can specify whether " + "to allow or disallow insecure connections at a domain level.") DEF_SWITCH( ForceMultithreading, "force-multithreading", diff --git a/shell/common/vsync_waiter.cc b/shell/common/vsync_waiter.cc index ae86fc547ea3b..7947d42cf6125 100644 --- a/shell/common/vsync_waiter.cc +++ b/shell/common/vsync_waiter.cc @@ -122,9 +122,6 @@ void VsyncWaiter::FireCallback(fml::TimePoint frame_start_time, [callback, flow_identifier, frame_start_time, frame_target_time]() { FML_TRACE_EVENT("flutter", kVsyncTraceName, "StartTime", frame_start_time, "TargetTime", frame_target_time); - fml::tracing::TraceEventAsyncComplete( - "flutter", "VsyncSchedulingOverhead", fml::TimePoint::Now(), - frame_start_time); callback(frame_start_time, frame_target_time); TRACE_FLOW_END("flutter", kVsyncFlowName, flow_identifier); }, diff --git a/shell/gpu/BUILD.gn b/shell/gpu/BUILD.gn index 7e9295218d370..a4c59e28b765f 100644 --- a/shell/gpu/BUILD.gn +++ b/shell/gpu/BUILD.gn @@ -5,69 +5,56 @@ import("//flutter/common/config.gni") import("//flutter/shell/config.gni") -gpu_dir = "//flutter/shell/gpu" - gpu_common_deps = [ "//flutter/common", + "//flutter/flow", "//flutter/fml", + "//flutter/shell/common", "//third_party/skia", ] -gpu_common_deps_legacy_and_next = [ - "//flutter/flow:flow", - "//flutter/shell/common:common", -] - -source_set_maybe_fuchsia_legacy("gpu_surface_software") { +source_set("gpu_surface_software") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_software.cc", - "$gpu_dir/gpu_surface_software.h", - "$gpu_dir/gpu_surface_software_delegate.cc", - "$gpu_dir/gpu_surface_software_delegate.h", + "gpu_surface_delegate.h", + "gpu_surface_software.cc", + "gpu_surface_software.h", + "gpu_surface_software_delegate.cc", + "gpu_surface_software_delegate.h", ] deps = gpu_common_deps - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } -source_set_maybe_fuchsia_legacy("gpu_surface_gl") { +source_set("gpu_surface_gl") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_gl.cc", - "$gpu_dir/gpu_surface_gl.h", - "$gpu_dir/gpu_surface_gl_delegate.cc", - "$gpu_dir/gpu_surface_gl_delegate.h", + "gpu_surface_delegate.h", + "gpu_surface_gl.cc", + "gpu_surface_gl.h", + "gpu_surface_gl_delegate.cc", + "gpu_surface_gl_delegate.h", ] deps = gpu_common_deps - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } -source_set_maybe_fuchsia_legacy("gpu_surface_vulkan") { +source_set("gpu_surface_vulkan") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_vulkan.cc", - "$gpu_dir/gpu_surface_vulkan.h", - "$gpu_dir/gpu_surface_vulkan_delegate.cc", - "$gpu_dir/gpu_surface_vulkan_delegate.h", + "gpu_surface_delegate.h", + "gpu_surface_vulkan.cc", + "gpu_surface_vulkan.h", + "gpu_surface_vulkan_delegate.cc", + "gpu_surface_vulkan_delegate.h", ] deps = gpu_common_deps + [ "//flutter/vulkan" ] - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } -source_set_maybe_fuchsia_legacy("gpu_surface_metal") { +source_set("gpu_surface_metal") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_metal.h", - "$gpu_dir/gpu_surface_metal.mm", + "gpu_surface_delegate.h", + "gpu_surface_metal.h", + "gpu_surface_metal.mm", ] deps = gpu_common_deps - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } diff --git a/shell/gpu/gpu.gni b/shell/gpu/gpu.gni index e064e1ea4b984..65464c8f6499b 100644 --- a/shell/gpu/gpu.gni +++ b/shell/gpu/gpu.gni @@ -33,30 +33,4 @@ template("shell_gpu_configuration") { public_deps += [ "//flutter/shell/gpu:gpu_surface_metal" ] } } - - if (is_fuchsia) { - legagcy_suffix = "_fuchsia_legacy" - group(target_name + legagcy_suffix) { - public_deps = [] - - if (invoker.enable_software) { - public_deps += - [ "//flutter/shell/gpu:gpu_surface_software" + legagcy_suffix ] - } - - if (invoker.enable_gl) { - public_deps += [ "//flutter/shell/gpu:gpu_surface_gl" + legagcy_suffix ] - } - - if (invoker.enable_vulkan) { - public_deps += - [ "//flutter/shell/gpu:gpu_surface_vulkan" + legagcy_suffix ] - } - - if (invoker.enable_metal) { - public_deps += - [ "//flutter/shell/gpu:gpu_surface_metal" + legagcy_suffix ] - } - } - } } diff --git a/shell/gpu/gpu_surface_gl.cc b/shell/gpu/gpu_surface_gl.cc index c492a456bb812..5386683a27474 100644 --- a/shell/gpu/gpu_surface_gl.cc +++ b/shell/gpu/gpu_surface_gl.cc @@ -204,10 +204,12 @@ bool GPUSurfaceGL::CreateOrUpdateSurfaces(const SkISize& size) { sk_sp onscreen_surface; + GLFrameInfo frame_info = {static_cast(size.width()), + static_cast(size.height())}; onscreen_surface = - WrapOnscreenSurface(context_.get(), // GL context - size, // root surface size - delegate_->GLContextFBO() // window FBO ID + WrapOnscreenSurface(context_.get(), // GL context + size, // root surface size + delegate_->GLContextFBO(frame_info) // window FBO ID ); if (onscreen_surface == nullptr) { @@ -287,13 +289,16 @@ bool GPUSurfaceGL::PresentSurface(SkCanvas* canvas) { auto current_size = SkISize::Make(onscreen_surface_->width(), onscreen_surface_->height()); + GLFrameInfo frame_info = {static_cast(current_size.width()), + static_cast(current_size.height())}; + // The FBO has changed, ask the delegate for the new FBO and do a surface // re-wrap. - auto new_onscreen_surface = - WrapOnscreenSurface(context_.get(), // GL context - current_size, // root surface size - delegate_->GLContextFBO() // window FBO ID - ); + auto new_onscreen_surface = WrapOnscreenSurface( + context_.get(), // GL context + current_size, // root surface size + delegate_->GLContextFBO(frame_info) // window FBO ID + ); if (!new_onscreen_surface) { return false; diff --git a/shell/gpu/gpu_surface_gl_delegate.h b/shell/gpu/gpu_surface_gl_delegate.h index 9fb9765ac9be2..7fd111dd3ebe5 100644 --- a/shell/gpu/gpu_surface_gl_delegate.h +++ b/shell/gpu/gpu_surface_gl_delegate.h @@ -14,6 +14,13 @@ namespace flutter { +// A structure to represent the frame information which is passed to the +// embedder when requesting a frame buffer object. +struct GLFrameInfo { + uint32_t width; + uint32_t height; +}; + class GPUSurfaceGLDelegate : public GPUSurfaceDelegate { public: ~GPUSurfaceGLDelegate() override; @@ -33,13 +40,13 @@ class GPUSurfaceGLDelegate : public GPUSurfaceDelegate { virtual bool GLContextPresent() = 0; // The ID of the main window bound framebuffer. Typically FBO0. - virtual intptr_t GLContextFBO() const = 0; + virtual intptr_t GLContextFBO(GLFrameInfo frame_info) const = 0; // The rendering subsystem assumes that the ID of the main window bound // framebuffer remains constant throughout. If this assumption in incorrect, // embedders are required to return true from this method. In such cases, - // GLContextFBO() will be called again to acquire the new FBO ID for rendering - // subsequent frames. + // GLContextFBO(frame_info) will be called again to acquire the new FBO ID for + // rendering subsequent frames. virtual bool GLContextFBOResetAfterPresent() const; // Indicates whether or not the surface supports pixel readback as used in diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 308c70af691e4..eed010506b3b9 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -153,6 +153,8 @@ android_java_sources = [ "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", + "io/flutter/embedding/engine/loader/ApplicationInfoLoader.java", + "io/flutter/embedding/engine/loader/FlutterApplicationInfo.java", "io/flutter/embedding/engine/loader/FlutterLoader.java", "io/flutter/embedding/engine/loader/ResourceExtractor.java", "io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java", @@ -430,8 +432,10 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/PluginComponentTest.java", "test/io/flutter/embedding/engine/RenderingComponentTest.java", "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", + "test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java", "test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java", "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", + "test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java", "test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java", "test/io/flutter/external/FlutterLaunchTests.java", "test/io/flutter/plugin/common/StandardMessageCodecTest.java", diff --git a/shell/platform/android/android_external_texture_gl.cc b/shell/platform/android/android_external_texture_gl.cc index 8e071ffba2420..ae5101012f78b 100644 --- a/shell/platform/android/android_external_texture_gl.cc +++ b/shell/platform/android/android_external_texture_gl.cc @@ -7,6 +7,7 @@ #include #include "third_party/skia/include/gpu/GrBackendSurface.h" +#include "third_party/skia/include/gpu/GrDirectContext.h" namespace flutter { @@ -54,8 +55,8 @@ void AndroidExternalTextureGL::Paint(SkCanvas& canvas, GL_RGBA8_OES}; GrBackendTexture backendTexture(1, 1, GrMipMapped::kNo, textureInfo); sk_sp image = SkImage::MakeFromTexture( - canvas.getGrContext(), backendTexture, kTopLeft_GrSurfaceOrigin, - kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr); + context, backendTexture, kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType, + kPremul_SkAlphaType, nullptr); if (image) { SkAutoCanvasRestore autoRestore(&canvas, true); canvas.translate(bounds.x(), bounds.y()); diff --git a/shell/platform/android/android_shell_holder.cc b/shell/platform/android/android_shell_holder.cc index c9667dcb7edf3..7272571d71a4f 100644 --- a/shell/platform/android/android_shell_holder.cc +++ b/shell/platform/android/android_shell_holder.cc @@ -22,10 +22,10 @@ namespace flutter { -static WindowData GetDefaultWindowData() { - WindowData window_data; - window_data.lifecycle_state = "AppLifecycleState.detached"; - return window_data; +static PlatformData GetDefaultPlatformData() { + PlatformData platform_data; + platform_data.lifecycle_state = "AppLifecycleState.detached"; + return platform_data; } bool AndroidShellHolder::use_embedded_view; @@ -81,8 +81,7 @@ AndroidShellHolder::AndroidShellHolder( }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); + return std::make_unique(shell); }; // The current thread will be used as the platform thread. Ensure that the @@ -121,9 +120,9 @@ AndroidShellHolder::AndroidShellHolder( ); shell_ = - Shell::Create(task_runners, // task runners - GetDefaultWindowData(), // window data - settings_, // settings + Shell::Create(task_runners, // task runners + GetDefaultPlatformData(), // window data + settings_, // settings on_create_platform_view, // platform view create callback on_create_rasterizer // rasterizer create callback ); @@ -137,9 +136,9 @@ AndroidShellHolder::AndroidShellHolder( ); shell_ = - Shell::Create(task_runners, // task runners - GetDefaultWindowData(), // window data - settings_, // settings + Shell::Create(task_runners, // task runners + GetDefaultPlatformData(), // window data + settings_, // settings on_create_platform_view, // platform view create callback on_create_rasterizer // rasterizer create callback ); diff --git a/shell/platform/android/android_shell_holder.h b/shell/platform/android/android_shell_holder.h index 6fb6695801733..28ad8610882f7 100644 --- a/shell/platform/android/android_shell_holder.h +++ b/shell/platform/android/android_shell_holder.h @@ -10,7 +10,7 @@ #include "flutter/fml/macros.h" #include "flutter/fml/unique_fd.h" #include "flutter/lib/ui/window/viewport_metrics.h" -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" #include "flutter/shell/common/run_configuration.h" #include "flutter/shell/common/shell.h" #include "flutter/shell/common/thread_host.h" diff --git a/shell/platform/android/android_surface_gl.cc b/shell/platform/android/android_surface_gl.cc index a3356653df9a8..7f692f007634a 100644 --- a/shell/platform/android/android_surface_gl.cc +++ b/shell/platform/android/android_surface_gl.cc @@ -115,7 +115,7 @@ bool AndroidSurfaceGL::GLContextPresent() { return onscreen_surface_->SwapBuffers(); } -intptr_t AndroidSurfaceGL::GLContextFBO() const { +intptr_t AndroidSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { FML_DCHECK(IsValid()); // The default window bound framebuffer on Android. return 0; diff --git a/shell/platform/android/android_surface_gl.h b/shell/platform/android/android_surface_gl.h index 281a273351908..1ebbf8f60b1a5 100644 --- a/shell/platform/android/android_surface_gl.h +++ b/shell/platform/android/android_surface_gl.h @@ -59,7 +59,7 @@ class AndroidSurfaceGL final : public GPUSurfaceGLDelegate, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| ExternalViewEmbedder* GetExternalViewEmbedder() override; diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.cc b/shell/platform/android/external_view_embedder/external_view_embedder.cc index 9a32dc171eabf..b5479284aa0ef 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder.cc @@ -295,4 +295,9 @@ void AndroidExternalViewEmbedder::EndFrame( } } +// |ExternalViewEmbedder| +bool AndroidExternalViewEmbedder::SupportsDynamicThreadMerging() { + return true; +} + } // namespace flutter diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.h b/shell/platform/android/external_view_embedder/external_view_embedder.h index 9e827b83b0ee0..ce755f252a8ac 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.h +++ b/shell/platform/android/external_view_embedder/external_view_embedder.h @@ -70,6 +70,8 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { bool should_resubmit_frame, fml::RefPtr raster_thread_merger) override; + bool SupportsDynamicThreadMerging() override; + // Gets the rect based on the device pixel ratio of a platform view displayed // on the screen. SkRect GetViewRect(int view_id) const; diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index 93356347ae322..bdb8ca1c6c286 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -556,5 +556,13 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { raster_thread_merger); } +TEST(AndroidExternalViewEmbedder, SupportsDynamicThreadMerging) { + auto jni_mock = std::make_shared(); + + auto embedder = + std::make_unique(nullptr, jni_mock, nullptr); + ASSERT_TRUE(embedder->SupportsDynamicThreadMerging()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 92d7586a32a70..0d0c0d203bc8b 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -397,10 +397,9 @@ private void ensureFlutterFragmentCreated() { */ @NonNull protected FlutterFragment createFlutterFragment() { - BackgroundMode backgroundMode = getBackgroundMode(); - RenderMode renderMode = - backgroundMode == BackgroundMode.opaque ? RenderMode.surface : RenderMode.texture; - TransparencyMode transparencyMode = + final BackgroundMode backgroundMode = getBackgroundMode(); + final RenderMode renderMode = getRenderMode(); + final TransparencyMode transparencyMode = backgroundMode == BackgroundMode.opaque ? TransparencyMode.opaque : TransparencyMode.transparent; @@ -690,6 +689,19 @@ protected BackgroundMode getBackgroundMode() { } } + /** + * Returns the desired {@link RenderMode} for the {@link FlutterView} displayed in this {@code + * FlutterFragmentActivity}. + * + *

That is, {@link RenderMode#surface} if {@link FlutterFragmentActivity#getBackgroundMode()} + * is {@link BackgroundMode.opaque} or {@link RenderMode#texture} otherwise. + */ + @NonNull + protected RenderMode getRenderMode() { + final BackgroundMode backgroundMode = getBackgroundMode(); + return backgroundMode == BackgroundMode.opaque ? RenderMode.surface : RenderMode.texture; + } + /** * Returns true if Flutter is running in "debug mode", and false otherwise. * diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 76b0f2461141c..860d9c4786025 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1063,7 +1063,7 @@ public FlutterEngine getAttachedFlutterEngine() { } /** - * Adds a {@link FlutterEngineAttachmentListener}, which is notifed whenever this {@code + * Adds a {@link FlutterEngineAttachmentListener}, which is notified whenever this {@code * FlutterView} attached to/detaches from a {@link FlutterEngine}. */ @VisibleForTesting diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java new file mode 100644 index 0000000000000..c29ddd9f21a9c --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java @@ -0,0 +1,161 @@ +// 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. + +package io.flutter.embedding.engine.loader; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.security.NetworkSecurityPolicy; +import androidx.annotation.NonNull; +import java.io.IOException; +import org.json.JSONArray; +import org.xmlpull.v1.XmlPullParserException; + +/** Loads application information given a Context. */ +final class ApplicationInfoLoader { + // XML Attribute keys supported in AndroidManifest.xml + static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = + FlutterLoader.class.getName() + '.' + FlutterLoader.AOT_SHARED_LIBRARY_NAME; + static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.VM_SNAPSHOT_DATA_KEY; + static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.ISOLATE_SNAPSHOT_DATA_KEY; + static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.FLUTTER_ASSETS_DIR_KEY; + static final String NETWORK_POLICY_METADATA_KEY = "io.flutter.network-policy"; + + @NonNull + private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { + try { + return applicationContext + .getPackageManager() + .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + private static String getString(Bundle metadata, String key) { + if (metadata == null) { + return null; + } + return metadata.getString(key, null); + } + + private static String getNetworkPolicy(ApplicationInfo appInfo, Context context) { + // We cannot use reflection to look at networkSecurityConfigRes because + // Android throws an error when we try to access fields marked as @hide. + // Instead we rely on metadata. + Bundle metadata = appInfo.metaData; + if (metadata == null) { + return null; + } + + int networkSecurityConfigRes = metadata.getInt(NETWORK_POLICY_METADATA_KEY, 0); + if (networkSecurityConfigRes <= 0) { + return null; + } + + JSONArray output = new JSONArray(); + try { + XmlResourceParser xrp = context.getResources().getXml(networkSecurityConfigRes); + xrp.next(); + int eventType = xrp.getEventType(); + while (eventType != XmlResourceParser.END_DOCUMENT) { + if (eventType == XmlResourceParser.START_TAG) { + if (xrp.getName().equals("domain-config")) { + parseDomainConfig(xrp, output, false); + } + } + eventType = xrp.next(); + } + } catch (IOException | XmlPullParserException e) { + return null; + } + return output.toString(); + } + + private static boolean getUseEmbeddedView(ApplicationInfo appInfo) { + Bundle bundle = appInfo.metaData; + return bundle != null && bundle.getBoolean("io.flutter.embedded_views_preview"); + } + + private static void parseDomainConfig( + XmlResourceParser xrp, JSONArray output, boolean inheritedCleartextPermitted) + throws IOException, XmlPullParserException { + boolean cleartextTrafficPermitted = + xrp.getAttributeBooleanValue( + null, "cleartextTrafficPermitted", inheritedCleartextPermitted); + while (true) { + int eventType = xrp.next(); + if (eventType == XmlResourceParser.START_TAG) { + if (xrp.getName().equals("domain")) { + // There can be multiple domains. + parseDomain(xrp, output, cleartextTrafficPermitted); + } else if (xrp.getName().equals("domain-config")) { + parseDomainConfig(xrp, output, cleartextTrafficPermitted); + } else { + skipTag(xrp); + } + } else if (eventType == XmlResourceParser.END_TAG) { + break; + } + } + } + + private static void skipTag(XmlResourceParser xrp) throws IOException, XmlPullParserException { + String name = xrp.getName(); + int eventType = xrp.getEventType(); + while (eventType != XmlResourceParser.END_TAG || xrp.getName() != name) { + eventType = xrp.next(); + } + } + + private static void parseDomain( + XmlResourceParser xrp, JSONArray output, boolean cleartextPermitted) + throws IOException, XmlPullParserException { + boolean includeSubDomains = xrp.getAttributeBooleanValue(null, "includeSubdomains", false); + xrp.next(); + if (xrp.getEventType() != XmlResourceParser.TEXT) { + throw new IllegalStateException("Expected text"); + } + String domain = xrp.getText().trim(); + JSONArray outputArray = new JSONArray(); + outputArray.put(domain); + outputArray.put(includeSubDomains); + outputArray.put(cleartextPermitted); + output.put(outputArray); + xrp.next(); + if (xrp.getEventType() != XmlResourceParser.END_TAG) { + throw new IllegalStateException("Expected end of domain tag"); + } + } + + /** + * Initialize our Flutter config values by obtaining them from the manifest XML file, falling back + * to default values. + */ + @NonNull + public static FlutterApplicationInfo load(@NonNull Context applicationContext) { + ApplicationInfo appInfo = getApplicationInfo(applicationContext); + // Prior to API 23, cleartext traffic is allowed. + boolean clearTextPermitted = true; + if (android.os.Build.VERSION.SDK_INT >= 23) { + clearTextPermitted = NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(); + } + + return new FlutterApplicationInfo( + getString(appInfo.metaData, PUBLIC_AOT_SHARED_LIBRARY_NAME), + getString(appInfo.metaData, PUBLIC_VM_SNAPSHOT_DATA_KEY), + getString(appInfo.metaData, PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY), + getString(appInfo.metaData, PUBLIC_FLUTTER_ASSETS_DIR_KEY), + getNetworkPolicy(appInfo, applicationContext), + appInfo.nativeLibraryDir, + clearTextPermitted, + getUseEmbeddedView(appInfo)); + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java new file mode 100644 index 0000000000000..3d5c2b10c40d6 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java @@ -0,0 +1,46 @@ +// 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. + +package io.flutter.embedding.engine.loader; + +/** Encapsulates all the information that Flutter needs from application manifest. */ +public final class FlutterApplicationInfo { + private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; + private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; + private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; + private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; + + final String aotSharedLibraryName; + final String vmSnapshotData; + final String isolateSnapshotData; + final String flutterAssetsDir; + final String domainNetworkPolicy; + final String nativeLibraryDir; + final boolean clearTextPermitted; + // TODO(cyanlaz): Remove this when dynamic thread merging is done. + // https://github.com/flutter/flutter/issues/59930 + final boolean useEmbeddedView; + + public FlutterApplicationInfo( + String aotSharedLibraryName, + String vmSnapshotData, + String isolateSnapshotData, + String flutterAssetsDir, + String domainNetworkPolicy, + String nativeLibraryDir, + boolean clearTextPermitted, + boolean useEmbeddedView) { + this.aotSharedLibraryName = + aotSharedLibraryName == null ? DEFAULT_AOT_SHARED_LIBRARY_NAME : aotSharedLibraryName; + this.vmSnapshotData = vmSnapshotData == null ? DEFAULT_VM_SNAPSHOT_DATA : vmSnapshotData; + this.isolateSnapshotData = + isolateSnapshotData == null ? DEFAULT_ISOLATE_SNAPSHOT_DATA : isolateSnapshotData; + this.flutterAssetsDir = + flutterAssetsDir == null ? DEFAULT_FLUTTER_ASSETS_DIR : flutterAssetsDir; + this.nativeLibraryDir = nativeLibraryDir; + this.domainNetworkPolicy = domainNetworkPolicy == null ? "" : domainNetworkPolicy; + this.clearTextPermitted = clearTextPermitted; + this.useEmbeddedView = useEmbeddedView; + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 9156a64ef061e..2aee56dee94e3 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -5,10 +5,8 @@ package io.flutter.embedding.engine.loader; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; -import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -31,35 +29,15 @@ public class FlutterLoader { private static final String TAG = "FlutterLoader"; // Must match values in flutter::switches - private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; - private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; - private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; - private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; - private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; - - // XML Attribute keys supported in AndroidManifest.xml - private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = - FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; - private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; - private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY; - private static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = - FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY; + static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; + static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; + static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; + static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; + static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; // Resource names used for components of the precompiled snapshot. - private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; - private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; - private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; private static final String DEFAULT_LIBRARY = "libflutter.so"; private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; - private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; - - // Mutable because default values can be overridden via config properties - private String aotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; - private String vmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; - private String isolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; - private String flutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; private static FlutterLoader instance; @@ -78,9 +56,17 @@ public static FlutterLoader getInstance() { return instance; } + @NonNull + public static FlutterLoader getInstanceForTest(FlutterApplicationInfo flutterApplicationInfo) { + FlutterLoader loader = new FlutterLoader(); + loader.flutterApplicationInfo = flutterApplicationInfo; + return loader; + } + private boolean initialized = false; @Nullable private Settings settings; private long initStartTimestampMillis; + private FlutterApplicationInfo flutterApplicationInfo; private static class InitResult { final String appStoragePath; @@ -131,7 +117,7 @@ public void startInitialization(@NonNull Context applicationContext, @NonNull Se this.settings = settings; initStartTimestampMillis = SystemClock.uptimeMillis(); - initConfig(appContext); + flutterApplicationInfo = ApplicationInfoLoader.load(appContext); VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE)) .init(); @@ -195,10 +181,9 @@ public void ensureInitializationComplete( List shellArgs = new ArrayList<>(); shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); - ApplicationInfo applicationInfo = getApplicationInfo(applicationContext); shellArgs.add( "--icu-native-lib-path=" - + applicationInfo.nativeLibraryDir + + flutterApplicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); if (args != null) { @@ -207,13 +192,16 @@ public void ensureInitializationComplete( String kernelPath = null; if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { - String snapshotAssetPath = result.dataDirPath + File.separator + flutterAssetsDir; + String snapshotAssetPath = + result.dataDirPath + File.separator + flutterApplicationInfo.flutterAssetsDir; kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); - shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData); - shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData); + shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.vmSnapshotData); + shellArgs.add( + "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.isolateSnapshotData); } else { - shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName); + shellArgs.add( + "--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName); // Most devices can load the AOT shared library based on the library name // with no directory path. Provide a fully qualified path to the library @@ -222,28 +210,28 @@ public void ensureInitializationComplete( "--" + AOT_SHARED_LIBRARY_NAME + "=" - + applicationInfo.nativeLibraryDir + + flutterApplicationInfo.nativeLibraryDir + File.separator - + aotSharedLibraryName); + + flutterApplicationInfo.aotSharedLibraryName); } shellArgs.add("--cache-dir-path=" + result.engineCachesPath); + // TODO(mehmetf): Announce this since it is a breaking change then enable it. + // if (!flutterApplicationInfo.clearTextPermitted) { + // shellArgs.add("--disallow-insecure-connections"); + // } + if (flutterApplicationInfo.domainNetworkPolicy != null) { + shellArgs.add("--domain-network-policy=" + flutterApplicationInfo.domainNetworkPolicy); + } + if (flutterApplicationInfo.useEmbeddedView) { + shellArgs.add("--use-embedded-view"); + } if (settings.getLogTag() != null) { shellArgs.add("--log-tag=" + settings.getLogTag()); } long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; - // TODO(cyanlaz): Remove this when dynamic thread merging is done. - // https://github.com/flutter/flutter/issues/59930 - Bundle bundle = applicationInfo.metaData; - if (bundle != null) { - boolean use_embedded_view = bundle.getBoolean("io.flutter.embedded_views_preview"); - if (use_embedded_view) { - shellArgs.add("--use-embedded-view"); - } - } - FlutterJNI.nativeInit( applicationContext, shellArgs.toArray(new String[0]), @@ -306,40 +294,6 @@ public void run() { }); } - @NonNull - private ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { - try { - return applicationContext - .getPackageManager() - .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException(e); - } - } - - /** - * Initialize our Flutter config values by obtaining them from the manifest XML file, falling back - * to default values. - */ - private void initConfig(@NonNull Context applicationContext) { - Bundle metadata = getApplicationInfo(applicationContext).metaData; - - // There isn't a `` tag as a direct child of `` in - // `AndroidManifest.xml`. - if (metadata == null) { - return; - } - - aotSharedLibraryName = - metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); - flutterAssetsDir = - metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); - - vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); - isolateSnapshotData = - metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); - } - /** Extract assets out of the APK that need to be cached as uncompressed files on disk. */ private ResourceExtractor initResources(@NonNull Context applicationContext) { ResourceExtractor resourceExtractor = null; @@ -354,8 +308,8 @@ private ResourceExtractor initResources(@NonNull Context applicationContext) { // In debug/JIT mode these assets will be written to disk and then // mapped into memory so they can be provided to the Dart VM. resourceExtractor - .addResource(fullAssetPathFrom(vmSnapshotData)) - .addResource(fullAssetPathFrom(isolateSnapshotData)) + .addResource(fullAssetPathFrom(flutterApplicationInfo.vmSnapshotData)) + .addResource(fullAssetPathFrom(flutterApplicationInfo.isolateSnapshotData)) .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB)); resourceExtractor.start(); @@ -365,7 +319,7 @@ private ResourceExtractor initResources(@NonNull Context applicationContext) { @NonNull public String findAppBundlePath() { - return flutterAssetsDir; + return flutterApplicationInfo.flutterAssetsDir; } /** @@ -396,7 +350,7 @@ public String getLookupKeyForAsset(@NonNull String asset, @NonNull String packag @NonNull private String fullAssetPathFrom(@NonNull String filePath) { - return flutterAssetsDir + File.separator + filePath; + return flutterApplicationInfo.flutterAssetsDir + File.separator + filePath; } public static class Settings { diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java index ddb39fd0e5093..06833cbe2c225 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -30,7 +30,7 @@ public class PlatformChannel { @Nullable private PlatformMessageHandler platformMessageHandler; @NonNull @VisibleForTesting - protected final MethodChannel.MethodCallHandler parsingMethodCallHandler = + final MethodChannel.MethodCallHandler parsingMethodCallHandler = new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { @@ -155,6 +155,14 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result.success(null); break; } + case "Clipboard.hasStrings": + { + boolean hasStrings = platformMessageHandler.clipboardHasStrings(); + JSONObject response = new JSONObject(); + response.put("value", hasStrings); + result.success(response); + break; + } default: result.notImplemented(); break; @@ -426,6 +434,12 @@ public interface PlatformMessageHandler { * {@code text}. */ void setClipboardData(@NonNull String text); + + /** + * The Flutter application would like to know if the clipboard currently contains a string that + * can be pasted. + */ + boolean clipboardHasStrings(); } /** Types of sounds the Android OS can play on behalf of an application. */ diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index 8cb778daf1167..b05921c84bb1b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -1,6 +1,7 @@ package io.flutter.embedding.engine.systemchannels; import android.os.Build; +import android.os.Bundle; import android.view.View; import android.view.inputmethod.EditorInfo; import androidx.annotation.NonNull; @@ -13,6 +14,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -112,6 +114,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result textInputMethodHandler.clearClient(); result.success(null); break; + case "TextInput.sendAppPrivateCommand": + try { + final JSONObject arguments = (JSONObject) args; + final String action = arguments.getString("action"); + final String data = arguments.getString("data"); + Bundle bundle = null; + if (data != null && !data.isEmpty()) { + bundle = new Bundle(); + bundle.putString("data", data); + } + textInputMethodHandler.sendAppPrivateCommand(action, bundle); + result.success(null); + } catch (JSONException exception) { + result.error("error", exception.getMessage(), null); + } + break; case "TextInput.finishAutofillContext": textInputMethodHandler.finishAutofillContext((boolean) args); result.success(null); @@ -266,6 +284,38 @@ public void unspecifiedAction(int inputClientId) { Arrays.asList(inputClientId, "TextInputAction.unspecified")); } + public void performPrivateCommand(int inputClientId, String action, Bundle data) { + HashMap json = new HashMap<>(); + json.put("action", action); + if (data != null) { + HashMap dataMap = new HashMap<>(); + Set keySet = data.keySet(); + for (String key : keySet) { + Object value = data.get(key); + if (value instanceof byte[]) { + dataMap.put(key, data.getByteArray(key)); + } else if (value instanceof Byte) { + dataMap.put(key, data.getByte(key)); + } else if (value instanceof char[]) { + dataMap.put(key, data.getCharArray(key)); + } else if (value instanceof Character) { + dataMap.put(key, data.getChar(key)); + } else if (value instanceof CharSequence[]) { + dataMap.put(key, data.getCharSequenceArray(key)); + } else if (value instanceof CharSequence) { + dataMap.put(key, data.getCharSequence(key)); + } else if (value instanceof float[]) { + dataMap.put(key, data.getFloatArray(key)); + } else if (value instanceof Float) { + dataMap.put(key, data.getFloat(key)); + } + } + json.put("data", dataMap); + } + channel.invokeMethod( + "TextInputClient.performPrivateCommand", Arrays.asList(inputClientId, json)); + } + /** * Sets the {@link TextInputMethodHandler} which receives all events and requests that are parsed * from the underlying platform channel. @@ -328,6 +378,17 @@ public interface TextInputMethodHandler { // TODO(mattcarroll): javadoc void clearClient(); + + /** + * Sends client app private command to the current text input client(input method). The app + * private command result will be informed through {@code performPrivateCommand}. + * + * @param action Name of the command to be performed. This must be a scoped name. i.e. prefixed + * with a package name you own, so that different developers will not create conflicting + * commands. + * @param data Any data to include with the command. + */ + void sendAppPrivateCommand(String action, Bundle data); } /** A text editing configuration. */ diff --git a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java index 7b0acfae88747..c1a9a51c00596 100644 --- a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java @@ -70,6 +70,17 @@ public ByteBuffer encodeErrorEnvelope( .put(JSONUtil.wrap(errorDetails))); } + @Override + public ByteBuffer encodeErrorEnvelopeWithStacktrace( + String errorCode, String errorMessage, Object errorDetails, String errorStacktrace) { + return JSONMessageCodec.INSTANCE.encodeMessage( + new JSONArray() + .put(errorCode) + .put(JSONUtil.wrap(errorMessage)) + .put(JSONUtil.wrap(errorDetails)) + .put(JSONUtil.wrap(errorStacktrace))); + } + @Override public Object decodeEnvelope(ByteBuffer envelope) { try { diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index 41dbae9c9f8dd..81e50e3b938c3 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -11,6 +11,9 @@ import io.flutter.BuildConfig; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; import java.nio.ByteBuffer; /** @@ -90,7 +93,7 @@ public void invokeMethod(@NonNull String method, @Nullable Object arguments) { * @param callback a {@link Result} callback for the invocation result, or null. */ @UiThread - public void invokeMethod(String method, @Nullable Object arguments, Result callback) { + public void invokeMethod(String method, @Nullable Object arguments, @Nullable Result callback) { messenger.send( name, codec.encodeMethodCall(new MethodCall(method, arguments)), @@ -247,8 +250,16 @@ public void notImplemented() { }); } catch (RuntimeException e) { Log.e(TAG + name, "Failed to handle method call", e); - reply.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); + reply.reply( + codec.encodeErrorEnvelopeWithStacktrace( + "error", e.getMessage(), null, getStackTrace(e))); } } + + private String getStackTrace(Exception e) { + Writer result = new StringWriter(); + e.printStackTrace(new PrintWriter(result)); + return result.toString(); + } } } diff --git a/shell/platform/android/io/flutter/plugin/common/MethodCodec.java b/shell/platform/android/io/flutter/plugin/common/MethodCodec.java index fba950f9a499c..f958a5307ae9c 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodCodec.java @@ -55,6 +55,20 @@ public interface MethodCodec { */ ByteBuffer encodeErrorEnvelope(String errorCode, String errorMessage, Object errorDetails); + /** + * Encodes an error result into a binary envelope message with the native stacktrace. + * + * @param errorCode An error code String. + * @param errorMessage An error message String, possibly null. + * @param errorDetails Error details, possibly null. Consider supporting {@link Throwable} in your + * codec. This is the most common value passed to this field. + * @param errorStacktrace Platform stacktrace for the error. possibly null. + * @return a {@link ByteBuffer} containing the encoding between position 0 and the current + * position. + */ + ByteBuffer encodeErrorEnvelopeWithStacktrace( + String errorCode, String errorMessage, Object errorDetails, String errorStacktrace); + /** * Decodes a result envelope from binary. * diff --git a/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java b/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java index 913001f5b2bcf..942bd0e99bf29 100644 --- a/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java @@ -79,6 +79,24 @@ public ByteBuffer encodeErrorEnvelope( return buffer; } + @Override + public ByteBuffer encodeErrorEnvelopeWithStacktrace( + String errorCode, String errorMessage, Object errorDetails, String errorStacktrace) { + final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream(); + stream.write(1); + messageCodec.writeValue(stream, errorCode); + messageCodec.writeValue(stream, errorMessage); + if (errorDetails instanceof Throwable) { + messageCodec.writeValue(stream, getStackTrace((Throwable) errorDetails)); + } else { + messageCodec.writeValue(stream, errorDetails); + } + messageCodec.writeValue(stream, errorStacktrace); + final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size()); + buffer.put(stream.buffer(), 0, stream.size()); + return buffer; + } + @Override public Object decodeEnvelope(ByteBuffer envelope) { envelope.order(ByteOrder.nativeOrder()); diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 29b3fb859e1ef..ab4ed5c6b1d82 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -9,6 +9,7 @@ import android.content.ClipboardManager; import android.content.Context; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.text.DynamicLayout; import android.text.Editable; @@ -477,6 +478,12 @@ public boolean performContextMenuAction(int id) { return false; } + @Override + public boolean performPrivateCommand(String action, Bundle data) { + textInputChannel.performPrivateCommand(mClient, action, data); + return true; + } + @Override public boolean performEditorAction(int actionCode) { markDirty(); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 1aea8fbb279e3..1a635f13acdc5 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -8,6 +8,7 @@ import android.content.Context; import android.graphics.Rect; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.text.Editable; import android.text.InputType; @@ -119,6 +120,11 @@ public void setEditableSizeAndTransform(double width, double height, double[] tr public void clearClient() { clearTextInputClient(); } + + @Override + public void sendAppPrivateCommand(String action, Bundle data) { + sendTextInputAppPrivateCommand(action, data); + } }); textInputChannel.requestExistingInputState(); @@ -303,6 +309,10 @@ public void clearPlatformViewClient(int platformViewId) { } } + public void sendTextInputAppPrivateCommand(String action, Bundle data) { + mImm.sendAppPrivateCommand(mView, action, data); + } + private void showTextInput(View view) { view.requestFocus(); mImm.showSoftInput(view, 0); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 38f2b275fd275..cc87aa2eaf254 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -30,7 +30,8 @@ public class PlatformPlugin { private PlatformChannel.SystemChromeStyle currentTheme; private int mEnabledOverlays; - private final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = + @VisibleForTesting + final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = new PlatformChannel.PlatformMessageHandler() { @Override public void playSystemSound(@NonNull PlatformChannel.SoundType soundType) { @@ -85,6 +86,14 @@ public CharSequence getClipboardData( public void setClipboardData(@NonNull String text) { PlatformPlugin.this.setClipboardData(text); } + + @Override + public boolean clipboardHasStrings() { + CharSequence data = + PlatformPlugin.this.getClipboardData( + PlatformChannel.ClipboardContentFormat.PLAIN_TEXT); + return data != null && data.length() > 0; + } }; public PlatformPlugin(Activity activity, PlatformChannel platformChannel) { diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 7c064a6f90818..75d724605c69d 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -17,6 +17,7 @@ import android.view.View; import android.widget.FrameLayout; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.android.AndroidTouchProcessor; @@ -79,9 +80,20 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // it is associated with(e.g if a platform view creates other views in the same virtual display. private final HashMap contextToPlatformView; - private final SparseArray platformViewRequests; + // The views returned by `PlatformView#getView()`. + // + // This only applies to hybrid composition. private final SparseArray platformViews; - private final SparseArray mutatorViews; + + // The platform view parents that are appended to `FlutterView`. + // If an entry in `platformViews` doesn't have an entry in this array, the platform view isn't + // in the view hierarchy. + // + // This view provides a wrapper that applies scene builder operations to the platform view. + // For example, a transform matrix, or setting opacity on the platform view layer. + // + // This is only applies to hybrid composition. + private final SparseArray platformViewParent; // Map of unique IDs to views that render overlay layers. private final SparseArray overlayLayerViews; @@ -107,25 +119,57 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega @Override public void createAndroidViewForPlatformView( @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { - // API level 19 is required for android.graphics.ImageReader. + // API level 19 is required for `android.graphics.ImageReader`. ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT); - platformViewRequests.put(request.viewId, request); + + if (!validateDirection(request.direction)) { + throw new IllegalStateException( + "Trying to create a view with unknown direction value: " + + request.direction + + "(view id: " + + request.viewId + + ")"); + } + + final PlatformViewFactory factory = registry.getFactory(request.viewType); + if (factory == null) { + throw new IllegalStateException( + "Trying to create a platform view of unregistered type: " + request.viewType); + } + + Object createParams = null; + if (request.params != null) { + createParams = factory.getCreateArgsCodec().decodeMessage(request.params); + } + + final PlatformView platformView = factory.create(context, request.viewId, createParams); + final View view = platformView.getView(); + if (view == null) { + throw new IllegalStateException( + "PlatformView#getView() returned null, but an Android view reference was expected."); + } + if (view.getParent() != null) { + throw new IllegalStateException( + "The Android view returned from PlatformView#getView() was already added to a parent view."); + } + platformViews.put(request.viewId, view); } @Override public void disposeAndroidViewForPlatformView(int viewId) { // Hybrid view. - if (platformViewRequests.get(viewId) != null) { - platformViewRequests.remove(viewId); - } - final View platformView = platformViews.get(viewId); + final FlutterMutatorView parentView = platformViewParent.get(viewId); if (platformView != null) { - final FlutterMutatorView mutatorView = mutatorViews.get(viewId); - mutatorView.removeView(platformView); - ((FlutterView) flutterView).removeView(mutatorView); + if (parentView != null) { + parentView.removeView(platformView); + } platformViews.remove(viewId); - mutatorViews.remove(viewId); + } + + if (parentView != null) { + ((FlutterView) flutterView).removeView(parentView); + platformViewParent.remove(viewId); } } @@ -378,9 +422,8 @@ public PlatformViewsController() { currentFrameUsedOverlayLayerIds = new HashSet<>(); currentFrameUsedPlatformViewIds = new HashSet<>(); - platformViewRequests = new SparseArray<>(); platformViews = new SparseArray<>(); - mutatorViews = new SparseArray<>(); + platformViewParent = new SparseArray<>(); motionEventTracker = MotionEventTracker.getInstance(); } @@ -489,7 +532,12 @@ public void detachTextInputPlugin() { * if the view was created in a platform view's VD, delegates the decision to the platform view's * {@link View#checkInputConnectionProxy(View)} method. Else returns false. */ - public boolean checkInputConnectionProxy(View view) { + public boolean checkInputConnectionProxy(@Nullable View view) { + // View can be null on some devices + // See: https://github.com/flutter/flutter/issues/36517 + if (view == null) { + return false; + } if (!contextToPlatformView.containsKey(view.getContext())) { return false; } @@ -651,55 +699,20 @@ private void initializeRootImageViewIfNeeded() { @VisibleForTesting void initializePlatformViewIfNeeded(int viewId) { - if (platformViews.get(viewId) != null) { - return; - } - - PlatformViewsChannel.PlatformViewCreationRequest request = platformViewRequests.get(viewId); - if (request == null) { - throw new IllegalStateException( - "Platform view hasn't been initialized from the platform view channel."); - } - - if (!validateDirection(request.direction)) { - throw new IllegalStateException( - "Trying to create a view with unknown direction value: " - + request.direction - + "(view id: " - + viewId - + ")"); - } - - PlatformViewFactory factory = registry.getFactory(request.viewType); - if (factory == null) { - throw new IllegalStateException( - "Trying to create a platform view of unregistered type: " + request.viewType); - } - - Object createParams = null; - if (request.params != null) { - createParams = factory.getCreateArgsCodec().decodeMessage(request.params); - } - - PlatformView platformView = factory.create(context, viewId, createParams); - View view = platformView.getView(); - + final View view = platformViews.get(viewId); if (view == null) { throw new IllegalStateException( - "PlatformView#getView() returned null, but an Android view reference was expected."); + "Platform view hasn't been initialized from the platform view channel."); } - if (view.getParent() != null) { - throw new IllegalStateException( - "The Android view returned from PlatformView#getView() was already added to a parent view."); + if (platformViewParent.get(viewId) != null) { + return; } - platformViews.put(viewId, view); - - FlutterMutatorView mutatorView = + final FlutterMutatorView parentView = new FlutterMutatorView( context, context.getResources().getDisplayMetrics().density, androidTouchProcessor); - mutatorViews.put(viewId, mutatorView); - mutatorView.addView(view); - ((FlutterView) flutterView).addView(mutatorView); + platformViewParent.put(viewId, parentView); + parentView.addView(view); + ((FlutterView) flutterView).addView(parentView); } public void attachToFlutterRenderer(FlutterRenderer flutterRenderer) { @@ -718,13 +731,14 @@ public void onDisplayPlatformView( initializeRootImageViewIfNeeded(); initializePlatformViewIfNeeded(viewId); - FlutterMutatorView mutatorView = mutatorViews.get(viewId); - mutatorView.readyToDisplay(mutatorsStack, x, y, width, height); - mutatorView.setVisibility(View.VISIBLE); - mutatorView.bringToFront(); + final FlutterMutatorView parentView = platformViewParent.get(viewId); + parentView.readyToDisplay(mutatorsStack, x, y, width, height); + parentView.setVisibility(View.VISIBLE); + parentView.bringToFront(); - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, viewHeight); - View platformView = platformViews.get(viewId); + final FrameLayout.LayoutParams layoutParams = + new FrameLayout.LayoutParams(viewWidth, viewHeight); + final View platformView = platformViews.get(viewId); platformView.setLayoutParams(layoutParams); platformView.bringToFront(); currentFrameUsedPlatformViewIds.add(viewId); @@ -733,7 +747,7 @@ public void onDisplayPlatformView( public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) { initializeRootImageViewIfNeeded(); - FlutterImageView overlayView = overlayLayerViews.get(id); + final FlutterImageView overlayView = overlayLayerViews.get(id); if (overlayView.getParent() == null) { ((FlutterView) flutterView).addView(overlayView); } @@ -776,19 +790,19 @@ public void onEndFrame() { // If one of the surfaces doesn't have an image, the frame may be incomplete and must be // dropped. // For example, a toolbar widget painted by Flutter may not be rendered. - boolean isFrameRenderedUsingImageReaders = + final boolean isFrameRenderedUsingImageReaders = flutterViewConvertedToImageView && view.acquireLatestImageViewFrame(); finishFrame(isFrameRenderedUsingImageReaders); } private void finishFrame(boolean isFrameRenderedUsingImageReaders) { for (int i = 0; i < overlayLayerViews.size(); i++) { - int overlayId = overlayLayerViews.keyAt(i); - FlutterImageView overlayView = overlayLayerViews.valueAt(i); + final int overlayId = overlayLayerViews.keyAt(i); + final FlutterImageView overlayView = overlayLayerViews.valueAt(i); if (currentFrameUsedOverlayLayerIds.contains(overlayId)) { ((FlutterView) flutterView).attachOverlaySurfaceToRender(overlayView); - boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage(); + final boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage(); isFrameRenderedUsingImageReaders &= didAcquireOverlaySurfaceImage; } else { // If the background surface isn't rendered by the image view, then the @@ -802,22 +816,20 @@ private void finishFrame(boolean isFrameRenderedUsingImageReaders) { } } - for (int i = 0; i < platformViews.size(); i++) { - int viewId = platformViews.keyAt(i); - View platformView = platformViews.get(viewId); - View mutatorView = mutatorViews.get(viewId); + for (int i = 0; i < platformViewParent.size(); i++) { + final int viewId = platformViewParent.keyAt(i); + final View parentView = platformViewParent.get(viewId); // Show platform views only if the surfaces have images available in this frame, // and if the platform view is rendered in this frame. + // The platform view is appended to a mutator view. // // Otherwise, hide the platform view, but don't remove it from the view hierarchy yet as // they are removed when the framework diposes the platform view widget. if (isFrameRenderedUsingImageReaders && currentFrameUsedPlatformViewIds.contains(viewId)) { - platformView.setVisibility(View.VISIBLE); - mutatorView.setVisibility(View.VISIBLE); + parentView.setVisibility(View.VISIBLE); } else { - platformView.setVisibility(View.GONE); - mutatorView.setVisibility(View.GONE); + parentView.setVisibility(View.GONE); } } } diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index f6ed0179be89a..b54b76b02b886 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1548,6 +1548,17 @@ private void sendWindowChangeEvent(@NonNull SemanticsNode route) { AccessibilityEvent event = obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); String routeName = route.getRouteName(); + if (routeName == null) { + // The routeName will be null when there is no semantics node that represnets namesRoute in + // the scopeRoute. The TYPE_WINDOW_STATE_CHANGED only works the route name is not null and not + // empty. Gives it a whitespace will make it focus the first semantics node without + // pronouncing any word. + // + // The other way to trigger a focus change is to send a TYPE_VIEW_FOCUSED to the + // rootAccessibilityView. However, it is less predictable which semantics node it will focus + // next. + routeName = " "; + } event.getText().add(routeName); sendAccessibilityEvent(event); } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index f472b07c0753c..36f74850fad2f 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -432,6 +432,7 @@ public void destroy() { if (!isAttached()) return; getHolder().removeCallback(mSurfaceCallback); + releaseAccessibilityNodeProvider(); mNativeView.destroy(); mNativeView = null; @@ -749,9 +750,7 @@ protected void onAttachedToWindow() { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - - mAccessibilityNodeProvider.release(); - mAccessibilityNodeProvider = null; + releaseAccessibilityNodeProvider(); } // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise @@ -776,6 +775,13 @@ public AccessibilityNodeProvider getAccessibilityNodeProvider() { } } + private void releaseAccessibilityNodeProvider() { + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.release(); + mAccessibilityNodeProvider = null; + } + } + @Override @TargetApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N) diff --git a/shell/platform/android/surface/android_surface_mock.cc b/shell/platform/android/surface/android_surface_mock.cc index 5b0e7adad8cdd..f7098f7c56b74 100644 --- a/shell/platform/android/surface/android_surface_mock.cc +++ b/shell/platform/android/surface/android_surface_mock.cc @@ -18,7 +18,7 @@ bool AndroidSurfaceMock::GLContextPresent() { return true; } -intptr_t AndroidSurfaceMock::GLContextFBO() const { +intptr_t AndroidSurfaceMock::GLContextFBO(GLFrameInfo frame_info) const { return 0; } diff --git a/shell/platform/android/surface/android_surface_mock.h b/shell/platform/android/surface/android_surface_mock.h index 688681e01c8c0..7d5fddbf3d922 100644 --- a/shell/platform/android/surface/android_surface_mock.h +++ b/shell/platform/android/surface/android_surface_mock.h @@ -48,7 +48,7 @@ class AndroidSurfaceMock final : public GPUSurfaceGLDelegate, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| ExternalViewEmbedder* GetExternalViewEmbedder() override; diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 075e33c24b53e..3436a14eb601e 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -17,6 +17,7 @@ import io.flutter.embedding.engine.RenderingComponentTest; import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest; import io.flutter.embedding.engine.renderer.FlutterRendererTest; +import io.flutter.embedding.engine.systemchannels.PlatformChannelTest; import io.flutter.embedding.engine.systemchannels.RestorationChannelTest; import io.flutter.external.FlutterLaunchTests; import io.flutter.plugin.common.StandardMessageCodecTest; @@ -68,6 +69,7 @@ TextInputPluginTest.class, MouseCursorPluginTest.class, AccessibilityBridgeTest.class, + PlatformChannelTest.class, RestorationChannelTest.class, }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java index 39384aca2e716..88ef48cbdd8e9 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java @@ -1,8 +1,10 @@ package io.flutter.embedding.android; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import android.content.Intent; import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -11,17 +13,63 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class FlutterFragmentActivityTest { + + @Test + public void createFlutterFragment__defaultRenderModeSurface() { + final FlutterFragmentActivity activity = new FakeFlutterFragmentActivity(); + assertEquals(activity.createFlutterFragment().getRenderMode(), RenderMode.surface); + } + @Test - public void placeholder() { - // This is just a placeholder since this file only has a compile check currently. - // Delete when adding the first real test. - assertTrue(true); + public void createFlutterFragment__defaultRenderModeTexture() { + final FlutterFragmentActivity activity = + new FakeFlutterFragmentActivity() { + @Override + protected BackgroundMode getBackgroundMode() { + return BackgroundMode.transparent; + } + }; + assertEquals(activity.createFlutterFragment().getRenderMode(), RenderMode.texture); + } + + @Test + public void createFlutterFragment__customRenderMode() { + final FlutterFragmentActivity activity = + new FakeFlutterFragmentActivity() { + @Override + protected RenderMode getRenderMode() { + return RenderMode.texture; + } + }; + assertEquals(activity.createFlutterFragment().getRenderMode(), RenderMode.texture); + } + + private static class FakeFlutterFragmentActivity extends FlutterFragmentActivity { + @Override + public Intent getIntent() { + return new Intent(); + } + + @Override + public String getDartEntrypointFunctionName() { + return ""; + } + + @Override + protected String getInitialRoute() { + return ""; + } + + @Override + protected String getAppBundlePath() { + return ""; + } } // This is just a compile time check to ensure that it's possible for FlutterFragmentActivity // subclasses // to provide their own intent builders which builds their own runtime types. - static class FlutterFragmentActivityWithIntentBuilders extends FlutterFragmentActivity { + private static class FlutterFragmentActivityWithIntentBuilders extends FlutterFragmentActivity { public static NewEngineIntentBuilder withNewEngine() { return new NewEngineIntentBuilder(FlutterFragmentActivityWithIntentBuilders.class); } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java index f4860fd62e845..01d012c38b83a 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterApplicationInfo; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.FlutterPlugin; import org.junit.Test; @@ -26,6 +27,8 @@ public void pluginsCanAccessFlutterAssetPaths() { // Setup test. FlutterJNI flutterJNI = mock(FlutterJNI.class); when(flutterJNI.isAttached()).thenReturn(true); + FlutterApplicationInfo emptyInfo = + new FlutterApplicationInfo(null, null, null, null, null, null, false, false); // FlutterLoader is the object to which the PluginRegistry defers for obtaining // the path to a Flutter asset. Ideally in this component test we would use a @@ -44,7 +47,8 @@ public void pluginsCanAccessFlutterAssetPaths() { public String answer(InvocationOnMock invocation) throws Throwable { // Defer to a real FlutterLoader to return the asset path. String fileNameOrSubpath = (String) invocation.getArguments()[0]; - return FlutterLoader.getInstance().getLookupKeyForAsset(fileNameOrSubpath); + return FlutterLoader.getInstanceForTest(emptyInfo) + .getLookupKeyForAsset(fileNameOrSubpath); } }); when(flutterLoader.getLookupKeyForAsset(any(String.class), any(String.class))) @@ -55,7 +59,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { // Defer to a real FlutterLoader to return the asset path. String fileNameOrSubpath = (String) invocation.getArguments()[0]; String packageName = (String) invocation.getArguments()[1]; - return FlutterLoader.getInstance() + return FlutterLoader.getInstanceForTest(emptyInfo) .getLookupKeyForAsset(fileNameOrSubpath, packageName); } }); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java new file mode 100644 index 0000000000000..769525f7db3c9 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java @@ -0,0 +1,193 @@ +package io.flutter.embedding.engine.loader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.security.NetworkSecurityPolicy; +import java.io.StringReader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class ApplicationInfoLoaderTest { + + @Test + public void itGeneratesCorrectApplicationInfoWithDefaultManifest() { + FlutterApplicationInfo info = ApplicationInfoLoader.load(RuntimeEnvironment.application); + assertNotNull(info); + assertEquals("libapp.so", info.aotSharedLibraryName); + assertEquals("vm_snapshot_data", info.vmSnapshotData); + assertEquals("isolate_snapshot_data", info.isolateSnapshotData); + assertEquals("flutter_assets", info.flutterAssetsDir); + assertEquals("", info.domainNetworkPolicy); + assertNull(info.nativeLibraryDir); + assertEquals(true, info.clearTextPermitted); + assertEquals(false, info.useEmbeddedView); + } + + @Config(shadows = {ApplicationInfoLoaderTest.ShadowNetworkSecurityPolicy.class}) + @Test + public void itVotesAgainstClearTextIfSecurityPolicySaysSo() { + FlutterApplicationInfo info = ApplicationInfoLoader.load(RuntimeEnvironment.application); + assertNotNull(info); + assertEquals(false, info.clearTextPermitted); + } + + @Implements(NetworkSecurityPolicy.class) + public static class ShadowNetworkSecurityPolicy { + @Implementation + public boolean isCleartextTrafficPermitted() { + return false; + } + } + + private Context generateMockContext(Bundle metadata, String networkPolicyXml) throws Exception { + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + ApplicationInfo applicationInfo = mock(ApplicationInfo.class); + applicationInfo.metaData = metadata; + Resources resources = mock(Resources.class); + when(context.getPackageManager()).thenReturn(packageManager); + when(context.getResources()).thenReturn(resources); + when(packageManager.getApplicationInfo(any(String.class), any(int.class))) + .thenReturn(applicationInfo); + if (networkPolicyXml != null) { + metadata.putInt(ApplicationInfoLoader.NETWORK_POLICY_METADATA_KEY, 5); + doAnswer(invocationOnMock -> createMockResourceParser(networkPolicyXml)) + .when(resources) + .getXml(5); + } + return context; + } + + @Test + public void itGeneratesCorrectApplicationInfoWithCustomValues() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString(ApplicationInfoLoader.PUBLIC_AOT_SHARED_LIBRARY_NAME, "testaot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_VM_SNAPSHOT_DATA_KEY, "testvmsnapshot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, "testisolatesnapshot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "testassets"); + bundle.putBoolean("io.flutter.embedded_views_preview", true); + Context context = generateMockContext(bundle, null); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("testaot", info.aotSharedLibraryName); + assertEquals("testvmsnapshot", info.vmSnapshotData); + assertEquals("testisolatesnapshot", info.isolateSnapshotData); + assertEquals("testassets", info.flutterAssetsDir); + assertNull(info.nativeLibraryDir); + assertEquals("", info.domainNetworkPolicy); + assertEquals(true, info.useEmbeddedView); + } + + @Test + public void itGeneratesCorrectNetworkPolicy() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "secure.example.com" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("[[\"secure.example.com\",true,false]]", info.domainNetworkPolicy); + } + + @Test + public void itHandlesBogusInformationInNetworkPolicy() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "secure.example.com" + + "" + + "7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=" + + "" + + "fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=" + + "" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("[[\"secure.example.com\",true,false]]", info.domainNetworkPolicy); + } + + @Test + public void itHandlesNestedSubDomains() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "example.com" + + "" + + "insecure.example.com" + + "" + + "" + + "secure.example.com" + + "" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals( + "[[\"example.com\",true,true],[\"insecure.example.com\",true,true],[\"secure.example.com\",true,false]]", + info.domainNetworkPolicy); + } + + // The following ridiculousness is needed because Android gives no way for us + // to customize XmlResourceParser. We have to mock it and tie each method + // we use to an actual Xml parser. + private XmlResourceParser createMockResourceParser(String xml) throws Exception { + final XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput(new StringReader(xml)); + XmlResourceParser resourceParser = mock(XmlResourceParser.class); + final Answer invokeMethodOnRealParser = + invocation -> invocation.getMethod().invoke(xpp, invocation.getArguments()); + when(resourceParser.next()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getName()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getEventType()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getText()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeCount()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeName(anyInt())).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeValue(anyInt())).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeValue(any(String.class), any(String.class))) + .thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeBooleanValue( + any(String.class), any(String.class), any(Boolean.class))) + .thenAnswer( + invocation -> { + Object[] args = invocation.getArguments(); + String result = xpp.getAttributeValue((String) args[0], (String) args[1]); + if (result == null) { + return (Boolean) args[2]; + } + return Boolean.parseBoolean(result); + }); + return resourceParser; + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java new file mode 100644 index 0000000000000..3a215e3e5507d --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java @@ -0,0 +1,45 @@ +package io.flutter.embedding.engine.systemchannels; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class PlatformChannelTest { + @Test + public void platformChannel_hasStringsMessage() { + MethodChannel rawChannel = mock(MethodChannel.class); + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class)); + PlatformChannel fakePlatformChannel = new PlatformChannel(dartExecutor); + PlatformChannel.PlatformMessageHandler mockMessageHandler = + mock(PlatformChannel.PlatformMessageHandler.class); + fakePlatformChannel.setPlatformMessageHandler(mockMessageHandler); + Boolean returnValue = true; + when(mockMessageHandler.clipboardHasStrings()).thenReturn(returnValue); + MethodCall methodCall = new MethodCall("Clipboard.hasStrings", null); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + fakePlatformChannel.parsingMethodCallHandler.onMethodCall(methodCall, mockResult); + + JSONObject expected = new JSONObject(); + try { + expected.put("value", returnValue); + } catch (JSONException e) { + } + verify(mockResult).success(Matchers.refEq(expected)); + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java b/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java index c99a30f5d7fac..f87815f401f1e 100644 --- a/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java +++ b/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.fail; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -89,4 +90,27 @@ public void encodeErrorEnvelopeWithThrowableTest() { "at io.flutter.plugin.common.StandardMethodCodecTest.encodeErrorEnvelopeWithThrowableTest(StandardMethodCodecTest.java:")); } } + + @Test + public void encodeErrorEnvelopeWithStacktraceTest() { + final Exception e = new IllegalArgumentException("foo"); + final ByteBuffer buffer = + StandardMethodCodec.INSTANCE.encodeErrorEnvelopeWithStacktrace( + "code", e.getMessage(), e, "error stacktrace"); + assertNotNull(buffer); + buffer.flip(); + buffer.order(ByteOrder.nativeOrder()); + final byte flag = buffer.get(); + final Object code = StandardMessageCodec.INSTANCE.readValue(buffer); + final Object message = StandardMessageCodec.INSTANCE.readValue(buffer); + final Object details = StandardMessageCodec.INSTANCE.readValue(buffer); + final Object stacktrace = StandardMessageCodec.INSTANCE.readValue(buffer); + assertEquals("code", (String) code); + assertEquals("foo", (String) message); + String stack = (String) details; + assertTrue( + stack.contains( + "at io.flutter.plugin.common.StandardMethodCodecTest.encodeErrorEnvelopeWithStacktraceTest(StandardMethodCodecTest.java:")); + assertEquals("error stacktrace", (String) stacktrace); + } } diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java index d3afb22e5976d..458e75b7cfb02 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.eq; @@ -14,6 +15,7 @@ import android.content.ClipboardManager; import android.content.res.AssetManager; +import android.os.Bundle; import android.text.Editable; import android.text.Emoji; import android.text.InputType; @@ -26,9 +28,16 @@ import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; import io.flutter.util.FakeKeyEvent; +import java.nio.ByteBuffer; +import org.json.JSONArray; +import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -37,6 +46,21 @@ @Config(manifest = Config.NONE, shadows = ShadowClipboardManager.class) @RunWith(RobolectricTestRunner.class) public class InputConnectionAdaptorTest { + // Verifies the method and arguments for a captured method call. + private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs) + throws JSONException { + buffer.rewind(); + MethodCall methodCall = JSONMethodCodec.INSTANCE.decodeMethodCall(buffer); + assertEquals(methodName, methodCall.method); + if (expectedArgs != null) { + JSONArray args = methodCall.arguments(); + assertEquals(expectedArgs.length, args.length()); + for (int i = 0; i < args.length(); i++) { + assertEquals(expectedArgs[i], args.get(i).toString()); + } + } + } + @Test public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { View testView = new View(RuntimeEnvironment.application); @@ -125,6 +149,294 @@ public void testPerformContextMenuAction_paste() { assertTrue(editable.toString().startsWith(textToBePasted)); } + @Test + public void testPerformPrivateCommand_dataIsNull() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + adaptor.performPrivateCommand("actionCommand", null); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsByteArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + byte[] buffer = new byte[] {'a', 'b', 'c', 'd'}; + bundle.putByteArray("keyboard_layout", buffer); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[97,98,99,100]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsByte() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + byte b = 3; + bundle.putByte("keyboard_layout", b); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":3},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsCharArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + char[] buffer = new char[] {'a', 'b', 'c', 'd'}; + bundle.putCharArray("keyboard_layout", buffer); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", + "{\"data\":{\"keyboard_layout\":[\"a\",\"b\",\"c\",\"d\"]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsChar() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + char b = 'a'; + bundle.putChar("keyboard_layout", b); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":\"a\"},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsCharSequenceArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + CharSequence charSequence1 = new StringBuffer("abc"); + CharSequence charSequence2 = new StringBuffer("efg"); + CharSequence[] value = {charSequence1, charSequence2}; + bundle.putCharSequenceArray("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[\"abc\",\"efg\"]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsCharSequence() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + CharSequence charSequence = new StringBuffer("abc"); + bundle.putCharSequence("keyboard_layout", charSequence); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":\"abc\"},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsFloat() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + float value = 0.5f; + bundle.putFloat("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":0.5},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsFloatArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + float[] value = {0.5f, 0.6f}; + bundle.putFloatArray("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[0.5,0.6]},\"action\":\"actionCommand\"}" + }); + } + @Test public void testSendKeyEvent_shiftKeyUpCancelsSelection() { int selStart = 5; diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index bd4eb6f4b32ec..1a6b53c6b7080 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -18,6 +18,7 @@ import android.content.Context; import android.content.res.AssetManager; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.util.SparseIntArray; import android.view.KeyEvent; @@ -40,6 +41,7 @@ import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -566,6 +568,74 @@ public void respondsToInputChannelMessages() { verify(mockHandler, times(1)).finishAutofillContext(false); } + @Test + public void sendAppPrivateCommand_dataIsEmpty() throws JSONException { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger); + + EventHandler mockEventHandler = mock(EventHandler.class); + TestImm testImm = + Shadow.extract( + RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); + testImm.setEventHandler(mockEventHandler); + + View testView = new View(RuntimeEnvironment.application); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + JSONObject arguments = new JSONObject(); + arguments.put("action", "actionCommand"); + arguments.put("data", ""); + + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + sendToBinaryMessageHandler(binaryMessageHandler, "TextInput.sendAppPrivateCommand", arguments); + verify(mockEventHandler, times(1)) + .sendAppPrivateCommand(any(View.class), eq("actionCommand"), eq(null)); + } + + @Test + public void sendAppPrivateCommand_hasData() throws JSONException { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger); + + EventHandler mockEventHandler = mock(EventHandler.class); + TestImm testImm = + Shadow.extract( + RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); + testImm.setEventHandler(mockEventHandler); + + View testView = new View(RuntimeEnvironment.application); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + JSONObject arguments = new JSONObject(); + arguments.put("action", "actionCommand"); + arguments.put("data", "actionData"); + + ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + sendToBinaryMessageHandler(binaryMessageHandler, "TextInput.sendAppPrivateCommand", arguments); + verify(mockEventHandler, times(1)) + .sendAppPrivateCommand(any(View.class), eq("actionCommand"), bundleCaptor.capture()); + assertEquals("actionData", bundleCaptor.getValue().getCharSequence("data")); + } + + interface EventHandler { + void sendAppPrivateCommand(View view, String action, Bundle data); + } + @Implements(InputMethodManager.class) public static class TestImm extends ShadowInputMethodManager { private InputMethodSubtype currentInputMethodSubtype; @@ -573,6 +643,7 @@ public static class TestImm extends ShadowInputMethodManager { private CursorAnchorInfo cursorAnchorInfo; private ArrayList selectionUpdateValues; private boolean trackSelection = false; + private EventHandler handler; public TestImm() { selectionUpdateValues = new ArrayList(); @@ -597,6 +668,15 @@ public int getRestartCount(View view) { return restartCounter.get(view.hashCode(), /*defaultValue=*/ 0); } + public void setEventHandler(EventHandler eventHandler) { + handler = eventHandler; + } + + @Implementation + public void sendAppPrivateCommand(View view, String action, Bundle data) { + handler.sendAppPrivateCommand(view, action, data); + } + @Implementation public void updateCursorAnchorInfo(View view, CursorAnchorInfo cursorAnchorInfo) { this.cursorAnchorInfo = cursorAnchorInfo; diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 848b19161a3f9..355ec4e9ea5ee 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -1,15 +1,20 @@ package io.flutter.plugin.platform; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.app.Activity; +import android.content.ClipboardManager; +import android.content.Context; import android.view.View; import android.view.Window; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @Config(manifest = Config.NONE) @@ -32,4 +37,25 @@ public void itIgnoresNewHapticEventsOnOldAndroidPlatforms() { // SELECTION_CLICK haptic response is only available on "LOLLIPOP" (21) and later. platformPlugin.vibrateHapticFeedback(PlatformChannel.HapticFeedbackType.SELECTION_CLICK); } + + @Test + public void platformPlugin_hasStrings() { + ClipboardManager clipboardManager = + RuntimeEnvironment.application.getSystemService(ClipboardManager.class); + + View fakeDecorView = mock(View.class); + Window fakeWindow = mock(Window.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + Activity fakeActivity = mock(Activity.class); + when(fakeActivity.getWindow()).thenReturn(fakeWindow); + when(fakeActivity.getSystemService(Context.CLIPBOARD_SERVICE)).thenReturn(clipboardManager); + PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); + PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + + clipboardManager.setText("iamastring"); + assertTrue(platformPlugin.mPlatformMessageHandler.clipboardHasStrings()); + + clipboardManager.setText(""); + assertFalse(platformPlugin.mPlatformMessageHandler.clipboardHasStrings()); + } } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 659056afc5132..0e585b3e508c4 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -7,6 +7,7 @@ import android.content.Context; import android.content.res.AssetManager; +import android.util.SparseArray; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; @@ -28,7 +29,9 @@ import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.common.FlutterException; import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.common.StandardMethodCodec; import io.flutter.plugin.localization.LocalizationPlugin; import java.nio.ByteBuffer; @@ -242,7 +245,29 @@ public void getPlatformViewById__hybridComposition() { @Test @Config(shadows = {ShadowFlutterJNI.class}) - public void initializePlatformViewIfNeeded__throwsIfViewIsNull() { + public void createPlatformViewMessage__initializesAndroidView() { + PlatformViewsController platformViewsController = new PlatformViewsController(); + + int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + PlatformView platformView = mock(PlatformView.class); + when(platformView.getView()).thenReturn(mock(View.class)); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + FlutterJNI jni = new FlutterJNI(); + attach(jni, platformViewsController); + + // Simulate create call from the framework. + createPlatformView(jni, platformViewsController, platformViewId, "testType"); + verify(platformView, times(1)).getView(); + } + + @Test + @Config(shadows = {ShadowFlutterJNI.class}) + public void createPlatformViewMessage__throwsIfViewIsNull() { PlatformViewsController platformViewsController = new PlatformViewsController(); int platformViewId = 0; @@ -259,22 +284,28 @@ public void initializePlatformViewIfNeeded__throwsIfViewIsNull() { // Simulate create call from the framework. createPlatformView(jni, platformViewsController, platformViewId, "testType"); + assertEquals(ShadowFlutterJNI.getResponses().size(), 1); + + final ByteBuffer responseBuffer = ShadowFlutterJNI.getResponses().get(0); + responseBuffer.rewind(); + StandardMethodCodec methodCodec = new StandardMethodCodec(new StandardMessageCodec()); try { - platformViewsController.initializePlatformViewIfNeeded(platformViewId); - } catch (Exception exception) { - assertTrue(exception instanceof IllegalStateException); - assertEquals( - exception.getMessage(), - "PlatformView#getView() returned null, but an Android view reference was expected."); + methodCodec.decodeEnvelope(responseBuffer); + } catch (FlutterException exception) { + assertTrue( + exception + .getMessage() + .contains( + "PlatformView#getView() returned null, but an Android view reference was expected.")); return; } - assertTrue(false); + assertFalse(true); } @Test @Config(shadows = {ShadowFlutterJNI.class}) - public void initializePlatformViewIfNeeded__throwsIfViewHasParent() { + public void createPlatformViewMessage__throwsIfViewHasParent() { PlatformViewsController platformViewsController = new PlatformViewsController(); int platformViewId = 0; @@ -293,16 +324,23 @@ public void initializePlatformViewIfNeeded__throwsIfViewHasParent() { // Simulate create call from the framework. createPlatformView(jni, platformViewsController, platformViewId, "testType"); + assertEquals(ShadowFlutterJNI.getResponses().size(), 1); + + final ByteBuffer responseBuffer = ShadowFlutterJNI.getResponses().get(0); + responseBuffer.rewind(); + + StandardMethodCodec methodCodec = new StandardMethodCodec(new StandardMessageCodec()); try { - platformViewsController.initializePlatformViewIfNeeded(platformViewId); - } catch (Exception exception) { - assertTrue(exception instanceof IllegalStateException); - assertEquals( - exception.getMessage(), - "The Android view returned from PlatformView#getView() was already added to a parent view."); + methodCodec.decodeEnvelope(responseBuffer); + } catch (FlutterException exception) { + assertTrue( + exception + .getMessage() + .contains( + "The Android view returned from PlatformView#getView() was already added to a parent view.")); return; } - assertTrue(false); + assertFalse(true); } @Test @@ -407,6 +445,87 @@ public void onEndFrame__destroysOverlaySurfaceAfterFrameOnFlutterSurfaceView() { verify(overlayImageView, times(1)).detachFromRenderer(); } + @Test + @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + public void onEndFrame__removesPlatformView() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + + final int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + final PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + final PlatformView platformView = mock(PlatformView.class); + final View androidView = mock(View.class); + when(platformView.getView()).thenReturn(androidView); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + final FlutterJNI jni = new FlutterJNI(); + jni.attachToNative(false); + attach(jni, platformViewsController); + + jni.onFirstFrame(); + + // Simulate create call from the framework. + createPlatformView(jni, platformViewsController, platformViewId, "testType"); + + // Simulate first frame from the framework. + jni.onFirstFrame(); + platformViewsController.onBeginFrame(); + + platformViewsController.onEndFrame(); + verify(androidView, never()).setVisibility(View.GONE); + + final ViewParent parentView = mock(ViewParent.class); + when(androidView.getParent()).thenReturn(parentView); + } + + @Test + @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + public void onEndFrame__removesPlatformViewParent() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + + final int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + final PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + final PlatformView platformView = mock(PlatformView.class); + final View androidView = mock(View.class); + when(platformView.getView()).thenReturn(androidView); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + final FlutterJNI jni = new FlutterJNI(); + jni.attachToNative(false); + + final FlutterView flutterView = attach(jni, platformViewsController); + + jni.onFirstFrame(); + + // Simulate create call from the framework. + createPlatformView(jni, platformViewsController, platformViewId, "testType"); + platformViewsController.initializePlatformViewIfNeeded(platformViewId); + assertEquals(flutterView.getChildCount(), 2); + + // Simulate first frame from the framework. + jni.onFirstFrame(); + platformViewsController.onBeginFrame(); + platformViewsController.onEndFrame(); + + // Simulate dispose call from the framework. + disposePlatformView(jni, platformViewsController, platformViewId); + assertEquals(flutterView.getChildCount(), 1); + } + + @Test + public void checkInputConnectionProxy__falseIfViewIsNull() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + boolean shouldProxying = platformViewsController.checkInputConnectionProxy(null); + assertFalse(shouldProxying); + } + private static byte[] encodeMethodCall(MethodCall call) { final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call); buffer.rewind(); @@ -446,7 +565,8 @@ private static void disposePlatformView( "flutter/platform_views", encodeMethodCall(platformDisposeMethodCall), /*replyId=*/ 0); } - private static void attach(FlutterJNI jni, PlatformViewsController platformViewsController) { + private static FlutterView attach( + FlutterJNI jni, PlatformViewsController platformViewsController) { final DartExecutor executor = new DartExecutor(jni, mock(AssetManager.class)); executor.onAttachedToJNI(); @@ -477,10 +597,12 @@ public FlutterImageView createImageView() { view.attachToFlutterEngine(engine); platformViewsController.attachToView(view); + return view; } @Implements(FlutterJNI.class) public static class ShadowFlutterJNI { + private static SparseArray replies = new SparseArray<>(); public ShadowFlutterJNI() {} @@ -527,7 +649,13 @@ public void setViewportMetrics( @Implementation public void invokePlatformMessageResponseCallback( - int responseId, ByteBuffer message, int position) {} + int responseId, ByteBuffer message, int position) { + replies.put(responseId, message); + } + + public static SparseArray getResponses() { + return replies; + } } @Implements(SurfaceView.class) diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 38a6d7e695ed8..96ade8718a9cd 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -132,6 +132,41 @@ public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() { assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); } + @Test + public void itAnnouncesWhiteSpaceWhenNoNamesRoute() { + AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class); + AccessibilityManager mockManager = mock(AccessibilityManager.class); + View mockRootView = mock(View.class); + Context context = mock(Context.class); + when(mockRootView.getContext()).thenReturn(context); + when(context.getPackageName()).thenReturn("test"); + AccessibilityBridge accessibilityBridge = + setUpBridge(mockRootView, mockManager, mockViewEmbedder); + ViewParent mockParent = mock(ViewParent.class); + when(mockRootView.getParent()).thenReturn(mockParent); + when(mockManager.isEnabled()).thenReturn(true); + + // Sent a11y tree with scopeRoute without namesRoute. + TestSemanticsNode root = new TestSemanticsNode(); + root.id = 0; + TestSemanticsNode scopeRoute = new TestSemanticsNode(); + scopeRoute.id = 1; + scopeRoute.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE); + root.children.add(scopeRoute); + TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); + accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings); + + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mockParent, times(2)) + .requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture()); + AccessibilityEvent event = eventCaptor.getAllValues().get(0); + assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + List sentences = event.getText(); + assertEquals(sentences.size(), 1); + assertEquals(sentences.get(0).toString(), " "); + } + @Test public void itHoverOverOutOfBoundsDoesNotCrash() { // SementicsNode.hitTest() returns null when out of bounds. diff --git a/shell/platform/common/cpp/client_wrapper/BUILD.gn b/shell/platform/common/cpp/client_wrapper/BUILD.gn index 0b4f243c215df..6c297103d4360 100644 --- a/shell/platform/common/cpp/client_wrapper/BUILD.gn +++ b/shell/platform/common/cpp/client_wrapper/BUILD.gn @@ -8,7 +8,8 @@ import("core_wrapper_files.gni") # Client library build for internal use by the shell implementation. source_set("client_wrapper") { sources = core_cpp_client_wrapper_sources - public = core_cpp_client_wrapper_includes + public = core_cpp_client_wrapper_includes + + core_cpp_client_wrapper_internal_headers deps = [ "//flutter/shell/platform/common/cpp:common_cpp_library_headers" ] @@ -19,6 +20,24 @@ source_set("client_wrapper") { [ "//flutter/shell/platform/common/cpp:relative_flutter_library_headers" ] } +# Temporary test for the legacy EncodableValue implementation. Remove once the +# legacy version is removed. +source_set("client_wrapper_legacy_encodable_value") { + sources = core_cpp_client_wrapper_sources + public = core_cpp_client_wrapper_includes + + core_cpp_client_wrapper_internal_headers + + deps = [ "//flutter/shell/platform/common/cpp:common_cpp_library_headers" ] + + configs += + [ "//flutter/shell/platform/common/cpp:desktop_library_implementation" ] + + defines = [ "USE_LEGACY_ENCODABLE_VALUE" ] + + public_configs = + [ "//flutter/shell/platform/common/cpp:relative_flutter_library_headers" ] +} + source_set("client_wrapper_library_stubs") { sources = [ "testing/stub_flutter_api.cc", @@ -48,14 +67,17 @@ executable("client_wrapper_unittests") { "plugin_registrar_unittests.cc", "standard_message_codec_unittests.cc", "standard_method_codec_unittests.cc", - "testing/encodable_value_utils.cc", - "testing/encodable_value_utils.h", + "testing/test_codec_extensions.cc", + "testing/test_codec_extensions.h", ] deps = [ ":client_wrapper", ":client_wrapper_fixtures", ":client_wrapper_library_stubs", + + # Build the legacy version as well as a sanity check. + ":client_wrapper_unittests_legacy_encodable_value", "//flutter/testing", # TODO(chunhtai): Consider refactoring flutter_root/testing so that there's a testing @@ -66,3 +88,31 @@ executable("client_wrapper_unittests") { defines = [ "FLUTTER_DESKTOP_LIBRARY" ] } + +# Ensures that the legacy EncodableValue codepath still compiles. +executable("client_wrapper_unittests_legacy_encodable_value") { + testonly = true + + sources = [ + "encodable_value_unittests.cc", + "standard_message_codec_unittests.cc", + "testing/test_codec_extensions.h", + ] + + deps = [ + ":client_wrapper_fixtures", + ":client_wrapper_legacy_encodable_value", + ":client_wrapper_library_stubs", + "//flutter/testing", + + # TODO(chunhtai): Consider refactoring flutter_root/testing so that there's a testing + # target that doesn't require a Dart runtime to be linked in. + # https://github.com/flutter/flutter/issues/41414. + "//third_party/dart/runtime:libdart_jit", + ] + + defines = [ + "FLUTTER_DESKTOP_LIBRARY", + "USE_LEGACY_ENCODABLE_VALUE", + ] +} diff --git a/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc b/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc index 5098e89b30dd6..08f6a138f551b 100644 --- a/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/basic_message_channel.h" - #include #include +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/basic_message_channel.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h" #include "gtest/gtest.h" @@ -56,7 +55,7 @@ TEST(BasicMessageChannelTest, Registration) { callback_called = true; // Ensure that the wrapper recieved a correctly decoded message and a // reply. - EXPECT_EQ(message.StringValue(), message_value); + EXPECT_EQ(std::get(message), message_value); EXPECT_NE(reply, nullptr); }); EXPECT_EQ(messenger.last_message_handler_channel(), channel_name); diff --git a/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h b/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h new file mode 100644 index 0000000000000..28fa475c11633 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h @@ -0,0 +1,50 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BINARY_MESSENGER_IMPL_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BINARY_MESSENGER_IMPL_H_ + +#include + +#include +#include + +#include "include/flutter/binary_messenger.h" + +namespace flutter { + +// Wrapper around a FlutterDesktopMessengerRef that implements the +// BinaryMessenger API. +class BinaryMessengerImpl : public BinaryMessenger { + public: + explicit BinaryMessengerImpl(FlutterDesktopMessengerRef core_messenger); + + virtual ~BinaryMessengerImpl(); + + // Prevent copying. + BinaryMessengerImpl(BinaryMessengerImpl const&) = delete; + BinaryMessengerImpl& operator=(BinaryMessengerImpl const&) = delete; + + // |flutter::BinaryMessenger| + void Send(const std::string& channel, + const uint8_t* message, + size_t message_size, + BinaryReply reply) const override; + + // |flutter::BinaryMessenger| + void SetMessageHandler(const std::string& channel, + BinaryMessageHandler handler) override; + + private: + // Handle for interacting with the C API. + FlutterDesktopMessengerRef messenger_; + + // A map from channel names to the BinaryMessageHandler that should be called + // for incoming messages on that channel. + std::map handlers_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BINARY_MESSENGER_IMPL_H_ diff --git a/shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h b/shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h similarity index 58% rename from shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h rename to shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h index 96ab0b0d06210..e054e00f2d04c 100644 --- a/shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h +++ b/shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h @@ -2,30 +2,31 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_STREAM_WRAPPERS_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_STREAM_WRAPPERS_H_ - -// Utility classes for interacting with a buffer of bytes as a stream, for use -// in message channel codecs. +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_BUFFER_STREAMS_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_BUFFER_STREAMS_H_ +#include #include #include #include #include +#include "include/flutter/byte_streams.h" + namespace flutter { -// Wraps an array of bytes with utility methods for treating it as a readable -// stream. -class ByteBufferStreamReader { +// Implementation of ByteStreamReader base on a byte array. +class ByteBufferStreamReader : public ByteStreamReader { public: // Createa a reader reading from |bytes|, which must have a length of |size|. // |bytes| must remain valid for the lifetime of this object. explicit ByteBufferStreamReader(const uint8_t* bytes, size_t size) : bytes_(bytes), size_(size) {} - // Reads and returns the next byte from the stream. - uint8_t ReadByte() { + virtual ~ByteBufferStreamReader() = default; + + // |ByteStreamReader| + uint8_t ReadByte() override { if (location_ >= size_) { std::cerr << "Invalid read in StandardCodecByteStreamReader" << std::endl; return 0; @@ -33,9 +34,8 @@ class ByteBufferStreamReader { return bytes_[location_++]; } - // Reads the next |length| bytes from the stream into |buffer|. The caller - // is responsible for ensuring that |buffer| is large enough. - void ReadBytes(uint8_t* buffer, size_t length) { + // |ByteStreamReader| + void ReadBytes(uint8_t* buffer, size_t length) override { if (location_ + length > size_) { std::cerr << "Invalid read in StandardCodecByteStreamReader" << std::endl; return; @@ -44,9 +44,8 @@ class ByteBufferStreamReader { location_ += length; } - // Advances the read cursor to the next multiple of |alignment| relative to - // the start of the wrapped byte buffer, unless it is already aligned. - void ReadAlignment(uint8_t alignment) { + // |ByteStreamReader| + void ReadAlignment(uint8_t alignment) override { uint8_t mod = location_ % alignment; if (mod) { location_ += alignment - mod; @@ -62,30 +61,28 @@ class ByteBufferStreamReader { size_t location_ = 0; }; -// Wraps an array of bytes with utility methods for treating it as a writable -// stream. -class ByteBufferStreamWriter { +// Implementation of ByteStreamWriter based on a byte array. +class ByteBufferStreamWriter : public ByteStreamWriter { public: - // Createa a writter that writes into |buffer|. + // Creates a writer that writes into |buffer|. // |buffer| must remain valid for the lifetime of this object. explicit ByteBufferStreamWriter(std::vector* buffer) : bytes_(buffer) { assert(buffer); } - // Writes |byte| to the wrapped buffer. + virtual ~ByteBufferStreamWriter() = default; + + // |ByteStreamWriter| void WriteByte(uint8_t byte) { bytes_->push_back(byte); } - // Writes the next |length| bytes from |bytes| into the wrapped buffer. - // The caller is responsible for ensuring that |buffer| is large enough. + // |ByteStreamWriter| void WriteBytes(const uint8_t* bytes, size_t length) { assert(length > 0); bytes_->insert(bytes_->end(), bytes, bytes + length); } - // Writes 0s until the next multiple of |alignment| relative to - // the start of the wrapped byte buffer, unless the write positition is - // already aligned. + // |ByteStreamWriter| void WriteAlignment(uint8_t alignment) { uint8_t mod = bytes_->size() % alignment; if (mod) { @@ -102,4 +99,4 @@ class ByteBufferStreamWriter { } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_STREAM_WRAPPERS_H_ +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_BUFFER_STREAMS_H_ diff --git a/shell/platform/common/cpp/client_wrapper/core_implementations.cc b/shell/platform/common/cpp/client_wrapper/core_implementations.cc new file mode 100644 index 0000000000000..2cafc7f9fe3a5 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/core_implementations.cc @@ -0,0 +1,150 @@ +// 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. + +// This file contains the implementations of any class in the wrapper that +// - is not fully inline, and +// - is necessary for all clients of the wrapper (either app or plugin). +// It exists instead of the usual structure of having some_class_name.cc files +// so that changes to the set of things that need non-header implementations +// are not breaking changes for the template. +// +// If https://github.com/flutter/flutter/issues/57146 is fixed, this can be +// removed in favor of the normal structure since templates will no longer +// manually include files. + +#include + +#include + +#include "binary_messenger_impl.h" +#include "include/flutter/engine_method_result.h" + +namespace flutter { + +// ========== binary_messenger_impl.h ========== + +namespace { +// Passes |message| to |user_data|, which must be a BinaryMessageHandler, along +// with a BinaryReply that will send a response on |message|'s response handle. +// +// This serves as an adaptor between the function-pointer-based message callback +// interface provided by the C API and the std::function-based message handler +// interface of BinaryMessenger. +void ForwardToHandler(FlutterDesktopMessengerRef messenger, + const FlutterDesktopMessage* message, + void* user_data) { + auto* response_handle = message->response_handle; + BinaryReply reply_handler = [messenger, response_handle]( + const uint8_t* reply, + size_t reply_size) mutable { + if (!response_handle) { + std::cerr << "Error: Response can be set only once. Ignoring " + "duplicate response." + << std::endl; + return; + } + FlutterDesktopMessengerSendResponse(messenger, response_handle, reply, + reply_size); + // The engine frees the response handle once + // FlutterDesktopSendMessageResponse is called. + response_handle = nullptr; + }; + + const BinaryMessageHandler& message_handler = + *static_cast(user_data); + + message_handler(message->message, message->message_size, + std::move(reply_handler)); +} +} // namespace + +BinaryMessengerImpl::BinaryMessengerImpl( + FlutterDesktopMessengerRef core_messenger) + : messenger_(core_messenger) {} + +BinaryMessengerImpl::~BinaryMessengerImpl() = default; + +void BinaryMessengerImpl::Send(const std::string& channel, + const uint8_t* message, + size_t message_size, + BinaryReply reply) const { + if (reply == nullptr) { + FlutterDesktopMessengerSend(messenger_, channel.c_str(), message, + message_size); + return; + } + struct Captures { + BinaryReply reply; + }; + auto captures = new Captures(); + captures->reply = reply; + + auto message_reply = [](const uint8_t* data, size_t data_size, + void* user_data) { + auto captures = reinterpret_cast(user_data); + captures->reply(data, data_size); + delete captures; + }; + bool result = FlutterDesktopMessengerSendWithReply( + messenger_, channel.c_str(), message, message_size, message_reply, + captures); + if (!result) { + delete captures; + } +} + +void BinaryMessengerImpl::SetMessageHandler(const std::string& channel, + BinaryMessageHandler handler) { + if (!handler) { + handlers_.erase(channel); + FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), nullptr, + nullptr); + return; + } + // Save the handler, to keep it alive. + handlers_[channel] = std::move(handler); + BinaryMessageHandler* message_handler = &handlers_[channel]; + // Set an adaptor callback that will invoke the handler. + FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), + ForwardToHandler, message_handler); +} + +// ========== engine_method_result.h ========== + +namespace internal { + +ReplyManager::ReplyManager(BinaryReply reply_handler) + : reply_handler_(std::move(reply_handler)) { + assert(reply_handler_); +} + +ReplyManager::~ReplyManager() { + if (reply_handler_) { + // Warn, rather than send a not-implemented response, since the engine may + // no longer be valid at this point. + std::cerr + << "Warning: Failed to respond to a message. This is a memory leak." + << std::endl; + } +} + +void ReplyManager::SendResponseData(const std::vector* data) { + if (!reply_handler_) { + std::cerr + << "Error: Only one of Success, Error, or NotImplemented can be " + "called," + << " and it can be called exactly once. Ignoring duplicate result." + << std::endl; + return; + } + + const uint8_t* message = data && !data->empty() ? data->data() : nullptr; + size_t message_size = data ? data->size() : 0; + reply_handler_(message, message_size); + reply_handler_ = nullptr; +} + +} // namespace internal + +} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni b/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni index 18b0897e2a987..264bf774e6111 100644 --- a/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni +++ b/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni @@ -6,6 +6,7 @@ core_cpp_client_wrapper_includes = get_path_info([ "include/flutter/basic_message_channel.h", "include/flutter/binary_messenger.h", + "include/flutter/byte_streams.h", "include/flutter/encodable_value.h", "include/flutter/engine_method_result.h", "include/flutter/event_channel.h", @@ -20,19 +21,32 @@ core_cpp_client_wrapper_includes = "include/flutter/method_result.h", "include/flutter/plugin_registrar.h", "include/flutter/plugin_registry.h", + "include/flutter/standard_codec_serializer.h", "include/flutter/standard_message_codec.h", "include/flutter/standard_method_codec.h", ], "abspath") +# Headers that aren't public for clients of the wrapper, but are considered +# public for the purpose of BUILD dependencies (e.g., to allow +# windows/client_wrapper implementation files to include them). +core_cpp_client_wrapper_internal_headers = + get_path_info([ + "binary_messenger_impl.h", + "byte_buffer_streams.h", + ], + "abspath") + # TODO: Once the wrapper API is more stable, consolidate to as few files as is # reasonable (without forcing different kinds of clients to take unnecessary # code) to simplify use. core_cpp_client_wrapper_sources = get_path_info([ - "byte_stream_wrappers.h", - "engine_method_result.cc", + "core_implementations.cc", "plugin_registrar.cc", - "standard_codec_serializer.h", "standard_codec.cc", ], "abspath") + +# Temporary shim, published for backwards compatibility. +# See comment in the file for more detail. +temporary_shim_files = get_path_info([ "engine_method_result.cc" ], "abspath") diff --git a/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc b/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc index 480314252255d..e64beae8b3a8f 100644 --- a/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc @@ -3,96 +3,84 @@ // found in the LICENSE file. // FLUTTER_NOLINT -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" - #include +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" #include "gtest/gtest.h" namespace flutter { -// Verifies that value.type() is |type|, and that of all the Is* methods, only -// the one that matches the type is true. -void VerifyType(EncodableValue& value, - EncodableValue::EncodableValue::Type type) { - EXPECT_EQ(value.type(), type); - - EXPECT_EQ(value.IsNull(), type == EncodableValue::Type::kNull); - EXPECT_EQ(value.IsBool(), type == EncodableValue::Type::kBool); - EXPECT_EQ(value.IsInt(), type == EncodableValue::Type::kInt); - EXPECT_EQ(value.IsLong(), type == EncodableValue::Type::kLong); - EXPECT_EQ(value.IsDouble(), type == EncodableValue::Type::kDouble); - EXPECT_EQ(value.IsString(), type == EncodableValue::Type::kString); - EXPECT_EQ(value.IsByteList(), type == EncodableValue::Type::kByteList); - EXPECT_EQ(value.IsIntList(), type == EncodableValue::Type::kIntList); - EXPECT_EQ(value.IsLongList(), type == EncodableValue::Type::kLongList); - EXPECT_EQ(value.IsDoubleList(), type == EncodableValue::Type::kDoubleList); - EXPECT_EQ(value.IsList(), type == EncodableValue::Type::kList); - EXPECT_EQ(value.IsMap(), type == EncodableValue::Type::kMap); -} - TEST(EncodableValueTest, Null) { EncodableValue value; - VerifyType(value, EncodableValue::Type::kNull); + value.IsNull(); } +#ifndef USE_LEGACY_ENCODABLE_VALUE + TEST(EncodableValueTest, Bool) { EncodableValue value(false); - VerifyType(value, EncodableValue::Type::kBool); - EXPECT_FALSE(value.BoolValue()); + EXPECT_FALSE(std::get(value)); value = true; - EXPECT_TRUE(value.BoolValue()); + EXPECT_TRUE(std::get(value)); } TEST(EncodableValueTest, Int) { EncodableValue value(42); - VerifyType(value, EncodableValue::Type::kInt); - EXPECT_EQ(value.IntValue(), 42); + EXPECT_EQ(std::get(value), 42); value = std::numeric_limits::max(); - EXPECT_EQ(value.IntValue(), std::numeric_limits::max()); + EXPECT_EQ(std::get(value), std::numeric_limits::max()); } -TEST(EncodableValueTest, LongValueFromInt) { +// Test the int/long convenience wrapper. +TEST(EncodableValueTest, LongValue) { EncodableValue value(std::numeric_limits::max()); EXPECT_EQ(value.LongValue(), std::numeric_limits::max()); + value = std::numeric_limits::max(); + EXPECT_EQ(value.LongValue(), std::numeric_limits::max()); } TEST(EncodableValueTest, Long) { EncodableValue value(INT64_C(42)); - VerifyType(value, EncodableValue::Type::kLong); - EXPECT_EQ(value.LongValue(), 42); + EXPECT_EQ(std::get(value), 42); value = std::numeric_limits::max(); - EXPECT_EQ(value.LongValue(), std::numeric_limits::max()); + EXPECT_EQ(std::get(value), std::numeric_limits::max()); } TEST(EncodableValueTest, Double) { EncodableValue value(3.14); - VerifyType(value, EncodableValue::Type::kDouble); - EXPECT_EQ(value.DoubleValue(), 3.14); + EXPECT_EQ(std::get(value), 3.14); value = std::numeric_limits::max(); - EXPECT_EQ(value.DoubleValue(), std::numeric_limits::max()); + EXPECT_EQ(std::get(value), std::numeric_limits::max()); } TEST(EncodableValueTest, String) { std::string hello("Hello, world!"); EncodableValue value(hello); - VerifyType(value, EncodableValue::Type::kString); - EXPECT_EQ(value.StringValue(), hello); + EXPECT_EQ(std::get(value), hello); + value = std::string("Goodbye"); + EXPECT_EQ(std::get(value), "Goodbye"); +} + +// Explicitly verify that the overrides to prevent char*->bool conversions work. +TEST(EncodableValueTest, CString) { + const char* hello = "Hello, world!"; + EncodableValue value(hello); + + EXPECT_EQ(std::get(value), hello); value = "Goodbye"; - EXPECT_EQ(value.StringValue(), "Goodbye"); + EXPECT_EQ(std::get(value), "Goodbye"); } TEST(EncodableValueTest, UInt8List) { std::vector data = {0, 2}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kByteList); - std::vector& list_value = value.ByteListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], 0); EXPECT_EQ(list_value[1], 2); @@ -105,9 +93,8 @@ TEST(EncodableValueTest, UInt8List) { TEST(EncodableValueTest, Int32List) { std::vector data = {-10, 2}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kIntList); - std::vector& list_value = value.IntListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], -10); EXPECT_EQ(list_value[1], 2); @@ -120,9 +107,8 @@ TEST(EncodableValueTest, Int32List) { TEST(EncodableValueTest, Int64List) { std::vector data = {-10, 2}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kLongList); - std::vector& list_value = value.LongListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], -10); EXPECT_EQ(list_value[1], 2); @@ -135,9 +121,8 @@ TEST(EncodableValueTest, Int64List) { TEST(EncodableValueTest, DoubleList) { std::vector data = {-10.0, 2.0}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kDoubleList); - std::vector& list_value = value.DoubleListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], -10.0); EXPECT_EQ(list_value[1], 2.0); @@ -154,18 +139,17 @@ TEST(EncodableValueTest, List) { EncodableValue("Three"), }; EncodableValue value(encodables); - VerifyType(value, EncodableValue::Type::kList); - EncodableList& list_value = value.ListValue(); - EXPECT_EQ(list_value[0].IntValue(), 1); - EXPECT_EQ(list_value[1].DoubleValue(), 2.0); - EXPECT_EQ(list_value[2].StringValue(), "Three"); + auto& list_value = std::get(value); + EXPECT_EQ(std::get(list_value[0]), 1); + EXPECT_EQ(std::get(list_value[1]), 2.0); + EXPECT_EQ(std::get(list_value[2]), "Three"); // Ensure that it's a modifiable copy of the original array. list_value.push_back(EncodableValue(true)); ASSERT_EQ(list_value.size(), 4u); EXPECT_EQ(encodables.size(), 3u); - EXPECT_EQ(value.ListValue()[3].BoolValue(), true); + EXPECT_EQ(std::get(std::get(value)[3]), true); } TEST(EncodableValueTest, Map) { @@ -175,43 +159,19 @@ TEST(EncodableValueTest, Map) { {EncodableValue("two"), EncodableValue(7)}, }; EncodableValue value(encodables); - VerifyType(value, EncodableValue::Type::kMap); - EncodableMap& map_value = value.MapValue(); - EXPECT_EQ(map_value[EncodableValue()].IsIntList(), true); - EXPECT_EQ(map_value[EncodableValue(1)].LongValue(), INT64_C(10000)); - EXPECT_EQ(map_value[EncodableValue("two")].IntValue(), 7); + auto& map_value = std::get(value); + EXPECT_EQ( + std::holds_alternative>(map_value[EncodableValue()]), + true); + EXPECT_EQ(std::get(map_value[EncodableValue(1)]), INT64_C(10000)); + EXPECT_EQ(std::get(map_value[EncodableValue("two")]), 7); // Ensure that it's a modifiable copy of the original map. map_value[EncodableValue(true)] = EncodableValue(false); ASSERT_EQ(map_value.size(), 4u); EXPECT_EQ(encodables.size(), 3u); - EXPECT_EQ(map_value[EncodableValue(true)].BoolValue(), false); -} - -TEST(EncodableValueTest, EmptyTypeConstructor) { - EXPECT_TRUE(EncodableValue(EncodableValue::Type::kNull).IsNull()); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kBool).BoolValue(), false); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kInt).IntValue(), 0); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kLong).LongValue(), - INT64_C(0)); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kDouble).DoubleValue(), 0.0); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kString).StringValue().size(), - 0u); - EXPECT_EQ( - EncodableValue(EncodableValue::Type::kByteList).ByteListValue().size(), - 0u); - EXPECT_EQ( - EncodableValue(EncodableValue::Type::kIntList).IntListValue().size(), 0u); - EXPECT_EQ( - EncodableValue(EncodableValue::Type::kLongList).LongListValue().size(), - 0u); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kDoubleList) - .DoubleListValue() - .size(), - 0u); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kList).ListValue().size(), 0u); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kMap).MapValue().size(), 0u); + EXPECT_EQ(std::get(map_value[EncodableValue(true)]), false); } // Tests that the < operator meets the requirements of using EncodableValue as @@ -248,8 +208,8 @@ TEST(EncodableValueTest, Comparison) { EncodableValue(std::vector{0, INT64_C(1)}), EncodableValue(std::vector{0, INT64_C(100)}), // DoubleList - EncodableValue(std::vector{0, INT64_C(1)}), - EncodableValue(std::vector{0, INT64_C(100)}), + EncodableValue(std::vector{0, INT64_C(1)}), + EncodableValue(std::vector{0, INT64_C(100)}), // List EncodableValue(EncodableList{EncodableValue(), EncodableValue(true)}), EncodableValue(EncodableList{EncodableValue(), EncodableValue(1.0)}), @@ -272,23 +232,19 @@ TEST(EncodableValueTest, Comparison) { } else { // All other comparisons should be consistent, but the direction doesn't // matter. - EXPECT_NE(a < b, b < a); + EXPECT_NE(a < b, b < a) << "Indexes: " << i << ", " << j; } } - // Different non-collection objects with the same value should be equal; - // different collections should always be unequal regardless of contents. - bool is_collection = a.IsByteList() || a.IsIntList() || a.IsLongList() || - a.IsDoubleList() || a.IsList() || a.IsMap(); + // Copies should always be equal. EncodableValue copy(a); - bool is_equal = !(a < copy || copy < a); - EXPECT_EQ(is_equal, !is_collection); + EXPECT_FALSE(a < copy || copy < a); } } // Tests that structures are deep-copied. TEST(EncodableValueTest, DeepCopy) { - EncodableList encodables = { + EncodableList original = { EncodableValue(EncodableMap{ {EncodableValue(), EncodableValue(std::vector{1, 2, 3})}, {EncodableValue(1), EncodableValue(INT64_C(0000))}, @@ -302,30 +258,30 @@ TEST(EncodableValueTest, DeepCopy) { }), }; - EncodableValue value(encodables); - ASSERT_TRUE(value.IsList()); + EncodableValue copy(original); + ASSERT_TRUE(std::holds_alternative(copy)); // Spot-check innermost collection values. - EXPECT_EQ(value.ListValue()[0].MapValue()[EncodableValue("two")].IntValue(), - 7); - EXPECT_EQ(value.ListValue()[1] - .ListValue()[2] - .MapValue()[EncodableValue("a")] - .StringValue(), - "b"); + auto& root_list = std::get(copy); + auto& first_child = std::get(root_list[0]); + EXPECT_EQ(std::get(first_child[EncodableValue("two")]), 7); + auto& second_child = std::get(root_list[1]); + auto& innermost_map = std::get(second_child[2]); + EXPECT_EQ(std::get(innermost_map[EncodableValue("a")]), "b"); // Modify those values in the original structure. - encodables[0].MapValue()[EncodableValue("two")] = EncodableValue(); - encodables[1].ListValue()[2].MapValue()[EncodableValue("a")] = 99; - - // Re-check innermost collection values to ensure that they haven't changed. - EXPECT_EQ(value.ListValue()[0].MapValue()[EncodableValue("two")].IntValue(), - 7); - EXPECT_EQ(value.ListValue()[1] - .ListValue()[2] - .MapValue()[EncodableValue("a")] - .StringValue(), - "b"); + first_child[EncodableValue("two")] = EncodableValue(); + innermost_map[EncodableValue("a")] = 99; + + // Re-check innermost collection values of the original to ensure that they + // haven't changed. + first_child = std::get(original[0]); + EXPECT_EQ(std::get(first_child[EncodableValue("two")]), 7); + second_child = std::get(original[1]); + innermost_map = std::get(second_child[2]); + EXPECT_EQ(std::get(innermost_map[EncodableValue("a")]), "b"); } +#endif // !LEGACY_ENCODABLE_VALUE + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/engine_method_result.cc b/shell/platform/common/cpp/client_wrapper/engine_method_result.cc index 03e17a82e6bd5..65eaf5d4358cb 100644 --- a/shell/platform/common/cpp/client_wrapper/engine_method_result.cc +++ b/shell/platform/common/cpp/client_wrapper/engine_method_result.cc @@ -2,44 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "include/flutter/engine_method_result.h" +// This file is deprecated in favor of core_implementations.cc. This is a +// temporary forwarding implementation so that the switch to +// core_implementations.cc isn't an immediate breaking change, allowing for the +// template to be updated to include it and update the template version before +// removing this file. -#include -#include - -namespace flutter { -namespace internal { - -ReplyManager::ReplyManager(BinaryReply reply_handler) - : reply_handler_(std::move(reply_handler)) { - assert(reply_handler_); -} - -ReplyManager::~ReplyManager() { - if (reply_handler_) { - // Warn, rather than send a not-implemented response, since the engine may - // no longer be valid at this point. - std::cerr - << "Warning: Failed to respond to a message. This is a memory leak." - << std::endl; - } -} - -void ReplyManager::SendResponseData(const std::vector* data) { - if (!reply_handler_) { - std::cerr - << "Error: Only one of Success, Error, or NotImplemented can be " - "called," - << " and it can be called exactly once. Ignoring duplicate result." - << std::endl; - return; - } - - const uint8_t* message = data && !data->empty() ? data->data() : nullptr; - size_t message_size = data ? data->size() : 0; - reply_handler_(message, message_size); - reply_handler_ = nullptr; -} - -} // namespace internal -} // namespace flutter +#include "core_implementations.cc" diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h b/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h new file mode 100644 index 0000000000000..f5314c0b9b575 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h @@ -0,0 +1,85 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_BYTE_STREAMS_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_BYTE_STREAMS_H_ + +// Interfaces for interacting with a stream of bytes, for use in codecs. + +namespace flutter { + +// An interface for a class that reads from a byte stream. +class ByteStreamReader { + public: + explicit ByteStreamReader() = default; + virtual ~ByteStreamReader() = default; + + // Reads and returns the next byte from the stream. + virtual uint8_t ReadByte() = 0; + + // Reads the next |length| bytes from the stream into |buffer|. The caller + // is responsible for ensuring that |buffer| is large enough. + virtual void ReadBytes(uint8_t* buffer, size_t length) = 0; + + // Advances the read cursor to the next multiple of |alignment| relative to + // the start of the stream, unless it is already aligned. + virtual void ReadAlignment(uint8_t alignment) = 0; + + // Reads and returns the next 32-bit integer from the stream. + int32_t ReadInt32() { + int32_t value = 0; + ReadBytes(reinterpret_cast(&value), 4); + return value; + } + + // Reads and returns the next 64-bit integer from the stream. + int64_t ReadInt64() { + int64_t value = 0; + ReadBytes(reinterpret_cast(&value), 8); + return value; + } + + // Reads and returns the next 64-bit floating point number from the stream. + double ReadDouble() { + double value = 0; + ReadBytes(reinterpret_cast(&value), 8); + return value; + } +}; + +// An interface for a class that writes to a byte stream. +class ByteStreamWriter { + public: + explicit ByteStreamWriter() = default; + virtual ~ByteStreamWriter() = default; + + // Writes |byte| to the stream. + virtual void WriteByte(uint8_t byte) = 0; + + // Writes the next |length| bytes from |bytes| to the stream + virtual void WriteBytes(const uint8_t* bytes, size_t length) = 0; + + // Writes 0s until the next multiple of |alignment| relative to the start + // of the stream, unless the write positition is already aligned. + virtual void WriteAlignment(uint8_t alignment) = 0; + + // Writes the given 32-bit int to the stream. + void WriteInt32(int32_t value) { + WriteBytes(reinterpret_cast(&value), 4); + } + + // Writes the given 64-bit int to the stream. + void WriteInt64(int64_t value) { + WriteBytes(reinterpret_cast(&value), 8); + } + + // Writes the given 36-bit double to the stream. + void WriteDouble(double value) { + WriteBytes(reinterpret_cast(&value), 8); + } +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_BYTE_STREAMS_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h b/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h index a63a4335c2b8f..0b021292d0fc8 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h @@ -5,17 +5,207 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_ENCODABLE_VALUE_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_ENCODABLE_VALUE_H_ -#include +#include +#include #include #include #include #include +#include #include +// Unless overridden, attempt to detect the RTTI state from the compiler. +#ifndef FLUTTER_ENABLE_RTTI +#if defined(_MSC_VER) +#ifdef _CPPRTTI +#define FLUTTER_ENABLE_RTTI 1 +#endif +#elif defined(__clang__) +#if __has_feature(cxx_rtti) +#define FLUTTER_ENABLE_RTTI 1 +#endif +#elif defined(__GNUC__) +#ifdef __GXX_RTTI +#define FLUTTER_ENABLE_RTTI 1 +#endif +#endif +#endif // #ifndef FLUTTER_ENABLE_RTTI + namespace flutter { static_assert(sizeof(double) == 8, "EncodableValue requires a 64-bit double"); +// Defining USE_LEGACY_ENCODABLE_VALUE will use the original EncodableValue +// implementation. This is a temporary measure to minimize the impact of the +// breaking change; it will be removed in the future. If you set this, you +// should update your code as soon as possible to use the new std::variant +// version, or it will break when the legacy version is removed. +#ifndef USE_LEGACY_ENCODABLE_VALUE + +// A container for arbitrary types in EncodableValue. +// +// This is used in conjunction with StandardCodecExtension to allow using other +// types with a StandardMethodCodec/StandardMessageCodec. It is implicitly +// convertible to EncodableValue, so constructing an EncodableValue from a +// custom type can generally be written as: +// CustomEncodableValue(MyType(...)) +// rather than: +// EncodableValue(CustomEncodableValue(MyType(...))) +// +// For extracting recieved custom types, it is implicitly convertible to +// std::any. For example: +// const MyType& my_type_value = +// std::any_cast(std::get(value)); +// +// If RTTI is enabled, different extension types can be checked with type(): +// if (custom_value->type() == typeid(SomeData)) { ... } +// Clients that wish to disable RTTI would need to decide on another approach +// for distinguishing types (e.g., in StandardCodecExtension::WriteValueOfType) +// if multiple custom types are needed. For instance, wrapping all of the +// extension types in an EncodableValue-style variant, and only ever storing +// that variant in CustomEncodableValue. +class CustomEncodableValue { + public: + explicit CustomEncodableValue(const std::any& value) : value_(value) {} + ~CustomEncodableValue() = default; + + // Allow implict conversion to std::any to allow direct use of any_cast. + operator std::any &() { return value_; } + operator const std::any &() const { return value_; } + +#if defined(FLUTTER_ENABLE_RTTI) && FLUTTER_ENABLE_RTTI + // Passthrough to std::any's type(). + const std::type_info& type() const noexcept { return value_.type(); } +#endif + + // This operator exists only to provide a stable ordering for use as a + // std::map key, to satisfy the compiler requirements for EncodableValue. + // It does not attempt to provide useful ordering semantics, and using a + // custom value as a map key is not recommended. + bool operator<(const CustomEncodableValue& other) const { + return this < &other; + } + bool operator==(const CustomEncodableValue& other) const { + return this == &other; + } + + private: + std::any value_; +}; + +class EncodableValue; + +// Convenience type aliases. +using EncodableList = std::vector; +using EncodableMap = std::map; + +namespace internal { +// The base class for EncodableValue. Do not use this directly; it exists only +// for EncodableValue to inherit from. +// +// Do not change the order or indexes of the items here; see the comment on +// EncodableValue +using EncodableValueVariant = std::variant, + std::vector, + std::vector, + std::vector, + EncodableList, + EncodableMap, + CustomEncodableValue>; +} // namespace internal + +// An object that can contain any value or collection type supported by +// Flutter's standard method codec. +// +// For details, see: +// https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html +// +// As an example, the following Dart structure: +// { +// 'flag': true, +// 'name': 'Thing', +// 'values': [1, 2.0, 4], +// } +// would correspond to: +// EncodableValue(EncodableMap{ +// {EncodableValue("flag"), EncodableValue(true)}, +// {EncodableValue("name"), EncodableValue("Thing")}, +// {EncodableValue("values"), EncodableValue(EncodableList{ +// EncodableValue(1), +// EncodableValue(2.0), +// EncodableValue(4), +// })}, +// }) +// +// The primary API surface for this object is std::variant. For instance, +// getting a string value from an EncodableValue, with type checking: +// if (std::holds_alternative(value)) { +// std::string some_string = std::get(value); +// } +// +// The order/indexes of the variant types is part of the API surface, and is +// guaranteed not to change. +class EncodableValue : public internal::EncodableValueVariant { + public: + // Rely on std::variant for most of the constructors/operators. + using super = internal::EncodableValueVariant; + using super::super; + using super::operator=; + + explicit EncodableValue() = default; + + // Avoid the C++17 pitfall of conversion from char* to bool. Should not be + // needed for C++20. + explicit EncodableValue(const char* string) : super(std::string(string)) {} + EncodableValue& operator=(const char* other) { + *this = std::string(other); + return *this; + } + + // Allow implicit conversion from CustomEncodableValue; the only reason to + // make a CustomEncodableValue (which can only be constructed explicitly) is + // to use it with EncodableValue, so the risk of unintended conversions is + // minimal, and it avoids the need for the verbose: + // EncodableValue(CustomEncodableValue(...)). + EncodableValue(const CustomEncodableValue& v) : super(v) {} + + // Override the conversion constructors from std::variant to make them + // explicit, to avoid implicit conversion. + // + // While implicit conversion can be convenient in some cases, it can have very + // surprising effects. E.g., calling a function that takes an EncodableValue + // but accidentally passing an EncodableValue* would, instead of failing to + // compile, go through a pointer->bool->EncodableValue(bool) chain and + // silently call the function with a temp-constructed EncodableValue(true). + template + constexpr explicit EncodableValue(T&& t) noexcept : super(t) {} + + // Returns true if the value is null. Convenience wrapper since unlike the + // other types, std::monostate uses aren't self-documenting. + bool IsNull() const { return std::holds_alternative(*this); } + + // Convience method to simplify handling objects received from Flutter where + // the values may be larger than 32-bit, since they have the same type on the + // Dart side, but will be either 32-bit or 64-bit here depending on the value. + // + // Calling this method if the value doesn't contain either an int32_t or an + // int64_t will throw an exception. + int64_t LongValue() { + if (std::holds_alternative(*this)) { + return std::get(*this); + } + return std::get(*this); + } +}; + +#else + class EncodableValue; // Convenience type aliases for list and map EncodableValue types. using EncodableList = std::vector; @@ -564,6 +754,8 @@ class EncodableValue { Type type_ = Type::kNull; }; +#endif + } // namespace flutter #endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_ENCODABLE_VALUE_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h b/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h index 301c73a418195..23fd7940752db 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h @@ -144,14 +144,14 @@ class EventChannel { const MethodCodec* codec_; protected: - void SuccessInternal(T* event = nullptr) override { + void SuccessInternal(const T* event = nullptr) override { auto result = codec_->EncodeSuccessEnvelope(event); messenger_->Send(name_, result->data(), result->size()); } void ErrorInternal(const std::string& error_code, const std::string& error_message, - T* error_details) override { + const T* error_details) override { auto result = codec_->EncodeErrorEnvelope(error_code, error_message, error_details); messenger_->Send(name_, result->data(), result->size()); diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h b/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h index 6223ad7b6a572..764ea9d9f1fb8 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h @@ -19,28 +19,49 @@ class EventSink { EventSink(EventSink const&) = delete; EventSink& operator=(EventSink const&) = delete; + // DEPRECATED. Use the reference version below. This will be removed in the + // near future. + void Success(const T* event) { SuccessInternal(event); } + + // Consumes a successful event + void Success(const T& event) { SuccessInternal(&event); } + // Consumes a successful event. - void Success(T* event = nullptr) { SuccessInternal(event); } + void Success() { SuccessInternal(nullptr); } - // Consumes an error event. + // DEPRECATED. Use the reference version below. This will be removed in the + // near future. void Error(const std::string& error_code, - const std::string& error_message = "", - T* error_details = nullptr) { + const std::string& error_message, + const T* error_details) { ErrorInternal(error_code, error_message, error_details); } + // Consumes an error event. + void Error(const std::string& error_code, + const std::string& error_message, + const T& error_details) { + ErrorInternal(error_code, error_message, &error_details); + } + + // Consumes an error event. + void Error(const std::string& error_code, + const std::string& error_message = "") { + ErrorInternal(error_code, error_message, nullptr); + } + // Consumes end of stream. Ensuing calls to Success() or // Error(), if any, are ignored. void EndOfStream() { EndOfStreamInternal(); } protected: // Implementation of the public interface, to be provided by subclasses. - virtual void SuccessInternal(T* event = nullptr) = 0; + virtual void SuccessInternal(const T* event = nullptr) = 0; // Implementation of the public interface, to be provided by subclasses. virtual void ErrorInternal(const std::string& error_code, const std::string& error_message, - T* error_details) = 0; + const T* error_details) = 0; // Implementation of the public interface, to be provided by subclasses. virtual void EndOfStreamInternal() = 0; diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h index 1e9c822e253e8..c2b2df5791058 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h @@ -22,20 +22,48 @@ class MethodResult { MethodResult(MethodResult const&) = delete; MethodResult& operator=(MethodResult const&) = delete; - // Sends a success response, indicating that the call completed successfully. - // An optional value can be provided as part of the success message. - void Success(const T* result = nullptr) { SuccessInternal(result); } + // DEPRECATED. Use the reference versions below. This will be removed in the + // near future. + void Success(const T* result) { SuccessInternal(result); } - // Sends an error response, indicating that the call was understood but - // handling failed in some way. A string error code must be provided, and in - // addition an optional user-readable error_message and/or details object can - // be included. + // Sends a success response, indicating that the call completed successfully + // with the given result. + void Success(const T& result) { SuccessInternal(&result); } + + // Sends a success response, indicating that the call completed successfully + // with no result. + void Success() { SuccessInternal(nullptr); } + + // DEPRECATED. Use the reference versions below. This will be removed in the + // near future. void Error(const std::string& error_code, - const std::string& error_message = "", - const T* error_details = nullptr) { + const std::string& error_message, + const T* error_details) { ErrorInternal(error_code, error_message, error_details); } + // Sends an error response, indicating that the call was understood but + // handling failed in some way. + // + // error_code: A string error code describing the error. + // error_message: A user-readable error message. + // error_details: Arbitrary extra details about the error. + void Error(const std::string& error_code, + const std::string& error_message, + const T& error_details) { + ErrorInternal(error_code, error_message, &error_details); + } + + // Sends an error response, indicating that the call was understood but + // handling failed in some way. + // + // error_code: A string error code describing the error. + // error_message: A user-readable error message (optional). + void Error(const std::string& error_code, + const std::string& error_message = "") { + ErrorInternal(error_code, error_message, nullptr); + } + // Sends a not-implemented response, indicating that the method either was not // recognized, or has not been implemented. void NotImplemented() { NotImplementedInternal(); } diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h index 647b7cbb48e8b..1ad67a1b41044 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h @@ -5,13 +5,13 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_H_ +#include + #include #include #include #include -#include - #include "binary_messenger.h" namespace flutter { @@ -48,11 +48,8 @@ class PluginRegistrar { // that they stay valid for any registered callbacks. void AddPlugin(std::unique_ptr plugin); - // Enables input blocking on the given channel name. - // - // If set, then the parent window should disable input callbacks - // while waiting for the handler for messages on that channel to run. - void EnableInputBlockingForChannel(const std::string& channel); + protected: + FlutterDesktopPluginRegistrarRef registrar() { return registrar_; } private: // Handle for interacting with the C API's registrar. diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h new file mode 100644 index 0000000000000..f571b3256e360 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h @@ -0,0 +1,76 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_CODEC_SERIALIZER_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_CODEC_SERIALIZER_H_ + +#include "byte_streams.h" +#include "encodable_value.h" + +namespace flutter { + +// Encapsulates the logic for encoding/decoding EncodableValues to/from the +// standard codec binary representation. +// +// This can be subclassed to extend the standard codec with support for new +// types. +class StandardCodecSerializer { + public: + virtual ~StandardCodecSerializer(); + + // Returns the shared serializer instance. + static const StandardCodecSerializer& GetInstance(); + + // Prevent copying. + StandardCodecSerializer(StandardCodecSerializer const&) = delete; + StandardCodecSerializer& operator=(StandardCodecSerializer const&) = delete; + + // Reads and returns the next value from |stream|. + EncodableValue ReadValue(ByteStreamReader* stream) const; + + // Writes the encoding of |value| to |stream|, including the initial type + // discrimination byte. + // + // Can be overridden by a subclass to extend the codec. + virtual void WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const; + + protected: + // Codecs require long-lived serializers, so clients should always use + // GetInstance(). + StandardCodecSerializer(); + + // Reads and returns the next value from |stream|, whose discrimination byte + // was |type|. + // + // The discrimination byte will already have been read from the stream when + // this is called. + // + // Can be overridden by a subclass to extend the codec. + virtual EncodableValue ReadValueOfType(uint8_t type, + ByteStreamReader* stream) const; + + // Reads the variable-length size from the current position in |stream|. + size_t ReadSize(ByteStreamReader* stream) const; + + // Writes the variable-length size encoding to |stream|. + void WriteSize(size_t size, ByteStreamWriter* stream) const; + + private: + // Reads a fixed-type list whose values are of type T from the current + // position in |stream|, and returns it as the corresponding EncodableValue. + // |T| must correspond to one of the supported list value types of + // EncodableValue. + template + EncodableValue ReadVector(ByteStreamReader* stream) const; + + // Writes |vector| to |stream| as a fixed-type list. |T| must correspond to + // one of the supported list value types of EncodableValue. + template + void WriteVector(const std::vector vector, ByteStreamWriter* stream) const; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_CODEC_SERIALIZER_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h index 75644c7a85f31..735dcda580c7e 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h @@ -5,8 +5,11 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_MESSAGE_CODEC_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_MESSAGE_CODEC_H_ +#include + #include "encodable_value.h" #include "message_codec.h" +#include "standard_codec_serializer.h" namespace flutter { @@ -14,8 +17,17 @@ namespace flutter { // Flutter engine via message channels. class StandardMessageCodec : public MessageCodec { public: - // Returns the shared instance of the codec. - static const StandardMessageCodec& GetInstance(); + // Returns an instance of the codec, optionally using a custom serializer to + // add support for more types. + // + // If provided, |serializer| must be long-lived. If no serializer is provided, + // the default will be used. + // + // The instance returned for a given |serializer| will be shared, and + // any instance returned from this will be long-lived, and can be safely + // passed to, e.g., channel constructors. + static const StandardMessageCodec& GetInstance( + const StandardCodecSerializer* serializer = nullptr); ~StandardMessageCodec(); @@ -24,9 +36,6 @@ class StandardMessageCodec : public MessageCodec { StandardMessageCodec& operator=(StandardMessageCodec const&) = delete; protected: - // Instances should be obtained via GetInstance. - StandardMessageCodec(); - // |flutter::MessageCodec| std::unique_ptr DecodeMessageInternal( const uint8_t* binary_message, @@ -35,6 +44,12 @@ class StandardMessageCodec : public MessageCodec { // |flutter::MessageCodec| std::unique_ptr> EncodeMessageInternal( const EncodableValue& message) const override; + + private: + // Instances should be obtained via GetInstance. + explicit StandardMessageCodec(const StandardCodecSerializer* serializer); + + const StandardCodecSerializer* serializer_; }; } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h index ef40897893183..729babc2b62b0 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h @@ -5,28 +5,37 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_METHOD_CODEC_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_METHOD_CODEC_H_ +#include + #include "encodable_value.h" #include "method_call.h" #include "method_codec.h" +#include "standard_codec_serializer.h" namespace flutter { // An implementation of MethodCodec that uses a binary serialization. class StandardMethodCodec : public MethodCodec { public: - // Returns the shared instance of the codec. - static const StandardMethodCodec& GetInstance(); + // Returns an instance of the codec, optionally using a custom serializer to + // add support for more types. + // + // If provided, |serializer| must be long-lived. If no serializer is provided, + // the default will be used. + // + // The instance returned for a given |extension| will be shared, and + // any instance returned from this will be long-lived, and can be safely + // passed to, e.g., channel constructors. + static const StandardMethodCodec& GetInstance( + const StandardCodecSerializer* serializer = nullptr); - ~StandardMethodCodec() = default; + ~StandardMethodCodec(); // Prevent copying. StandardMethodCodec(StandardMethodCodec const&) = delete; StandardMethodCodec& operator=(StandardMethodCodec const&) = delete; protected: - // Instances should be obtained via GetInstance. - StandardMethodCodec() = default; - // |flutter::MethodCodec| std::unique_ptr> DecodeMethodCallInternal( const uint8_t* message, @@ -51,6 +60,12 @@ class StandardMethodCodec : public MethodCodec { const uint8_t* response, size_t response_size, MethodResult* result) const override; + + private: + // Instances should be obtained via GetInstance. + explicit StandardMethodCodec(const StandardCodecSerializer* serializer); + + const StandardCodecSerializer* serializer_; }; } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc b/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc index ee62b526eaf12..13cbb50f88c63 100644 --- a/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h" - #include #include #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" #include "gtest/gtest.h" @@ -68,6 +67,7 @@ TEST(MethodChannelTest, Registration) { // result. EXPECT_EQ(call.method_name(), method_name); EXPECT_NE(result, nullptr); + result->Success(); }); EXPECT_EQ(messenger.last_message_handler_channel(), channel_name); EXPECT_NE(messenger.last_message_handler(), nullptr); @@ -119,7 +119,7 @@ TEST(MethodChannelTest, InvokeWithResponse) { auto result_handler = std::make_unique>( [&received_reply, reply](const EncodableValue* success_value) { received_reply = true; - EXPECT_EQ(success_value->StringValue(), reply); + EXPECT_EQ(std::get(*success_value), reply); }, nullptr, nullptr); diff --git a/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc b/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc index 98750dbba244b..9c32da9774a7c 100644 --- a/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" - #include #include +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" #include "gtest/gtest.h" namespace flutter { @@ -29,7 +28,7 @@ TEST(MethodChannelTest, Success) { EXPECT_EQ(*i, value); }, nullptr, nullptr); - result.Success(&value); + result.Success(value); EXPECT_TRUE(called); } @@ -50,7 +49,7 @@ TEST(MethodChannelTest, Error) { EXPECT_EQ(*details, error_details); }, nullptr); - result.Error(error_code, error_message, &error_details); + result.Error(error_code, error_message, error_details); EXPECT_TRUE(called); } diff --git a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc index 9527dd73ead3d..a779d6e26df53 100644 --- a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc +++ b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc @@ -7,125 +7,12 @@ #include #include +#include "binary_messenger_impl.h" #include "include/flutter/engine_method_result.h" #include "include/flutter/method_channel.h" namespace flutter { -namespace { - -// Passes |message| to |user_data|, which must be a BinaryMessageHandler, along -// with a BinaryReply that will send a response on |message|'s response handle. -// -// This serves as an adaptor between the function-pointer-based message callback -// interface provided by the C API and the std::function-based message handler -// interface of BinaryMessenger. -void ForwardToHandler(FlutterDesktopMessengerRef messenger, - const FlutterDesktopMessage* message, - void* user_data) { - auto* response_handle = message->response_handle; - BinaryReply reply_handler = [messenger, response_handle]( - const uint8_t* reply, - size_t reply_size) mutable { - if (!response_handle) { - std::cerr << "Error: Response can be set only once. Ignoring " - "duplicate response." - << std::endl; - return; - } - FlutterDesktopMessengerSendResponse(messenger, response_handle, reply, - reply_size); - // The engine frees the response handle once - // FlutterDesktopSendMessageResponse is called. - response_handle = nullptr; - }; - - const BinaryMessageHandler& message_handler = - *static_cast(user_data); - - message_handler(message->message, message->message_size, - std::move(reply_handler)); -} - -} // namespace - -// Wrapper around a FlutterDesktopMessengerRef that implements the -// BinaryMessenger API. -class BinaryMessengerImpl : public BinaryMessenger { - public: - explicit BinaryMessengerImpl(FlutterDesktopMessengerRef core_messenger) - : messenger_(core_messenger) {} - - virtual ~BinaryMessengerImpl() = default; - - // Prevent copying. - BinaryMessengerImpl(BinaryMessengerImpl const&) = delete; - BinaryMessengerImpl& operator=(BinaryMessengerImpl const&) = delete; - - // |flutter::BinaryMessenger| - void Send(const std::string& channel, - const uint8_t* message, - size_t message_size, - BinaryReply reply) const override; - - // |flutter::BinaryMessenger| - void SetMessageHandler(const std::string& channel, - BinaryMessageHandler handler) override; - - private: - // Handle for interacting with the C API. - FlutterDesktopMessengerRef messenger_; - - // A map from channel names to the BinaryMessageHandler that should be called - // for incoming messages on that channel. - std::map handlers_; -}; - -void BinaryMessengerImpl::Send(const std::string& channel, - const uint8_t* message, - size_t message_size, - BinaryReply reply) const { - if (reply == nullptr) { - FlutterDesktopMessengerSend(messenger_, channel.c_str(), message, - message_size); - return; - } - struct Captures { - BinaryReply reply; - }; - auto captures = new Captures(); - captures->reply = reply; - - auto message_reply = [](const uint8_t* data, size_t data_size, - void* user_data) { - auto captures = reinterpret_cast(user_data); - captures->reply(data, data_size); - delete captures; - }; - bool result = FlutterDesktopMessengerSendWithReply( - messenger_, channel.c_str(), message, message_size, message_reply, - captures); - if (!result) { - delete captures; - } -} - -void BinaryMessengerImpl::SetMessageHandler(const std::string& channel, - BinaryMessageHandler handler) { - if (!handler) { - handlers_.erase(channel); - FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), nullptr, - nullptr); - return; - } - // Save the handler, to keep it alive. - handlers_[channel] = std::move(handler); - BinaryMessageHandler* message_handler = &handlers_[channel]; - // Set an adaptor callback that will invoke the handler. - FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), - ForwardToHandler, message_handler); -} - // ===== PluginRegistrar ===== PluginRegistrar::PluginRegistrar(FlutterDesktopPluginRegistrarRef registrar) @@ -140,11 +27,6 @@ void PluginRegistrar::AddPlugin(std::unique_ptr plugin) { plugins_.insert(std::move(plugin)); } -void PluginRegistrar::EnableInputBlockingForChannel( - const std::string& channel) { - FlutterDesktopRegistrarEnableInputBlocking(registrar_, channel.c_str()); -} - // ===== PluginRegistrarManager ===== // static diff --git a/shell/platform/common/cpp/client_wrapper/publish.gni b/shell/platform/common/cpp/client_wrapper/publish.gni index e52bf6a319369..907197b6cced6 100644 --- a/shell/platform/common/cpp/client_wrapper/publish.gni +++ b/shell/platform/common/cpp/client_wrapper/publish.gni @@ -71,7 +71,9 @@ template("publish_client_wrapper_core") { "visibility", ]) public = core_cpp_client_wrapper_includes - sources = core_cpp_client_wrapper_sources + [ _wrapper_readme ] + sources = core_cpp_client_wrapper_sources + + core_cpp_client_wrapper_internal_headers + [ _wrapper_readme ] + + temporary_shim_files } } diff --git a/shell/platform/common/cpp/client_wrapper/standard_codec.cc b/shell/platform/common/cpp/client_wrapper/standard_codec.cc index 609329c8a7376..bb6309844fb9f 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_codec.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_codec.cc @@ -8,17 +8,18 @@ // together to simplify use of the client wrapper, since the common case is // that any client that needs one of these files needs all three. -#include "include/flutter/standard_message_codec.h" -#include "include/flutter/standard_method_codec.h" -#include "standard_codec_serializer.h" - -#include +#include #include #include #include #include #include +#include "byte_buffer_streams.h" +#include "include/flutter/standard_codec_serializer.h" +#include "include/flutter/standard_message_codec.h" +#include "include/flutter/standard_method_codec.h" + namespace flutter { // ===== standard_codec_serializer.h ===== @@ -45,6 +46,7 @@ enum class EncodedType { // Returns the encoded type that should be written when serializing |value|. EncodedType EncodedTypeForValue(const EncodableValue& value) { +#ifdef USE_LEGACY_ENCODABLE_VALUE switch (value.type()) { case EncodableValue::Type::kNull: return EncodedType::kNull; @@ -71,6 +73,34 @@ EncodedType EncodedTypeForValue(const EncodableValue& value) { case EncodableValue::Type::kMap: return EncodedType::kMap; } +#else + switch (value.index()) { + case 0: + return EncodedType::kNull; + case 1: + return std::get(value) ? EncodedType::kTrue : EncodedType::kFalse; + case 2: + return EncodedType::kInt32; + case 3: + return EncodedType::kInt64; + case 4: + return EncodedType::kFloat64; + case 5: + return EncodedType::kString; + case 6: + return EncodedType::kUInt8List; + case 7: + return EncodedType::kInt32List; + case 8: + return EncodedType::kInt64List; + case 9: + return EncodedType::kFloat64List; + case 10: + return EncodedType::kList; + case 11: + return EncodedType::kMap; + } +#endif assert(false); return EncodedType::kNull; } @@ -81,97 +111,36 @@ StandardCodecSerializer::StandardCodecSerializer() = default; StandardCodecSerializer::~StandardCodecSerializer() = default; +const StandardCodecSerializer& StandardCodecSerializer::GetInstance() { + static StandardCodecSerializer sInstance; + return sInstance; +}; + EncodableValue StandardCodecSerializer::ReadValue( - ByteBufferStreamReader* stream) const { - EncodedType type = static_cast(stream->ReadByte()); - switch (type) { - case EncodedType::kNull: - return EncodableValue(); - case EncodedType::kTrue: - return EncodableValue(true); - case EncodedType::kFalse: - return EncodableValue(false); - case EncodedType::kInt32: { - int32_t int_value = 0; - stream->ReadBytes(reinterpret_cast(&int_value), 4); - return EncodableValue(int_value); - } - case EncodedType::kInt64: { - int64_t long_value = 0; - stream->ReadBytes(reinterpret_cast(&long_value), 8); - return EncodableValue(long_value); - } - case EncodedType::kFloat64: { - double double_value = 0; - stream->ReadAlignment(8); - stream->ReadBytes(reinterpret_cast(&double_value), 8); - return EncodableValue(double_value); - } - case EncodedType::kLargeInt: - case EncodedType::kString: { - size_t size = ReadSize(stream); - std::string string_value; - string_value.resize(size); - stream->ReadBytes(reinterpret_cast(&string_value[0]), size); - return EncodableValue(string_value); - } - case EncodedType::kUInt8List: - return ReadVector(stream); - case EncodedType::kInt32List: - return ReadVector(stream); - case EncodedType::kInt64List: - return ReadVector(stream); - case EncodedType::kFloat64List: - return ReadVector(stream); - case EncodedType::kList: { - size_t length = ReadSize(stream); - EncodableList list_value; - list_value.reserve(length); - for (size_t i = 0; i < length; ++i) { - list_value.push_back(ReadValue(stream)); - } - return EncodableValue(list_value); - } - case EncodedType::kMap: { - size_t length = ReadSize(stream); - EncodableMap map_value; - for (size_t i = 0; i < length; ++i) { - EncodableValue key = ReadValue(stream); - EncodableValue value = ReadValue(stream); - map_value.emplace(std::move(key), std::move(value)); - } - return EncodableValue(map_value); - } - } - std::cerr << "Unknown type in StandardCodecSerializer::ReadValue: " - << static_cast(type) << std::endl; - return EncodableValue(); + ByteStreamReader* stream) const { + uint8_t type = stream->ReadByte(); + return ReadValueOfType(type, stream); } void StandardCodecSerializer::WriteValue(const EncodableValue& value, - ByteBufferStreamWriter* stream) const { + ByteStreamWriter* stream) const { stream->WriteByte(static_cast(EncodedTypeForValue(value))); +#ifdef USE_LEGACY_ENCODABLE_VALUE switch (value.type()) { case EncodableValue::Type::kNull: case EncodableValue::Type::kBool: // Null and bool are encoded directly in the type. break; - case EncodableValue::Type::kInt: { - int32_t int_value = value.IntValue(); - stream->WriteBytes(reinterpret_cast(&int_value), 4); + case EncodableValue::Type::kInt: + stream->WriteInt32(value.IntValue()); break; - } - case EncodableValue::Type::kLong: { - int64_t long_value = value.LongValue(); - stream->WriteBytes(reinterpret_cast(&long_value), 8); + case EncodableValue::Type::kLong: + stream->WriteInt64(value.LongValue()); break; - } - case EncodableValue::Type::kDouble: { + case EncodableValue::Type::kDouble: stream->WriteAlignment(8); - double double_value = value.DoubleValue(); - stream->WriteBytes(reinterpret_cast(&double_value), 8); + stream->WriteDouble(value.DoubleValue()); break; - } case EncodableValue::Type::kString: { const auto& string_value = value.StringValue(); size_t size = string_value.size(); @@ -208,9 +177,130 @@ void StandardCodecSerializer::WriteValue(const EncodableValue& value, } break; } +#else + // TODO: Consider replacing this this with a std::visitor. + switch (value.index()) { + case 0: + case 1: + // Null and bool are encoded directly in the type. + break; + case 2: + stream->WriteInt32(std::get(value)); + break; + case 3: + stream->WriteInt64(std::get(value)); + break; + case 4: + stream->WriteAlignment(8); + stream->WriteDouble(std::get(value)); + break; + case 5: { + const auto& string_value = std::get(value); + size_t size = string_value.size(); + WriteSize(size, stream); + if (size > 0) { + stream->WriteBytes( + reinterpret_cast(string_value.data()), size); + } + break; + } + case 6: + WriteVector(std::get>(value), stream); + break; + case 7: + WriteVector(std::get>(value), stream); + break; + case 8: + WriteVector(std::get>(value), stream); + break; + case 9: + WriteVector(std::get>(value), stream); + break; + case 10: { + const auto& list = std::get(value); + WriteSize(list.size(), stream); + for (const auto& item : list) { + WriteValue(item, stream); + } + break; + } + case 11: { + const auto& map = std::get(value); + WriteSize(map.size(), stream); + for (const auto& pair : map) { + WriteValue(pair.first, stream); + WriteValue(pair.second, stream); + } + break; + } + case 12: + std::cerr + << "Unhandled custom type in StandardCodecSerializer::WriteValue. " + << "Custom types require codec extensions." << std::endl; + break; + } +#endif +} + +EncodableValue StandardCodecSerializer::ReadValueOfType( + uint8_t type, + ByteStreamReader* stream) const { + switch (static_cast(type)) { + case EncodedType::kNull: + return EncodableValue(); + case EncodedType::kTrue: + return EncodableValue(true); + case EncodedType::kFalse: + return EncodableValue(false); + case EncodedType::kInt32: + return EncodableValue(stream->ReadInt32()); + case EncodedType::kInt64: + return EncodableValue(stream->ReadInt64()); + case EncodedType::kFloat64: + stream->ReadAlignment(8); + return EncodableValue(stream->ReadDouble()); + case EncodedType::kLargeInt: + case EncodedType::kString: { + size_t size = ReadSize(stream); + std::string string_value; + string_value.resize(size); + stream->ReadBytes(reinterpret_cast(&string_value[0]), size); + return EncodableValue(string_value); + } + case EncodedType::kUInt8List: + return ReadVector(stream); + case EncodedType::kInt32List: + return ReadVector(stream); + case EncodedType::kInt64List: + return ReadVector(stream); + case EncodedType::kFloat64List: + return ReadVector(stream); + case EncodedType::kList: { + size_t length = ReadSize(stream); + EncodableList list_value; + list_value.reserve(length); + for (size_t i = 0; i < length; ++i) { + list_value.push_back(ReadValue(stream)); + } + return EncodableValue(list_value); + } + case EncodedType::kMap: { + size_t length = ReadSize(stream); + EncodableMap map_value; + for (size_t i = 0; i < length; ++i) { + EncodableValue key = ReadValue(stream); + EncodableValue value = ReadValue(stream); + map_value.emplace(std::move(key), std::move(value)); + } + return EncodableValue(map_value); + } + } + std::cerr << "Unknown type in StandardCodecSerializer::ReadValueOfType: " + << static_cast(type) << std::endl; + return EncodableValue(); } -size_t StandardCodecSerializer::ReadSize(ByteBufferStreamReader* stream) const { +size_t StandardCodecSerializer::ReadSize(ByteStreamReader* stream) const { uint8_t byte = stream->ReadByte(); if (byte < 254) { return byte; @@ -226,7 +316,7 @@ size_t StandardCodecSerializer::ReadSize(ByteBufferStreamReader* stream) const { } void StandardCodecSerializer::WriteSize(size_t size, - ByteBufferStreamWriter* stream) const { + ByteStreamWriter* stream) const { if (size < 254) { stream->WriteByte(static_cast(size)); } else if (size <= 0xffff) { @@ -242,7 +332,7 @@ void StandardCodecSerializer::WriteSize(size_t size, template EncodableValue StandardCodecSerializer::ReadVector( - ByteBufferStreamReader* stream) const { + ByteStreamReader* stream) const { size_t count = ReadSize(stream); std::vector vector; vector.resize(count); @@ -256,9 +346,8 @@ EncodableValue StandardCodecSerializer::ReadVector( } template -void StandardCodecSerializer::WriteVector( - const std::vector vector, - ByteBufferStreamWriter* stream) const { +void StandardCodecSerializer::WriteVector(const std::vector vector, + ByteStreamWriter* stream) const { size_t count = vector.size(); WriteSize(count, stream); if (count == 0) { @@ -275,69 +364,115 @@ void StandardCodecSerializer::WriteVector( // ===== standard_message_codec.h ===== // static -const StandardMessageCodec& StandardMessageCodec::GetInstance() { - static StandardMessageCodec sInstance; - return sInstance; +const StandardMessageCodec& StandardMessageCodec::GetInstance( + const StandardCodecSerializer* serializer) { + if (!serializer) { + serializer = &StandardCodecSerializer::GetInstance(); + } + auto* sInstances = new std::map>; + auto it = sInstances->find(serializer); + if (it == sInstances->end()) { + // Uses new due to private constructor (to prevent API clients from + // accidentally passing temporary codec instances to channels). + auto emplace_result = sInstances->emplace( + serializer, std::unique_ptr( + new StandardMessageCodec(serializer))); + it = emplace_result.first; + } + return *(it->second); } -StandardMessageCodec::StandardMessageCodec() = default; +StandardMessageCodec::StandardMessageCodec( + const StandardCodecSerializer* serializer) + : serializer_(serializer) {} StandardMessageCodec::~StandardMessageCodec() = default; std::unique_ptr StandardMessageCodec::DecodeMessageInternal( const uint8_t* binary_message, size_t message_size) const { - StandardCodecSerializer serializer; ByteBufferStreamReader stream(binary_message, message_size); - return std::make_unique(serializer.ReadValue(&stream)); + return std::make_unique(serializer_->ReadValue(&stream)); } std::unique_ptr> StandardMessageCodec::EncodeMessageInternal( const EncodableValue& message) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); - serializer.WriteValue(message, &stream); + serializer_->WriteValue(message, &stream); return encoded; } // ===== standard_method_codec.h ===== // static -const StandardMethodCodec& StandardMethodCodec::GetInstance() { - static StandardMethodCodec sInstance; - return sInstance; +const StandardMethodCodec& StandardMethodCodec::GetInstance( + const StandardCodecSerializer* serializer) { + if (!serializer) { + serializer = &StandardCodecSerializer::GetInstance(); + } + auto* sInstances = new std::map>; + auto it = sInstances->find(serializer); + if (it == sInstances->end()) { + // Uses new due to private constructor (to prevent API clients from + // accidentally passing temporary codec instances to channels). + auto emplace_result = sInstances->emplace( + serializer, std::unique_ptr( + new StandardMethodCodec(serializer))); + it = emplace_result.first; + } + return *(it->second); } +StandardMethodCodec::StandardMethodCodec( + const StandardCodecSerializer* serializer) + : serializer_(serializer) {} + +StandardMethodCodec::~StandardMethodCodec() = default; + std::unique_ptr> StandardMethodCodec::DecodeMethodCallInternal(const uint8_t* message, size_t message_size) const { - StandardCodecSerializer serializer; ByteBufferStreamReader stream(message, message_size); - EncodableValue method_name = serializer.ReadValue(&stream); +#ifdef USE_LEGACY_ENCODABLE_VALUE + EncodableValue method_name = serializer_->ReadValue(&stream); if (!method_name.IsString()) { std::cerr << "Invalid method call; method name is not a string." << std::endl; return nullptr; } auto arguments = - std::make_unique(serializer.ReadValue(&stream)); + std::make_unique(serializer_->ReadValue(&stream)); return std::make_unique>(method_name.StringValue(), std::move(arguments)); +#else + EncodableValue method_name_value = serializer_->ReadValue(&stream); + const auto* method_name = std::get_if(&method_name_value); + if (!method_name) { + std::cerr << "Invalid method call; method name is not a string." + << std::endl; + return nullptr; + } + auto arguments = + std::make_unique(serializer_->ReadValue(&stream)); + return std::make_unique>(*method_name, + std::move(arguments)); +#endif } std::unique_ptr> StandardMethodCodec::EncodeMethodCallInternal( const MethodCall& method_call) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); - serializer.WriteValue(EncodableValue(method_call.method_name()), &stream); + serializer_->WriteValue(EncodableValue(method_call.method_name()), &stream); if (method_call.arguments()) { - serializer.WriteValue(*method_call.arguments(), &stream); + serializer_->WriteValue(*method_call.arguments(), &stream); } else { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } return encoded; } @@ -345,14 +480,13 @@ StandardMethodCodec::EncodeMethodCallInternal( std::unique_ptr> StandardMethodCodec::EncodeSuccessEnvelopeInternal( const EncodableValue* result) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); stream.WriteByte(0); if (result) { - serializer.WriteValue(*result, &stream); + serializer_->WriteValue(*result, &stream); } else { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } return encoded; } @@ -362,20 +496,19 @@ StandardMethodCodec::EncodeErrorEnvelopeInternal( const std::string& error_code, const std::string& error_message, const EncodableValue* error_details) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); stream.WriteByte(1); - serializer.WriteValue(EncodableValue(error_code), &stream); + serializer_->WriteValue(EncodableValue(error_code), &stream); if (error_message.empty()) { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } else { - serializer.WriteValue(EncodableValue(error_message), &stream); + serializer_->WriteValue(EncodableValue(error_message), &stream); } if (error_details) { - serializer.WriteValue(*error_details, &stream); + serializer_->WriteValue(*error_details, &stream); } else { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } return encoded; } @@ -384,22 +517,39 @@ bool StandardMethodCodec::DecodeAndProcessResponseEnvelopeInternal( const uint8_t* response, size_t response_size, MethodResult* result) const { - StandardCodecSerializer serializer; ByteBufferStreamReader stream(response, response_size); uint8_t flag = stream.ReadByte(); switch (flag) { case 0: { - EncodableValue value = serializer.ReadValue(&stream); - result->Success(value.IsNull() ? nullptr : &value); + EncodableValue value = serializer_->ReadValue(&stream); + if (value.IsNull()) { + result->Success(); + } else { + result->Success(value); + } return true; } case 1: { - EncodableValue code = serializer.ReadValue(&stream); - EncodableValue message = serializer.ReadValue(&stream); - EncodableValue details = serializer.ReadValue(&stream); - result->Error(code.StringValue(), - message.IsNull() ? "" : message.StringValue(), - details.IsNull() ? nullptr : &details); + EncodableValue code = serializer_->ReadValue(&stream); + EncodableValue message = serializer_->ReadValue(&stream); + EncodableValue details = serializer_->ReadValue(&stream); +#ifdef USE_LEGACY_ENCODABLE_VALUE + if (details.IsNull()) { + result->Error(code.StringValue(), + message.IsNull() ? "" : message.StringValue()); + } else { + result->Error(code.StringValue(), + message.IsNull() ? "" : message.StringValue(), details); + } +#else + const std::string& message_string = + message.IsNull() ? "" : std::get(message); + if (details.IsNull()) { + result->Error(std::get(code), message_string); + } else { + result->Error(std::get(code), message_string, details); + } +#endif return true; } default: diff --git a/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h b/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h deleted file mode 100644 index 89aab3b9988f8..0000000000000 --- a/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h +++ /dev/null @@ -1,54 +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. - -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_ENCODABLE_VALUE_SERIALIZER_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_ENCODABLE_VALUE_SERIALIZER_H_ - -#include "byte_stream_wrappers.h" -#include "include/flutter/encodable_value.h" - -namespace flutter { - -// Encapsulates the logic for encoding/decoding EncodableValues to/from the -// standard codec binary representation. -class StandardCodecSerializer { - public: - StandardCodecSerializer(); - ~StandardCodecSerializer(); - - // Prevent copying. - StandardCodecSerializer(StandardCodecSerializer const&) = delete; - StandardCodecSerializer& operator=(StandardCodecSerializer const&) = delete; - - // Reads and returns the next value from |stream|. - EncodableValue ReadValue(ByteBufferStreamReader* stream) const; - - // Writes the encoding of |value| to |stream|. - void WriteValue(const EncodableValue& value, - ByteBufferStreamWriter* stream) const; - - protected: - // Reads the variable-length size from the current position in |stream|. - size_t ReadSize(ByteBufferStreamReader* stream) const; - - // Writes the variable-length size encoding to |stream|. - void WriteSize(size_t size, ByteBufferStreamWriter* stream) const; - - // Reads a fixed-type list whose values are of type T from the current - // position in |stream|, and returns it as the corresponding EncodableValue. - // |T| must correspond to one of the support list value types of - // EncodableValue. - template - EncodableValue ReadVector(ByteBufferStreamReader* stream) const; - - // Writes |vector| to |stream| as a fixed-type list. |T| must correspond to - // one of the support list value types of EncodableValue. - template - void WriteVector(const std::vector vector, - ByteBufferStreamWriter* stream) const; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_ENCODABLE_VALUE_SERIALIZER_H_ diff --git a/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc b/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc index a0b4437b195f8..03459dc75ddfa 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc @@ -2,27 +2,52 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h" - #include #include -#include "flutter/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h" #include "gtest/gtest.h" +#ifndef USE_LEGACY_ENCODABLE_VALUE +#include "flutter/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h" +#endif + namespace flutter { // Validates round-trip encoding and decoding of |value|, and checks that the // encoded value matches |expected_encoding|. -static void CheckEncodeDecode(const EncodableValue& value, - const std::vector& expected_encoding) { - const StandardMessageCodec& codec = StandardMessageCodec::GetInstance(); +// +// If testing with CustomEncodableValues, |serializer| must be provided to +// handle the encoding/decoding, and |custom_comparator| must be provided to +// validate equality since CustomEncodableValue doesn't define a useful ==. +static void CheckEncodeDecode( + const EncodableValue& value, + const std::vector& expected_encoding, + const StandardCodecSerializer* serializer = nullptr, + std::function + custom_comparator = nullptr) { + const StandardMessageCodec& codec = + StandardMessageCodec::GetInstance(serializer); auto encoded = codec.EncodeMessage(value); ASSERT_TRUE(encoded); EXPECT_EQ(*encoded, expected_encoding); auto decoded = codec.DecodeMessage(*encoded); - EXPECT_TRUE(testing::EncodableValuesAreEqual(value, *decoded)); +#ifdef USE_LEGACY_ENCODABLE_VALUE + // Full equality isn't implemented for the legacy path; just do a sanity test + // of basic types. + if (value.IsNull() || value.IsBool() || value.IsInt() || value.IsLong() || + value.IsDouble() || value.IsString()) { + EXPECT_FALSE(value < *decoded); + EXPECT_FALSE(*decoded < value); + } +#else + if (custom_comparator) { + EXPECT_TRUE(custom_comparator(value, *decoded)); + } else { + EXPECT_EQ(value, *decoded); + } +#endif } // Validates round-trip encoding and decoding of |value|, and checks that the @@ -34,7 +59,11 @@ static void CheckEncodeDecodeWithEncodePrefix( const EncodableValue& value, const std::vector& expected_encoding_prefix, size_t expected_encoding_length) { +#ifdef USE_LEGACY_ENCODABLE_VALUE EXPECT_TRUE(value.IsMap()); +#else + EXPECT_TRUE(std::holds_alternative(value)); +#endif const StandardMessageCodec& codec = StandardMessageCodec::GetInstance(); auto encoded = codec.EncodeMessage(value); ASSERT_TRUE(encoded); @@ -46,7 +75,12 @@ static void CheckEncodeDecodeWithEncodePrefix( expected_encoding_prefix.begin(), expected_encoding_prefix.end())); auto decoded = codec.DecodeMessage(*encoded); - EXPECT_TRUE(testing::EncodableValuesAreEqual(value, *decoded)); + +#ifdef USE_LEGACY_ENCODABLE_VALUE + EXPECT_NE(decoded, nullptr); +#else + EXPECT_EQ(value, *decoded); +#endif } TEST(StandardMessageCodec, CanEncodeAndDecodeNull) { @@ -170,4 +204,43 @@ TEST(StandardMessageCodec, CanEncodeAndDecodeFloat64Array) { CheckEncodeDecode(value, bytes); } +#ifndef USE_LEGACY_ENCODABLE_VALUE + +TEST(StandardMessageCodec, CanEncodeAndDecodeSimpleCustomType) { + std::vector bytes = {0x80, 0x09, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00}; + auto point_comparator = [](const EncodableValue& a, const EncodableValue& b) { + const Point& a_point = + std::any_cast(std::get(a)); + const Point& b_point = + std::any_cast(std::get(b)); + return a_point == b_point; + }; + CheckEncodeDecode(CustomEncodableValue(Point(9, 16)), bytes, + &PointExtensionSerializer::GetInstance(), point_comparator); +} + +TEST(StandardMessageCodec, CanEncodeAndDecodeVariableLengthCustomType) { + std::vector bytes = { + 0x81, // custom type + 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // data + 0x07, 0x04, // string type and length + 0x74, 0x65, 0x73, 0x74 // string characters + }; + auto some_data_comparator = [](const EncodableValue& a, + const EncodableValue& b) { + const SomeData& data_a = + std::any_cast(std::get(a)); + const SomeData& data_b = + std::any_cast(std::get(b)); + return data_a.data() == data_b.data() && data_a.label() == data_b.label(); + }; + CheckEncodeDecode(CustomEncodableValue( + SomeData("test", {0x00, 0x01, 0x02, 0x03, 0x04, 0x05})), + bytes, &SomeDataExtensionSerializer::GetInstance(), + some_data_comparator); +} + +#endif // !USE_LEGACY_ENCODABLE_VALUE + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc b/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc index 4b43b40229038..bb7a6f930f788 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" - #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" -#include "flutter/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h" #include "gtest/gtest.h" namespace flutter { @@ -24,7 +23,11 @@ bool MethodCallsAreEqual(const MethodCall& a, (!b.arguments() || b.arguments()->IsNull())) { return true; } - return testing::EncodableValuesAreEqual(*a.arguments(), *b.arguments()); + // If only one is nullptr, fail early rather than throw below. + if (!a.arguments() || !b.arguments()) { + return false; + } + return *a.arguments() == *b.arguments(); } } // namespace @@ -86,7 +89,7 @@ TEST(StandardMethodCodec, HandlesSuccessEnvelopesWithResult) { MethodResultFunctions result_handler( [&decoded_successfully](const EncodableValue* result) { decoded_successfully = true; - EXPECT_EQ(result->IntValue(), 42); + EXPECT_EQ(std::get(*result), 42); }, nullptr, nullptr); codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), @@ -145,9 +148,10 @@ TEST(StandardMethodCodec, HandlesErrorEnvelopesWithDetails) { decoded_successfully = true; EXPECT_EQ(code, "errorCode"); EXPECT_EQ(message, "something failed"); - EXPECT_TRUE(details->IsList()); - EXPECT_EQ(details->ListValue()[0].StringValue(), "a"); - EXPECT_EQ(details->ListValue()[1].IntValue(), 42); + const auto* details_list = std::get_if(details); + ASSERT_NE(details_list, nullptr); + EXPECT_EQ(std::get((*details_list)[0]), "a"); + EXPECT_EQ(std::get((*details_list)[1]), 42); }, nullptr); codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), @@ -155,4 +159,21 @@ TEST(StandardMethodCodec, HandlesErrorEnvelopesWithDetails) { EXPECT_TRUE(decoded_successfully); } +TEST(StandardMethodCodec, HandlesCustomTypeArguments) { + const StandardMethodCodec& codec = StandardMethodCodec::GetInstance( + &PointExtensionSerializer::GetInstance()); + Point point(7, 9); + MethodCall call( + "hello", std::make_unique(CustomEncodableValue(point))); + auto encoded = codec.EncodeMethodCall(call); + ASSERT_NE(encoded.get(), nullptr); + std::unique_ptr> decoded = + codec.DecodeMethodCall(*encoded); + ASSERT_NE(decoded.get(), nullptr); + + const Point& decoded_point = std::any_cast( + std::get(*decoded->arguments())); + EXPECT_EQ(point, decoded_point); +}; + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc b/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc deleted file mode 100644 index 607d69b18f599..0000000000000 --- a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc +++ /dev/null @@ -1,89 +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. - -#include "flutter/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h" - -#include - -namespace flutter { -namespace testing { - -bool EncodableValuesAreEqual(const EncodableValue& a, const EncodableValue& b) { - if (a.type() != b.type()) { - return false; - } - - switch (a.type()) { - case EncodableValue::Type::kNull: - return true; - case EncodableValue::Type::kBool: - return a.BoolValue() == b.BoolValue(); - case EncodableValue::Type::kInt: - return a.IntValue() == b.IntValue(); - case EncodableValue::Type::kLong: - return a.LongValue() == b.LongValue(); - case EncodableValue::Type::kDouble: - // This is a crude epsilon, but fine for the values in the unit tests. - return std::abs(a.DoubleValue() - b.DoubleValue()) < 0.0001l; - case EncodableValue::Type::kString: - return a.StringValue() == b.StringValue(); - case EncodableValue::Type::kByteList: - return a.ByteListValue() == b.ByteListValue(); - case EncodableValue::Type::kIntList: - return a.IntListValue() == b.IntListValue(); - case EncodableValue::Type::kLongList: - return a.LongListValue() == b.LongListValue(); - case EncodableValue::Type::kDoubleList: - return a.DoubleListValue() == b.DoubleListValue(); - case EncodableValue::Type::kList: { - const auto& a_list = a.ListValue(); - const auto& b_list = b.ListValue(); - if (a_list.size() != b_list.size()) { - return false; - } - for (size_t i = 0; i < a_list.size(); ++i) { - if (!EncodableValuesAreEqual(a_list[0], b_list[0])) { - return false; - } - } - return true; - } - case EncodableValue::Type::kMap: { - const auto& a_map = a.MapValue(); - const auto& b_map = b.MapValue(); - if (a_map.size() != b_map.size()) { - return false; - } - // Store references to all the keys in |b|. - std::vector unmatched_b_keys; - for (auto& pair : b_map) { - unmatched_b_keys.push_back(&pair.first); - } - // For each key,value in |a|, see if any of the not-yet-matched key,value - // pairs in |b| match by value; if so, remove that match and continue. - for (const auto& pair : a_map) { - bool found_match = false; - for (size_t i = 0; i < unmatched_b_keys.size(); ++i) { - const EncodableValue& b_key = *unmatched_b_keys[i]; - if (EncodableValuesAreEqual(pair.first, b_key) && - EncodableValuesAreEqual(pair.second, b_map.at(b_key))) { - found_match = true; - unmatched_b_keys.erase(unmatched_b_keys.begin() + i); - break; - } - } - if (!found_match) { - return false; - } - } - // If all entries had matches, consider the maps equal. - return true; - } - } - assert(false); - return false; -} - -} // namespace testing -} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h b/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h deleted file mode 100644 index 465afb0e96efd..0000000000000 --- a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h +++ /dev/null @@ -1,22 +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. - -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_ENCODABLE_VALUE_UTILS_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_ENCODABLE_VALUE_UTILS_H_ - -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" - -namespace flutter { -namespace testing { - -// Returns true if |a| and |b| have equivalent values, recursively comparing -// the contents of collections (unlike the < operator defined on EncodableValue, -// which doesn't consider different collections with the same contents to be -// the same). -bool EncodableValuesAreEqual(const EncodableValue& a, const EncodableValue& b); - -} // namespace testing -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_ENCODABLE_VALUE_UTILS_H_ diff --git a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc index 580df48645aa9..1f60548952249 100644 --- a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc +++ b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc @@ -52,14 +52,6 @@ void FlutterDesktopRegistrarSetDestructionHandler( } } -void FlutterDesktopRegistrarEnableInputBlocking( - FlutterDesktopPluginRegistrarRef registrar, - const char* channel) { - if (s_stub_implementation) { - s_stub_implementation->RegistrarEnableInputBlocking(channel); - } -} - bool FlutterDesktopMessengerSend(FlutterDesktopMessengerRef messenger, const char* channel, const uint8_t* message, diff --git a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h index 284d15e974947..ae55cdc9010f0 100644 --- a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h +++ b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h @@ -38,9 +38,6 @@ class StubFlutterApi { virtual void RegistrarSetDestructionHandler( FlutterDesktopOnRegistrarDestroyed callback) {} - // Called for FlutterDesktopRegistrarEnableInputBlocking. - virtual void RegistrarEnableInputBlocking(const char* channel) {} - // Called for FlutterDesktopMessengerSend. virtual bool MessengerSend(const char* channel, const uint8_t* message, diff --git a/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc new file mode 100644 index 0000000000000..53bdfe93a54e8 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc @@ -0,0 +1,80 @@ +// 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. + +#include "flutter/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h" + +namespace flutter { + +PointExtensionSerializer::PointExtensionSerializer() = default; +PointExtensionSerializer::~PointExtensionSerializer() = default; + +// static +const PointExtensionSerializer& PointExtensionSerializer::GetInstance() { + static PointExtensionSerializer sInstance; + return sInstance; +} + +EncodableValue PointExtensionSerializer::ReadValueOfType( + uint8_t type, + ByteStreamReader* stream) const { + if (type == kPointType) { + int32_t x = stream->ReadInt32(); + int32_t y = stream->ReadInt32(); + return CustomEncodableValue(Point(x, y)); + } + return StandardCodecSerializer::ReadValueOfType(type, stream); +} + +void PointExtensionSerializer::WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const { + auto custom_value = std::get_if(&value); + if (!custom_value) { + StandardCodecSerializer::WriteValue(value, stream); + return; + } + stream->WriteByte(kPointType); + const Point& point = std::any_cast(*custom_value); + stream->WriteInt32(point.x()); + stream->WriteInt32(point.y()); +} + +SomeDataExtensionSerializer::SomeDataExtensionSerializer() = default; +SomeDataExtensionSerializer::~SomeDataExtensionSerializer() = default; + +// static +const SomeDataExtensionSerializer& SomeDataExtensionSerializer::GetInstance() { + static SomeDataExtensionSerializer sInstance; + return sInstance; +} + +EncodableValue SomeDataExtensionSerializer::ReadValueOfType( + uint8_t type, + ByteStreamReader* stream) const { + if (type == kSomeDataType) { + size_t size = ReadSize(stream); + std::vector data; + data.resize(size); + stream->ReadBytes(data.data(), size); + EncodableValue label = ReadValue(stream); + return CustomEncodableValue(SomeData(std::get(label), data)); + } + return StandardCodecSerializer::ReadValueOfType(type, stream); +} + +void SomeDataExtensionSerializer::WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const { + auto custom_value = std::get_if(&value); + if (!custom_value) { + StandardCodecSerializer::WriteValue(value, stream); + return; + } + stream->WriteByte(kSomeDataType); + const SomeData& some_data = std::any_cast(*custom_value); + size_t data_size = some_data.data().size(); + WriteSize(data_size, stream); + stream->WriteBytes(some_data.data().data(), data_size); + WriteValue(EncodableValue(some_data.label()), stream); +} + +} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h new file mode 100644 index 0000000000000..cbe01c8a886ff --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h @@ -0,0 +1,89 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_TEST_CODEC_EXTENSIONS_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_TEST_CODEC_EXTENSIONS_H_ + +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h" + +namespace flutter { + +// A representation of a point, for custom type testing of a simple type. +class Point { + public: + Point(int x, int y) : x_(x), y_(y) {} + ~Point() = default; + + int x() const { return x_; } + int y() const { return y_; } + + bool operator==(const Point& other) const { + return x_ == other.x_ && y_ == other.y_; + } + + private: + int x_; + int y_; +}; + +// A typed binary data object with extra fields, for custom type testing of a +// variable-length type that includes types handled by the core standard codec. +class SomeData { + public: + SomeData(const std::string label, const std::vector& data) + : label_(label), data_(data) {} + ~SomeData() = default; + + const std::string& label() const { return label_; } + const std::vector& data() const { return data_; } + + private: + std::string label_; + std::vector data_; +}; + +// Codec extension for Point. +class PointExtensionSerializer : public StandardCodecSerializer { + public: + PointExtensionSerializer(); + virtual ~PointExtensionSerializer(); + + static const PointExtensionSerializer& GetInstance(); + + // |TestCodecSerializer| + EncodableValue ReadValueOfType(uint8_t type, + ByteStreamReader* stream) const override; + + // |TestCodecSerializer| + void WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const override; + + private: + static constexpr uint8_t kPointType = 128; +}; + +// Codec extension for SomeData. +class SomeDataExtensionSerializer : public StandardCodecSerializer { + public: + SomeDataExtensionSerializer(); + virtual ~SomeDataExtensionSerializer(); + + static const SomeDataExtensionSerializer& GetInstance(); + + // |TestCodecSerializer| + EncodableValue ReadValueOfType(uint8_t type, + ByteStreamReader* stream) const override; + + // |TestCodecSerializer| + void WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const override; + + private: + static constexpr uint8_t kSomeDataType = 129; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_TEST_CODEC_EXTENSIONS_H_ diff --git a/shell/platform/common/cpp/json_method_codec.cc b/shell/platform/common/cpp/json_method_codec.cc index 8fae8b7ed9b82..f026061ad235f 100644 --- a/shell/platform/common/cpp/json_method_codec.cc +++ b/shell/platform/common/cpp/json_method_codec.cc @@ -131,7 +131,11 @@ bool JsonMethodCodec::DecodeAndProcessResponseEnvelopeInternal( case 1: { std::unique_ptr value = ExtractElement(json_response.get(), &((*json_response)[0])); - result->Success(value->IsNull() ? nullptr : value.get()); + if (value->IsNull()) { + result->Success(); + } else { + result->Success(*value); + } return true; } case 3: { @@ -139,7 +143,11 @@ bool JsonMethodCodec::DecodeAndProcessResponseEnvelopeInternal( std::string message = (*json_response)[1].GetString(); std::unique_ptr details = ExtractElement(json_response.get(), &((*json_response)[2])); - result->Error(code, message, details->IsNull() ? nullptr : details.get()); + if (details->IsNull()) { + result->Error(code, message); + } else { + result->Error(code, message, *details); + } return true; } default: diff --git a/shell/platform/common/cpp/public/flutter_plugin_registrar.h b/shell/platform/common/cpp/public/flutter_plugin_registrar.h index 95f0abf139608..e27e125530ac9 100644 --- a/shell/platform/common/cpp/public/flutter_plugin_registrar.h +++ b/shell/platform/common/cpp/public/flutter_plugin_registrar.h @@ -31,19 +31,6 @@ FLUTTER_EXPORT void FlutterDesktopRegistrarSetDestructionHandler( FlutterDesktopPluginRegistrarRef registrar, FlutterDesktopOnRegistrarDestroyed callback); -// Enables input blocking on the given channel. -// -// If set, then the Flutter window will disable input callbacks -// while waiting for the handler for messages on that channel to run. This is -// useful if handling the message involves showing a modal window, for instance. -// -// This must be called after FlutterDesktopSetMessageHandler, as setting a -// handler on a channel will reset the input blocking state back to the -// default of disabled. -FLUTTER_EXPORT void FlutterDesktopRegistrarEnableInputBlocking( - FlutterDesktopPluginRegistrarRef registrar, - const char* channel); - #if defined(__cplusplus) } // extern "C" #endif diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 737e72731232f..327dfdd1605ca 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -171,6 +171,7 @@ source_set("ios_test_flutter_mrc") { ] sources = [ "framework/Source/FlutterEnginePlatformViewTest.mm", + "framework/Source/FlutterPlatformPluginTest.mm", "framework/Source/FlutterPlatformViewsTest.mm", "framework/Source/accessibility_bridge_test.mm", ] @@ -207,6 +208,7 @@ shared_library("ios_test_flutter") { ] sources = [ "framework/Source/FlutterBinaryMessengerRelayTest.mm", + "framework/Source/FlutterDartProjectTest.mm", "framework/Source/FlutterEngineTest.mm", "framework/Source/FlutterPluginAppLifeCycleDelegateTest.m", "framework/Source/FlutterTextInputPluginTest.m", diff --git a/shell/platform/darwin/ios/framework/Headers/Flutter.h b/shell/platform/darwin/ios/framework/Headers/Flutter.h index 9135c8200603c..d91eba7576fec 100644 --- a/shell/platform/darwin/ios/framework/Headers/Flutter.h +++ b/shell/platform/darwin/ios/framework/Headers/Flutter.h @@ -5,52 +5,6 @@ #ifndef FLUTTER_FLUTTER_H_ #define FLUTTER_FLUTTER_H_ -/** - BREAKING CHANGES: - - December 17, 2018: - - Changed designated initializer on FlutterEngine - - October 5, 2018: - - Removed FlutterNavigationController.h/.mm - - Changed return signature of `FlutterDartHeadlessCodeRunner.run*` from void - to bool - - Removed HeadlessPlatformViewIOS - - Marked FlutterDartHeadlessCodeRunner deprecated - - August 31, 2018: Marked -[FlutterDartProject - initFromDefaultSourceForConfiguration] and FlutterStandardBigInteger as - unavailable. - - July 26, 2018: Marked -[FlutterDartProject - initFromDefaultSourceForConfiguration] deprecated. - - February 28, 2018: Removed "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot". - - January 15, 2018: Marked "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot" as unavailable following the - deprecation from December 11, 2017. Scheduled to be removed on February - 19, 2018. - - January 09, 2018: Deprecated "FlutterStandardBigInteger" and its use in - "FlutterStandardMessageCodec" and "FlutterStandardMethodCodec". Scheduled to - be marked as unavailable once the deprecation has been available on the - flutter/flutter alpha branch for four weeks. "FlutterStandardBigInteger" was - needed because the Dart 1.0 int type had no size limit. With Dart 2.0, the - int type is a fixed-size, 64-bit signed integer. If you need to communicate - larger integers, use NSString encoding instead. - - December 11, 2017: Deprecated "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot" and scheculed the same to be marked as - unavailable on January 15, 2018. Instead, "initWithFlutterAssets" and - "initWithFlutterAssetsWithScriptSnapshot" should be used. The reason for this - change is that the FLX archive will be deprecated and replaced with a flutter - assets directory containing the same files as the FLX did. - - November 29, 2017: Added a BREAKING CHANGES section. - */ - #include "FlutterAppDelegate.h" #include "FlutterBinaryMessenger.h" #include "FlutterCallbackCache.h" diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index 46980d609a078..87b7753317e73 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -24,6 +24,11 @@ NS_ASSUME_NONNULL_BEGIN */ extern NSString* const FlutterDefaultDartEntrypoint; +/** + * The default Flutter initial route ("/"). + */ +extern NSString* const FlutterDefaultInitialRoute; + /** * The FlutterEngine class coordinates a single instance of execution for a * `FlutterDartProject`. It may have zero or one `FlutterViewController` at a @@ -53,6 +58,24 @@ extern NSString* const FlutterDefaultDartEntrypoint; FLUTTER_EXPORT @interface FlutterEngine : NSObject +/** + * Default initializer for a FlutterEngine. + * + * Threads created by this FlutterEngine will appear as "FlutterEngine #" in + * Instruments. The prefix can be customized using `initWithName`. + * + * The engine will execute the project located in the bundle with the identifier + * "io.flutter.flutter.app" (the default for Flutter projects). + * + * A newly initialized engine will not run until either `-runWithEntrypoint:` or + * `-runWithEntrypoint:libraryURI:` is called. + * + * FlutterEngine created with this method will have allowHeadlessExecution set to `YES`. + * This means that the engine will continue to run regardless of whether a `FlutterViewController` + * is attached to it or not, until `-destroyContext:` is called or the process finishes. + */ +- (instancetype)init; + /** * Initialize this FlutterEngine. * @@ -114,17 +137,12 @@ FLUTTER_EXPORT project:(nullable FlutterDartProject*)project allowHeadlessExecution:(BOOL)allowHeadlessExecution NS_DESIGNATED_INITIALIZER; -/** - * The default initializer is not available for this object. - * Callers must use `-[FlutterEngine initWithName:project:]`. - */ -- (instancetype)init NS_UNAVAILABLE; - + (instancetype)new NS_UNAVAILABLE; /** * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that - * contains `main()`), using `main()` as the entrypoint (the default for Flutter projects). + * contains `main()`), using `main()` as the entrypoint (the default for Flutter projects), + * and using "/" (the default route) as the initial route. * * The first call to this method will create a new Isolate. Subsequent calls will return * immediately and have no effect. @@ -135,7 +153,7 @@ FLUTTER_EXPORT /** * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that - * contains `main()`). + * contains `main()`), using "/" (the default route) as the initial route. * * The first call to this method will create a new Isolate. Subsequent calls will return * immediately and have no effect. @@ -149,6 +167,25 @@ FLUTTER_EXPORT */ - (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint; +/** + * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that + * contains `main()`). + * + * The first call to this method will create a new Isolate. Subsequent calls will return + * immediately and have no effect. + * + * @param entrypoint The name of a top-level function from the same Dart + * library that contains the app's main() function. If this is FlutterDefaultDartEntrypoint (or + * nil), it will default to `main()`. If it is not the app's main() function, that function must + * be decorated with `@pragma(vm:entry-point)` to ensure the method is not tree-shaken by the Dart + * compiler. + * @param initialRoute The name of the initial Flutter `Navigator` `Route` to load. If this is + * FlutterDefaultInitialRoute (or nil), it will default to the "/" route. + * @return YES if the call succeeds in creating and running a Flutter Engine instance; NO otherwise. + */ +- (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint + initialRoute:(nullable NSString*)initialRoute; + /** * Runs a Dart program on an Isolate using the specified entrypoint and Dart library, * which may not be the same as the library containing the Dart program's `main()` function. diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 6f434af1047f7..4468ce7ea770c 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -55,7 +55,7 @@ FLUTTER_EXPORT * * The initialized viewcontroller will attach itself to the engine as part of this process. * - * @param engine The `FlutterEngine` instance to attach to. + * @param engine The `FlutterEngine` instance to attach to. Cannot be nil. * @param nibName The NIB name to initialize this UIViewController with. * @param nibBundle The NIB bundle. */ @@ -78,6 +78,23 @@ FLUTTER_EXPORT nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; +/** + * Initializes a new FlutterViewController and `FlutterEngine` with the specified + * `FlutterDartProject` and `initialRoute`. + * + * This will implicitly create a new `FlutterEngine` which is retrievable via the `engine` property + * after initialization. + * + * @param project The `FlutterDartProject` to initialize the `FlutterEngine` with. + * @param initialRoute The initial `Navigator` route to load. + * @param nibName The NIB name to initialize this UIViewController with. + * @param nibBundle The NIB bundle. + */ +- (instancetype)initWithProject:(nullable FlutterDartProject*)project + initialRoute:(nullable NSString*)initialRoute + nibName:(nullable NSString*)nibName + bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; + /** * Initializer that is called from loading a FlutterViewController from a XIB. * @@ -117,6 +134,8 @@ FLUTTER_EXPORT - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package; /** + * Deprecated API to set initial route. + * * Attempts to set the first route that the Flutter app shows if the Flutter * runtime hasn't yet started. The default is "/". * @@ -127,9 +146,15 @@ FLUTTER_EXPORT * Setting this after the Flutter started running has no effect. See `pushRoute` * and `popRoute` to change the route after Flutter started running. * + * This is deprecated because it needs to be called at the time of initialization + * and thus should just be in the `initWithProject` initializer. If using + * `initWithEngine`, the initial route should be set on the engine's + * initializer. + * * @param route The name of the first route to show. */ -- (void)setInitialRoute:(NSString*)route; +- (void)setInitialRoute:(NSString*)route + FLUTTER_DEPRECATED("Use FlutterViewController initializer to specify initial route"); /** * Instructs the Flutter Navigator (if any) to go back. @@ -138,8 +163,7 @@ FLUTTER_EXPORT /** * Instructs the Flutter Navigator (if any) to push a route on to the navigation - * stack. The setInitialRoute method should be preferred if this is called before the - * FlutterViewController has come into view. + * stack. * * @param route The name of the route to push to the navigation stack. */ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 3e8bc4727b64b..4f6a2acdf7b76 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -132,6 +132,20 @@ } } + // Domain network configuration + NSDictionary* appTransportSecurity = + [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; + settings.may_insecurely_connect_to_all_domains = + [FlutterDartProject allowsArbitraryLoads:appTransportSecurity]; + settings.domain_network_policy = + [FlutterDartProject domainNetworkPolicy:appTransportSecurity].UTF8String; + + // TODO(mehmetf): We need to announce this change since it is breaking. + // Remove these two lines after we announce and we know which release this is + // going to be part of. + settings.may_insecurely_connect_to_all_domains = true; + settings.domain_network_policy = ""; + #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG // There are no ownership concerns here as all mappings are owned by the // embedder and not the engine. @@ -168,12 +182,12 @@ - (instancetype)initWithPrecompiledDartBundle:(nullable NSBundle*)bundle { return self; } -#pragma mark - WindowData accessors +#pragma mark - PlatformData accessors -- (const flutter::WindowData)defaultWindowData { - flutter::WindowData windowData; - windowData.lifecycle_state = std::string("AppLifecycleState.detached"); - return windowData; +- (const flutter::PlatformData)defaultPlatformData { + flutter::PlatformData PlatformData; + PlatformData.lifecycle_state = std::string("AppLifecycleState.detached"); + return PlatformData; } #pragma mark - Settings accessors @@ -219,6 +233,34 @@ + (NSString*)flutterAssetsName:(NSBundle*)bundle { return flutterAssetsName; } ++ (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity { + // https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity/nsexceptiondomains + NSDictionary* exceptionDomains = [appTransportSecurity objectForKey:@"NSExceptionDomains"]; + if (exceptionDomains == nil) { + return @""; + } + NSMutableArray* networkConfigArray = [[NSMutableArray alloc] init]; + for (NSString* domain in exceptionDomains) { + NSDictionary* domainConfiguration = [exceptionDomains objectForKey:domain]; + // Default value is false. + bool includesSubDomains = + [[domainConfiguration objectForKey:@"NSIncludesSubdomains"] boolValue]; + bool allowsCleartextCommunication = + [[domainConfiguration objectForKey:@"NSExceptionAllowsInsecureHTTPLoads"] boolValue]; + [networkConfigArray addObject:@[ + domain, includesSubDomains ? @YES : @NO, allowsCleartextCommunication ? @YES : @NO + ]]; + } + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:networkConfigArray + options:0 + error:NULL]; + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + ++ (bool)allowsArbitraryLoads:(NSDictionary*)appTransportSecurity { + return [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue]; +} + + (NSString*)lookupKeyForAsset:(NSString*)asset { return [self lookupKeyForAsset:asset fromBundle:nil]; } @@ -261,6 +303,6 @@ - (void)setPersistentIsolateData:(NSData*)data { ); } -#pragma mark - windowData utilities +#pragma mark - PlatformData utilities @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm new file mode 100644 index 0000000000000..eed1bd9cc8969 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm @@ -0,0 +1,85 @@ +// 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 +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterDartProjectTest : XCTestCase +@end + +@implementation FlutterDartProjectTest + +- (void)setUp { +} + +- (void)tearDown { +} + +- (void)testMainBundleSettingsAreCorrectlyParsed { + NSBundle* mainBundle = [NSBundle mainBundle]; + NSDictionary* appTransportSecurity = + [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; + XCTAssertTrue([FlutterDartProject allowsArbitraryLoads:appTransportSecurity]); + XCTAssertEqualObjects( + @"[[\"invalid-site.com\",true,false],[\"sub.invalid-site.com\",false,false]]", + [FlutterDartProject domainNetworkPolicy:appTransportSecurity]); +} + +- (void)testEmptySettingsAreCorrect { + XCTAssertFalse([FlutterDartProject allowsArbitraryLoads:[[NSDictionary alloc] init]]); + XCTAssertEqualObjects(@"", [FlutterDartProject domainNetworkPolicy:[[NSDictionary alloc] init]]); +} + +- (void)testAllowsArbitraryLoads { + XCTAssertFalse([FlutterDartProject allowsArbitraryLoads:@{@"NSAllowsArbitraryLoads" : @false}]); + XCTAssertTrue([FlutterDartProject allowsArbitraryLoads:@{@"NSAllowsArbitraryLoads" : @true}]); +} + +- (void)testProperlyFormedExceptionDomains { + NSDictionary* domainInfoOne = @{ + @"NSIncludesSubdomains" : @false, + @"NSExceptionAllowsInsecureHTTPLoads" : @true, + @"NSExceptionMinimumTLSVersion" : @"4.0" + }; + NSDictionary* domainInfoTwo = @{ + @"NSIncludesSubdomains" : @true, + @"NSExceptionAllowsInsecureHTTPLoads" : @false, + @"NSExceptionMinimumTLSVersion" : @"4.0" + }; + NSDictionary* domainInfoThree = @{ + @"NSIncludesSubdomains" : @false, + @"NSExceptionAllowsInsecureHTTPLoads" : @true, + @"NSExceptionMinimumTLSVersion" : @"4.0" + }; + NSDictionary* exceptionDomains = @{ + @"domain.name" : domainInfoOne, + @"sub.domain.name" : domainInfoTwo, + @"sub.two.domain.name" : domainInfoThree + }; + NSDictionary* appTransportSecurity = @{@"NSExceptionDomains" : exceptionDomains}; + XCTAssertEqualObjects(@"[[\"domain.name\",false,true],[\"sub.domain.name\",true,false]," + @"[\"sub.two.domain.name\",false,true]]", + [FlutterDartProject domainNetworkPolicy:appTransportSecurity]); +} + +- (void)testExceptionDomainsWithMissingInfo { + NSDictionary* domainInfoOne = @{@"NSExceptionMinimumTLSVersion" : @"4.0"}; + NSDictionary* domainInfoTwo = @{ + @"NSIncludesSubdomains" : @true, + }; + NSDictionary* domainInfoThree = @{}; + NSDictionary* exceptionDomains = @{ + @"domain.name" : domainInfoOne, + @"sub.domain.name" : domainInfoTwo, + @"sub.two.domain.name" : domainInfoThree + }; + NSDictionary* appTransportSecurity = @{@"NSExceptionDomains" : exceptionDomains}; + XCTAssertEqualObjects(@"[[\"domain.name\",false,false],[\"sub.domain.name\",true,false]," + @"[\"sub.two.domain.name\",false,false]]", + [FlutterDartProject domainNetworkPolicy:appTransportSecurity]); +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h index b21a38a9be6fd..daac68e663786 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h @@ -6,7 +6,7 @@ #define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERDARTPROJECT_INTERNAL_H_ #include "flutter/common/settings.h" -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" #include "flutter/shell/common/engine.h" #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterDartProject.h" @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FlutterDartProject () - (const flutter::Settings&)settings; -- (const flutter::WindowData)defaultWindowData; +- (const flutter::PlatformData)defaultPlatformData; - (flutter::RunConfiguration)runConfiguration; - (flutter::RunConfiguration)runConfigurationForEntrypoint:(nullable NSString*)entrypointOrNil; @@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN libraryOrNil:(nullable NSString*)dartLibraryOrNil; + (NSString*)flutterAssetsName:(NSBundle*)bundle; ++ (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity; ++ (bool)allowsArbitraryLoads:(NSDictionary*)appTransportSecurity; /** * The embedder can specify data that the isolate can request synchronously on launch. Engines diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 57d531ec7420d..13394a976c92c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -17,6 +17,9 @@ #include "flutter/shell/common/switches.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/platform/darwin/common/command_line.h" +#include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" +#include "flutter/shell/profiling/sampling_profiler.h" + #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h" @@ -28,10 +31,9 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/profiler_metrics_ios.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" -#include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" -#include "flutter/shell/profiling/sampling_profiler.h" NSString* const FlutterDefaultDartEntrypoint = nil; +NSString* const FlutterDefaultInitialRoute = nil; static constexpr int kNumProfilerSamplesPerSec = 5; @interface FlutterEngineRegistrar : NSObject @@ -46,6 +48,7 @@ @interface FlutterEngine () @property(nonatomic, readonly) NSMutableDictionary* registrars; @property(nonatomic, readwrite, copy) NSString* isolateId; +@property(nonatomic, copy) NSString* initialRoute; @property(nonatomic, retain) id flutterViewControllerWillDeallocObserver; @end @@ -82,6 +85,10 @@ @implementation FlutterEngine { std::unique_ptr _connections; } +- (instancetype)init { + return [self initWithName:@"FlutterEngine" project:nil allowHeadlessExecution:YES]; +} + - (instancetype)initWithName:(NSString*)labelPrefix { return [self initWithName:labelPrefix project:nil allowHeadlessExecution:YES]; } @@ -160,6 +167,7 @@ - (void)dealloc { }]; [_labelPrefix release]; + [_initialRoute release]; [_pluginPublications release]; [_registrars release]; _binaryMessenger.parent = nil; @@ -367,6 +375,13 @@ - (void)setupChannels { binaryMessenger:self.binaryMessenger codec:[FlutterJSONMethodCodec sharedInstance]]); + if ([_initialRoute length] > 0) { + // Flutter isn't ready to receive this method call yet but the channel buffer will cache this. + [_navigationChannel invokeMethod:@"setInitialRoute" arguments:_initialRoute]; + [_initialRoute release]; + _initialRoute = nil; + } + _platformChannel.reset([[FlutterMethodChannel alloc] initWithName:@"flutter/platform" binaryMessenger:self.binaryMessenger @@ -436,16 +451,19 @@ - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil { libraryOrNil:libraryOrNil]); } -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute { if (_shell != nullptr) { FML_LOG(WARNING) << "This FlutterEngine was already invoked."; return NO; } static size_t shellCount = 1; + self.initialRoute = initialRoute; auto settings = [_dartProject.get() settings]; - auto windowData = [_dartProject.get() defaultWindowData]; + auto platformData = [_dartProject.get() defaultPlatformData]; if (libraryURI) { FML_DCHECK(entrypoint) << "Must specify entrypoint if specifying library"; @@ -488,48 +506,21 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { }; flutter::Shell::CreateCallback on_create_rasterizer = - [](flutter::Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }; - - if (flutter::IsIosEmbeddedViewsPreviewEnabled()) { - // Embedded views requires the gpu and the platform views to be the same. - // The plan is to eventually dynamically merge the threads when there's a - // platform view in the layer tree. - // For now we use a fixed thread configuration with the same thread used as the - // gpu and platform task runner. - // TODO(amirh/chinmaygarde): remove this, and dynamically change the thread configuration. - // https://github.com/flutter/flutter/issues/23975 - - flutter::TaskRunners task_runners(threadLabel.UTF8String, // label - fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform - fml::MessageLoop::GetCurrent().GetTaskRunner(), // raster - _threadHost.ui_thread->GetTaskRunner(), // ui - _threadHost.io_thread->GetTaskRunner() // io - ); - // Create the shell. This is a blocking operation. - _shell = flutter::Shell::Create(std::move(task_runners), // task runners - std::move(windowData), // window data - std::move(settings), // settings - on_create_platform_view, // platform view creation - on_create_rasterizer // rasterzier creation - ); - } else { - flutter::TaskRunners task_runners(threadLabel.UTF8String, // label - fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform - _threadHost.raster_thread->GetTaskRunner(), // raster - _threadHost.ui_thread->GetTaskRunner(), // ui - _threadHost.io_thread->GetTaskRunner() // io - ); - // Create the shell. This is a blocking operation. - _shell = flutter::Shell::Create(std::move(task_runners), // task runners - std::move(windowData), // window data - std::move(settings), // settings - on_create_platform_view, // platform view creation - on_create_rasterizer // rasterzier creation - ); - } + [](flutter::Shell& shell) { return std::make_unique(shell); }; + + flutter::TaskRunners task_runners(threadLabel.UTF8String, // label + fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform + _threadHost.raster_thread->GetTaskRunner(), // raster + _threadHost.ui_thread->GetTaskRunner(), // ui + _threadHost.io_thread->GetTaskRunner() // io + ); + // Create the shell. This is a blocking operation. + _shell = flutter::Shell::Create(std::move(task_runners), // task runners + std::move(platformData), // window data + std::move(settings), // settings + on_create_platform_view, // platform view creation + on_create_rasterizer // rasterzier creation + ); if (_shell == nullptr) { FML_LOG(ERROR) << "Could not start a shell FlutterEngine with entrypoint: " @@ -552,21 +543,35 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { } - (BOOL)run { - return [self runWithEntrypoint:FlutterDefaultDartEntrypoint libraryURI:nil]; + return [self runWithEntrypoint:FlutterDefaultDartEntrypoint + libraryURI:nil + initialRoute:FlutterDefaultInitialRoute]; } - (BOOL)runWithEntrypoint:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { - if ([self createShell:entrypoint libraryURI:libraryURI]) { + return [self runWithEntrypoint:entrypoint + libraryURI:libraryURI + initialRoute:FlutterDefaultInitialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint { + return [self runWithEntrypoint:entrypoint libraryURI:nil initialRoute:FlutterDefaultInitialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint initialRoute:(NSString*)initialRoute { + return [self runWithEntrypoint:entrypoint libraryURI:nil initialRoute:initialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute { + if ([self createShell:entrypoint libraryURI:libraryURI initialRoute:initialRoute]) { [self launchEngine:entrypoint libraryURI:libraryURI]; } return _shell != nullptr; } -- (BOOL)runWithEntrypoint:(NSString*)entrypoint { - return [self runWithEntrypoint:entrypoint libraryURI:nil]; -} - - (void)notifyLowMemory { if (_shell) { _shell->NotifyLowMemoryWarning(); @@ -669,6 +674,15 @@ - (void)showAutocorrectionPromptRectForStart:(NSUInteger)start return _binaryMessenger; } +// For test only. Ideally we should create a dependency injector for all dependencies and +// remove this. +- (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger { + // Discard the previous messenger and keep the new one. + _binaryMessenger.parent = nil; + [_binaryMessenger release]; + _binaryMessenger = [binaryMessenger retain]; +} + #pragma mark - FlutterBinaryMessenger - (void)sendOnChannel:(NSString*)channel message:(NSData*)message { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 7fe3cb16e0775..e7a68f9a7d2bd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -5,7 +5,8 @@ #import #import #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" -#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h" FLUTTER_ASSERT_ARC @@ -79,4 +80,23 @@ - (void)testNotifyPluginOfDealloc { OCMVerify([plugin detachFromEngineForRegistrar:[OCMArg any]]); } +- (void)testRunningInitialRouteSendsNavigationMessage { + id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]); + + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine setBinaryMessenger:mockBinaryMessenger]; + + // Run with an initial route. + [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"]; + + // Now check that an encoded method call has been made on the binary messenger to set the + // initial route to "test". + FlutterMethodCall* setInitialRouteMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"]; + NSData* encodedSetInitialRouteMethod = + [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall]; + OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation" + message:encodedSetInitialRouteMethod]); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 52558eaf71ab3..93e6cbac58514 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -43,7 +43,9 @@ - (flutter::FlutterPlatformViewsController*)platformViewsController; - (FlutterTextInputPlugin*)textInputPlugin; - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryOrNil + initialRoute:(NSString*)initialRoute; - (void)attachView; - (void)notifyLowMemory; - (flutter::PlatformViewIOS*)iosPlatformView; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h new file mode 100644 index 0000000000000..7be2f68d77b50 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h @@ -0,0 +1,10 @@ +// 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 "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" + +// Category to add test-only visibility. +@interface FlutterEngine (Test) +- (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger; +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 5238be44eaef1..87db2313dfb8e 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -88,6 +88,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([method isEqualToString:@"Clipboard.setData"]) { [self setClipboardData:args]; result(nil); + } else if ([method isEqualToString:@"Clipboard.hasStrings"]) { + result([self clipboardHasStrings]); } else { result(FlutterMethodNotImplemented); } @@ -248,4 +250,16 @@ - (void)setClipboardData:(NSDictionary*)data { } } +- (NSDictionary*)clipboardHasStrings { + bool hasStrings = false; + UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; + if (@available(iOS 10, *)) { + hasStrings = pasteboard.hasStrings; + } else { + NSString* stringInPasteboard = pasteboard.string; + hasStrings = stringInPasteboard != nil; + } + return @{@"value" : @(hasStrings)}; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm new file mode 100644 index 0000000000000..01f3ca4e611d4 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -0,0 +1,51 @@ +// 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 + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" +#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" +#import "third_party/ocmock/Source/OCMock/OCMock.h" + +@interface FlutterPlatformPluginTest : XCTestCase +@end + +@implementation FlutterPlatformPluginTest + +- (void)testHasStrings { + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + FlutterPlatformPlugin* plugin = + [[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()]; + + // Set some string to the pasteboard. + __block bool calledSet = false; + FlutterResult resultSet = ^(id result) { + calledSet = true; + }; + FlutterMethodCall* methodCallSet = + [FlutterMethodCall methodCallWithMethodName:@"Clipboard.setClipboardData" + arguments:@{@"text" : @"some string"}]; + [plugin handleMethodCall:methodCallSet result:resultSet]; + XCTAssertEqual(calledSet, true); + + // Call hasStrings and expect it to be true. + __block bool called = false; + __block bool value; + FlutterResult result = ^(id result) { + called = true; + value = result[@"value"]; + }; + FlutterMethodCall* methodCall = + [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil]; + [plugin handleMethodCall:methodCall result:result]; + + XCTAssertEqual(called, true); + XCTAssertEqual(value, true); +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 9c43d355b3210..891f8ed1a86e9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -167,7 +167,11 @@ touch_interceptors_[viewId] = fml::scoped_nsobject([touch_interceptor retain]); - root_views_[viewId] = fml::scoped_nsobject([touch_interceptor retain]); + + ChildClippingView* clipping_view = + [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease]; + [clipping_view addSubview:touch_interceptor]; + root_views_[viewId] = fml::scoped_nsobject([clipping_view retain]); result(nil); } @@ -317,83 +321,60 @@ return clipCount; } -UIView* FlutterPlatformViewsController::ReconstructClipViewsChain(int number_of_clips, - UIView* platform_view, - UIView* head_clip_view) { - NSInteger indexInFlutterView = -1; - if (head_clip_view.superview) { - // TODO(cyanglaz): potentially cache the index of oldPlatformViewRoot to make this a O(1). - // https://github.com/flutter/flutter/issues/35023 - indexInFlutterView = [flutter_view_.get().subviews indexOfObject:head_clip_view]; - [head_clip_view removeFromSuperview]; - } - UIView* head = platform_view; - int clipIndex = 0; - // Re-use as much existing clip views as needed. - while (head != head_clip_view && clipIndex < number_of_clips) { - head = head.superview; - clipIndex++; - } - // If there were not enough existing clip views, add more. - while (clipIndex < number_of_clips) { - ChildClippingView* clippingView = - [[[ChildClippingView alloc] initWithFrame:flutter_view_.get().bounds] autorelease]; - [clippingView addSubview:head]; - head = clippingView; - clipIndex++; - } - [head removeFromSuperview]; - - if (indexInFlutterView > -1) { - // The chain was previously attached; attach it to the same position. - [flutter_view_.get() insertSubview:head atIndex:indexInFlutterView]; - } - return head; -} - void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view) { FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity)); - UIView* head = embedded_view; - ResetAnchor(head.layer); + ResetAnchor(embedded_view.layer); + ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview; - std::vector>::const_reverse_iterator iter = mutators_stack.Bottom(); - while (iter != mutators_stack.Top()) { + // The UIKit frame is set based on the logical resolution instead of physical. + // (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html). + // However, flow is based on the physical resolution. For example, 1000 pixels in flow equals + // 500 points in UIKit. And until this point, we did all the calculation based on the flow + // resolution. So we need to scale down to match UIKit's logical resolution. + CGFloat screenScale = [UIScreen mainScreen].scale; + CATransform3D finalTransform = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1); + + // Mask view needs to be full screen because we might draw platform view pixels outside of the + // `ChildClippingView`. Since the mask view's frame will be based on the `clipView`'s coordinate + // system, we need to convert the flutter_view's frame to the clipView's coordinate system. The + // mask view is not displayed on the screen. + CGRect maskViewFrame = [flutter_view_ convertRect:flutter_view_.get().frame toView:clipView]; + FlutterClippingMaskView* maskView = + [[[FlutterClippingMaskView alloc] initWithFrame:maskViewFrame] autorelease]; + auto iter = mutators_stack.Begin(); + while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { case transform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); - head.layer.transform = CATransform3DConcat(head.layer.transform, transform); + finalTransform = CATransform3DConcat(transform, finalTransform); break; } case clip_rect: + [maskView clipRect:(*iter)->GetRect() matrix:finalTransform]; + break; case clip_rrect: - case clip_path: { - ChildClippingView* clipView = (ChildClippingView*)head.superview; - clipView.layer.transform = CATransform3DIdentity; - [clipView setClip:(*iter)->GetType() - rect:(*iter)->GetRect() - rrect:(*iter)->GetRRect() - path:(*iter)->GetPath()]; - ResetAnchor(clipView.layer); - head = clipView; + [maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform]; + break; + case clip_path: + [maskView clipPath:(*iter)->GetPath() matrix:finalTransform]; break; - } case opacity: embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; } ++iter; } - // Reverse scale based on screen scale. + // Reverse the offset of the clipView. + // The clipView's frame includes the final translate of the final transform matrix. + // So we need to revese this translate so the platform view can layout at the correct offset. // - // The UIKit frame is set based on the logical resolution instead of physical. - // (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html). - // However, flow is based on the physical resolution. For example, 1000 pixels in flow equals - // 500 points in UIKit. And until this point, we did all the calculation based on the flow - // resolution. So we need to scale down to match UIKit's logical resolution. - CGFloat screenScale = [UIScreen mainScreen].scale; - head.layer.transform = CATransform3DConcat( - head.layer.transform, CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1)); + // Note that we don't apply this transform matrix the clippings because clippings happen on the + // mask view, whose origin is alwasy (0,0) to the flutter_view. + CATransform3D reverseTranslate = + CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0); + embedded_view.layer.transform = CATransform3DConcat(finalTransform, reverseTranslate); + clipView.maskView = maskView; } void FlutterPlatformViewsController::CompositeWithParams(int view_id, @@ -406,17 +387,15 @@ touchInterceptor.alpha = 1; const MutatorsStack& mutatorStack = params.mutatorsStack(); - int currentClippingCount = CountClips(mutatorStack); - int previousClippingCount = clip_count_[view_id]; - if (currentClippingCount != previousClippingCount) { - clip_count_[view_id] = currentClippingCount; - // If we have a different clipping count in this frame, we need to reconstruct the - // ClippingChildView chain to prepare for `ApplyMutators`. - UIView* oldPlatformViewRoot = root_views_[view_id].get(); - UIView* newPlatformViewRoot = - ReconstructClipViewsChain(currentClippingCount, touchInterceptor, oldPlatformViewRoot); - root_views_[view_id] = fml::scoped_nsobject([newPlatformViewRoot retain]); - } + UIView* clippingView = root_views_[view_id].get(); + // The frame of the clipping view should be the final bounding rect. + // Because the translate matrix in the Mutator Stack also includes the offset, + // when we apply the transforms matrix in |ApplyMutators|, we need + // to remember to do a reverse translate. + const SkRect& rect = params.finalBoundingRect(); + CGFloat screenScale = [UIScreen mainScreen].scale; + clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale, + rect.width() / screenScale, rect.height() / screenScale); ApplyMutators(mutatorStack, touchInterceptor); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index e2a0088dfc96b..0e8397e1146b4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -14,6 +14,7 @@ FLUTTER_ASSERT_NOT_ARC @class FlutterPlatformViewsTestMockPlatformView; static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil; +const float kFloatCompareEpsilon = 0.001; @interface FlutterPlatformViewsTestMockPlatformView : UIView @end @@ -143,4 +144,385 @@ - (void)testCanCreatePlatformViewWithoutFlutterView { flutterPlatformViewsController->Reset(); } +- (void)testChildClippingViewHitTests { + ChildClippingView* childClippingView = + [[[ChildClippingView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease]; + UIView* childView = [[[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)] autorelease]; + [childClippingView addSubview:childView]; + + XCTAssertFalse([childClippingView pointInside:CGPointMake(50, 50) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(99, 100) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(100, 99) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(201, 200) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(200, 201) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(99, 200) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(200, 299) withEvent:nil]); + + XCTAssertTrue([childClippingView pointInside:CGPointMake(150, 150) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(100, 100) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 100) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(100, 199) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]); +} + +- (void)testCompositePlatformView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a translate matrix + SkMatrix translateMatrix = SkMatrix::MakeTrans(100, 100); + stack.PushTransform(translateMatrix); + SkMatrix finalMatrix; + finalMatrix.setConcat(screenScaleMatrix, translateMatrix); + + auto embeddedViewParams = + std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds + toView:mockFlutterView]; + XCTAssertTrue(CGRectEqualToRect(platformViewRectInFlutterView, CGRectMake(100, 100, 300, 300))); + flutterPlatformViewsController->Reset(); +} + +- (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a rotate matrix + SkMatrix rotateMatrix; + rotateMatrix.setRotate(10); + stack.PushTransform(rotateMatrix); + SkMatrix finalMatrix; + finalMatrix.setConcat(screenScaleMatrix, rotateMatrix); + + auto embeddedViewParams = + std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds + toView:mockFlutterView]; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + // The childclippingview's frame is set based on flow, but the platform view's frame is set based + // on quartz. Although they should be the same, but we should tolerate small floating point + // errors. + XCTAssertLessThan(fabs(platformViewRectInFlutterView.origin.x - childClippingView.frame.origin.x), + kFloatCompareEpsilon); + XCTAssertLessThan(fabs(platformViewRectInFlutterView.origin.y - childClippingView.frame.origin.y), + kFloatCompareEpsilon); + XCTAssertLessThan( + fabs(platformViewRectInFlutterView.size.width - childClippingView.frame.size.width), + kFloatCompareEpsilon); + XCTAssertLessThan( + fabs(platformViewRectInFlutterView.size.height - childClippingView.frame.size.height), + kFloatCompareEpsilon); + + flutterPlatformViewsController->Reset(); +} + +- (void)testClipRect { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a clip rect + SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3); + stack.PushClipRect(rect); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + gMockPlatformView.backgroundColor = UIColor.redColor; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + CGPoint point = CGPointMake(i, j); + int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; + // Edges of the clipping might have a semi transparent pixel, we only check the pixels that + // are fully inside the clipped area. + CGRect insideClipping = CGRectMake(3, 3, 1, 1); + if (CGRectContainsPoint(insideClipping, point)) { + XCTAssertEqual(alpha, 255); + } else { + XCTAssertLessThan(alpha, 255); + } + } + } + flutterPlatformViewsController->Reset(); +} + +- (void)testClipRRect { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a clip rrect + SkRRect rrect = SkRRect::MakeRectXY(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1); + stack.PushClipRRect(rrect); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + gMockPlatformView.backgroundColor = UIColor.redColor; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + CGPoint point = CGPointMake(i, j); + int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; + // Edges of the clipping might have a semi transparent pixel, we only check the pixels that + // are fully inside the clipped area. + CGRect insideClipping = CGRectMake(3, 3, 4, 4); + if (CGRectContainsPoint(insideClipping, point)) { + XCTAssertEqual(alpha, 255); + } else { + XCTAssertLessThan(alpha, 255); + } + } + } + flutterPlatformViewsController->Reset(); +} + +- (void)testClipPath { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a clip path + SkPath path; + path.addRoundRect(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1); + stack.PushClipPath(path); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + gMockPlatformView.backgroundColor = UIColor.redColor; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + CGPoint point = CGPointMake(i, j); + int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; + // Edges of the clipping might have a semi transparent pixel, we only check the pixels that + // are fully inside the clipped area. + CGRect insideClipping = CGRectMake(3, 3, 4, 4); + if (CGRectContainsPoint(insideClipping, point)) { + XCTAssertEqual(alpha, 255); + } else { + XCTAssertLessThan(alpha, 255); + } + } + } + flutterPlatformViewsController->Reset(); +} + +- (int)alphaOfPoint:(CGPoint)point onView:(UIView*)view { + unsigned char pixel[4] = {0}; + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + + // Draw the pixel on `point` in the context. + CGContextRef context = CGBitmapContextCreate( + pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast); + CGContextTranslateCTM(context, -point.x, -point.y); + [view.layer renderInContext:context]; + + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + // Get the alpha from the pixel that we just rendered. + return pixel[3]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 2b6bcf961310c..311414b9d682d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -16,6 +16,33 @@ #include "flutter/shell/platform/darwin/ios/ios_context.h" #include "third_party/skia/include/core/SkPictureRecorder.h" +// A UIView that acts as a clipping mask for the |ChildClippingView|. +// +// On the [UIView drawRect:] method, this view performs a series of clipping operations and sets the +// alpha channel to the final resulting area to be 1; it also sets the "clipped out" area's alpha +// channel to be 0. +// +// When a UIView sets a |FlutterClippingMaskView| as its `maskView`, the alpha channel of the UIView +// is replaced with the alpha channel of the |FlutterClippingMaskView|. +@interface FlutterClippingMaskView : UIView + +// Adds a clip rect operation to the queue. +// +// The `clipSkRect` is transformed with the `matrix` before adding to the queue. +- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix; + +// Adds a clip rrect operation to the queue. +// +// The `clipSkRRect` is transformed with the `matrix` before adding to the queue. +- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix; + +// Adds a clip path operation to the queue. +// +// The `path` is transformed with the `matrix` before adding to the queue. +- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix; + +@end + // A UIView that is used as the parent for embedded UIViews. // // This view has 2 roles: @@ -37,14 +64,6 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView -// Performs the clipping based on the type. -// -// The `type` must be one of the 3: clip_rect, clip_rrect, clip_path. -- (void)setClip:(flutter::MutatorType)type - rect:(const SkRect&)rect - rrect:(const SkRRect&)rrect - path:(const SkPath&)path; - @end namespace flutter { @@ -253,20 +272,6 @@ class FlutterPlatformViewsController { // Traverse the `mutators_stack` and return the number of clip operations. int CountClips(const MutatorsStack& mutators_stack); - // Make sure that platform_view has exactly clip_count ChildClippingView ancestors. - // - // Existing ChildClippingViews are re-used. If there are currently more ChildClippingView - // ancestors than needed, the extra views are detached. If there are less ChildClippingView - // ancestors than needed, new ChildClippingViews will be added. - // - // If head_clip_view was attached as a subview to FlutterView, the head of the newly constructed - // ChildClippingViews chain is attached to FlutterView in the same position. - // - // Returns the new head of the clip views chain. - UIView* ReconstructClipViewsChain(int number_of_clips, - UIView* platform_view, - UIView* head_clip_view); - // Applies the mutators in the mutators_stack to the UIView chain that was constructed by // `ReconstructClipViewsChain` // diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 551535a2c7faf..5e9ed80279975 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -53,32 +53,72 @@ void ResetAnchor(CALayer* layer) { @implementation ChildClippingView -+ (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect { - return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft, - clipSkRect.fBottom - clipSkRect.fTop); +// The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to +// be hit tested and consumed by this view if they are inside the embedded platform view which could +// be smaller the embedded platform view is rotated. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { + for (UIView* view in self.subviews) { + if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) { + return YES; + } + } + return NO; +} + +@end + +@interface FlutterClippingMaskView () + +- (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix; +- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect; + +@end + +@implementation FlutterClippingMaskView { + std::vector> paths_; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if ([super initWithFrame:frame]) { + self.backgroundColor = UIColor.clearColor; + } + return self; } -- (void)clipRect:(const SkRect&)clipSkRect { - CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRect]; - fml::CFRef pathRef(CGPathCreateWithRect(clipRect, nil)); - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; +- (void)drawRect:(CGRect)rect { + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSaveGState(context); + + // For mask view, only the alpha channel is used. + CGContextSetAlpha(context, 1); + + for (size_t i = 0; i < paths_.size(); i++) { + CGContextAddPath(context, paths_.at(i)); + CGContextClip(context); + } + CGContextFillRect(context, rect); + CGContextRestoreGState(context); +} + +- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix { + CGRect clipRect = [self getCGRectFromSkRect:clipSkRect]; + CGPathRef path = CGPathCreateWithRect(clipRect, nil); + paths_.push_back([self getTransformedPath:path matrix:matrix]); } -- (void)clipRRect:(const SkRRect&)clipSkRRect { +- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix { CGPathRef pathRef = nullptr; switch (clipSkRRect.getType()) { case SkRRect::kEmpty_Type: { break; } case SkRRect::kRect_Type: { - [self clipRect:clipSkRRect.rect()]; + [self clipRect:clipSkRRect.rect() matrix:matrix]; return; } case SkRRect::kOval_Type: case SkRRect::kSimple_Type: { - CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRRect.rect()]; + CGRect clipRect = [self getCGRectFromSkRect:clipSkRRect.rect()]; pathRef = CGPathCreateWithRoundedRect(clipRect, clipSkRRect.getSimpleRadii().x(), clipSkRRect.getSimpleRadii().y(), nil); break; @@ -129,23 +169,17 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect { // TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that // the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge // clipping on iOS. - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; - CGPathRelease(pathRef); + paths_.push_back([self getTransformedPath:pathRef matrix:matrix]); } -- (void)clipPath:(const SkPath&)path { +- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix { if (!path.isValid()) { return; } - fml::CFRef pathRef(CGPathCreateMutable()); if (path.isEmpty()) { - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; return; } + CGMutablePathRef pathRef = CGPathCreateMutable(); // Loop through all verbs and translate them into CGPath SkPath::Iter iter(path, true); @@ -197,42 +231,20 @@ - (void)clipPath:(const SkPath&)path { } verb = iter.next(pts); } - - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; + paths_.push_back([self getTransformedPath:pathRef matrix:matrix]); } -- (void)setClip:(flutter::MutatorType)type - rect:(const SkRect&)rect - rrect:(const SkRRect&)rrect - path:(const SkPath&)path { - FML_CHECK(type == flutter::clip_rect || type == flutter::clip_rrect || - type == flutter::clip_path); - switch (type) { - case flutter::clip_rect: - [self clipRect:rect]; - break; - case flutter::clip_rrect: - [self clipRRect:rrect]; - break; - case flutter::clip_path: - [self clipPath:path]; - break; - default: - break; - } +- (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix { + CGAffineTransform affine = + CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); + CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine); + CGPathRelease(path); + return fml::CFRef(transformedPath); } -// The ChildClippingView is as big as the FlutterView, we only want touches to be hit tested and -// consumed by this view if they are inside the smaller child view. -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { - for (UIView* view in self.subviews) { - if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) { - return YES; - } - } - return NO; +- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect { + return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft, + clipSkRect.fBottom - clipSkRect.fTop); } @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 1e04170da2159..77b65e63554ca 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -461,7 +461,6 @@ - (void)configureWithDictionary:(NSDictionary*)configuration { self.secureTextEntry = [configuration[kSecureTextEntry] boolValue]; self.keyboardType = ToUIKeyboardType(inputType); - self.keyboardType = UIKeyboardTypeNamePhonePad; self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]); self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration); @@ -538,35 +537,21 @@ - (BOOL)setTextInputState:(NSDictionary*)state { FlutterTextRange* newMarkedRange = composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil; needsEditingStateUpdate = - needsEditingStateUpdate || newMarkedRange == nil - ? self.markedTextRange == nil - : [newMarkedRange isEqualTo:(FlutterTextRange*)self.markedTextRange]; + needsEditingStateUpdate || + (!newMarkedRange ? self.markedTextRange != nil + : ![newMarkedRange isEqualTo:(FlutterTextRange*)self.markedTextRange]); self.markedTextRange = newMarkedRange; - NSInteger selectionBase = [state[@"selectionBase"] intValue]; - NSInteger selectionExtent = [state[@"selectionExtent"] intValue]; - NSRange selectedRange = [self clampSelection:NSMakeRange(MIN(selectionBase, selectionExtent), - ABS(selectionBase - selectionExtent)) - forText:self.text]; + NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue] + extent:[state[@"selectionExtent"] intValue] + forText:self.text]; + NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range]; - if (selectedRange.location != oldSelectedRange.location || - selectedRange.length != oldSelectedRange.length) { + if (!NSEqualRanges(selectedRange, oldSelectedRange)) { needsEditingStateUpdate = YES; [self.inputDelegate selectionWillChange:self]; - // The state may contain an invalid selection, such as when no selection was - // explicitly set in the framework. This is handled here by setting the - // selection to (0,0). In contrast, Android handles this situation by - // clearing the selection, but the result in both cases is that the cursor - // is placed at the beginning of the field. - bool selectionBaseIsValid = selectionBase > 0 && selectionBase <= ((NSInteger)self.text.length); - bool selectionExtentIsValid = - selectionExtent > 0 && selectionExtent <= ((NSInteger)self.text.length); - if (selectionBaseIsValid && selectionExtentIsValid) { - [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; - } else { - [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 0)]]; - } + [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; _selectionAffinity = _kTextAffinityDownstream; if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) @@ -582,6 +567,22 @@ - (BOOL)setTextInputState:(NSDictionary*)state { return needsEditingStateUpdate; } +// Extracts the selection information from the editing state dictionary. +// +// The state may contain an invalid selection, such as when no selection was +// explicitly set in the framework. This is handled here by setting the +// selection to (0,0). In contrast, Android handles this situation by +// clearing the selection, but the result in both cases is that the cursor +// is placed at the beginning of the field. +- (NSRange)clampSelectionFromBase:(int)selectionBase + extent:(int)selectionExtent + forText:(NSString*)text { + int loc = MIN(selectionBase, selectionExtent); + int len = ABS(selectionExtent - selectionBase); + return loc < 0 ? NSMakeRange(0, 0) + : [self clampSelection:NSMakeRange(loc, len) forText:self.text]; +} + - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text { int start = MIN(MAX(range.location, 0), text.length); int length = MIN(range.length, text.length - start); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 7f69a19664de1..a6d10993de755 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -13,7 +13,8 @@ @interface FlutterTextInputView () @property(nonatomic, copy) NSString* autofillId; -- (void)setTextInputState:(NSDictionary*)state; +- (BOOL)setTextInputState:(NSDictionary*)state; +- (void)updateEditingState; - (BOOL)isVisibleToAutofill; @end @@ -63,23 +64,6 @@ - (void)setClientId:(int)clientId configuration:(NSDictionary*)config { }]; } -- (void)commitAutofillContextAndVerify { - FlutterMethodCall* methodCall = - [FlutterMethodCall methodCallWithMethodName:@"TextInput.finishAutofillContext" - arguments:@YES]; - [textInputPlugin handleMethodCall:methodCall - result:^(id _Nullable result){ - }]; - - XCTAssertEqual(self.viewsVisibleToAutofill.count, - [textInputPlugin.activeView isVisibleToAutofill] ? 1 : 0); - XCTAssertNotEqual(textInputPlugin.textInputView, nil); - // The active view should still be installed so it doesn't get - // deallocated. - XCTAssertEqual(self.installedInputViews.count, 1); - XCTAssertEqual(textInputPlugin.autofillContext.count, 0); -} - - (NSMutableDictionary*)mutableTemplateCopy { if (!_template) { _template = @{ @@ -96,22 +80,6 @@ - (NSMutableDictionary*)mutableTemplateCopy { return [_template mutableCopy]; } -- (NSMutableDictionary*)mutablePasswordTemplateCopy { - if (!_passwordTemplate) { - _passwordTemplate = @{ - @"inputType" : @{@"name" : @"TextInuptType.text"}, - @"keyboardAppearance" : @"Brightness.light", - @"obscureText" : @YES, - @"inputAction" : @"TextInputAction.unspecified", - @"smartDashesType" : @"0", - @"smartQuotesType" : @"0", - @"autocorrect" : @YES - }; - } - - return [_passwordTemplate mutableCopy]; -} - - (NSArray*)installedInputViews { UIWindow* keyWindow = [[[UIApplication sharedApplication] windows] @@ -123,11 +91,6 @@ - (NSMutableDictionary*)mutablePasswordTemplateCopy { [FlutterTextInputView class]]]; } -- (NSArray*)viewsVisibleToAutofill { - return [self.installedInputViews - filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]]; -} - #pragma mark - Tests - (void)testSecureInput { @@ -145,6 +108,9 @@ - (void)testSecureInput { // Verify secureTextEntry is set to the correct value. XCTAssertTrue(inputView.secureTextEntry); + // Verify keyboardType is set to the default value. + XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault); + // We should have only ever created one FlutterTextInputView. XCTAssertEqual(inputFields.count, 1); @@ -157,6 +123,86 @@ - (void)testSecureInput { XCTAssert(inputView.autofillId.length > 0); } +- (void)testKeyboardType { + NSDictionary* config = self.mutableTemplateCopy; + [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"]; + [self setClientId:123 configuration:config]; + + // Find all the FlutterTextInputViews we created. + NSArray* inputFields = self.installedInputViews; + + FlutterTextInputView* inputView = inputFields[0]; + + // Verify keyboardType is set to the value specified in config. + XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL); +} + +- (void)testAutocorrectionPromptRectAppears { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; + inputView.textInputDelegate = engine; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + + // Verify behavior. + OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]); +} + +- (void)testTextRangeFromPositionMatchesUITextViewBehavior { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; + FlutterTextPosition* fromPosition = [[FlutterTextPosition alloc] initWithIndex:2]; + FlutterTextPosition* toPosition = [[FlutterTextPosition alloc] initWithIndex:0]; + + FlutterTextRange* flutterRange = (FlutterTextRange*)[inputView textRangeFromPosition:fromPosition + toPosition:toPosition]; + NSRange range = flutterRange.range; + + XCTAssertEqual(range.location, 0); + XCTAssertEqual(range.length, 2); +} + +- (void)testNoZombies { + // Regression test for https://github.com/flutter/flutter/issues/62501. + FlutterSecureTextInputView* passwordView = [[FlutterSecureTextInputView alloc] init]; + + @autoreleasepool { + // Initialize the lazy textField. + [passwordView.textField description]; + } + XCTAssert([[passwordView.textField description] containsString:@"TextField"]); +} + +#pragma mark - EditingState tests + +- (void)testUITextInputCallsUpdateEditingStateOnce { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + __block int updateCount = 0; + OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) + .andDo(^(NSInvocation* invocation) { + updateCount++; + }); + + [inputView insertText:@"text to insert"]; + // Update the framework exactly once. + XCTAssertEqual(updateCount, 1); + + [inputView deleteBackward]; + XCTAssertEqual(updateCount, 2); + + inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]; + XCTAssertEqual(updateCount, 3); + + [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] + withText:@"replace text"]; + XCTAssertEqual(updateCount, 4); + + [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + XCTAssertEqual(updateCount, 5); + + [inputView unmarkText]; + XCTAssertEqual(updateCount, 6); +} + - (void)testTextChangesTriggerUpdateEditingClient { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -166,12 +212,10 @@ - (void)testTextChangesTriggerUpdateEditingClient { inputView.selectedTextRange = nil; // Text changes trigger update. - [inputView setTextInputState:@{@"text" : @"AFTER"}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue([inputView setTextInputState:@{@"text" : @"AFTER"}]); // Don't send anything if there's nothing new. - [inputView setTextInputState:@{@"text" : @"AFTER"}]; - OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); + XCTAssertFalse([inputView setTextInputState:@{@"text" : @"AFTER"}]); } - (void)testSelectionChangeTriggersUpdateEditingClient { @@ -182,22 +226,22 @@ - (void)testSelectionChangeTriggersUpdateEditingClient { inputView.markedTextRange = nil; inputView.selectedTextRange = nil; - [inputView + BOOL shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); // Don't send anything if there's nothing new. - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}]; - OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); + XCTAssertFalse(shouldUpdate); } - (void)testComposingChangeTriggersUpdateEditingClient { @@ -209,22 +253,22 @@ - (void)testComposingChangeTriggersUpdateEditingClient { inputView.markedTextRange = nil; inputView.selectedTextRange = nil; - [inputView + BOOL shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @0, @"composingExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); // Don't send anything if there's nothing new. - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}]; - OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); + XCTAssertFalse(shouldUpdate); } - (void)testUpdateEditingClientNegativeSelection { @@ -240,13 +284,131 @@ - (void)testUpdateEditingClientNegativeSelection { @"selectionBase" : @-1, @"selectionExtent" : @-1 }]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); + + // Returns (0, 0) when either end goes below 0. + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); + + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); +} + +- (void)testUpdateEditingClientSelectionClamping { + // Regression test for https://github.com/flutter/flutter/issues/62992. + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + [inputView.text setString:@"SELECTION"]; + inputView.markedTextRange = nil; + inputView.selectedTextRange = nil; + + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}]; + [inputView updateEditingState]; OCMVerify([engine updateEditingClient:0 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { return ([state[@"selectionBase"] intValue]) == 0 && ([state[@"selectionExtent"] intValue] == 0); }]]); + + // Needs clamping. + [inputView setTextInputState:@{ + @"text" : @"SELECTION", + @"selectionBase" : @0, + @"selectionExtent" : @9999 + }]; + [inputView updateEditingState]; + + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 9); + }]]); + + // No clamping needed, but in reverse direction. + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 1); + }]]); + + // Both ends need clamping. + [inputView setTextInputState:@{ + @"text" : @"SELECTION", + @"selectionBase" : @9999, + @"selectionExtent" : @9999 + }]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 9 && + ([state[@"selectionExtent"] intValue] == 9); + }]]); +} + +#pragma mark - Autofill - Utilities + +- (NSMutableDictionary*)mutablePasswordTemplateCopy { + if (!_passwordTemplate) { + _passwordTemplate = @{ + @"inputType" : @{@"name" : @"TextInuptType.text"}, + @"keyboardAppearance" : @"Brightness.light", + @"obscureText" : @YES, + @"inputAction" : @"TextInputAction.unspecified", + @"smartDashesType" : @"0", + @"smartQuotesType" : @"0", + @"autocorrect" : @YES + }; + } + + return [_passwordTemplate mutableCopy]; +} + +- (NSArray*)viewsVisibleToAutofill { + return [self.installedInputViews + filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]]; +} + +- (void)commitAutofillContextAndVerify { + FlutterMethodCall* methodCall = + [FlutterMethodCall methodCallWithMethodName:@"TextInput.finishAutofillContext" + arguments:@YES]; + [textInputPlugin handleMethodCall:methodCall + result:^(id _Nullable result){ + }]; + + XCTAssertEqual(self.viewsVisibleToAutofill.count, + [textInputPlugin.activeView isVisibleToAutofill] ? 1 : 0); + XCTAssertNotEqual(textInputPlugin.textInputView, nil); + // The active view should still be installed so it doesn't get + // deallocated. + XCTAssertEqual(self.installedInputViews.count, 1); + XCTAssertEqual(textInputPlugin.autofillContext.count, 0); } +#pragma mark - Autofill - Tests + - (void)testAutofillContext { NSMutableDictionary* field1 = self.mutableTemplateCopy; @@ -448,67 +610,4 @@ - (void)testPasswordAutofillHack { XCTAssertNotEqual([inputView performSelector:@selector(font)], nil); } -- (void)testAutocorrectionPromptRectAppears { - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; - inputView.textInputDelegate = engine; - [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; - - // Verify behavior. - OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]); -} - -- (void)testTextRangeFromPositionMatchesUITextViewBehavior { - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; - FlutterTextPosition* fromPosition = [[FlutterTextPosition alloc] initWithIndex:2]; - FlutterTextPosition* toPosition = [[FlutterTextPosition alloc] initWithIndex:0]; - - FlutterTextRange* flutterRange = (FlutterTextRange*)[inputView textRangeFromPosition:fromPosition - toPosition:toPosition]; - NSRange range = flutterRange.range; - - XCTAssertEqual(range.location, 0); - XCTAssertEqual(range.length, 2); -} - -- (void)testUITextInputCallsUpdateEditingStateOnce { - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; - inputView.textInputDelegate = engine; - - __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) - .andDo(^(NSInvocation* invocation) { - updateCount++; - }); - - [inputView insertText:@"text to insert"]; - // Update the framework exactly once. - XCTAssertEqual(updateCount, 1); - - [inputView deleteBackward]; - XCTAssertEqual(updateCount, 2); - - inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]; - XCTAssertEqual(updateCount, 3); - - [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] - withText:@"replace text"]; - XCTAssertEqual(updateCount, 4); - - [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; - XCTAssertEqual(updateCount, 5); - - [inputView unmarkText]; - XCTAssertEqual(updateCount, 6); -} - -- (void)testNoZombies { - // Regression test for https://github.com/flutter/flutter/issues/62501. - FlutterSecureTextInputView* passwordView = [[FlutterSecureTextInputView alloc] init]; - - @autoreleasepool { - // Initialize the lazy textField. - [passwordView.textField description]; - } - XCTAssert([[passwordView.textField description] containsString:@"TextField"]); -} @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index d788b461586f6..6114281d8695b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -111,26 +111,24 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine return self; } -- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project { - _viewOpaque = YES; - _weakFactory = std::make_unique>(self); - _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" - project:project - allowHeadlessExecution:self.engineAllowHeadlessExecution]); - _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); - [_engine.get() createShell:nil libraryURI:nil]; - _engineNeedsLaunch = YES; - _ongoingTouches = [[NSMutableSet alloc] init]; - [self loadDefaultSplashScreenView]; - [self performCommonViewControllerInitialization]; +- (instancetype)initWithProject:(FlutterDartProject*)project + nibName:(NSString*)nibName + bundle:(NSBundle*)nibBundle { + self = [super initWithNibName:nibName bundle:nibBundle]; + if (self) { + [self sharedSetupWithProject:project initialRoute:nil]; + } + + return self; } -- (instancetype)initWithProject:(nullable FlutterDartProject*)project - nibName:(nullable NSString*)nibName - bundle:(nullable NSBundle*)nibBundle { +- (instancetype)initWithProject:(FlutterDartProject*)project + initialRoute:(NSString*)initialRoute + nibName:(NSString*)nibName + bundle:(NSBundle*)nibBundle { self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { - [self sharedSetupWithProject:project]; + [self sharedSetupWithProject:project initialRoute:initialRoute]; } return self; @@ -148,7 +146,7 @@ - (instancetype)initWithCoder:(NSCoder*)aDecoder { - (void)awakeFromNib { [super awakeFromNib]; if (!_engine.get()) { - [self sharedSetupWithProject:nil]; + [self sharedSetupWithProject:nil initialRoute:nil]; } } @@ -156,6 +154,21 @@ - (instancetype)init { return [self initWithProject:nil nibName:nil bundle:nil]; } +- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project + initialRoute:(nullable NSString*)initialRoute { + _viewOpaque = YES; + _weakFactory = std::make_unique>(self); + _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" + project:project + allowHeadlessExecution:self.engineAllowHeadlessExecution]); + _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); + [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute]; + _engineNeedsLaunch = YES; + _ongoingTouches = [[NSMutableSet alloc] init]; + [self loadDefaultSplashScreenView]; + [self performCommonViewControllerInitialization]; +} + - (BOOL)isViewOpaque { return _viewOpaque; } @@ -469,7 +482,12 @@ - (UIView*)splashScreenFromStoryboard:(NSString*)name { } - (UIView*)splashScreenFromXib:(NSString*)name { - NSArray* objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil]; + NSArray* objects = nil; + @try { + objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil]; + } @catch (NSException* exception) { + return nil; + } if ([objects count] != 0) { UIView* view = [objects objectAtIndex:0]; return view; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index e508d031eb265..2418ea58bc72b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -13,7 +13,9 @@ FLUTTER_ASSERT_ARC @interface FlutterEngine () -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI; +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute; @end @interface FlutterEngine (TestLowMemory) @@ -513,7 +515,7 @@ - (void)testWillDeallocNotification { - (void)testDoesntLoadViewInInit { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; - [engine createShell:@"" libraryURI:@""]; + [engine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; @@ -523,7 +525,7 @@ - (void)testDoesntLoadViewInInit { - (void)testHideOverlay { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; - [engine createShell:@"" libraryURI:@""]; + [engine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index 2addc2a50932f..1470bca82270a 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -453,6 +453,7 @@ - (BOOL)accessibilityPerformEscape { - (void)accessibilityElementDidBecomeFocused { if (![self isAccessibilityBridgeAlive]) return; + [self bridge]->AccessibilityFocusDidChange([self uid]); if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden) || [self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) { [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen); diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm index 3c42aeacb3efb..8b23f8d9369f0 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -33,6 +33,7 @@ void DispatchSemanticsAction(int32_t id, SemanticsActionObservation observation(id, action); observations.push_back(observation); } + void AccessibilityFocusDidChange(int32_t id) override {} FlutterPlatformViewsController* GetPlatformViewsController() const override { return nil; } std::vector observations; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h index 77e5813792ad1..758b63fdf7545 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h @@ -18,6 +18,7 @@ #include "flutter/lib/ui/semantics/custom_accessibility_action.h" #include "flutter/lib/ui/semantics/semantics_node.h" #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" +#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" #include "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" @@ -42,12 +43,13 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { virtual ~IosDelegate() = default; /// Returns true when the FlutterViewController associated with the `view` /// is presenting a modal view controller. - virtual bool IsFlutterViewControllerPresentingModalViewController(UIView* view) = 0; + virtual bool IsFlutterViewControllerPresentingModalViewController( + FlutterViewController* view_controller) = 0; virtual void PostAccessibilityNotification(UIAccessibilityNotifications notification, id argument) = 0; }; - AccessibilityBridge(UIView* view, + AccessibilityBridge(FlutterViewController* view_controller, PlatformViewIOS* platform_view, FlutterPlatformViewsController* platform_views_controller, std::unique_ptr ios_delegate = nullptr); @@ -59,10 +61,11 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action, std::vector args) override; + void AccessibilityFocusDidChange(int32_t id) override; UIView* textInputView() override; - UIView* view() const override { return view_; } + UIView* view() const override { return view_controller_.view; } fml::WeakPtr GetWeakPtr(); @@ -78,9 +81,10 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { NSMutableArray* doomed_uids); void HandleEvent(NSDictionary* annotatedEvent); - UIView* view_; + FlutterViewController* view_controller_; PlatformViewIOS* platform_view_; FlutterPlatformViewsController* platform_views_controller_; + int32_t last_focused_semantics_object_id_; fml::scoped_nsobject> objects_; fml::scoped_nsprotocol accessibility_channel_; fml::WeakPtrFactory weak_factory_; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index 6fd197e69ceb2..d8f99a3b198b4 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -16,27 +16,12 @@ namespace flutter { namespace { -FlutterViewController* _Nullable GetFlutterViewControllerForView(UIView* view) { - // There is no way to get a view's view controller in UIKit directly, this is - // somewhat of a hacky solution to get that. This could be eliminated if the - // bridge actually kept a reference to a FlutterViewController instead of a - // UIView. - id nextResponder = [view nextResponder]; - if ([nextResponder isKindOfClass:[FlutterViewController class]]) { - return nextResponder; - } else if ([nextResponder isKindOfClass:[UIView class]]) { - return GetFlutterViewControllerForView(nextResponder); - } else { - return nil; - } -} - class DefaultIosDelegate : public AccessibilityBridge::IosDelegate { public: - bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { - FlutterViewController* viewController = GetFlutterViewControllerForView(view); - if (viewController) { - return viewController.isPresentingViewController; + bool IsFlutterViewControllerPresentingModalViewController( + FlutterViewController* view_controller) override { + if (view_controller) { + return view_controller.isPresentingViewController; } else { return false; } @@ -49,13 +34,14 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, }; } // namespace -AccessibilityBridge::AccessibilityBridge(UIView* view, +AccessibilityBridge::AccessibilityBridge(FlutterViewController* view_controller, PlatformViewIOS* platform_view, FlutterPlatformViewsController* platform_views_controller, std::unique_ptr ios_delegate) - : view_(view), + : view_controller_(view_controller), platform_view_(platform_view), platform_views_controller_(platform_views_controller), + last_focused_semantics_object_id_(0), objects_([[NSMutableDictionary alloc] init]), weak_factory_(this), previous_route_id_(0), @@ -74,13 +60,17 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, AccessibilityBridge::~AccessibilityBridge() { [accessibility_channel_.get() setMessageHandler:nil]; clearState(); - view_.accessibilityElements = nil; + view_controller_.view.accessibilityElements = nil; } UIView* AccessibilityBridge::textInputView() { return [[platform_view_->GetOwnerViewController().get().engine textInputPlugin] textInputView]; } +void AccessibilityBridge::AccessibilityFocusDidChange(int32_t id) { + last_focused_semantics_object_id_ = id; +} + void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes, flutter::CustomAccessibilityActionUpdates actions) { BOOL layoutChanged = NO; @@ -164,8 +154,8 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, SemanticsObject* lastAdded = nil; if (root) { - if (!view_.accessibilityElements) { - view_.accessibilityElements = @[ [root accessibilityContainer] ]; + if (!view_controller_.view.accessibilityElements) { + view_controller_.view.accessibilityElements = @[ [root accessibilityContainer] ]; } NSMutableArray* newRoutes = [[[NSMutableArray alloc] init] autorelease]; [root collectRoutes:newRoutes]; @@ -188,7 +178,7 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, previous_routes_.push_back([route uid]); } } else { - view_.accessibilityElements = nil; + view_controller_.view.accessibilityElements = nil; } NSMutableArray* doomed_uids = [NSMutableArray arrayWithArray:[objects_.get() allKeys]]; @@ -198,17 +188,21 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, layoutChanged = layoutChanged || [doomed_uids count] > 0; if (routeChanged) { - if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_)) { + if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) { ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification, [lastAdded routeFocusObject]); } } else if (layoutChanged) { - // TODO(goderbauer): figure out which node to focus next. - ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, nil); + // Tries to refocus the previous focused semantics object to avoid random jumps. + ios_delegate_->PostAccessibilityNotification( + UIAccessibilityLayoutChangedNotification, + [objects_.get() objectForKey:@(last_focused_semantics_object_id_)]); } if (scrollOccured) { - // TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5") - ios_delegate_->PostAccessibilityNotification(UIAccessibilityPageScrolledNotification, @""); + // Tries to refocus the previous focused semantics object to avoid random jumps. + ios_delegate_->PostAccessibilityNotification( + UIAccessibilityPageScrolledNotification, + [objects_.get() objectForKey:@(last_focused_semantics_object_id_)]); } } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h index c2546ac7c3a2c..19b49140edc54 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h @@ -24,6 +24,13 @@ class AccessibilityBridgeIos { virtual void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action, std::vector args) = 0; + /** + * A callback that is called after the accessibility focus has moved to a new + * SemanticObject. + * + * The input id is the uid of the newly focused SemanticObject. + */ + virtual void AccessibilityFocusDidChange(int32_t id) = 0; virtual FlutterPlatformViewsController* GetPlatformViewsController() const = 0; }; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index b6e07133672e1..d5f972aa62ffc 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -96,7 +96,8 @@ void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} class MockIosDelegate : public AccessibilityBridge::IosDelegate { public: - bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { + bool IsFlutterViewControllerPresentingModalViewController( + FlutterViewController* view_controller) override { return result_IsFlutterViewControllerPresentingModalViewController_; }; @@ -157,9 +158,11 @@ - (void)testUpdateSemanticsEmpty { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg isNil]]); auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil); flutter::SemanticsNodeUpdates nodes; @@ -181,10 +184,12 @@ - (void)testUpdateSemanticsOneNode { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; __block auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil); @@ -224,6 +229,8 @@ - (void)testSemanticsDeallocated { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; auto flutterPlatformViewsController = @@ -243,7 +250,7 @@ - (void)testSemanticsDeallocated { result); auto bridge = std::make_unique( - /*view=*/mockFlutterView, + /*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/flutterPlatformViewsController.get()); @@ -274,6 +281,8 @@ - (void)testAnnouncesRouteChanges { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; NSMutableArray*>* accessibility_notifications = @@ -287,7 +296,7 @@ - (void)testAnnouncesRouteChanges { }]; }; __block auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil, /*ios_delegate=*/std::move(ios_delegate)); @@ -297,7 +306,6 @@ - (void)testAnnouncesRouteChanges { flutter::SemanticsNode route_node; route_node.id = 1; - route_node.label = label; route_node.flags = static_cast(flutter::SemanticsFlags::kScopesRoute) | static_cast(flutter::SemanticsFlags::kNamesRoute); route_node.label = "route"; @@ -318,6 +326,213 @@ - (void)testAnnouncesRouteChanges { UIAccessibilityScreenChangedNotification); } +- (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + id mockFlutterView = OCMClassMock([FlutterView class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + ios_delegate->on_PostAccessibilityNotification_ = + [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { + [accessibility_notifications addObject:@{ + @"notification" : @(notification), + @"argument" : argument ? argument : [NSNull null], + }]; + }; + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates first_update; + + flutter::SemanticsNode route_node; + route_node.id = 1; + route_node.label = "route"; + first_update[route_node.id] = route_node; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = "root"; + root_node.childrenInTraversalOrder = {1}; + root_node.childrenInHitTestOrder = {1}; + first_update[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions); + + XCTAssertEqual([accessibility_notifications count], 0ul); + // Simulates the focusing on the node 1. + bridge->AccessibilityFocusDidChange(1); + + flutter::SemanticsNodeUpdates second_update; + // Simulates the removal of the node 1 + flutter::SemanticsNode new_root_node; + new_root_node.id = kRootNodeId; + new_root_node.label = "root"; + second_update[root_node.id] = new_root_node; + bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions); + NSNull* focusObject = accessibility_notifications[0][@"argument"]; + // The node 1 was removed, so the bridge will set the focus object to nil. + XCTAssertEqual(focusObject, [NSNull null]); + XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], + UIAccessibilityLayoutChangedNotification); +} + +- (void)testAnnouncesLayoutChangeWithLastFocused { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + id mockFlutterView = OCMClassMock([FlutterView class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + ios_delegate->on_PostAccessibilityNotification_ = + [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { + [accessibility_notifications addObject:@{ + @"notification" : @(notification), + @"argument" : argument ? argument : [NSNull null], + }]; + }; + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates first_update; + + flutter::SemanticsNode node_one; + node_one.id = 1; + node_one.label = "route1"; + first_update[node_one.id] = node_one; + flutter::SemanticsNode node_two; + node_two.id = 2; + node_two.label = "route2"; + first_update[node_two.id] = node_two; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = "root"; + root_node.childrenInTraversalOrder = {1, 2}; + root_node.childrenInHitTestOrder = {1, 2}; + first_update[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions); + + XCTAssertEqual([accessibility_notifications count], 0ul); + // Simulates the focusing on the node 1. + bridge->AccessibilityFocusDidChange(1); + + flutter::SemanticsNodeUpdates second_update; + // Simulates the removal of the node 2. + flutter::SemanticsNode new_root_node; + new_root_node.id = kRootNodeId; + new_root_node.label = "root"; + new_root_node.childrenInTraversalOrder = {1}; + new_root_node.childrenInHitTestOrder = {1}; + second_update[root_node.id] = new_root_node; + bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions); + SemanticsObject* focusObject = accessibility_notifications[0][@"argument"]; + // Since we have focused on the node 1 right before the layout changed, the bridge should refocus + // the node 1. + XCTAssertEqual([focusObject uid], 1); + XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], + UIAccessibilityLayoutChangedNotification); +} + +- (void)testAnnouncesScrollChangeWithLastFocused { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + id mockFlutterView = OCMClassMock([FlutterView class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + ios_delegate->on_PostAccessibilityNotification_ = + [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { + [accessibility_notifications addObject:@{ + @"notification" : @(notification), + @"argument" : argument ? argument : [NSNull null], + }]; + }; + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates first_update; + + flutter::SemanticsNode node_one; + node_one.id = 1; + node_one.label = "route1"; + node_one.scrollPosition = 0.0; + first_update[node_one.id] = node_one; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = "root"; + root_node.childrenInTraversalOrder = {1}; + root_node.childrenInHitTestOrder = {1}; + first_update[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions); + + // The first update will trigger a scroll announcement, but we are not interested in it. + [accessibility_notifications removeAllObjects]; + + // Simulates the focusing on the node 1. + bridge->AccessibilityFocusDidChange(1); + + flutter::SemanticsNodeUpdates second_update; + // Simulates the scrolling on the node 1. + flutter::SemanticsNode new_node_one; + new_node_one.id = 1; + new_node_one.label = "route1"; + new_node_one.scrollPosition = 1.0; + second_update[new_node_one.id] = new_node_one; + bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions); + SemanticsObject* focusObject = accessibility_notifications[0][@"argument"]; + // Since we have focused on the node 1 right before the scrolling, the bridge should refocus the + // node 1. + XCTAssertEqual([focusObject uid], 1); + XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], + UIAccessibilityPageScrolledNotification); +} + - (void)testAnnouncesIgnoresRouteChangesWhenModal { flutter::MockDelegate mock_delegate; auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); @@ -331,6 +546,8 @@ - (void)testAnnouncesIgnoresRouteChangesWhenModal { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; NSMutableArray*>* accessibility_notifications = @@ -345,7 +562,7 @@ - (void)testAnnouncesIgnoresRouteChangesWhenModal { }; ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true; __block auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil, /*ios_delegate=*/std::move(ios_delegate)); diff --git a/shell/platform/darwin/ios/ios_external_texture_gl.h b/shell/platform/darwin/ios/ios_external_texture_gl.h index 5c608506bb82f..6a63357a77381 100644 --- a/shell/platform/darwin/ios/ios_external_texture_gl.h +++ b/shell/platform/darwin/ios/ios_external_texture_gl.h @@ -25,6 +25,9 @@ class IOSExternalTextureGL final : public Texture { fml::CFRef cache_ref_; fml::CFRef texture_ref_; fml::CFRef buffer_ref_; + OSType pixel_format_ = 0; + fml::CFRef y_texture_ref_; + fml::CFRef uv_texture_ref_; // |Texture| void Paint(SkCanvas& canvas, @@ -51,6 +54,16 @@ class IOSExternalTextureGL final : public Texture { bool NeedUpdateTexture(bool freeze); + bool IsTexturesAvailable() const; + + void CreateYUVTexturesFromPixelBuffer(); + + void CreateRGBATextureFromPixelBuffer(); + + sk_sp CreateImageFromYUVTextures(GrContext* context, const SkRect& bounds); + + sk_sp CreateImageFromRGBATexture(GrContext* context, const SkRect& bounds); + FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalTextureGL); }; diff --git a/shell/platform/darwin/ios/ios_external_texture_gl.mm b/shell/platform/darwin/ios/ios_external_texture_gl.mm index bb56fc2849bcf..627da33f09793 100644 --- a/shell/platform/darwin/ios/ios_external_texture_gl.mm +++ b/shell/platform/darwin/ios/ios_external_texture_gl.mm @@ -10,8 +10,10 @@ #include "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" #include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/core/SkYUVAIndex.h" #include "third_party/skia/include/gpu/GrBackendSurface.h" #include "third_party/skia/include/gpu/GrDirectContext.h" +#include "third_party/skia/src/gpu/gl/GrGLDefines.h" namespace flutter { @@ -42,10 +44,19 @@ if (buffer_ref_ == nullptr) { return; } + if (pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + CreateYUVTexturesFromPixelBuffer(); + } else { + CreateRGBATextureFromPixelBuffer(); + } +} + +void IOSExternalTextureGL::CreateRGBATextureFromPixelBuffer() { CVOpenGLESTextureRef texture; CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( - kCFAllocatorDefault, cache_ref_, buffer_ref_, nullptr, GL_TEXTURE_2D, GL_RGBA, - static_cast(CVPixelBufferGetWidth(buffer_ref_)), + kCFAllocatorDefault, cache_ref_, buffer_ref_, /*textureAttributes=*/nullptr, GL_TEXTURE_2D, + GL_RGBA, static_cast(CVPixelBufferGetWidth(buffer_ref_)), static_cast(CVPixelBufferGetHeight(buffer_ref_)), GL_BGRA, GL_UNSIGNED_BYTE, 0, &texture); if (err != noErr) { @@ -55,10 +66,83 @@ } } +void IOSExternalTextureGL::CreateYUVTexturesFromPixelBuffer() { + size_t width = CVPixelBufferGetWidth(buffer_ref_); + size_t height = CVPixelBufferGetHeight(buffer_ref_); + { + CVOpenGLESTextureRef yTexture; + CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, cache_ref_, buffer_ref_, /*textureAttributes=*/nullptr, GL_TEXTURE_2D, + GL_LUMINANCE, width, height, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &yTexture); + if (err != noErr) { + FML_DCHECK(yTexture) << "Could not create texture from pixel buffer: " << err; + } else { + y_texture_ref_.Reset(yTexture); + } + } + + { + CVOpenGLESTextureRef uvTexture; + CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, cache_ref_, buffer_ref_, /*textureAttributes=*/nullptr, GL_TEXTURE_2D, + GL_LUMINANCE_ALPHA, width / 2, height / 2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, + &uvTexture); + if (err != noErr) { + FML_DCHECK(uvTexture) << "Could not create texture from pixel buffer: " << err; + } else { + uv_texture_ref_.Reset(uvTexture); + } + } +} + +sk_sp IOSExternalTextureGL::CreateImageFromRGBATexture(GrContext* context, + const SkRect& bounds) { + GrGLTextureInfo textureInfo = {CVOpenGLESTextureGetTarget(texture_ref_), + CVOpenGLESTextureGetName(texture_ref_), GL_RGBA8_OES}; + GrBackendTexture backendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, textureInfo); + sk_sp image = SkImage::MakeFromTexture(context, backendTexture, kTopLeft_GrSurfaceOrigin, + kRGBA_8888_SkColorType, kPremul_SkAlphaType, + /*imageColorSpace=*/nullptr); + return image; +} + +sk_sp IOSExternalTextureGL::CreateImageFromYUVTextures(GrContext* context, + const SkRect& bounds) { + GrGLTextureInfo yTextureInfo = {CVOpenGLESTextureGetTarget(y_texture_ref_), + CVOpenGLESTextureGetName(y_texture_ref_), GR_GL_LUMINANCE8}; + GrBackendTexture yBackendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, yTextureInfo); + GrGLTextureInfo uvTextureInfo = {CVOpenGLESTextureGetTarget(uv_texture_ref_), + CVOpenGLESTextureGetName(uv_texture_ref_), GR_GL_RGBA8}; + GrBackendTexture uvBackendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, + uvTextureInfo); + GrBackendTexture nv12TextureHandles[] = {yBackendTexture, uvBackendTexture}; + SkYUVAIndex yuvaIndices[4] = { + SkYUVAIndex{0, SkColorChannel::kR}, // Read Y data from the red channel of the first texture + SkYUVAIndex{1, SkColorChannel::kR}, // Read U data from the red channel of the second texture + SkYUVAIndex{ + 1, SkColorChannel::kA}, // Read V data from the alpha channel of the second texture, + // normal NV12 data V should be taken from the green channel, but + // currently only the uv texture created by GL_LUMINANCE_ALPHA + // can be used, so the V value is taken from the alpha channel + SkYUVAIndex{-1, SkColorChannel::kA}}; //-1 means to omit the alpha data of YUVA + SkISize size{yBackendTexture.width(), yBackendTexture.height()}; + sk_sp image = SkImage::MakeFromYUVATextures( + context, kRec601_SkYUVColorSpace, nv12TextureHandles, yuvaIndices, size, + kTopLeft_GrSurfaceOrigin, /*imageColorSpace=*/nullptr); + return image; +} + +bool IOSExternalTextureGL::IsTexturesAvailable() const { + return ((pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) && + (y_texture_ref_ && uv_texture_ref_)) || + (pixel_format_ == kCVPixelFormatType_32BGRA && texture_ref_); +} + bool IOSExternalTextureGL::NeedUpdateTexture(bool freeze) { // Update texture if `texture_ref_` is reset to `nullptr` when GrContext // is destroyed or new frame is ready. - return (!freeze && new_frame_ready_) || !texture_ref_; + return (!freeze && new_frame_ready_) || !IsTexturesAvailable(); } void IOSExternalTextureGL::Paint(SkCanvas& canvas, @@ -71,19 +155,23 @@ auto pixelBuffer = [external_texture_.get() copyPixelBuffer]; if (pixelBuffer) { buffer_ref_.Reset(pixelBuffer); + pixel_format_ = CVPixelBufferGetPixelFormatType(buffer_ref_); } CreateTextureFromPixelBuffer(); new_frame_ready_ = false; } - if (!texture_ref_) { + if (!IsTexturesAvailable()) { return; } - GrGLTextureInfo textureInfo = {CVOpenGLESTextureGetTarget(texture_ref_), - CVOpenGLESTextureGetName(texture_ref_), GL_RGBA8_OES}; - GrBackendTexture backendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, textureInfo); - sk_sp image = - SkImage::MakeFromTexture(context, backendTexture, kTopLeft_GrSurfaceOrigin, - kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr); + + sk_sp image = nullptr; + if (pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + image = CreateImageFromYUVTextures(context, bounds); + } else { + image = CreateImageFromRGBATexture(context, bounds); + } + FML_DCHECK(image) << "Failed to create SkImage from Texture."; if (image) { SkPaint paint; diff --git a/shell/platform/darwin/ios/ios_external_texture_metal.h b/shell/platform/darwin/ios/ios_external_texture_metal.h index 9cbf84431eb9d..90e9e95746778 100644 --- a/shell/platform/darwin/ios/ios_external_texture_metal.h +++ b/shell/platform/darwin/ios/ios_external_texture_metal.h @@ -33,6 +33,7 @@ class IOSExternalTextureMetal final : public Texture { std::atomic_bool texture_frame_available_; fml::CFRef last_pixel_buffer_; sk_sp external_image_; + OSType pixel_format_ = 0; // |Texture| void Paint(SkCanvas& canvas, @@ -55,6 +56,10 @@ class IOSExternalTextureMetal final : public Texture { sk_sp WrapExternalPixelBuffer(fml::CFRef pixel_buffer, GrDirectContext* context) const; + sk_sp WrapRGBAExternalPixelBuffer(fml::CFRef pixel_buffer, + GrDirectContext* context) const; + sk_sp WrapNV12ExternalPixelBuffer(fml::CFRef pixel_buffer, + GrDirectContext* context) const; FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalTextureMetal); }; diff --git a/shell/platform/darwin/ios/ios_external_texture_metal.mm b/shell/platform/darwin/ios/ios_external_texture_metal.mm index 46eff3415273b..058a4738ce76c 100644 --- a/shell/platform/darwin/ios/ios_external_texture_metal.mm +++ b/shell/platform/darwin/ios/ios_external_texture_metal.mm @@ -5,6 +5,7 @@ #include "flutter/shell/platform/darwin/ios/ios_external_texture_metal.h" #include "flutter/fml/logging.h" +#include "third_party/skia/include/core/SkYUVAIndex.h" #include "third_party/skia/include/gpu/GrBackendSurface.h" #include "third_party/skia/include/gpu/GrDirectContext.h" #include "third_party/skia/include/gpu/mtl/GrMtlTypes.h" @@ -35,6 +36,8 @@ auto pixel_buffer = fml::CFRef([external_texture_ copyPixelBuffer]); if (!pixel_buffer) { pixel_buffer = std::move(last_pixel_buffer_); + } else { + pixel_format_ = CVPixelBufferGetPixelFormatType(pixel_buffer); } // If the application told us there was a texture frame available but did not provide one when @@ -65,21 +68,130 @@ return nullptr; } + sk_sp image = nullptr; + if (pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + image = WrapNV12ExternalPixelBuffer(pixel_buffer, context); + } else { + image = WrapRGBAExternalPixelBuffer(pixel_buffer, context); + } + + if (!image) { + FML_DLOG(ERROR) << "Could not wrap Metal texture as a Skia image."; + } + + return image; +} + +sk_sp IOSExternalTextureMetal::WrapNV12ExternalPixelBuffer( + fml::CFRef pixel_buffer, + GrDirectContext* context) const { auto texture_size = SkISize::Make(CVPixelBufferGetWidth(pixel_buffer), CVPixelBufferGetHeight(pixel_buffer)); + CVMetalTextureRef y_metal_texture_raw = nullptr; + { + auto cv_return = + CVMetalTextureCacheCreateTextureFromImage(/*allocator=*/kCFAllocatorDefault, + /*textureCache=*/texture_cache_, + /*sourceImage=*/pixel_buffer, + /*textureAttributes=*/nullptr, + /*pixelFormat=*/MTLPixelFormatR8Unorm, + /*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*planeIndex=*/0u, + /*texture=*/&y_metal_texture_raw); + + if (cv_return != kCVReturnSuccess) { + FML_DLOG(ERROR) << "Could not create Metal texture from pixel buffer: CVReturn " << cv_return; + return nullptr; + } + } + + CVMetalTextureRef uv_metal_texture_raw = nullptr; + { + auto cv_return = + CVMetalTextureCacheCreateTextureFromImage(/*allocator=*/kCFAllocatorDefault, + /*textureCache=*/texture_cache_, + /*sourceImage=*/pixel_buffer, + /*textureAttributes=*/nullptr, + /*pixelFormat=*/MTLPixelFormatRG8Unorm, + /*width=*/texture_size.width() / 2, + /*height=*/texture_size.height() / 2, + /*planeIndex=*/1u, + /*texture=*/&uv_metal_texture_raw); + + if (cv_return != kCVReturnSuccess) { + FML_DLOG(ERROR) << "Could not create Metal texture from pixel buffer: CVReturn " << cv_return; + return nullptr; + } + } + + fml::CFRef y_metal_texture(y_metal_texture_raw); + + GrMtlTextureInfo y_skia_texture_info; + y_skia_texture_info.fTexture = sk_cf_obj{ + [reinterpret_cast(CVMetalTextureGetTexture(y_metal_texture)) retain]}; - CVMetalTextureRef metal_texture_raw = NULL; + GrBackendTexture y_skia_backend_texture(/*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*mipMapped=*/GrMipMapped ::kNo, + /*textureInfo=*/y_skia_texture_info); + + fml::CFRef uv_metal_texture(uv_metal_texture_raw); + + GrMtlTextureInfo uv_skia_texture_info; + uv_skia_texture_info.fTexture = sk_cf_obj{ + [reinterpret_cast(CVMetalTextureGetTexture(uv_metal_texture)) retain]}; + + GrBackendTexture uv_skia_backend_texture(/*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*mipMapped=*/GrMipMapped ::kNo, + /*textureInfo=*/uv_skia_texture_info); + GrBackendTexture nv12TextureHandles[] = {y_skia_backend_texture, uv_skia_backend_texture}; + SkYUVAIndex yuvaIndices[4] = { + SkYUVAIndex{0, SkColorChannel::kR}, // Read Y data from the red channel of the first texture + SkYUVAIndex{1, SkColorChannel::kR}, // Read U data from the red channel of the second texture + SkYUVAIndex{1, + SkColorChannel::kG}, // Read V data from the green channel of the second texture + SkYUVAIndex{-1, SkColorChannel::kA}}; //-1 means to omit the alpha data of YUVA + + struct ImageCaptures { + fml::CFRef buffer; + fml::CFRef y_texture; + fml::CFRef uv_texture; + }; + + auto captures = std::make_unique(); + captures->buffer = std::move(pixel_buffer); + captures->y_texture = std::move(y_metal_texture); + captures->uv_texture = std::move(uv_metal_texture); + + SkImage::TextureReleaseProc release_proc = [](SkImage::ReleaseContext release_context) { + auto captures = reinterpret_cast(release_context); + delete captures; + }; + sk_sp image = SkImage::MakeFromYUVATextures( + context, kRec601_SkYUVColorSpace, nv12TextureHandles, yuvaIndices, texture_size, + kTopLeft_GrSurfaceOrigin, /*imageColorSpace=*/nullptr, release_proc, captures.release()); + return image; +} + +sk_sp IOSExternalTextureMetal::WrapRGBAExternalPixelBuffer( + fml::CFRef pixel_buffer, + GrDirectContext* context) const { + auto texture_size = + SkISize::Make(CVPixelBufferGetWidth(pixel_buffer), CVPixelBufferGetHeight(pixel_buffer)); + CVMetalTextureRef metal_texture_raw = nullptr; auto cv_return = - CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, // allocator - texture_cache_, // texture cache - pixel_buffer, // source image - NULL, // texture attributes - MTLPixelFormatBGRA8Unorm, // pixel format - texture_size.width(), // width - texture_size.height(), // height - 0u, // plane index - &metal_texture_raw // [out] texture - ); + CVMetalTextureCacheCreateTextureFromImage(/*allocator=*/kCFAllocatorDefault, + /*textureCache=*/texture_cache_, + /*sourceImage=*/pixel_buffer, + /*textureAttributes=*/nullptr, + /*pixelFormat=*/MTLPixelFormatBGRA8Unorm, + /*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*planeIndex=*/0u, + /*texture=*/&metal_texture_raw); if (cv_return != kCVReturnSuccess) { FML_DLOG(ERROR) << "Could not create Metal texture from pixel buffer: CVReturn " << cv_return; @@ -92,11 +204,10 @@ skia_texture_info.fTexture = sk_cf_obj{ [reinterpret_cast(CVMetalTextureGetTexture(metal_texture)) retain]}; - GrBackendTexture skia_backend_texture(texture_size.width(), // width - texture_size.height(), // height - GrMipMapped ::kNo, // mip-mapped - skia_texture_info // texture info - ); + GrBackendTexture skia_backend_texture(/*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*mipMapped=*/GrMipMapped ::kNo, + /*textureInfo=*/skia_texture_info); struct ImageCaptures { fml::CFRef buffer; @@ -112,21 +223,12 @@ GrBackendTexture skia_backend_texture(texture_size.width(), // width delete captures; }; - auto image = SkImage::MakeFromTexture(context, // context - skia_backend_texture, // backend texture - kTopLeft_GrSurfaceOrigin, // origin - kBGRA_8888_SkColorType, // color type - kPremul_SkAlphaType, // alpha type - nullptr, // color space - release_proc, // release proc - captures.release() // release context - - ); - - if (!image) { - FML_DLOG(ERROR) << "Could not wrap Metal texture as a Skia image."; - } + auto image = + SkImage::MakeFromTexture(context, skia_backend_texture, kTopLeft_GrSurfaceOrigin, + kBGRA_8888_SkColorType, kPremul_SkAlphaType, + /*imageColorSpace=*/nullptr, release_proc, captures.release() + ); return image; } diff --git a/shell/platform/darwin/ios/ios_surface.h b/shell/platform/darwin/ios/ios_surface.h index 041e3bacedb32..a01f9f15caeed 100644 --- a/shell/platform/darwin/ios/ios_surface.h +++ b/shell/platform/darwin/ios/ios_surface.h @@ -34,8 +34,6 @@ class IOSSurface : public ExternalViewEmbedder { std::shared_ptr GetContext() const; - ExternalViewEmbedder* GetExternalViewEmbedderIfEnabled(); - virtual bool IsValid() const = 0; virtual void UpdateStorageSizeIfNecessary() = 0; @@ -88,6 +86,9 @@ class IOSSurface : public ExternalViewEmbedder { void EndFrame(bool should_resubmit_frame, fml::RefPtr raster_thread_merger) override; + // |ExternalViewEmbedder| + bool SupportsDynamicThreadMerging() override; + public: FML_DISALLOW_COPY_AND_ASSIGN(IOSSurface); }; diff --git a/shell/platform/darwin/ios/ios_surface.mm b/shell/platform/darwin/ios/ios_surface.mm index de330fb58fc42..65fccb052e78b 100644 --- a/shell/platform/darwin/ios/ios_surface.mm +++ b/shell/platform/darwin/ios/ios_surface.mm @@ -13,15 +13,6 @@ namespace flutter { -// The name of the Info.plist flag to enable the embedded iOS views preview. -constexpr const char* kEmbeddedViewsPreview = "io.flutter.embedded_views_preview"; - -bool IsIosEmbeddedViewsPreviewEnabled() { - static bool preview_enabled = - [[[NSBundle mainBundle] objectForInfoDictionaryKey:@(kEmbeddedViewsPreview)] boolValue]; - return preview_enabled; -} - std::unique_ptr IOSSurface::Create( std::shared_ptr context, fml::scoped_nsobject layer, @@ -75,14 +66,6 @@ bool IsIosEmbeddedViewsPreviewEnabled() { return nullptr; } -ExternalViewEmbedder* IOSSurface::GetExternalViewEmbedderIfEnabled() { - if (IsIosEmbeddedViewsPreviewEnabled()) { - return this; - } else { - return nullptr; - } -} - // |ExternalViewEmbedder| void IOSSurface::CancelFrame() { TRACE_EVENT0("flutter", "IOSSurface::CancelFrame"); @@ -155,4 +138,9 @@ bool IsIosEmbeddedViewsPreviewEnabled() { return platform_views_controller_->EndFrame(should_resubmit_frame, raster_thread_merger); } +// |ExternalViewEmbedder| +bool IOSSurface::SupportsDynamicThreadMerging() { + return true; +} + } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_gl.h b/shell/platform/darwin/ios/ios_surface_gl.h index e6433eb83ef56..745b5d0070dc4 100644 --- a/shell/platform/darwin/ios/ios_surface_gl.h +++ b/shell/platform/darwin/ios/ios_surface_gl.h @@ -43,7 +43,7 @@ class IOSSurfaceGL final : public IOSSurface, public GPUSurfaceGLDelegate { bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| bool SurfaceSupportsReadback() const override; diff --git a/shell/platform/darwin/ios/ios_surface_gl.mm b/shell/platform/darwin/ios/ios_surface_gl.mm index 3531f61c3c0df..05d2d313566fe 100644 --- a/shell/platform/darwin/ios/ios_surface_gl.mm +++ b/shell/platform/darwin/ios/ios_surface_gl.mm @@ -44,7 +44,7 @@ } // |GPUSurfaceGLDelegate| -intptr_t IOSSurfaceGL::GLContextFBO() const { +intptr_t IOSSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { return IsValid() ? render_target_->GetFramebuffer() : GL_NONE; } @@ -84,7 +84,7 @@ // |GPUSurfaceGLDelegate| ExternalViewEmbedder* IOSSurfaceGL::GetExternalViewEmbedder() { - return GetExternalViewEmbedderIfEnabled(); + return this; } } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_metal.mm b/shell/platform/darwin/ios/ios_surface_metal.mm index 60afa6fd69775..df0d2739cfce7 100644 --- a/shell/platform/darwin/ios/ios_surface_metal.mm +++ b/shell/platform/darwin/ios/ios_surface_metal.mm @@ -55,7 +55,7 @@ // |GPUSurfaceDelegate| ExternalViewEmbedder* IOSSurfaceMetal::GetExternalViewEmbedder() { - return GetExternalViewEmbedderIfEnabled(); + return this; } } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_software.mm b/shell/platform/darwin/ios/ios_surface_software.mm index a68e0509f8ab4..03e85aec611f7 100644 --- a/shell/platform/darwin/ios/ios_surface_software.mm +++ b/shell/platform/darwin/ios/ios_surface_software.mm @@ -124,7 +124,7 @@ // |GPUSurfaceSoftwareDelegate| ExternalViewEmbedder* IOSSurfaceSoftware::GetExternalViewEmbedder() { - return GetExternalViewEmbedderIfEnabled(); + return this; } } // namespace flutter diff --git a/shell/platform/darwin/ios/platform_view_ios.mm b/shell/platform/darwin/ios/platform_view_ios.mm index c00ecb9c0bc05..e36a3bb7ce2a2 100644 --- a/shell/platform/darwin/ios/platform_view_ios.mm +++ b/shell/platform/darwin/ios/platform_view_ios.mm @@ -107,9 +107,8 @@ FML_DCHECK(ios_surface_ != nullptr); if (accessibility_bridge_) { - accessibility_bridge_.reset( - new AccessibilityBridge(static_cast(owner_controller_.get().view), this, - [owner_controller_.get() platformViewsController])); + accessibility_bridge_.reset(new AccessibilityBridge( + owner_controller_.get(), this, [owner_controller_.get() platformViewsController])); } } @@ -150,9 +149,8 @@ new AccessibilityBridge(static_cast(owner_controller_.get().view), return; } if (enabled && !accessibility_bridge_) { - accessibility_bridge_.reset( - new AccessibilityBridge(static_cast(owner_controller_.get().view), this, - [owner_controller_.get() platformViewsController])); + accessibility_bridge_.reset(new AccessibilityBridge( + owner_controller_.get(), this, [owner_controller_.get() platformViewsController])); } else if (!enabled && accessibility_bridge_) { accessibility_bridge_.reset(); } else { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 104733867b317..36b24f3424ddc 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -13,11 +13,30 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/embedder/embedder.h" +/** + * Constructs and returns a FlutterLocale struct corresponding to |locale|, which must outlive + * the returned struct. + */ +static FlutterLocale FlutterLocaleFromNSLocale(NSLocale* locale) { + FlutterLocale flutterLocale = {}; + flutterLocale.struct_size = sizeof(FlutterLocale); + flutterLocale.language_code = [[locale objectForKey:NSLocaleLanguageCode] UTF8String]; + flutterLocale.country_code = [[locale objectForKey:NSLocaleCountryCode] UTF8String]; + flutterLocale.script_code = [[locale objectForKey:NSLocaleScriptCode] UTF8String]; + flutterLocale.variant_code = [[locale objectForKey:NSLocaleVariantCode] UTF8String]; + return flutterLocale; +} + /** * Private interface declaration for FlutterEngine. */ @interface FlutterEngine () +/** + * Sends the list of user-preferred locales to the Flutter engine. + */ +- (void)sendUserLocales; + /** * Called by the engine to make the context the engine should draw into current. */ @@ -181,6 +200,12 @@ - (instancetype)initWithName:(NSString*)labelPrefix _textures = [[NSMutableDictionary alloc] init]; _allowHeadlessExecution = allowHeadlessExecution; + NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self + selector:@selector(sendUserLocales) + name:NSCurrentLocaleDidChangeNotification + object:nil]; + return self; } @@ -254,6 +279,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { return NO; } + [self sendUserLocales]; [self updateWindowMetrics]; return YES; } @@ -314,6 +340,29 @@ - (void)sendPointerEvent:(const FlutterPointerEvent&)event { #pragma mark - Private methods +- (void)sendUserLocales { + if (!self.running) { + return; + } + + // Create a list of FlutterLocales corresponding to the preferred languages. + NSMutableArray* locales = [NSMutableArray array]; + std::vector flutterLocales; + flutterLocales.reserve(locales.count); + for (NSString* localeID in [NSLocale preferredLanguages]) { + NSLocale* locale = [[NSLocale alloc] initWithLocaleIdentifier:localeID]; + [locales addObject:locale]; + flutterLocales.push_back(FlutterLocaleFromNSLocale(locale)); + } + // Convert to a list of pointers, and send to the engine. + std::vector flutterLocaleList; + flutterLocaleList.reserve(flutterLocales.size()); + std::transform( + flutterLocales.begin(), flutterLocales.end(), std::back_inserter(flutterLocaleList), + [](const auto& arg) -> const auto* { return &arg; }); + FlutterEngineUpdateLocales(_engine, flutterLocaleList.data(), flutterLocaleList.size()); +} + - (bool)engineCallbackOnMakeCurrent { if (!_mainOpenGLContext) { return false; @@ -329,6 +378,7 @@ - (bool)engineCallbackOnClearCurrent { - (bool)engineCallbackOnPresent { if (!_mainOpenGLContext) { + return false; } [_mainOpenGLContext flushBuffer]; return true; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index bb928da63a79e..b1a6055e28f70 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -303,6 +303,7 @@ - (BOOL)launchEngine { } // Send the initial user settings such as brightness and text scale factor // to the engine. + // TODO(stuartmorgan): Move this logic to FlutterEngine. [self sendInitialSettings]; return YES; } diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 6b3fa9ceaddbb..8a94ee5eb2dc9 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -93,8 +93,17 @@ static bool IsOpenGLRendererConfigValid(const FlutterRendererConfig* config) { if (SAFE_ACCESS(open_gl_config, make_current, nullptr) == nullptr || SAFE_ACCESS(open_gl_config, clear_current, nullptr) == nullptr || - SAFE_ACCESS(open_gl_config, present, nullptr) == nullptr || - SAFE_ACCESS(open_gl_config, fbo_callback, nullptr) == nullptr) { + SAFE_ACCESS(open_gl_config, present, nullptr) == nullptr) { + return false; + } + + bool fbo_callback_exists = + SAFE_ACCESS(open_gl_config, fbo_callback, nullptr) != nullptr; + bool fbo_with_frame_info_callback_exists = + SAFE_ACCESS(open_gl_config, fbo_with_frame_info_callback, nullptr) != + nullptr; + // only one of these callbacks must exist. + if (fbo_callback_exists == fbo_with_frame_info_callback_exists) { return false; } @@ -168,8 +177,20 @@ InferOpenGLPlatformViewCreationCallback( return ptr(user_data); }; - auto gl_fbo_callback = [ptr = config->open_gl.fbo_callback, - user_data]() -> intptr_t { return ptr(user_data); }; + auto gl_fbo_callback = + [fbo_callback = config->open_gl.fbo_callback, + fbo_with_frame_info_callback = + config->open_gl.fbo_with_frame_info_callback, + user_data](flutter::GLFrameInfo gl_frame_info) -> intptr_t { + if (fbo_callback) { + return fbo_callback(user_data); + } else { + FlutterFrameInfo frame_info = {}; + frame_info.struct_size = sizeof(FlutterFrameInfo); + frame_info.size = {gl_frame_info.width, gl_frame_info.height}; + return fbo_with_frame_info_callback(user_data, &frame_info); + } + }; const FlutterOpenGLRendererConfig* open_gl_config = &config->open_gl; std::function gl_make_resource_current_callback = nullptr; @@ -991,8 +1012,7 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, flutter::Shell::CreateCallback on_create_rasterizer = [](flutter::Shell& shell) { - return std::make_unique( - shell, shell.GetTaskRunners(), shell.GetIsGpuDisabledSyncSwitch()); + return std::make_unique(shell); }; // TODO(chinmaygarde): This is the wrong spot for this. It belongs in the diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 6277ccd4e80c8..9e468e484d312 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -9,6 +9,39 @@ #include #include +// This file defines an Application Binary Interface (ABI), which requires more +// stability than regular code to remain functional for exchanging messages +// between different versions of the embedding and the engine, to allow for both +// forward and backward compatibility. +// +// Specifically, +// - The order, type, and size of the struct members below must remain the same, +// and members should not be removed. +// - New structures that are part of the ABI must be defined with "size_t +// struct_size;" as their first member, which should be initialized using +// "sizeof(Type)". +// - Enum values must not change or be removed. +// - Enum members without explicit values must not be reordered. +// - Function signatures (names, argument counts, argument order, and argument +// type) cannot change. +// - The core behavior of existing functions cannot change. +// +// These changes are allowed: +// - Adding new struct members at the end of a structure. +// - Adding new enum members with a new value. +// - Renaming a struct member as long as its type, size, and intent remain the +// same. +// - Renaming an enum member as long as its value and intent remains the same. +// +// It is expected that struct members and implicitly-valued enums will not +// always be declared in an order that is optimal for the reader, since members +// will be added over time, and they can't be reordered. +// +// Existing functions should continue to appear from the caller's point of view +// to operate as they did when they were first introduced, so introduce a new +// function instead of modifying the core behavior of a function (and continue +// to support the existing function with the previous behavior). + #if defined(__cplusplus) extern "C" { #endif @@ -273,12 +306,69 @@ typedef bool (*TextureFrameCallback)(void* /* user data */, FlutterOpenGLTexture* /* texture out */); typedef void (*VsyncCallback)(void* /* user data */, intptr_t /* baton */); +/// A structure to represent the width and height. +typedef struct { + double width; + double height; +} FlutterSize; + +/// A structure to represent the width and height. +/// +/// See: \ref FlutterSize when the value are not integers. +typedef struct { + uint32_t width; + uint32_t height; +} FlutterUIntSize; + +/// A structure to represent a rectangle. +typedef struct { + double left; + double top; + double right; + double bottom; +} FlutterRect; + +/// A structure to represent a 2D point. +typedef struct { + double x; + double y; +} FlutterPoint; + +/// A structure to represent a rounded rectangle. +typedef struct { + FlutterRect rect; + FlutterSize upper_left_corner_radius; + FlutterSize upper_right_corner_radius; + FlutterSize lower_right_corner_radius; + FlutterSize lower_left_corner_radius; +} FlutterRoundedRect; + +/// This information is passed to the embedder when requesting a frame buffer +/// object. +/// +/// See: \ref FlutterSoftwareRendererConfig.fbo_with_frame_info_callback. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterFrameInfo). + size_t struct_size; + /// The size of the surface that will be backed by the fbo. + FlutterUIntSize size; +} FlutterFrameInfo; + +typedef uint32_t (*UIntFrameInfoCallback)( + void* /* user data */, + const FlutterFrameInfo* /* frame info */); + typedef struct { /// The size of this struct. Must be sizeof(FlutterOpenGLRendererConfig). size_t struct_size; BoolCallback make_current; BoolCallback clear_current; BoolCallback present; + /// Specifying one (and only one) of the `fbo_callback` or + /// `fbo_with_frame_info_callback` is required. Specifying both is an error + /// and engine intialization will be terminated. The return value indicates + /// the id of the frame buffer object that flutter will obtain the gl surface + /// from. UIntCallback fbo_callback; /// This is an optional callback. Flutter will ask the emebdder to create a GL /// context current on a background thread. If the embedder is able to do so, @@ -309,6 +399,14 @@ typedef struct { /// that external texture details can be supplied to the engine for subsequent /// composition. TextureFrameCallback gl_external_texture_frame_callback; + /// Specifying one (and only one) of the `fbo_callback` or + /// `fbo_with_frame_info_callback` is required. Specifying both is an error + /// and engine intialization will be terminated. The return value indicates + /// the id of the frame buffer object (fbo) that flutter will obtain the gl + /// surface from. When using this variant, the embedder is passed a + /// `FlutterFrameInfo` struct that indicates the properties of the surface + /// that flutter will acquire from the returned fbo. + UIntFrameInfoCallback fbo_with_frame_info_callback; } FlutterOpenGLRendererConfig; typedef struct { @@ -457,31 +555,6 @@ typedef void (*FlutterDataCallback)(const uint8_t* /* data */, size_t /* size */, void* /* user data */); -typedef struct { - double left; - double top; - double right; - double bottom; -} FlutterRect; - -typedef struct { - double x; - double y; -} FlutterPoint; - -typedef struct { - double width; - double height; -} FlutterSize; - -typedef struct { - FlutterRect rect; - FlutterSize upper_left_corner_radius; - FlutterSize upper_right_corner_radius; - FlutterSize lower_right_corner_radius; - FlutterSize lower_left_corner_radius; -} FlutterRoundedRect; - /// The identifier of the platform view. This identifier is specified by the /// application when a platform view is added to the scene via the /// `SceneBuilder.addPlatformView` call. diff --git a/shell/platform/embedder/embedder_engine.h b/shell/platform/embedder/embedder_engine.h index 124f8af0cf9aa..151b489bb9465 100644 --- a/shell/platform/embedder/embedder_engine.h +++ b/shell/platform/embedder/embedder_engine.h @@ -12,7 +12,6 @@ #include "flutter/shell/common/shell.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/platform/embedder/embedder.h" -#include "flutter/shell/platform/embedder/embedder_engine.h" #include "flutter/shell/platform/embedder/embedder_external_texture_gl.h" #include "flutter/shell/platform/embedder/embedder_thread_host.h" diff --git a/shell/platform/embedder/embedder_surface_gl.cc b/shell/platform/embedder/embedder_surface_gl.cc index 3deb7b5f3032c..4a28d68ecb295 100644 --- a/shell/platform/embedder/embedder_surface_gl.cc +++ b/shell/platform/embedder/embedder_surface_gl.cc @@ -50,8 +50,8 @@ bool EmbedderSurfaceGL::GLContextPresent() { } // |GPUSurfaceGLDelegate| -intptr_t EmbedderSurfaceGL::GLContextFBO() const { - return gl_dispatch_table_.gl_fbo_callback(); +intptr_t EmbedderSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { + return gl_dispatch_table_.gl_fbo_callback(frame_info); } // |GPUSurfaceGLDelegate| diff --git a/shell/platform/embedder/embedder_surface_gl.h b/shell/platform/embedder/embedder_surface_gl.h index 2a3a76748db17..6a4b8ed94703d 100644 --- a/shell/platform/embedder/embedder_surface_gl.h +++ b/shell/platform/embedder/embedder_surface_gl.h @@ -19,7 +19,7 @@ class EmbedderSurfaceGL final : public EmbedderSurface, std::function gl_make_current_callback; // required std::function gl_clear_current_callback; // required std::function gl_present_callback; // required - std::function gl_fbo_callback; // required + std::function gl_fbo_callback; // required std::function gl_make_resource_current_callback; // optional std::function gl_surface_transformation_callback; // optional @@ -59,7 +59,7 @@ class EmbedderSurfaceGL final : public EmbedderSurface, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| bool GLContextFBOResetAfterPresent() const override; diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index 1c08b98fde1f9..f69161e6e23cc 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -35,8 +35,10 @@ EmbedderConfigBuilder::EmbedderConfigBuilder( opengl_renderer_config_.present = [](void* context) -> bool { return reinterpret_cast(context)->GLPresent(); }; - opengl_renderer_config_.fbo_callback = [](void* context) -> uint32_t { - return reinterpret_cast(context)->GLGetFramebuffer(); + opengl_renderer_config_.fbo_with_frame_info_callback = + [](void* context, const FlutterFrameInfo* frame_info) -> uint32_t { + return reinterpret_cast(context)->GLGetFramebuffer( + *frame_info); }; opengl_renderer_config_.make_resource_current = [](void* context) -> bool { return reinterpret_cast(context) @@ -110,6 +112,21 @@ void EmbedderConfigBuilder::SetSoftwareRendererConfig(SkISize surface_size) { context_.SetupOpenGLSurface(surface_size); } +void EmbedderConfigBuilder::SetOpenGLFBOCallBack() { + // SetOpenGLRendererConfig must be called before this. + FML_CHECK(renderer_config_.type == FlutterRendererType::kOpenGL); + renderer_config_.open_gl.fbo_callback = [](void* context) -> uint32_t { + FlutterFrameInfo frame_info = {}; + // fbo_callback doesn't use the frame size information, only + // fbo_callback_with_frame_info does. + frame_info.struct_size = sizeof(FlutterFrameInfo); + frame_info.size.width = 0; + frame_info.size.height = 0; + return reinterpret_cast(context)->GLGetFramebuffer( + frame_info); + }; +} + void EmbedderConfigBuilder::SetOpenGLRendererConfig(SkISize surface_size) { renderer_config_.type = FlutterRendererType::kOpenGL; renderer_config_.open_gl = opengl_renderer_config_; diff --git a/shell/platform/embedder/tests/embedder_config_builder.h b/shell/platform/embedder/tests/embedder_config_builder.h index a386cd91d0c6d..e6f0016918776 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.h +++ b/shell/platform/embedder/tests/embedder_config_builder.h @@ -49,6 +49,12 @@ class EmbedderConfigBuilder { void SetOpenGLRendererConfig(SkISize surface_size); + // Used to explicitly set an `open_gl.fbo_callback`. Using this method will + // cause your test to fail since the ctor for this class sets + // `open_gl.fbo_callback_with_frame_info`. This method exists as a utility to + // explicitly test this behavior. + void SetOpenGLFBOCallBack(); + void SetAssetsPath(); void SetSnapshots(); diff --git a/shell/platform/embedder/tests/embedder_test_context.cc b/shell/platform/embedder/tests/embedder_test_context.cc index be7f7641cc586..048d5d9c14111 100644 --- a/shell/platform/embedder/tests/embedder_test_context.cc +++ b/shell/platform/embedder/tests/embedder_test_context.cc @@ -197,11 +197,16 @@ bool EmbedderTestContext::GLPresent() { return true; } -uint32_t EmbedderTestContext::GLGetFramebuffer() { +uint32_t EmbedderTestContext::GLGetFramebuffer(FlutterFrameInfo frame_info) { FML_CHECK(gl_surface_) << "GL surface must be initialized."; + gl_surface_fbo_frame_infos_.push_back(frame_info); return gl_surface_->GetFramebuffer(); } +std::vector EmbedderTestContext::GetGLFBOFrameInfos() { + return gl_surface_fbo_frame_infos_; +} + bool EmbedderTestContext::GLMakeResourceCurrent() { FML_CHECK(gl_surface_) << "GL surface must be initialized."; return gl_surface_->MakeResourceCurrent(); diff --git a/shell/platform/embedder/tests/embedder_test_context.h b/shell/platform/embedder/tests/embedder_test_context.h index 56a44f6b5efe6..f4135c07d7f7c 100644 --- a/shell/platform/embedder/tests/embedder_test_context.h +++ b/shell/platform/embedder/tests/embedder_test_context.h @@ -79,6 +79,9 @@ class EmbedderTestContext { size_t GetSoftwareSurfacePresentCount() const; + // Returns the frame information for all the frames that were rendered. + std::vector GetGLFBOFrameInfos(); + private: // This allows the builder to access the hooks. friend class EmbedderConfigBuilder; @@ -101,6 +104,7 @@ class EmbedderTestContext { std::unique_ptr compositor_; NextSceneCallback next_scene_callback_; SkMatrix root_surface_transformation_; + std::vector gl_surface_fbo_frame_infos_; size_t gl_surface_present_count_ = 0; size_t software_surface_present_count_ = 0; @@ -133,7 +137,7 @@ class EmbedderTestContext { bool GLPresent(); - uint32_t GLGetFramebuffer(); + uint32_t GLGetFramebuffer(FlutterFrameInfo frame_info); bool GLMakeResourceCurrent(); diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index 558669ddb550c..f235613698f6a 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -4352,5 +4352,60 @@ TEST_F(EmbedderTest, CanLaunchAndShutdownWithAValidElfSource) { engine.reset(); } +TEST_F(EmbedderTest, FrameInfoContainsValidWidthAndHeight) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 1024)); + builder.SetDartEntrypoint("push_frames_over_and_over"); + + const auto root_surface_transformation = + SkMatrix().preTranslate(0, 1024).preRotate(-90, 0, 0); + + context.SetRootSurfaceTransformation(root_surface_transformation); + + auto engine = builder.LaunchEngine(); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 1024; + event.height = 600; + event.pixel_ratio = 1.0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + ASSERT_TRUE(engine.is_valid()); + + constexpr size_t frames_expected = 10; + fml::CountDownLatch frame_latch(frames_expected); + size_t frames_seen = 0; + context.AddNativeCallback("SignalNativeTest", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + frames_seen++; + frame_latch.CountDown(); + })); + frame_latch.Wait(); + + ASSERT_EQ(frames_expected, frames_seen); + ASSERT_EQ(context.GetGLFBOFrameInfos().size(), frames_seen); + + for (FlutterFrameInfo frame_info : context.GetGLFBOFrameInfos()) { + // width and height are rotated by 90 deg + ASSERT_EQ(frame_info.size.width, event.height); + ASSERT_EQ(frame_info.size.height, event.width); + } +} + +TEST_F(EmbedderTest, MustNotRunWithBothFBOCallbacksSet) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 1024)); + builder.SetOpenGLFBOCallBack(); + + auto engine = builder.LaunchEngine(); + ASSERT_FALSE(engine.is_valid()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart b/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart index 1b599e793f9e8..e2619c5c39e2a 100644 --- a/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart +++ b/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.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. -// @dart = 2.6 library fuchsia; import 'dart:io'; @@ -14,25 +13,25 @@ import 'dart:zircon'; // TODO: refactors this incomingServices instead @pragma('vm:entry-point') -Handle _environment; +Handle? _environment; @pragma('vm:entry-point') -Handle _outgoingServices; +Handle? _outgoingServices; @pragma('vm:entry-point') -Handle _viewRef; +Handle? _viewRef; class MxStartupInfo { // TODO: refactor Handle to a Channel // https://github.com/flutter/flutter/issues/49439 static Handle takeEnvironment() { - if (_outgoingServices == null && Platform.isFuchsia) { + if (_environment == null && Platform.isFuchsia) { throw Exception( 'Attempting to call takeEnvironment more than once per process'); } - Handle handle = _environment; + final handle = _environment; _environment = null; - return handle; + return handle!; } // TODO: refactor Handle to a Channel @@ -42,9 +41,9 @@ class MxStartupInfo { throw Exception( 'Attempting to call takeOutgoingServices more than once per process'); } - Handle handle = _outgoingServices; + final handle = _outgoingServices; _outgoingServices = null; - return handle; + return handle!; } // TODO: refactor Handle to a ViewRef @@ -54,9 +53,9 @@ class MxStartupInfo { throw Exception( 'Attempting to call takeViewRef more than once per process'); } - Handle handle = _viewRef; + final handle = _viewRef; _viewRef = null; - return handle; + return handle!; } } diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.dart index 2e78559bc5de2..da74c9bf658e7 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.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. -// @dart = 2.6 part of zircon; // ignore_for_file: native_function_body_in_non_sdk_code diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.dart index 169fa41efdca2..e233f42ca4f7b 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.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. -// @dart = 2.6 part of zircon; // ignore_for_file: native_function_body_in_non_sdk_code diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.dart index 69eba1377d1a1..425e321cff87f 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.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. -// @dart = 2.6 part of zircon; // ignore_for_file: native_function_body_in_non_sdk_code @@ -16,12 +15,12 @@ class _Namespace { // ignore: unused_element // Library private variable set by the embedder used to cache the // namespace (as an fdio_ns_t*). @pragma('vm:entry-point') - static int _namespace; // ignore: unused_field + static int? _namespace; // ignore: unused_field } /// An exception representing an error returned as an zx_status_t. class ZxStatusException implements Exception { - final String message; + final String? message; final int status; ZxStatusException(this.status, [this.message]); @@ -35,6 +34,9 @@ class ZxStatusException implements Exception { } } +/// Users of the [_Result] subclasses should check the status before +/// trying to read any data. Attempting to use a value stored in a result +/// when the status in not OK will result in an exception. class _Result { final int status; const _Result(this.status); @@ -42,78 +44,107 @@ class _Result { @pragma('vm:entry-point') class HandleResult extends _Result { - final Handle handle; + final Handle? _handle; + Handle get handle => _handle!; + @pragma('vm:entry-point') - const HandleResult(final int status, [this.handle]) : super(status); + const HandleResult(final int status, [this._handle]) : super(status); @override - String toString() => 'HandleResult(status=$status, handle=$handle)'; + String toString() => 'HandleResult(status=$status, handle=$_handle)'; } @pragma('vm:entry-point') class HandlePairResult extends _Result { - final Handle first; - final Handle second; + final Handle? _first; + final Handle? _second; + + Handle get first => _first!; + Handle get second => _second!; + @pragma('vm:entry-point') - const HandlePairResult(final int status, [this.first, this.second]) + const HandlePairResult(final int status, [this._first, this._second]) : super(status); @override String toString() => - 'HandlePairResult(status=$status, first=$first, second=$second)'; + 'HandlePairResult(status=$status, first=$_first, second=$_second)'; } @pragma('vm:entry-point') class ReadResult extends _Result { - final ByteData bytes; - final int numBytes; - final List handles; + final ByteData? _bytes; + final int? _numBytes; + final List? _handles; + + ByteData get bytes => _bytes!; + int get numBytes => _numBytes!; + List get handles => _handles!; + @pragma('vm:entry-point') - const ReadResult(final int status, [this.bytes, this.numBytes, this.handles]) + const ReadResult(final int status, [this._bytes, this._numBytes, this._handles]) : super(status); - Uint8List bytesAsUint8List() => - bytes.buffer.asUint8List(bytes.offsetInBytes, numBytes); + + /// Returns the bytes as a Uint8List. If status != OK this will throw + /// an exception. + Uint8List bytesAsUint8List() { + return _bytes!.buffer.asUint8List(_bytes!.offsetInBytes, _numBytes!); + } + + /// Returns the bytes as a String. If status != OK this will throw + /// an exception. String bytesAsUTF8String() => utf8.decode(bytesAsUint8List()); + @override String toString() => - 'ReadResult(status=$status, bytes=$bytes, numBytes=$numBytes, handles=$handles)'; + 'ReadResult(status=$status, bytes=$_bytes, numBytes=$_numBytes, handles=$_handles)'; } @pragma('vm:entry-point') class WriteResult extends _Result { - final int numBytes; + final int? _numBytes; + int get numBytes => _numBytes!; + @pragma('vm:entry-point') - const WriteResult(final int status, [this.numBytes]) : super(status); + const WriteResult(final int status, [this._numBytes]) : super(status); @override - String toString() => 'WriteResult(status=$status, numBytes=$numBytes)'; + String toString() => 'WriteResult(status=$status, numBytes=$_numBytes)'; } @pragma('vm:entry-point') class GetSizeResult extends _Result { - final int size; + final int? _size; + int get size => _size!; + @pragma('vm:entry-point') - const GetSizeResult(final int status, [this.size]) : super(status); + const GetSizeResult(final int status, [this._size]) : super(status); @override - String toString() => 'GetSizeResult(status=$status, size=$size)'; + String toString() => 'GetSizeResult(status=$status, size=$_size)'; } @pragma('vm:entry-point') class FromFileResult extends _Result { - final Handle handle; - final int numBytes; + final Handle? _handle; + final int? _numBytes; + + Handle get handle => _handle!; + int get numBytes => _numBytes!; + @pragma('vm:entry-point') - const FromFileResult(final int status, [this.handle, this.numBytes]) + const FromFileResult(final int status, [this._handle, this._numBytes]) : super(status); @override String toString() => - 'FromFileResult(status=$status, handle=$handle, numBytes=$numBytes)'; + 'FromFileResult(status=$status, handle=$_handle, numBytes=$_numBytes)'; } @pragma('vm:entry-point') class MapResult extends _Result { - final Uint8List data; + final Uint8List? _data; + Uint8List get data => _data!; + @pragma('vm:entry-point') - const MapResult(final int status, [this.data]) : super(status); + const MapResult(final int status, [this._data]) : super(status); @override - String toString() => 'MapResult(status=$status, data=$data)'; + String toString() => 'MapResult(status=$status, data=$_data)'; } @pragma('vm:entry-point') diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.dart index 691e59e7f89fd..5e80671ffb526 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.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. -// @dart = 2.6 library zircon; import 'dart:convert' show utf8; diff --git a/shell/platform/fuchsia/dart_runner/BUILD.gn b/shell/platform/fuchsia/dart_runner/BUILD.gn index c69df5c1222b6..4e3f514608a3a 100644 --- a/shell/platform/fuchsia/dart_runner/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/BUILD.gn @@ -18,6 +18,9 @@ template("runner") { invoker_output_name = invoker.output_name extra_defines = invoker.extra_defines extra_deps = invoker.extra_deps + if (is_debug) { + extra_defines += [ "DEBUG" ] # Needed due to direct dart dependencies. + } executable(target_name) { output_name = invoker_output_name diff --git a/shell/platform/fuchsia/dart_runner/embedder/builtin.dart b/shell/platform/fuchsia/dart_runner/embedder/builtin.dart index 937a5f21339b2..7972b08e0c932 100644 --- a/shell/platform/fuchsia/dart_runner/embedder/builtin.dart +++ b/shell/platform/fuchsia/dart_runner/embedder/builtin.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. -// @dart = 2.6 library fuchsia_builtin; import 'dart:async'; @@ -25,7 +24,7 @@ class _Logger { } @pragma('vm:entry-point') -String _rawScript; +late String _rawScript; Uri _scriptUri() { if (_rawScript.startsWith('http:') || diff --git a/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn b/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn index faca6e63aab06..7bab46d61592f 100644 --- a/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn @@ -21,7 +21,7 @@ compile_platform("kernel_platform_files") { args = [ "--enable-experiment=non-nullable", - "--nnbd-weak", + "--nnbd-agnostic", # TODO(dartbug.com/36342): enable bytecode for core libraries when performance of bytecode # pipeline is on par with default pipeline and continuously tracked. diff --git a/shell/platform/fuchsia/flutter/BUILD.gn b/shell/platform/fuchsia/flutter/BUILD.gn index ba68d1bac6905..a5da4925a2eff 100644 --- a/shell/platform/fuchsia/flutter/BUILD.gn +++ b/shell/platform/fuchsia/flutter/BUILD.gn @@ -12,29 +12,206 @@ import("//flutter/tools/fuchsia/dart.gni") import("//flutter/tools/fuchsia/fuchsia_archive.gni") import("//flutter/tools/fuchsia/fuchsia_libs.gni") import("//flutter/vulkan/config.gni") -import("engine_flutter_runner.gni") # Fuchsia uses its own custom Surface implementation. -shell_gpu_configuration("fuchsia_legacy_gpu_configuration") { +shell_gpu_configuration("fuchsia_gpu_configuration") { enable_software = false enable_gl = false enable_vulkan = false enable_metal = false } +config("runner_debug_config") { + defines = [ "DEBUG" ] # Needed due to direct dart dependencies. +} + +config("runner_flutter_profile_config") { + defines = [ "FLUTTER_PROFILE" ] +} + +config("runner_product_config") { + defines = [ "DART_PRODUCT" ] +} + +template("runner_sources") { + assert(defined(invoker.product), "runner_sources must define product") + + runner_configs = [] + if (is_debug) { + runner_configs += [ ":runner_debug_config" ] + } + if (flutter_runtime_mode == "profile") { + runner_configs += [ ":runner_flutter_profile_config" ] + } + if (invoker.product) { + runner_configs += [ ":runner_product_config" ] + } + + source_set(target_name) { + sources = [ + "accessibility_bridge.cc", + "accessibility_bridge.h", + "component.cc", + "component.h", + "compositor_context.cc", + "compositor_context.h", + "engine.cc", + "engine.h", + "flutter_runner_product_configuration.cc", + "flutter_runner_product_configuration.h", + "fuchsia_intl.cc", + "fuchsia_intl.h", + "isolate_configurator.cc", + "isolate_configurator.h", + "logging.h", + "loop.cc", + "loop.h", + "platform_view.cc", + "platform_view.h", + "runner.cc", + "runner.h", + "session_connection.cc", + "session_connection.h", + "surface.cc", + "surface.h", + "task_observers.cc", + "task_observers.h", + "task_runner_adapter.cc", + "task_runner_adapter.h", + "thread.cc", + "thread.h", + "unique_fdio_ns.h", + "vsync_recorder.cc", + "vsync_recorder.h", + "vsync_waiter.cc", + "vsync_waiter.h", + "vulkan_surface.cc", + "vulkan_surface.h", + "vulkan_surface_pool.cc", + "vulkan_surface_pool.h", + "vulkan_surface_producer.cc", + "vulkan_surface_producer.h", + ] + + public_configs = runner_configs + + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_public_deps = [ + "//flutter/flow", + "//flutter/lib/ui", + "//flutter/runtime", + "//flutter/shell/common", + ] + flutter_deps = [ + ":fuchsia_gpu_configuration", + "//flutter/assets", + "//flutter/common", + "//flutter/fml", + "//flutter/vulkan", + ] + + public_deps = [ + "$fuchsia_sdk_root/pkg:scenic_cpp", + "$fuchsia_sdk_root/pkg:sys_cpp", + "//flutter/shell/platform/fuchsia/runtime/dart/utils", + ] + flutter_public_deps + + deps = [ + "$fuchsia_sdk_root/fidl:fuchsia.accessibility.semantics", + "$fuchsia_sdk_root/fidl:fuchsia.fonts", + "$fuchsia_sdk_root/fidl:fuchsia.images", + "$fuchsia_sdk_root/fidl:fuchsia.intl", + "$fuchsia_sdk_root/fidl:fuchsia.io", + "$fuchsia_sdk_root/fidl:fuchsia.sys", + "$fuchsia_sdk_root/fidl:fuchsia.ui.app", + "$fuchsia_sdk_root/fidl:fuchsia.ui.scenic", + "$fuchsia_sdk_root/pkg:async-cpp", + "$fuchsia_sdk_root/pkg:async-default", + "$fuchsia_sdk_root/pkg:async-loop", + "$fuchsia_sdk_root/pkg:async-loop-cpp", + "$fuchsia_sdk_root/pkg:fdio", + "$fuchsia_sdk_root/pkg:fidl_cpp", + "$fuchsia_sdk_root/pkg:syslog", + "$fuchsia_sdk_root/pkg:trace", + "$fuchsia_sdk_root/pkg:trace-engine", + "$fuchsia_sdk_root/pkg:trace-provider-so", + "$fuchsia_sdk_root/pkg:vfs_cpp", + "$fuchsia_sdk_root/pkg:zx", + "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", + "//flutter/shell/platform/fuchsia/dart-pkg/zircon", + ] + flutter_deps + } +} + +runner_sources("flutter_runner_sources") { + product = false +} + +runner_sources("flutter_runner_sources_product") { + product = true +} + # Things that explicitly being excluded: # 1. Kernel snapshot framework mode. # 2. Profiler symbols. +# Builds a flutter_runner +# +# Parameters: +# +# output_name (required): +# The name of the resulting binary. +# +# extra_deps (required): +# Any additional dependencies. +# +# product (required): +# Whether to link against a Product mode Dart VM. +# +# extra_defines (optional): +# Any additional preprocessor defines. +template("flutter_runner") { + assert(defined(invoker.output_name), "flutter_runner must define output_name") + assert(defined(invoker.extra_deps), "flutter_runner must define extra_deps") + assert(defined(invoker.product), "flutter_runner must define product") + + invoker_output_name = invoker.output_name + extra_deps = invoker.extra_deps + + product_suffix = "" + if (invoker.product) { + product_suffix = "_product" + } + + executable(target_name) { + output_name = invoker_output_name + + sources = [ "main.cc" ] + + deps = [ + ":flutter_runner_sources${product_suffix}", + "$fuchsia_sdk_root/pkg:async-loop-cpp", + "$fuchsia_sdk_root/pkg:trace", + "$fuchsia_sdk_root/pkg:trace-provider-so", + ] + extra_deps + + # The flags below are needed so that Dart's CPU profiler can walk the + # C++ stack. + cflags = [ "-fno-omit-frame-pointer" ] + + if (!invoker.product) { + # This flag is needed so that the call to dladdr() in Dart's native symbol + # resolver can report good symbol information for the CPU profiler. + ldflags = [ "-rdynamic" ] + } + } +} + flutter_runner("jit") { output_name = "flutter_jit_runner" product = false - extra_defines = [] - if (flutter_runtime_mode == "profile") { - extra_defines += [ "FLUTTER_PROFILE" ] - } - extra_deps = [ "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", @@ -45,8 +222,6 @@ flutter_runner("jit_product") { output_name = "flutter_jit_product_runner" product = true - extra_defines = [ "DART_PRODUCT" ] - extra_deps = [ "//third_party/dart/runtime:libdart_jit_product", "//third_party/dart/runtime/platform:libdart_platform_jit_product", @@ -57,11 +232,6 @@ flutter_runner("aot") { output_name = "flutter_aot_runner" product = false - extra_defines = [] - if (flutter_runtime_mode == "profile") { - extra_defines += [ "FLUTTER_PROFILE" ] - } - extra_deps = [ "//third_party/dart/runtime:libdart_precompiled_runtime", "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime", @@ -72,8 +242,6 @@ flutter_runner("aot_product") { output_name = "flutter_aot_product_runner" product = true - extra_defines = [ "DART_PRODUCT" ] - extra_deps = [ "//third_party/dart/runtime:libdart_precompiled_runtime_product", "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime_product", @@ -267,66 +435,36 @@ executable("flutter_runner_unittests") { output_name = "flutter_runner_tests" sources = [ - "accessibility_bridge.cc", - "accessibility_bridge.h", "accessibility_bridge_unittest.cc", - "component.cc", - "component.h", "component_unittest.cc", "flutter_runner_fakes.h", - "flutter_runner_product_configuration.cc", - "flutter_runner_product_configuration.h", - "fuchsia_intl.cc", - "fuchsia_intl.h", "fuchsia_intl_unittest.cc", - "logging.h", - "loop.cc", - "loop.h", - "platform_view.cc", - "platform_view.h", "platform_view_unittest.cc", - "runner.cc", - "runner.h", "runner_unittest.cc", - "surface.cc", - "surface.h", - "task_observers.cc", - "task_observers.h", - "task_runner_adapter.cc", - "task_runner_adapter.h", "tests/flutter_runner_product_configuration_unittests.cc", "tests/vsync_recorder_unittests.cc", - "thread.cc", - "thread.h", - "vsync_recorder.cc", - "vsync_recorder.h", - "vsync_waiter.cc", - "vsync_waiter.h", "vsync_waiter_unittests.cc", ] # This is needed for //third_party/googletest for linking zircon symbols. libs = [ "//fuchsia/sdk/$host_os/arch/$target_cpu/sysroot/lib/libzircon.so" ] - deps = [ - ":aot", - ":flutter_runner_fixtures", - "//build/fuchsia/fidl:fuchsia.accessibility.semantics", - "//build/fuchsia/pkg:async-default", - "//build/fuchsia/pkg:async-loop-cpp", - "//build/fuchsia/pkg:async-loop-default", - "//build/fuchsia/pkg:scenic_cpp", - "//build/fuchsia/pkg:sys_cpp_testing", - "//flutter/common", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/shell/platform/fuchsia/runtime/dart/utils", - "//flutter/testing", + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/flow", + "//flutter/lib/ui", + "//flutter/shell/common", "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", ] + + deps = [ + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "//build/fuchsia/pkg:sys_cpp_testing", + "//flutter/testing", + ] + flutter_deps } executable("flutter_runner_tzdata_unittests") { @@ -334,34 +472,24 @@ executable("flutter_runner_tzdata_unittests") { output_name = "flutter_runner_tzdata_tests" - sources = [ - "runner.cc", - "runner.h", - "runner_tzdata_unittest.cc", - ] + sources = [ "runner_tzdata_unittest.cc" ] # This is needed for //third_party/googletest for linking zircon symbols. libs = [ "//fuchsia/sdk/$host_os/arch/$target_cpu/sysroot/lib/libzircon.so" ] - deps = [ - ":aot", - ":flutter_runner_fixtures", - "//build/fuchsia/fidl:fuchsia.accessibility.semantics", - "//build/fuchsia/pkg:async-loop-cpp", - "//build/fuchsia/pkg:async-loop-default", - "//build/fuchsia/pkg:scenic_cpp", - "//build/fuchsia/pkg:sys_cpp_testing", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/shell/platform/fuchsia/runtime/dart/utils", - "//flutter/testing", + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/lib/ui", "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", - "//third_party/icu", - "//third_party/skia", ] + + deps = [ + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "//flutter/testing", + ] + flutter_deps } executable("flutter_runner_scenic_unittests") { @@ -369,84 +497,27 @@ executable("flutter_runner_scenic_unittests") { output_name = "flutter_runner_scenic_tests" - sources = [ - "component.cc", - "component.h", - "compositor_context.cc", - "compositor_context.h", - "engine.cc", - "engine.h", - "fuchsia_intl.cc", - "fuchsia_intl.h", - "isolate_configurator.cc", - "isolate_configurator.h", - "logging.h", - "loop.cc", - "loop.h", - "platform_view.cc", - "platform_view.h", - "runner.cc", - "runner.h", - "session_connection.cc", - "session_connection.h", - "surface.cc", - "surface.h", - "task_observers.cc", - "task_observers.h", - "task_runner_adapter.cc", - "task_runner_adapter.h", - "tests/session_connection_unittests.cc", - "thread.cc", - "thread.h", - "unique_fdio_ns.h", - "vsync_recorder.cc", - "vsync_recorder.h", - "vsync_waiter.cc", - "vsync_waiter.h", - "vsync_waiter_unittests.cc", - "vulkan_surface.cc", - "vulkan_surface.h", - "vulkan_surface_pool.cc", - "vulkan_surface_pool.h", - "vulkan_surface_producer.cc", - "vulkan_surface_producer.h", - ] + sources = [ "tests/session_connection_unittests.cc" ] # This is needed for //third_party/googletest for linking zircon symbols. libs = [ "//fuchsia/sdk/$host_os/arch/$target_cpu/sysroot/lib/libzircon.so" ] - deps = [ - ":flutter_runner_fixtures", - ":jit", - "$fuchsia_sdk_root/fidl:fuchsia.ui.policy", - "$fuchsia_sdk_root/pkg:trace-provider-so", - "//build/fuchsia/fidl:fuchsia.accessibility.semantics", - "//build/fuchsia/pkg:async-default", - "//build/fuchsia/pkg:async-loop-cpp", - "//build/fuchsia/pkg:async-loop-default", - "//build/fuchsia/pkg:scenic_cpp", - "//build/fuchsia/pkg:sys_cpp_testing", - "//flutter/common", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", - "//flutter/shell/platform/fuchsia/dart-pkg/zircon", - "//flutter/shell/platform/fuchsia/runtime/dart/utils", - "//flutter/testing", - "//flutter/vulkan", + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/lib/ui", "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", - "//third_party/icu", - "//third_party/skia", ] - public_deps = [ "//third_party/googletest:gtest" ] + deps = [ + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "$fuchsia_sdk_root/fidl:fuchsia.ui.policy", + "//flutter/testing", + ] + flutter_deps } -# When adding a new dep here, please also ensure the dep is added to -# testing/fuchsia/run_tests.sh and testing/fuchsia/test_fars fuchsia_archive("flutter_runner_tests") { testonly = true @@ -570,30 +641,6 @@ fuchsia_test_archive("flow_tests") { ] } -fuchsia_test_archive("flow_tests_next") { - deps = [ "//flutter/flow:flow_unittests_next" ] - - binary = "flow_unittests_next" - - resources = [ - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_60fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_60fps.png" - }, - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_90fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_90fps.png" - }, - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_120fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_120fps.png" - }, - ] -} - fuchsia_test_archive("runtime_tests") { deps = [ "//flutter/runtime:runtime_fixtures", @@ -613,25 +660,6 @@ fuchsia_test_archive("runtime_tests") { ] } -fuchsia_test_archive("runtime_tests_next") { - deps = [ - "//flutter/runtime:runtime_fixtures", - "//flutter/runtime:runtime_unittests_next", - ] - - binary = "runtime_unittests_next" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/runtime/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - ] -} - fuchsia_test_archive("shell_tests") { deps = [ "//flutter/shell/common:shell_unittests", @@ -659,33 +687,6 @@ fuchsia_test_archive("shell_tests") { resources += vulkan_icds } -fuchsia_test_archive("shell_tests_next") { - deps = [ - "//flutter/shell/common:shell_unittests_fixtures", - "//flutter/shell/common:shell_unittests_next", - ] - - binary = "shell_unittests_next" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/shell/common/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - { - path = - "$root_gen_dir/flutter/shell/common/assets/shelltest_screenshot.png" - dest = "assets/shelltest_screenshot.png" - }, - ] - - libraries = vulkan_validation_libs - resources += vulkan_icds -} - fuchsia_test_archive("testing_tests") { deps = [ "//flutter/testing:testing_unittests" ] @@ -751,65 +752,21 @@ fuchsia_test_archive("ui_tests") { resources += vulkan_icds } -fuchsia_test_archive("ui_tests_next") { - deps = [ - "//flutter/lib/ui:ui_unittests_fixtures", - "//flutter/lib/ui:ui_unittests_next", - ] - - binary = "ui_unittests_next" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/lib/ui/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/DashInNooglerHat.jpg" - dest = "assets/DashInNooglerHat.jpg" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.jpg" - dest = "assets/Horizontal.jpg" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.png" - dest = "assets/Horizontal.png" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.gif" - dest = "assets/hello_loop_2.gif" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.webp" - dest = "assets/hello_loop_2.webp" - }, - ] - - libraries = vulkan_validation_libs - resources += vulkan_icds -} - +# When adding a new dep here, please also ensure the dep is added to +# testing/fuchsia/run_tests.sh and testing/fuchsia/test_fars group("tests") { testonly = true deps = [ ":flow_tests", - ":flow_tests_next", ":flutter_runner_scenic_tests", ":flutter_runner_tests", ":flutter_runner_tzdata_tests", ":fml_tests", ":runtime_tests", - ":runtime_tests_next", ":shell_tests", - ":shell_tests_next", ":testing_tests", ":txt_tests", ":ui_tests", - ":ui_tests_next", ] } diff --git a/shell/platform/fuchsia/flutter/component.cc b/shell/platform/fuchsia/flutter/component.cc index 00cd9d318ae83..0106931820887 100644 --- a/shell/platform/fuchsia/flutter/component.cc +++ b/shell/platform/fuchsia/flutter/component.cc @@ -365,6 +365,12 @@ Application::Application( // Controls whether category "skia" trace events are enabled. settings_.trace_skia = true; + settings_.verbose_logging = true; + + settings_.advisory_script_uri = debug_label_; + + settings_.advisory_script_entrypoint = debug_label_; + settings_.icu_data_path = ""; settings_.assets_dir = application_assets_directory_.get(); diff --git a/shell/platform/fuchsia/flutter/compositor_context.cc b/shell/platform/fuchsia/flutter/compositor_context.cc index b0bbfc7ecbc28..6911ed8ddc2d5 100644 --- a/shell/platform/fuchsia/flutter/compositor_context.cc +++ b/shell/platform/fuchsia/flutter/compositor_context.cc @@ -4,6 +4,8 @@ #include "compositor_context.h" +#include + #include "flutter/flow/layers/layer_tree.h" #include "third_party/skia/include/gpu/GrDirectContext.h" @@ -11,30 +13,38 @@ namespace flutter_runner { class ScopedFrame final : public flutter::CompositorContext::ScopedFrame { public: - ScopedFrame(flutter::CompositorContext& context, - const SkMatrix& root_surface_transformation, + ScopedFrame(CompositorContext& context, + GrContext* gr_context, + SkCanvas* canvas, flutter::ExternalViewEmbedder* view_embedder, + const SkMatrix& root_surface_transformation, bool instrumentation_enabled, - SessionConnection& session_connection) - : flutter::CompositorContext::ScopedFrame( - context, - session_connection.vulkan_surface_producer()->gr_context(), - nullptr, - view_embedder, - root_surface_transformation, - instrumentation_enabled, - true, - nullptr), - session_connection_(session_connection) {} + bool surface_supports_readback, + fml::RefPtr raster_thread_merger, + SessionConnection& session_connection, + VulkanSurfaceProducer& surface_producer, + flutter::SceneUpdateContext& scene_update_context) + : flutter::CompositorContext::ScopedFrame(context, + surface_producer.gr_context(), + canvas, + view_embedder, + root_surface_transformation, + instrumentation_enabled, + surface_supports_readback, + raster_thread_merger), + session_connection_(session_connection), + surface_producer_(surface_producer), + scene_update_context_(scene_update_context) {} private: SessionConnection& session_connection_; + VulkanSurfaceProducer& surface_producer_; + flutter::SceneUpdateContext& scene_update_context_; flutter::RasterStatus Raster(flutter::LayerTree& layer_tree, bool ignore_raster_cache) override { - if (!session_connection_.has_metrics()) { - return flutter::RasterStatus::kSuccess; - } + std::vector frame_paint_tasks; + std::vector> frame_surfaces; { // Preroll the Flutter layer tree. This allows Flutter to perform @@ -47,15 +57,80 @@ class ScopedFrame final : public flutter::CompositorContext::ScopedFrame { // Traverse the Flutter layer tree so that the necessary session ops to // represent the frame are enqueued in the underlying session. TRACE_EVENT0("flutter", "UpdateScene"); - layer_tree.UpdateScene(session_connection_.scene_update_context(), - session_connection_.root_node()); + layer_tree.UpdateScene(scene_update_context_); } { - // Flush all pending session ops. + // Flush all pending session ops: create surfaces and enqueue session + // Image ops for the frame's paint tasks, then Present. TRACE_EVENT0("flutter", "SessionPresent"); + frame_paint_tasks = scene_update_context_.GetPaintTasks(); + for (auto& task : frame_paint_tasks) { + SkISize physical_size = + SkISize::Make(layer_tree.device_pixel_ratio() * task.scale_x * + task.paint_bounds.width(), + layer_tree.device_pixel_ratio() * task.scale_y * + task.paint_bounds.height()); + if (physical_size.width() == 0 || physical_size.height() == 0) { + frame_surfaces.emplace_back(nullptr); + continue; + } + + std::unique_ptr surface = + surface_producer_.ProduceSurface(physical_size); + if (!surface) { + FML_LOG(ERROR) + << "Could not acquire a surface from the surface producer " + "of size: " + << physical_size.width() << "x" << physical_size.height(); + } else { + task.material.SetTexture(*(surface->GetImage())); + } + + frame_surfaces.emplace_back(std::move(surface)); + } + + session_connection_.Present(); + } - session_connection_.Present(this); + { + // Execute paint tasks in parallel with Scenic's side of the Present, then + // signal fences. + TRACE_EVENT0("flutter", "ExecutePaintTasks"); + size_t surface_index = 0; + for (auto& task : frame_paint_tasks) { + std::unique_ptr& task_surface = + frame_surfaces[surface_index++]; + if (!task_surface) { + continue; + } + + SkCanvas* canvas = task_surface->GetSkiaSurface()->getCanvas(); + flutter::Layer::PaintContext paint_context = { + canvas, + canvas, + gr_context(), + nullptr, + context().raster_time(), + context().ui_time(), + context().texture_registry(), + &context().raster_cache(), + false, + layer_tree.device_pixel_ratio()}; + canvas->restoreToCount(1); + canvas->save(); + canvas->clear(task.background_color); + canvas->scale(layer_tree.device_pixel_ratio() * task.scale_x, + layer_tree.device_pixel_ratio() * task.scale_y); + canvas->translate(-task.paint_bounds.left(), -task.paint_bounds.top()); + for (flutter::Layer* layer : task.layers) { + layer->Paint(paint_context); + } + } + + // Tell the surface producer that a present has occurred so it can perform + // book-keeping on buffer caches. + surface_producer_.OnSurfacesPresented(std::move(frame_surfaces)); } return flutter::RasterStatus::kSuccess; @@ -65,51 +140,16 @@ class ScopedFrame final : public flutter::CompositorContext::ScopedFrame { }; CompositorContext::CompositorContext( - std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, - fidl::InterfaceHandle session, - fml::closure session_error_callback, - zx_handle_t vsync_event_handle) - : debug_label_(std::move(debug_label)), - session_connection_( - debug_label_, - std::move(view_token), - std::move(view_ref_pair), - std::move(session), - session_error_callback, - [](auto) {}, - vsync_event_handle) {} - -void CompositorContext::OnSessionMetricsDidChange( - const fuchsia::ui::gfx::Metrics& metrics) { - session_connection_.set_metrics(metrics); -} + flutter::CompositorContext::Delegate& delegate, + SessionConnection& session_connection, + VulkanSurfaceProducer& surface_producer, + flutter::SceneUpdateContext& scene_update_context) + : flutter::CompositorContext(delegate), + session_connection_(session_connection), + surface_producer_(surface_producer), + scene_update_context_(scene_update_context) {} -void CompositorContext::OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - session_connection_.OnSessionSizeChangeHint(width_change_factor, - height_change_factor); -} - -void CompositorContext::OnWireframeEnabled(bool enabled) { - session_connection_.set_enable_wireframe(enabled); -} - -void CompositorContext::OnCreateView(int64_t view_id, - bool hit_testable, - bool focusable) { - session_connection_.scene_update_context().CreateView(view_id, hit_testable, - focusable); -} - -void CompositorContext::OnDestroyView(int64_t view_id) { - session_connection_.scene_update_context().DestroyView(view_id); -} - -CompositorContext::~CompositorContext() { - OnGrContextDestroyed(); -} +CompositorContext::~CompositorContext() = default; std::unique_ptr CompositorContext::AcquireFrame( @@ -120,16 +160,10 @@ CompositorContext::AcquireFrame( bool instrumentation_enabled, bool surface_supports_readback, fml::RefPtr raster_thread_merger) { - // TODO: The AcquireFrame interface is too broad and must be refactored to get - // rid of the context and canvas arguments as those seem to be only used for - // colorspace correctness purposes on the mobile shells. return std::make_unique( - *this, // - root_surface_transformation, // - view_embedder, - instrumentation_enabled, // - session_connection_ // - ); + *this, gr_context, canvas, view_embedder, root_surface_transformation, + instrumentation_enabled, surface_supports_readback, raster_thread_merger, + session_connection_, surface_producer_, scene_update_context_); } } // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/compositor_context.h b/shell/platform/fuchsia/flutter/compositor_context.h index 6ad28785b119c..eb57321c1215c 100644 --- a/shell/platform/fuchsia/flutter/compositor_context.h +++ b/shell/platform/fuchsia/flutter/compositor_context.h @@ -5,15 +5,15 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPOSITOR_CONTEXT_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPOSITOR_CONTEXT_H_ -#include -#include -#include -#include +#include #include "flutter/flow/compositor_context.h" #include "flutter/flow/embedded_views.h" +#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" + #include "session_connection.h" +#include "vulkan_surface_producer.h" namespace flutter_runner { @@ -21,31 +21,17 @@ namespace flutter_runner { // Fuchsia. class CompositorContext final : public flutter::CompositorContext { public: - CompositorContext(std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, - fidl::InterfaceHandle session, - fml::closure session_error_callback, - zx_handle_t vsync_event_handle); + CompositorContext(CompositorContext::Delegate& delegate, + SessionConnection& session_connection, + VulkanSurfaceProducer& surface_producer, + flutter::SceneUpdateContext& scene_update_context); ~CompositorContext() override; - void OnSessionMetricsDidChange(const fuchsia::ui::gfx::Metrics& metrics); - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor); - - void OnWireframeEnabled(bool enabled); - void OnCreateView(int64_t view_id, bool hit_testable, bool focusable); - void OnDestroyView(int64_t view_id); - - flutter::ExternalViewEmbedder* GetViewEmbedder() { - return &session_connection_.scene_update_context(); - } - private: - const std::string debug_label_; - scenic::ViewRefPair view_ref_pair_; - SessionConnection session_connection_; + SessionConnection& session_connection_; + VulkanSurfaceProducer& surface_producer_; + flutter::SceneUpdateContext& scene_update_context_; // |flutter::CompositorContext| std::unique_ptr AcquireFrame( diff --git a/shell/platform/fuchsia/flutter/engine.cc b/shell/platform/fuchsia/flutter/engine.cc index 1d5a2eca8c67f..9470f3aae8b38 100644 --- a/shell/platform/fuchsia/flutter/engine.cc +++ b/shell/platform/fuchsia/flutter/engine.cc @@ -7,8 +7,7 @@ #include #include -#include - +#include "../runtime/dart/utils/files.h" #include "compositor_context.h" #include "flutter/common/task_runners.h" #include "flutter/fml/make_copyable.h" @@ -20,15 +19,15 @@ #include "flutter_runner_product_configuration.h" #include "fuchsia_intl.h" #include "platform_view.h" -#include "runtime/dart/utils/files.h" #include "task_runner_adapter.h" #include "third_party/skia/include/ports/SkFontMgr_fuchsia.h" #include "thread.h" namespace flutter_runner { +namespace { -static void UpdateNativeThreadLabelNames(const std::string& label, - const flutter::TaskRunners& runners) { +void UpdateNativeThreadLabelNames(const std::string& label, + const flutter::TaskRunners& runners) { auto set_thread_name = [](fml::RefPtr runner, std::string prefix, std::string suffix) { if (!runner) { @@ -44,13 +43,15 @@ static void UpdateNativeThreadLabelNames(const std::string& label, set_thread_name(runners.GetIOTaskRunner(), label, ".io"); } -static fml::RefPtr MakeLocalizationPlatformMessage( +fml::RefPtr MakeLocalizationPlatformMessage( const fuchsia::intl::Profile& intl_profile) { return fml::MakeRefCounted( "flutter/localization", MakeLocalizationPlatformMessageData(intl_profile), nullptr); } +} // namespace + Engine::Engine(Delegate& delegate, std::string thread_label, std::shared_ptr svc, @@ -64,28 +65,72 @@ Engine::Engine(Delegate& delegate, FlutterRunnerProductConfiguration product_config) : delegate_(delegate), thread_label_(std::move(thread_label)), - settings_(std::move(settings)), weak_factory_(this) { if (zx::event::create(0, &vsync_event_) != ZX_OK) { FML_DLOG(ERROR) << "Could not create the vsync event."; return; } - // Launch the threads that will be used to run the shell. These threads will - // be joined in the destructor. - for (auto& thread : threads_) { - thread.reset(new Thread()); - } + // Get the task runners from the managed threads. The current thread will be + // used as the "platform" thread. + const flutter::TaskRunners task_runners( + thread_label_, // Dart thread labels + CreateFMLTaskRunner(async_get_default_dispatcher()), // platform + CreateFMLTaskRunner(threads_[0].dispatcher()), // raster + CreateFMLTaskRunner(threads_[1].dispatcher()), // ui + CreateFMLTaskRunner(threads_[2].dispatcher()) // io + ); + UpdateNativeThreadLabelNames(thread_label_, task_runners); - // Set up the session connection. + // Connect to Scenic. auto scenic = svc->Connect(); fidl::InterfaceHandle session; fidl::InterfaceHandle session_listener; auto session_listener_request = session_listener.NewRequest(); - scenic->CreateSession(session.NewRequest(), session_listener.Bind()); + fidl::InterfaceHandle focuser; + scenic->CreateSession2(session.NewRequest(), session_listener.Bind(), + focuser.NewRequest()); + + // Make clones of the `ViewRef` before sending it down to Scenic. + fuchsia::ui::views::ViewRef platform_view_ref, isolate_view_ref; + view_ref_pair.view_ref.Clone(&platform_view_ref); + view_ref_pair.view_ref.Clone(&isolate_view_ref); + + // Session is terminated on the raster thread, but we must terminate ourselves + // on the platform thread. + // + // This handles the fidl error callback when the Session connection is + // broken. The SessionListener interface also has an OnError method, which is + // invoked on the platform thread (in PlatformView). + fml::closure session_error_callback = [dispatcher = + async_get_default_dispatcher(), + weak = weak_factory_.GetWeakPtr()]() { + async::PostTask(dispatcher, [weak]() { + if (weak) { + weak->Terminate(); + } + }); + }; + + // Set up the session connection and other Scenic helpers on the raster + // thread. + task_runners.GetRasterTaskRunner()->PostTask(fml::MakeCopyable( + [this, session = std::move(session), + session_error_callback = std::move(session_error_callback), + view_token = std::move(view_token), + view_ref_pair = std::move(view_ref_pair), + vsync_handle = vsync_event_.get()]() mutable { + session_connection_.emplace( + thread_label_, std::move(session), + std::move(session_error_callback), [](auto) {}, vsync_handle); + surface_producer_.emplace(session_connection_->get()); + scene_update_context_.emplace(thread_label_, std::move(view_token), + std::move(view_ref_pair), + session_connection_.value()); + })); - // Grab the parent environment services. The platform view may want to access - // some of these services. + // Grab the parent environment services. The platform view may want to + // access some of these services. fuchsia::sys::EnvironmentPtr environment; svc->Connect(environment.NewRequest()); fidl::InterfaceHandle @@ -93,27 +138,26 @@ Engine::Engine(Delegate& delegate, environment->GetServices(parent_environment_service_provider.NewRequest()); environment.Unbind(); - // We need to manually schedule a frame when the session metrics change. - OnMetricsUpdate on_session_metrics_change_callback = std::bind( - &Engine::OnSessionMetricsDidChange, this, std::placeholders::_1); - - OnSizeChangeHint on_session_size_change_hint_callback = - std::bind(&Engine::OnSessionSizeChangeHint, this, std::placeholders::_1, - std::placeholders::_2); - OnEnableWireframe on_enable_wireframe_callback = std::bind( - &Engine::OnDebugWireframeSettingsChanged, this, std::placeholders::_1); + &Engine::DebugWireframeSettingsChanged, this, std::placeholders::_1); - flutter_runner::OnCreateView on_create_view_callback = - std::bind(&Engine::OnCreateView, this, std::placeholders::_1, + OnCreateView on_create_view_callback = + std::bind(&Engine::CreateView, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); - flutter_runner::OnDestroyView on_destroy_view_callback = - std::bind(&Engine::OnDestroyView, this, std::placeholders::_1); + OnUpdateView on_update_view_callback = + std::bind(&Engine::UpdateView, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + + OnDestroyView on_destroy_view_callback = + std::bind(&Engine::DestroyView, this, std::placeholders::_1); OnGetViewEmbedder on_get_view_embedder_callback = std::bind(&Engine::GetViewEmbedder, this); + OnGetGrContext on_get_gr_context_callback = + std::bind(&Engine::GetGrContext, this); + // SessionListener has a OnScenicError method; invoke this callback on the // platform thread when that happens. The Session itself should also be // disconnected when this happens, and it will also attempt to terminate. @@ -127,10 +171,6 @@ Engine::Engine(Delegate& delegate, }); }; - fuchsia::ui::views::ViewRef platform_view_ref, isolate_view_ref; - view_ref_pair.view_ref.Clone(&platform_view_ref); - view_ref_pair.view_ref.Clone(&isolate_view_ref); - // Setup the callback that will instantiate the platform view. flutter::Shell::CreateCallback on_create_platform_view = fml::MakeCopyable( @@ -139,18 +179,17 @@ Engine::Engine(Delegate& delegate, parent_environment_service_provider = std::move(parent_environment_service_provider), session_listener_request = std::move(session_listener_request), + focuser = std::move(focuser), on_session_listener_error_callback = std::move(on_session_listener_error_callback), - on_session_metrics_change_callback = - std::move(on_session_metrics_change_callback), - on_session_size_change_hint_callback = - std::move(on_session_size_change_hint_callback), on_enable_wireframe_callback = std::move(on_enable_wireframe_callback), on_create_view_callback = std::move(on_create_view_callback), + on_update_view_callback = std::move(on_update_view_callback), on_destroy_view_callback = std::move(on_destroy_view_callback), on_get_view_embedder_callback = std::move(on_get_view_embedder_callback), + on_get_gr_context_callback = std::move(on_get_gr_context_callback), vsync_handle = vsync_event_.get(), product_config = product_config](flutter::Shell& shell) mutable { return std::make_unique( @@ -161,83 +200,37 @@ Engine::Engine(Delegate& delegate, std::move(runner_services), std::move(parent_environment_service_provider), // services std::move(session_listener_request), // session listener + std::move(focuser), std::move(on_session_listener_error_callback), - std::move(on_session_metrics_change_callback), - std::move(on_session_size_change_hint_callback), std::move(on_enable_wireframe_callback), std::move(on_create_view_callback), + std::move(on_update_view_callback), std::move(on_destroy_view_callback), std::move(on_get_view_embedder_callback), + std::move(on_get_gr_context_callback), vsync_handle, // vsync handle product_config); }); - // Session can be terminated on the raster thread, but we must terminate - // ourselves on the platform thread. - // - // This handles the fidl error callback when the Session connection is - // broken. The SessionListener interface also has an OnError method, which is - // invoked on the platform thread (in PlatformView). - fml::closure on_session_error_callback = - [dispatcher = async_get_default_dispatcher(), - weak = weak_factory_.GetWeakPtr()]() { - async::PostTask(dispatcher, [weak]() { - if (weak) { - weak->Terminate(); - } - }); - }; - - // Get the task runners from the managed threads. The current thread will be - // used as the "platform" thread. - const flutter::TaskRunners task_runners( - thread_label_, // Dart thread labels - CreateFMLTaskRunner(async_get_default_dispatcher()), // platform - CreateFMLTaskRunner(threads_[0]->dispatcher()), // raster - CreateFMLTaskRunner(threads_[1]->dispatcher()), // ui - CreateFMLTaskRunner(threads_[2]->dispatcher()) // io - ); - // Setup the callback that will instantiate the rasterizer. flutter::Shell::CreateCallback on_create_rasterizer = - fml::MakeCopyable([thread_label = thread_label_, // - view_token = std::move(view_token), // - view_ref_pair = std::move(view_ref_pair), // - session = std::move(session), // - on_session_error_callback, // - vsync_event = vsync_event_.get() // - ](flutter::Shell& shell) mutable { - std::unique_ptr compositor_context; - { - TRACE_DURATION("flutter", "CreateCompositorContext"); - compositor_context = - std::make_unique( - thread_label, // debug label - std::move(view_token), // scenic view we attach our tree to - std::move(view_ref_pair), // scenic view ref/view ref control - std::move(session), // scenic session - on_session_error_callback, // session did encounter error - vsync_event); // vsync event handle - } + fml::MakeCopyable([this](flutter::Shell& shell) mutable { + FML_DCHECK(session_connection_); + FML_DCHECK(surface_producer_); + FML_DCHECK(scene_update_context_); + + std::unique_ptr compositor_context = + std::make_unique( + shell, session_connection_.value(), surface_producer_.value(), + scene_update_context_.value()); return std::make_unique( - /*task_runners=*/shell.GetTaskRunners(), - /*compositor_context=*/std::move(compositor_context), - /*is_gpu_disabled_sync_switch=*/shell.GetIsGpuDisabledSyncSwitch()); + shell, std::move(compositor_context)); }); - UpdateNativeThreadLabelNames(thread_label_, task_runners); - - settings_.verbose_logging = true; - - settings_.advisory_script_uri = thread_label_; - - settings_.advisory_script_entrypoint = thread_label_; - - settings_.root_isolate_create_callback = + settings.root_isolate_create_callback = std::bind(&Engine::OnMainIsolateStart, this); - - settings_.root_isolate_shutdown_callback = + settings.root_isolate_shutdown_callback = std::bind([weak = weak_factory_.GetWeakPtr(), runner = task_runners.GetPlatformTaskRunner()]() { runner->PostTask([weak = std::move(weak)] { @@ -247,7 +240,7 @@ Engine::Engine(Delegate& delegate, }); }); - auto vm = flutter::DartVMRef::Create(settings_); + auto vm = flutter::DartVMRef::Create(settings); if (!isolate_snapshot) { isolate_snapshot = vm->GetVMData()->GetIsolateSnapshot(); @@ -256,13 +249,13 @@ Engine::Engine(Delegate& delegate, { TRACE_EVENT0("flutter", "CreateShell"); shell_ = flutter::Shell::Create( - task_runners, // host task runners - flutter::WindowData(), // default window data - settings_, // shell launch settings - std::move(isolate_snapshot), // isolate snapshot - on_create_platform_view, // platform view create callback - on_create_rasterizer, // rasterizer create callback - std::move(vm) // vm reference + std::move(task_runners), // host task runners + flutter::PlatformData(), // default window data + std::move(settings), // shell launch settings + std::move(isolate_snapshot), // isolate snapshot + std::move(on_create_platform_view), // platform view create callback + std::move(on_create_rasterizer), // rasterizer create callback + std::move(vm) // vm reference ); } @@ -339,7 +332,7 @@ Engine::Engine(Delegate& delegate, // Launch the engine in the appropriate configuration. auto run_configuration = flutter::RunConfiguration::InferFromSettings( - settings_, task_runners.GetIOTaskRunner()); + shell_->GetSettings(), shell_->GetTaskRunners().GetIOTaskRunner()); auto on_run_failure = [weak = weak_factory_.GetWeakPtr()]() { // The engine could have been killed by the caller right after the @@ -376,11 +369,11 @@ Engine::Engine(Delegate& delegate, Engine::~Engine() { shell_.reset(); - for (const auto& thread : threads_) { - thread->Quit(); + for (auto& thread : threads_) { + thread.Quit(); } - for (const auto& thread : threads_) { - thread->Join(); + for (auto& thread : threads_) { + thread.Join(); } } @@ -485,105 +478,60 @@ void Engine::Terminate() { // collected this object. } -void Engine::OnSessionMetricsDidChange( - const fuchsia::ui::gfx::Metrics& metrics) { - if (!shell_) { +void Engine::DebugWireframeSettingsChanged(bool enabled) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), metrics]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - - compositor_context->OnSessionMetricsDidChange(metrics); - } - }); + [this, enabled]() { scene_update_context_->EnableWireframe(enabled); }); } -void Engine::OnDebugWireframeSettingsChanged(bool enabled) { - if (!shell_) { +void Engine::CreateView(int64_t view_id, bool hit_testable, bool focusable) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), enabled]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - - compositor_context->OnWireframeEnabled(enabled); - } + [this, view_id, hit_testable, focusable]() { + scene_update_context_->CreateView(view_id, hit_testable, focusable); }); } -void Engine::OnCreateView(int64_t view_id, bool hit_testable, bool focusable) { - if (!shell_) { +void Engine::UpdateView(int64_t view_id, bool hit_testable, bool focusable) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), view_id, hit_testable, - focusable]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - compositor_context->OnCreateView(view_id, hit_testable, focusable); - } + [this, view_id, hit_testable, focusable]() { + scene_update_context_->UpdateView(view_id, hit_testable, focusable); }); } -void Engine::OnDestroyView(int64_t view_id) { - if (!shell_) { +void Engine::DestroyView(int64_t view_id) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), view_id]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - compositor_context->OnDestroyView(view_id); - } - }); + [this, view_id]() { scene_update_context_->DestroyView(view_id); }); } flutter::ExternalViewEmbedder* Engine::GetViewEmbedder() { - // GetEmbedder should be called only after rasterizer is created. - FML_DCHECK(shell_); - FML_DCHECK(shell_->GetRasterizer()); + if (!scene_update_context_) { + return nullptr; + } - auto rasterizer = shell_->GetRasterizer(); - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - flutter::ExternalViewEmbedder* view_embedder = - compositor_context->GetViewEmbedder(); - return view_embedder; + return &scene_update_context_.value(); } -void Engine::OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - if (!shell_) { - return; - } +GrDirectContext* Engine::GetGrContext() { + // GetGrContext should be called only after rasterizer is created. + FML_DCHECK(shell_); + FML_DCHECK(shell_->GetRasterizer()); - shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), width_change_factor, - height_change_factor]() { - if (rasterizer) { - auto compositor_context = reinterpret_cast( - rasterizer->compositor_context()); - - compositor_context->OnSessionSizeChangeHint(width_change_factor, - height_change_factor); - } - }); + return surface_producer_->gr_context(); } #if !defined(DART_PRODUCT) diff --git a/shell/platform/fuchsia/flutter/engine.h b/shell/platform/fuchsia/flutter/engine.h index 5ed41394fd599..410ff63151b7a 100644 --- a/shell/platform/fuchsia/flutter/engine.h +++ b/shell/platform/fuchsia/flutter/engine.h @@ -15,11 +15,15 @@ #include #include "flutter/flow/embedded_views.h" +#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" #include "flutter/shell/common/shell.h" + #include "flutter_runner_product_configuration.h" #include "isolate_configurator.h" +#include "session_connection.h" #include "thread.h" +#include "vulkan_surface_producer.h" namespace flutter_runner { @@ -55,15 +59,22 @@ class Engine final { private: Delegate& delegate_; + const std::string thread_label_; - flutter::Settings settings_; - std::array, 3> threads_; + std::array threads_; + + std::optional session_connection_; + std::optional surface_producer_; + std::optional scene_update_context_; + std::unique_ptr isolate_configurator_; std::unique_ptr shell_; + + fuchsia::intl::PropertyProviderPtr intl_property_provider_; + zx::event vsync_event_; + fml::WeakPtrFactory weak_factory_; - // A stub for the FIDL protocol fuchsia.intl.PropertyProvider. - fuchsia::intl::PropertyProviderPtr intl_property_provider_; void OnMainIsolateStart(); @@ -71,18 +82,15 @@ class Engine final { void Terminate(); - void OnSessionMetricsDidChange(const fuchsia::ui::gfx::Metrics& metrics); - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor); - - void OnDebugWireframeSettingsChanged(bool enabled); - - void OnCreateView(int64_t view_id, bool hit_testable, bool focusable); - - void OnDestroyView(int64_t view_id); + void DebugWireframeSettingsChanged(bool enabled); + void CreateView(int64_t view_id, bool hit_testable, bool focusable); + void UpdateView(int64_t view_id, bool hit_testable, bool focusable); + void DestroyView(int64_t view_id); flutter::ExternalViewEmbedder* GetViewEmbedder(); + GrDirectContext* GetGrContext(); + FML_DISALLOW_COPY_AND_ASSIGN(Engine); }; diff --git a/shell/platform/fuchsia/flutter/engine_flutter_runner.gni b/shell/platform/fuchsia/flutter/engine_flutter_runner.gni deleted file mode 100644 index 630575b0980c4..0000000000000 --- a/shell/platform/fuchsia/flutter/engine_flutter_runner.gni +++ /dev/null @@ -1,150 +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. - -assert(is_fuchsia) - -import("//build/fuchsia/sdk.gni") - -# Builds a flutter_runner -# -# Parameters: -# -# output_name (required): -# The name of the resulting binary. -# -# extra_deps (required): -# Any additional dependencies. -# -# product (required): -# Whether to link against a Product mode Dart VM. -# -# extra_defines (optional): -# Any additional preprocessor defines. -template("flutter_runner") { - assert(defined(invoker.output_name), "flutter_runner must define output_name") - assert(defined(invoker.extra_deps), "flutter_runner must define extra_deps") - assert(defined(invoker.product), "flutter_runner must define product") - - invoker_output_name = invoker.output_name - extra_deps = invoker.extra_deps - - extra_defines = [] - if (defined(invoker.extra_defines)) { - extra_defines += invoker.extra_defines - } - - executable(target_name) { - output_name = invoker_output_name - - defines = extra_defines - - libs = [] - - sources = [ - "accessibility_bridge.cc", - "accessibility_bridge.h", - "component.cc", - "component.h", - "compositor_context.cc", - "compositor_context.h", - "engine.cc", - "engine.h", - "flutter_runner_product_configuration.cc", - "flutter_runner_product_configuration.h", - "fuchsia_intl.cc", - "fuchsia_intl.h", - "isolate_configurator.cc", - "isolate_configurator.h", - "logging.h", - "loop.cc", - "loop.h", - "main.cc", - "platform_view.cc", - "platform_view.h", - "runner.cc", - "runner.h", - "session_connection.cc", - "session_connection.h", - "surface.cc", - "surface.h", - "task_observers.cc", - "task_observers.h", - "task_runner_adapter.cc", - "task_runner_adapter.h", - "thread.cc", - "thread.h", - "unique_fdio_ns.h", - "vsync_recorder.cc", - "vsync_recorder.h", - "vsync_waiter.cc", - "vsync_waiter.h", - "vulkan_surface.cc", - "vulkan_surface.h", - "vulkan_surface_pool.cc", - "vulkan_surface_pool.h", - "vulkan_surface_producer.cc", - "vulkan_surface_producer.h", - ] - - # The use of these dependencies is temporary and will be moved behind the - # embedder API. - flutter_deps = [ - "../flutter:fuchsia_legacy_gpu_configuration", - "//flutter/assets", - "//flutter/common", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/fml", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/vulkan", - ] - - _fuchsia_platform = "//flutter/shell/platform/fuchsia" - - # TODO(kaushikiska) evaluate if all of these are needed. - fuchsia_deps = [ - "${_fuchsia_platform}/dart-pkg/fuchsia", - "${_fuchsia_platform}/dart-pkg/zircon", - "${_fuchsia_platform}/runtime/dart/utils", - ] - - deps = [ - "$fuchsia_sdk_root/fidl:fuchsia.accessibility.semantics", - "$fuchsia_sdk_root/fidl:fuchsia.fonts", - "$fuchsia_sdk_root/fidl:fuchsia.images", - "$fuchsia_sdk_root/fidl:fuchsia.intl", - "$fuchsia_sdk_root/fidl:fuchsia.io", - "$fuchsia_sdk_root/fidl:fuchsia.sys", - "$fuchsia_sdk_root/fidl:fuchsia.ui.app", - "$fuchsia_sdk_root/fidl:fuchsia.ui.scenic", - "$fuchsia_sdk_root/pkg:async-cpp", - "$fuchsia_sdk_root/pkg:async-default", - "$fuchsia_sdk_root/pkg:async-loop", - "$fuchsia_sdk_root/pkg:async-loop-cpp", - "$fuchsia_sdk_root/pkg:fdio", - "$fuchsia_sdk_root/pkg:fidl_cpp", - "$fuchsia_sdk_root/pkg:scenic_cpp", - "$fuchsia_sdk_root/pkg:sys_cpp", - "$fuchsia_sdk_root/pkg:syslog", - "$fuchsia_sdk_root/pkg:trace", - "$fuchsia_sdk_root/pkg:trace-engine", - "$fuchsia_sdk_root/pkg:trace-provider-so", - "$fuchsia_sdk_root/pkg:vfs_cpp", - "$fuchsia_sdk_root/pkg:zx", - "//third_party/skia", - "//flutter/third_party/tonic", - ] + fuchsia_deps + flutter_deps + extra_deps - - # The flags below are needed so that Dart's CPU profiler can walk the - # C++ stack. - cflags = [ "-fno-omit-frame-pointer" ] - - if (!invoker.product) { - # This flag is needed so that the call to dladdr() in Dart's native symbol - # resolver can report good symbol information for the CPU profiler. - ldflags = [ "-rdynamic" ] - } - } -} diff --git a/shell/platform/fuchsia/flutter/kernel/BUILD.gn b/shell/platform/fuchsia/flutter/kernel/BUILD.gn index d9dc3a69af98d..f221f4af1977c 100644 --- a/shell/platform/fuchsia/flutter/kernel/BUILD.gn +++ b/shell/platform/fuchsia/flutter/kernel/BUILD.gn @@ -21,7 +21,7 @@ compile_platform("kernel_platform_files") { args = [ "--enable-experiment=non-nullable", - "--nnbd-weak", + "--nnbd-agnostic", # TODO(dartbug.com/36342): enable bytecode for core libraries when performance of bytecode # pipeline is on par with default pipeline and continuously tracked. diff --git a/shell/platform/fuchsia/flutter/platform_view.cc b/shell/platform/fuchsia/flutter/platform_view.cc index df2b454336b1a..5359c1e809ceb 100644 --- a/shell/platform/fuchsia/flutter/platform_view.cc +++ b/shell/platform/fuchsia/flutter/platform_view.cc @@ -6,6 +6,7 @@ #include "platform_view.h" +#include #include #include "flutter/fml/logging.h" @@ -22,41 +23,6 @@ namespace flutter_runner { -namespace { - -inline fuchsia::ui::gfx::vec3 Add(const fuchsia::ui::gfx::vec3& a, - const fuchsia::ui::gfx::vec3& b) { - return {.x = a.x + b.x, .y = a.y + b.y, .z = a.z + b.z}; -} - -inline fuchsia::ui::gfx::vec3 Subtract(const fuchsia::ui::gfx::vec3& a, - const fuchsia::ui::gfx::vec3& b) { - return {.x = a.x - b.x, .y = a.y - b.y, .z = a.z - b.z}; -} - -inline fuchsia::ui::gfx::BoundingBox InsetBy( - const fuchsia::ui::gfx::BoundingBox& box, - const fuchsia::ui::gfx::vec3& inset_from_min, - const fuchsia::ui::gfx::vec3& inset_from_max) { - return {.min = Add(box.min, inset_from_min), - .max = Subtract(box.max, inset_from_max)}; -} - -inline fuchsia::ui::gfx::BoundingBox ViewPropertiesLayoutBox( - const fuchsia::ui::gfx::ViewProperties& view_properties) { - return InsetBy(view_properties.bounding_box, view_properties.inset_from_min, - view_properties.inset_from_max); -} - -inline fuchsia::ui::gfx::vec3 Max(const fuchsia::ui::gfx::vec3& v, - float min_val) { - return {.x = std::max(v.x, min_val), - .y = std::max(v.y, min_val), - .z = std::max(v.z, min_val)}; -} - -} // end namespace - static constexpr char kFlutterPlatformChannel[] = "flutter/platform"; static constexpr char kTextInputChannel[] = "flutter/textinput"; static constexpr char kKeyEventChannel[] = "flutter/keyevent"; @@ -88,27 +54,29 @@ PlatformView::PlatformView( parent_environment_service_provider_handle, fidl::InterfaceRequest session_listener_request, + fidl::InterfaceHandle focuser, fit::closure session_listener_error_callback, - OnMetricsUpdate session_metrics_did_change_callback, - OnSizeChangeHint session_size_change_hint_callback, OnEnableWireframe wireframe_enabled_callback, OnCreateView on_create_view_callback, + OnUpdateView on_update_view_callback, OnDestroyView on_destroy_view_callback, OnGetViewEmbedder on_get_view_embedder_callback, + OnGetGrContext on_get_gr_context_callback, zx_handle_t vsync_event_handle, FlutterRunnerProductConfiguration product_config) : flutter::PlatformView(delegate, std::move(task_runners)), debug_label_(std::move(debug_label)), view_ref_(std::move(view_ref)), + focuser_(focuser.Bind()), session_listener_binding_(this, std::move(session_listener_request)), session_listener_error_callback_( std::move(session_listener_error_callback)), - metrics_changed_callback_(std::move(session_metrics_did_change_callback)), - size_change_hint_callback_(std::move(session_size_change_hint_callback)), wireframe_enabled_callback_(std::move(wireframe_enabled_callback)), on_create_view_callback_(std::move(on_create_view_callback)), + on_update_view_callback_(std::move(on_update_view_callback)), on_destroy_view_callback_(std::move(on_destroy_view_callback)), on_get_view_embedder_callback_(std::move(on_get_view_embedder_callback)), + on_get_gr_context_callback_(std::move(on_get_gr_context_callback)), ime_client_(this), vsync_event_handle_(vsync_event_handle), product_config_(product_config) { @@ -152,64 +120,6 @@ void PlatformView::RegisterPlatformMessageHandlers() { this, std::placeholders::_1); } -void PlatformView::OnPropertiesChanged( - const fuchsia::ui::gfx::ViewProperties& view_properties) { - fuchsia::ui::gfx::BoundingBox layout_box = - ViewPropertiesLayoutBox(view_properties); - - fuchsia::ui::gfx::vec3 logical_size = - Max(Subtract(layout_box.max, layout_box.min), 0.f); - - metrics_.size.width = logical_size.x; - metrics_.size.height = logical_size.y; - metrics_.size.depth = logical_size.z; - metrics_.padding.left = view_properties.inset_from_min.x; - metrics_.padding.top = view_properties.inset_from_min.y; - metrics_.padding.front = view_properties.inset_from_min.z; - metrics_.padding.right = view_properties.inset_from_max.x; - metrics_.padding.bottom = view_properties.inset_from_max.y; - metrics_.padding.back = view_properties.inset_from_max.z; - - FlushViewportMetrics(); -} - -// TODO(SCN-975): Re-enable. -// void PlatformView::ConnectSemanticsProvider( -// fuchsia::ui::viewsv1token::ViewToken token) { -// semantics_bridge_.SetupEnvironment( -// token.value, parent_environment_service_provider_.get()); -// } - -void PlatformView::UpdateViewportMetrics( - const fuchsia::ui::gfx::Metrics& metrics) { - metrics_.scale = metrics.scale_x; - metrics_.scale_z = metrics.scale_z; - - FlushViewportMetrics(); -} - -void PlatformView::FlushViewportMetrics() { - const auto scale = metrics_.scale; - const auto scale_z = metrics_.scale_z; - - SetViewportMetrics({ - scale, // device_pixel_ratio - metrics_.size.width * scale, // physical_width - metrics_.size.height * scale, // physical_height - metrics_.size.depth * scale_z, // physical_depth - metrics_.padding.top * scale, // physical_padding_top - metrics_.padding.right * scale, // physical_padding_right - metrics_.padding.bottom * scale, // physical_padding_bottom - metrics_.padding.left * scale, // physical_padding_left - metrics_.view_inset.front * scale_z, // physical_view_inset_front - metrics_.view_inset.back * scale_z, // physical_view_inset_back - metrics_.view_inset.top * scale, // physical_view_inset_top - metrics_.view_inset.right * scale, // physical_view_inset_right - metrics_.view_inset.bottom * scale, // physical_view_inset_bottom - metrics_.view_inset.left * scale // physical_view_inset_left - }); -} - // |fuchsia::ui::input::InputMethodEditorClient| void PlatformView::DidUpdateState( fuchsia::ui::input::TextInputState state, @@ -302,27 +212,40 @@ void PlatformView::OnScenicError(std::string error) { void PlatformView::OnScenicEvent( std::vector events) { TRACE_EVENT0("flutter", "PlatformView::OnScenicEvent"); + bool should_update_metrics = false; for (const auto& event : events) { switch (event.Which()) { case fuchsia::ui::scenic::Event::Tag::kGfx: switch (event.gfx().Which()) { case fuchsia::ui::gfx::Event::Tag::kMetrics: { - if (!fidl::Equals(event.gfx().metrics().metrics, scenic_metrics_)) { - scenic_metrics_ = std::move(event.gfx().metrics().metrics); - metrics_changed_callback_(scenic_metrics_); - UpdateViewportMetrics(scenic_metrics_); + const fuchsia::ui::gfx::Metrics& metrics = + event.gfx().metrics().metrics; + const float new_view_pixel_ratio = metrics.scale_x; + + // Avoid metrics update when possible -- it is computationally + // expensive. + if (view_pixel_ratio_ != new_view_pixel_ratio) { + view_pixel_ratio_ = new_view_pixel_ratio; + should_update_metrics = true; } break; } - case fuchsia::ui::gfx::Event::Tag::kSizeChangeHint: { - size_change_hint_callback_( - event.gfx().size_change_hint().width_change_factor, - event.gfx().size_change_hint().height_change_factor); - break; - } case fuchsia::ui::gfx::Event::Tag::kViewPropertiesChanged: { - OnPropertiesChanged( - std::move(event.gfx().view_properties_changed().properties)); + const fuchsia::ui::gfx::BoundingBox& bounding_box = + event.gfx().view_properties_changed().properties.bounding_box; + const float new_view_width = + std::max(bounding_box.max.x - bounding_box.min.x, 0.0f); + const float new_view_height = + std::max(bounding_box.max.y - bounding_box.min.y, 0.0f); + + // Avoid metrics update when possible -- it is computationally + // expensive. + if (view_width_ != new_view_width || + view_height_ != new_view_width) { + view_width_ = new_view_width; + view_height_ = new_view_height; + should_update_metrics = true; + } break; } case fuchsia::ui::gfx::Event::Tag::kViewConnected: @@ -372,6 +295,26 @@ void PlatformView::OnScenicEvent( } } } + + if (should_update_metrics) { + SetViewportMetrics({ + view_pixel_ratio_, // device_pixel_ratio + view_width_ * view_pixel_ratio_, // physical_width + view_height_ * view_pixel_ratio_, // physical_height + 0.0f, // physical_padding_top + 0.0f, // physical_padding_right + 0.0f, // physical_padding_bottom + 0.0f, // physical_padding_left + 0.0f, // physical_view_inset_top + 0.0f, // physical_view_inset_right + 0.0f, // physical_view_inset_bottom + 0.0f, // physical_view_inset_left + 0.0f, // p_physical_system_gesture_inset_top + 0.0f, // p_physical_system_gesture_inset_right + 0.0f, // p_physical_system_gesture_inset_bottom + 0.0f, // p_physical_system_gesture_inset_left + }); + } } void PlatformView::OnChildViewConnected(scenic::ResourceId view_holder_id) { @@ -451,8 +394,9 @@ bool PlatformView::OnHandlePointerEvent( pointer_data.change = GetChangeFromPointerEventPhase(pointer.phase); pointer_data.kind = GetKindFromPointerType(pointer.type); pointer_data.device = pointer.pointer_id; - pointer_data.physical_x = pointer.x * metrics_.scale; - pointer_data.physical_y = pointer.y * metrics_.scale; + // Pointer events are in logical pixels, so scale to physical. + pointer_data.physical_x = pointer.x * view_pixel_ratio_; + pointer_data.physical_y = pointer.y * view_pixel_ratio_; // Buttons are single bit values starting with kMousePrimaryButton = 1. pointer_data.buttons = static_cast(pointer.buttons); @@ -578,8 +522,12 @@ std::unique_ptr PlatformView::CreateRenderingSurface() { // This platform does not repeatly lose and gain a surface connection. So the // surface is setup once during platform view setup and returned to the // shell on the initial (and only) |NotifyCreated| call. - auto view_embedder = on_get_view_embedder_callback_(); - return std::make_unique(debug_label_, view_embedder); + auto view_embedder = on_get_view_embedder_callback_ + ? on_get_view_embedder_callback_() + : nullptr; + auto gr_context = + on_get_gr_context_callback_ ? on_get_gr_context_callback_() : nullptr; + return std::make_unique(debug_label_, view_embedder, gr_context); } // |flutter::PlatformView| @@ -816,6 +764,35 @@ void PlatformView::HandleFlutterPlatformViewsChannelPlatformMessage( message->response()->Complete( std::make_unique((const uint8_t*)"[0]", 3u)); } + } else if (method->value == "View.update") { + auto args_it = root.FindMember("args"); + if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { + FML_LOG(ERROR) << "No arguments found."; + return; + } + const auto& args = args_it->value; + + auto view_id = args.FindMember("viewId"); + if (!view_id->value.IsUint64()) { + FML_LOG(ERROR) << "Argument 'viewId' is not a int64"; + return; + } + + auto hit_testable = args.FindMember("hitTestable"); + if (!hit_testable->value.IsBool()) { + FML_LOG(ERROR) << "Argument 'hitTestable' is not a bool"; + return; + } + + auto focusable = args.FindMember("focusable"); + if (!focusable->value.IsBool()) { + FML_LOG(ERROR) << "Argument 'focusable' is not a bool"; + return; + } + + on_update_view_callback_(view_id->value.GetUint64(), + hit_testable->value.GetBool(), + focusable->value.GetBool()); } else if (method->value == "View.dispose") { auto args_it = root.FindMember("args"); if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { @@ -830,6 +807,39 @@ void PlatformView::HandleFlutterPlatformViewsChannelPlatformMessage( return; } on_destroy_view_callback_(view_id->value.GetUint64()); + } else if (method->value == "View.requestFocus") { + auto args_it = root.FindMember("args"); + if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { + FML_LOG(ERROR) << "No arguments found."; + return; + } + const auto& args = args_it->value; + + auto view_ref = args.FindMember("viewRef"); + if (!view_ref->value.IsUint64()) { + FML_LOG(ERROR) << "Argument 'viewRef' is not a int64"; + return; + } + + zx_handle_t handle = view_ref->value.GetUint64(); + zx_handle_t out_handle; + zx_status_t status = + zx_handle_duplicate(handle, ZX_RIGHT_SAME_RIGHTS, &out_handle); + if (status != ZX_OK) { + FML_LOG(ERROR) << "Argument 'viewRef' is not valid"; + return; + } + auto ref = fuchsia::ui::views::ViewRef({ + .reference = zx::eventpair(out_handle), + }); + focuser_->RequestFocus( + std::move(ref), + [view_ref = view_ref->value.GetUint64()]( + fuchsia::ui::views::Focuser_RequestFocus_Result result) { + if (result.is_err()) { + FML_LOG(ERROR) << "Failed to request focus for view: " << view_ref; + } + }); } else { FML_DLOG(ERROR) << "Unknown " << message->channel() << " method " << method->value.GetString(); diff --git a/shell/platform/fuchsia/flutter/platform_view.h b/shell/platform/fuchsia/flutter/platform_view.h index 45e1e1fb97732..35275b3b84c03 100644 --- a/shell/platform/fuchsia/flutter/platform_view.h +++ b/shell/platform/fuchsia/flutter/platform_view.h @@ -5,7 +5,6 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_PLATFORM_VIEW_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_PLATFORM_VIEW_H_ -#include #include #include #include @@ -14,7 +13,6 @@ #include #include "flutter/fml/macros.h" -#include "flutter/lib/ui/window/viewport_metrics.h" #include "flutter/shell/common/platform_view.h" #include "flutter/shell/platform/fuchsia/flutter/accessibility_bridge.h" #include "flutter_runner_product_configuration.h" @@ -24,13 +22,12 @@ namespace flutter_runner { -using OnMetricsUpdate = fit::function; -using OnSizeChangeHint = - fit::function; using OnEnableWireframe = fit::function; using OnCreateView = fit::function; +using OnUpdateView = fit::function; using OnDestroyView = fit::function; using OnGetViewEmbedder = fit::function; +using OnGetGrContext = fit::function; // The per engine component residing on the platform thread is responsible for // all platform specific integrations. @@ -52,26 +49,19 @@ class PlatformView final : public flutter::PlatformView, parent_environment_service_provider, fidl::InterfaceRequest session_listener_request, + fidl::InterfaceHandle focuser, fit::closure on_session_listener_error_callback, - OnMetricsUpdate session_metrics_did_change_callback, - OnSizeChangeHint session_size_change_hint_callback, OnEnableWireframe wireframe_enabled_callback, OnCreateView on_create_view_callback, + OnUpdateView on_update_view_callback, OnDestroyView on_destroy_view_callback, OnGetViewEmbedder on_get_view_embedder_callback, + OnGetGrContext on_get_gr_context_callback, zx_handle_t vsync_event_handle, FlutterRunnerProductConfiguration product_config); - PlatformView(flutter::PlatformView::Delegate& delegate, - std::string debug_label, - flutter::TaskRunners task_runners, - fidl::InterfaceHandle - parent_environment_service_provider, - zx_handle_t vsync_event_handle); ~PlatformView(); - void UpdateViewportMetrics(const fuchsia::ui::gfx::Metrics& metrics); - // |flutter::PlatformView| // |flutter_runner::AccessibilityBridge::Delegate| void SetSemanticsEnabled(bool enabled) override; @@ -88,16 +78,17 @@ class PlatformView final : public flutter::PlatformView, // TODO(MI4-2490): remove once ViewRefControl is passed to Scenic and kept // alive there const fuchsia::ui::views::ViewRef view_ref_; + fuchsia::ui::views::FocuserPtr focuser_; std::unique_ptr accessibility_bridge_; fidl::Binding session_listener_binding_; fit::closure session_listener_error_callback_; - OnMetricsUpdate metrics_changed_callback_; - OnSizeChangeHint size_change_hint_callback_; OnEnableWireframe wireframe_enabled_callback_; OnCreateView on_create_view_callback_; + OnUpdateView on_update_view_callback_; OnDestroyView on_destroy_view_callback_; OnGetViewEmbedder on_get_view_embedder_callback_; + OnGetGrContext on_get_gr_context_callback_; int current_text_input_client_ = 0; fidl::Binding ime_client_; @@ -105,8 +96,7 @@ class PlatformView final : public flutter::PlatformView, fuchsia::ui::input::ImeServicePtr text_sync_service_; fuchsia::sys::ServiceProviderPtr parent_environment_service_provider_; - flutter::LogicalMetrics metrics_; - fuchsia::ui::gfx::Metrics scenic_metrics_; + // last_text_state_ is the last state of the text input as reported by the IME // or initialized by Flutter. We set it to null if Flutter doesn't want any // input, since then there is no text input state at all. @@ -124,16 +114,14 @@ class PlatformView final : public flutter::PlatformView, std::set unregistered_channels_; zx_handle_t vsync_event_handle_ = 0; + float view_width_ = 0.0f; // Width in logical pixels. + float view_height_ = 0.0f; // Height in logical pixels. + float view_pixel_ratio_ = 0.0f; // Logical / physical pixel ratio. + FlutterRunnerProductConfiguration product_config_; void RegisterPlatformMessageHandlers(); - void FlushViewportMetrics(); - - // Called when the view's properties have changed. - void OnPropertiesChanged( - const fuchsia::ui::gfx::ViewProperties& view_properties); - // |fuchsia::ui::input::InputMethodEditorClient| void DidUpdateState( fuchsia::ui::input::TextInputState state, diff --git a/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/shell/platform/fuchsia/flutter/platform_view_unittest.cc index 89a5658d8dd63..6d236e8c8a371 100644 --- a/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -4,7 +4,7 @@ #include "flutter/shell/platform/fuchsia/flutter/platform_view.h" -#include +#include #include #include #include @@ -15,11 +15,11 @@ #include #include -#include "flutter/flow/scene_update_context.h" +#include "flutter/flow/embedded_views.h" #include "flutter/lib/ui/window/platform_message.h" #include "flutter/lib/ui/window/window.h" -#include "fuchsia/ui/views/cpp/fidl.h" #include "gtest/gtest.h" + #include "task_runner_adapter.h" namespace flutter_runner_test::flutter_runner_a11y_test { @@ -41,6 +41,33 @@ class PlatformViewTests : public testing::Test { FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewTests); }; +class MockExternalViewEmbedder : public flutter::ExternalViewEmbedder { + public: + MockExternalViewEmbedder() = default; + ~MockExternalViewEmbedder() override = default; + + SkCanvas* GetRootCanvas() override { return nullptr; } + std::vector GetCurrentCanvases() override { + return std::vector(); + } + + void CancelFrame() override {} + void BeginFrame( + SkISize frame_size, + GrDirectContext* context, + double device_pixel_ratio, + fml::RefPtr raster_thread_merger) override {} + void SubmitFrame(GrDirectContext* context, + std::unique_ptr frame) override { + return; + } + + void PrerollCompositeEmbeddedView( + int view_id, + std::unique_ptr params) override {} + SkCanvas* CompositeEmbeddedView(int view_id) override { return nullptr; } +}; + class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { public: // |flutter::PlatformView::Delegate| @@ -92,6 +119,7 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { flutter::ExternalViewEmbedder* get_view_embedder() { return surface_->GetExternalViewEmbedder(); } + GrDirectContext* get_gr_context() { return surface_->GetContext(); } private: std::unique_ptr surface_; @@ -99,28 +127,18 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { int32_t semantics_features_ = 0; }; -class MockSurfaceProducer - : public flutter::SceneUpdateContext::SurfaceProducer { +class MockFocuser : public fuchsia::ui::views::Focuser { public: - std::unique_ptr - ProduceSurface(const SkISize& size, - const flutter::LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) override { - return nullptr; - } + MockFocuser() = default; + ~MockFocuser() override = default; - bool HasRetainedNode(const flutter::LayerRasterCacheKey& key) const override { - return false; - } + bool request_focus_called = false; - scenic::EntityNode* GetRetainedNode( - const flutter::LayerRasterCacheKey& key) override { - return nullptr; + private: + void RequestFocus(fuchsia::ui::views::ViewRef view_ref, + RequestFocusCallback callback) override { + request_focus_called = true; } - - void SubmitSurface( - std::unique_ptr - surface) override {} }; TEST_F(PlatformViewTests, ChangesAccessibilitySettings) { @@ -146,13 +164,14 @@ TEST_F(PlatformViewTests, ChangesAccessibilitySettings) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -202,13 +221,14 @@ TEST_F(PlatformViewTests, EnableWireframeTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback EnableWireframeCallback, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -269,13 +289,14 @@ TEST_F(PlatformViewTests, CreateViewTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, CreateViewCallback, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -308,6 +329,76 @@ TEST_F(PlatformViewTests, CreateViewTest) { EXPECT_TRUE(create_view_called); } +// Test to make sure that PlatformView correctly registers messages sent on +// the "flutter/platform_views" channel, correctly parses the JSON it receives +// and calls the UdpateViewCallback with the appropriate args. +TEST_F(PlatformViewTests, UpdateViewTest) { + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + zx::eventpair a, b; + zx::eventpair::create(/* flags */ 0u, &a, &b); + auto view_ref = fuchsia::ui::views::ViewRef({ + .reference = std::move(a), + }); + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + // Test wireframe callback function. If the message sent to the platform + // view was properly handled and parsed, this function should be called, + // setting |wireframe_enabled| to true. + int64_t update_view_called = false; + auto UpdateViewCallback = [&update_view_called]( + int64_t view_id, bool hit_testable, + bool focusable) { update_view_called = true; }; + + auto platform_view = flutter_runner::PlatformView( + delegate, // delegate + "test_platform_view", // label + std::move(view_ref), // view_refs + std::move(task_runners), // task_runners + services_provider.service_directory(), // runner_services + nullptr, // parent_environment_service_provider_handle + nullptr, // session_listener_request + nullptr, // focuser, + nullptr, // on_session_listener_error_callback + nullptr, // on_enable_wireframe_callback, + nullptr, // on_create_view_callback, + UpdateViewCallback, // on_update_view_callback, + nullptr, // on_destroy_view_callback, + nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, + 0u, // vsync_event_handle + {} // product_config + ); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = dynamic_cast(&platform_view); + EXPECT_TRUE(base_view); + + // JSON for the message to be passed into the PlatformView. + const uint8_t txt[] = + "{" + " \"method\":\"View.update\"," + " \"args\": {" + " \"viewId\":42," + " \"hitTestable\":true," + " \"focusable\":true" + " }" + "}"; + + fml::RefPtr message = + fml::MakeRefCounted( + "flutter/platform_views", + std::vector(txt, txt + sizeof(txt)), + fml::RefPtr()); + base_view->HandlePlatformMessage(message); + + RunLoopUntilIdle(); + + EXPECT_TRUE(update_view_called); +} + // Test to make sure that PlatformView correctly registers messages sent on // the "flutter/platform_views" channel, correctly parses the JSON it receives // and calls the DestroyViewCallback with the appropriate args. @@ -338,13 +429,14 @@ TEST_F(PlatformViewTests, DestroyViewTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, DestroyViewCallback, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -375,6 +467,72 @@ TEST_F(PlatformViewTests, DestroyViewTest) { EXPECT_TRUE(destroy_view_called); } +// Test to make sure that PlatformView correctly registers messages sent on +// the "flutter/platform_views" channel, correctly parses the JSON it receives +// and calls the focuser's RequestFocus with the appropriate args. +TEST_F(PlatformViewTests, RequestFocusTest) { + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + zx::eventpair a, b; + zx::eventpair::create(/* flags */ 0u, &a, &b); + auto view_ref = fuchsia::ui::views::ViewRef({ + .reference = std::move(a), + }); + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + MockFocuser mock_focuser; + fidl::BindingSet focuser_bindings; + auto focuser_handle = focuser_bindings.AddBinding(&mock_focuser); + + auto platform_view = flutter_runner::PlatformView( + delegate, // delegate + "test_platform_view", // label + std::move(view_ref), // view_refs + std::move(task_runners), // task_runners + services_provider.service_directory(), // runner_services + nullptr, // parent_environment_service_provider_handle + nullptr, // session_listener_request + std::move(focuser_handle), // focuser, + nullptr, // on_session_listener_error_callback + nullptr, // on_enable_wireframe_callback, + nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, + nullptr, // on_destroy_view_callback, + nullptr, // on_get_gr_context_callback, + nullptr, // on_get_view_embedder_callback, + 0u, // vsync_event_handle + {} // product_config + ); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = dynamic_cast(&platform_view); + EXPECT_TRUE(base_view); + + // JSON for the message to be passed into the PlatformView. + char buff[254]; + snprintf(buff, sizeof(buff), + "{" + " \"method\":\"View.requestFocus\"," + " \"args\": {" + " \"viewRef\":%u" + " }" + "}", + b.get()); + + fml::RefPtr message = + fml::MakeRefCounted( + "flutter/platform_views", + std::vector(buff, buff + sizeof(buff)), + fml::RefPtr()); + base_view->HandlePlatformMessage(message); + + RunLoopUntilIdle(); + + EXPECT_TRUE(mock_focuser.request_focus_called); +} + // Test to make sure that PlatformView correctly returns a Surface instance // that can surface the view_embedder provided from GetViewEmbedderCallback. TEST_F(PlatformViewTests, GetViewEmbedderTest) { @@ -395,11 +553,8 @@ TEST_F(PlatformViewTests, GetViewEmbedderTest) { ); // Test get view embedder callback function. - MockSurfaceProducer surfaceProducer; - flutter::SceneUpdateContext scene_update_context(nullptr, &surfaceProducer); - flutter::ExternalViewEmbedder* view_embedder = - reinterpret_cast(&scene_update_context); - auto GetViewEmbedderCallback = [view_embedder]() { return view_embedder; }; + MockExternalViewEmbedder view_embedder; + auto GetViewEmbedderCallback = [&view_embedder]() { return &view_embedder; }; auto platform_view = flutter_runner::PlatformView( delegate, // delegate @@ -409,13 +564,14 @@ TEST_F(PlatformViewTests, GetViewEmbedderTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, GetViewEmbedderCallback, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -426,7 +582,62 @@ TEST_F(PlatformViewTests, GetViewEmbedderTest) { RunLoopUntilIdle(); - EXPECT_EQ(view_embedder, delegate.get_view_embedder()); + EXPECT_EQ(&view_embedder, delegate.get_view_embedder()); +} + +// Test to make sure that PlatformView correctly returns a Surface instance +// that can surface the GrContext provided from GetGrContextCallback. +TEST_F(PlatformViewTests, GetGrContextTest) { + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + zx::eventpair a, b; + zx::eventpair::create(/* flags */ 0u, &a, &b); + auto view_ref = fuchsia::ui::views::ViewRef({ + .reference = std::move(a), + }); + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + nullptr, // platform + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // raster + nullptr, // ui + nullptr // io + ); + + // Test get GrContext callback function. + sk_sp gr_context = + GrDirectContext::MakeMock(nullptr, GrContextOptions()); + auto GetGrContextCallback = [gr_context = gr_context.get()]() { + return gr_context; + }; + + auto platform_view = flutter_runner::PlatformView( + delegate, // delegate + "test_platform_view", // label + std::move(view_ref), // view_refs + std::move(task_runners), // task_runners + services_provider.service_directory(), // runner_services + nullptr, // parent_environment_service_provider_handle + nullptr, // session_listener_request + nullptr, // focuser + nullptr, // on_session_listener_error_callback + nullptr, // on_enable_wireframe_callback, + nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, + nullptr, // on_destroy_view_callback, + nullptr, // on_get_view_embedder_callback, + GetGrContextCallback, // on_get_gr_context_callback, + 0u, // vsync_event_handle + {} // product_config + ); + + RunLoopUntilIdle(); + + platform_view.NotifyCreated(); + + RunLoopUntilIdle(); + + EXPECT_EQ(gr_context.get(), delegate.get_gr_context()); } } // namespace flutter_runner_test::flutter_runner_a11y_test diff --git a/shell/platform/fuchsia/flutter/session_connection.cc b/shell/platform/fuchsia/flutter/session_connection.cc index 133dc9a43b8be..c87368a8993b9 100644 --- a/shell/platform/fuchsia/flutter/session_connection.cc +++ b/shell/platform/fuchsia/flutter/session_connection.cc @@ -5,8 +5,8 @@ #include "session_connection.h" #include "flutter/fml/make_copyable.h" -#include "lib/fidl/cpp/optional.h" -#include "lib/ui/scenic/cpp/commands.h" +#include "flutter/fml/trace_event.h" + #include "vsync_recorder.h" #include "vsync_waiter.h" @@ -14,23 +14,11 @@ namespace flutter_runner { SessionConnection::SessionConnection( std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, fidl::InterfaceHandle session, fml::closure session_error_callback, on_frame_presented_event on_frame_presented_callback, zx_handle_t vsync_event_handle) - : debug_label_(std::move(debug_label)), - session_wrapper_(session.Bind(), nullptr), - root_view_(&session_wrapper_, - std::move(view_token), - std::move(view_ref_pair.control_ref), - std::move(view_ref_pair.view_ref), - debug_label), - root_node_(&session_wrapper_), - surface_producer_( - std::make_unique(&session_wrapper_)), - scene_update_context_(&session_wrapper_, surface_producer_.get()), + : session_wrapper_(session.Bind(), nullptr), on_frame_presented_callback_(std::move(on_frame_presented_callback)), vsync_event_handle_(vsync_event_handle) { session_wrapper_.set_error_handler( @@ -63,11 +51,7 @@ SessionConnection::SessionConnection( } // callback ); - session_wrapper_.SetDebugName(debug_label_); - - root_view_.AddChild(root_node_); - root_node_.SetEventMask(fuchsia::ui::gfx::kMetricsEventMask | - fuchsia::ui::gfx::kSizeChangeHintEventMask); + session_wrapper_.SetDebugName(debug_label); // Get information to finish initialization and only then allow Present()s. session_wrapper_.RequestPresentationTimes( @@ -91,8 +75,7 @@ SessionConnection::SessionConnection( SessionConnection::~SessionConnection() = default; -void SessionConnection::Present( - flutter::CompositorContext::ScopedFrame* frame) { +void SessionConnection::Present() { TRACE_EVENT0("gfx", "SessionConnection::Present"); TRACE_FLOW_BEGIN("gfx", "SessionConnection::PresentSession", @@ -114,21 +97,6 @@ void SessionConnection::Present( present_session_pending_ = true; ToggleSignal(vsync_event_handle_, false); } - - if (frame) { - // Execute paint tasks and signal fences. - auto surfaces_to_submit = scene_update_context_.ExecutePaintTasks(*frame); - - // Tell the surface producer that a present has occurred so it can perform - // book-keeping on buffer caches. - surface_producer_->OnSurfacesPresented(std::move(surfaces_to_submit)); - } -} - -void SessionConnection::OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - surface_producer_->OnSessionSizeChangeHint(width_change_factor, - height_change_factor); } fml::TimePoint SessionConnection::CalculateNextLatchPoint( @@ -160,17 +128,6 @@ fml::TimePoint SessionConnection::CalculateNextLatchPoint( return minimum_latch_point_to_target; } -void SessionConnection::set_enable_wireframe(bool enable) { - session_wrapper_.Enqueue( - scenic::NewSetEnableDebugViewBoundsCmd(root_view_.id(), enable)); -} - -void SessionConnection::EnqueueClearOps() { - // We are going to be sending down a fresh node hierarchy every frame. So just - // enqueue a detach op on the imported root node. - session_wrapper_.Enqueue(scenic::NewDetachChildrenCmd(root_node_.id())); -} - void SessionConnection::PresentSession() { TRACE_EVENT0("gfx", "SessionConnection::PresentSession"); @@ -232,10 +189,6 @@ void SessionConnection::PresentSession() { VsyncRecorder::GetInstance().UpdateNextPresentationInfo( std::move(info)); }); - - // Prepare for the next frame. These ops won't be processed till the next - // present. - EnqueueClearOps(); } void SessionConnection::ToggleSignal(zx_handle_t handle, bool set) { diff --git a/shell/platform/fuchsia/flutter/session_connection.h b/shell/platform/fuchsia/flutter/session_connection.h index dcd55f19afbf4..7630d15e77837 100644 --- a/shell/platform/fuchsia/flutter/session_connection.h +++ b/shell/platform/fuchsia/flutter/session_connection.h @@ -5,22 +5,14 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_SESSION_CONNECTION_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_SESSION_CONNECTION_H_ -#include +#include #include -#include #include -#include -#include -#include #include -#include -#include "flutter/flow/compositor_context.h" #include "flutter/flow/scene_update_context.h" #include "flutter/fml/closure.h" #include "flutter/fml/macros.h" -#include "flutter/fml/trace_event.h" -#include "vulkan_surface_producer.h" namespace flutter_runner { @@ -29,11 +21,9 @@ using on_frame_presented_event = // The component residing on the raster thread that is responsible for // maintaining the Scenic session connection and presenting node updates. -class SessionConnection final { +class SessionConnection final : public flutter::SessionWrapper { public: SessionConnection(std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, fidl::InterfaceHandle session, fml::closure session_error_callback, on_frame_presented_event on_frame_presented_callback, @@ -41,36 +31,8 @@ class SessionConnection final { ~SessionConnection(); - bool has_metrics() const { return scene_update_context_.has_metrics(); } - - const fuchsia::ui::gfx::MetricsPtr& metrics() const { - return scene_update_context_.metrics(); - } - - void set_metrics(const fuchsia::ui::gfx::Metrics& metrics) { - fuchsia::ui::gfx::Metrics metrics_copy; - metrics.Clone(&metrics_copy); - scene_update_context_.set_metrics( - fidl::MakeOptional(std::move(metrics_copy))); - } - - void set_enable_wireframe(bool enable); - - flutter::SceneUpdateContext& scene_update_context() { - return scene_update_context_; - } - - scenic::ContainerNode& root_node() { return root_node_; } - scenic::View* root_view() { return &root_view_; } - - void Present(flutter::CompositorContext::ScopedFrame* frame); - - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor); - - VulkanSurfaceProducer* vulkan_surface_producer() { - return surface_producer_.get(); - } + scenic::Session* get() override { return &session_wrapper_; } + void Present() override; static fml::TimePoint CalculateNextLatchPoint( fml::TimePoint present_requested_time, @@ -82,14 +44,8 @@ class SessionConnection final { future_presentation_infos); private: - const std::string debug_label_; scenic::Session session_wrapper_; - scenic::View root_view_; - scenic::EntityNode root_node_; - - std::unique_ptr surface_producer_; - flutter::SceneUpdateContext scene_update_context_; on_frame_presented_event on_frame_presented_callback_; zx_handle_t vsync_event_handle_; @@ -122,8 +78,6 @@ class SessionConnection final { bool present_session_pending_ = false; - void EnqueueClearOps(); - void PresentSession(); static void ToggleSignal(zx_handle_t handle, bool raise); diff --git a/shell/platform/fuchsia/flutter/surface.cc b/shell/platform/fuchsia/flutter/surface.cc index ba916fce2a41e..fdcb42a3c7dd7 100644 --- a/shell/platform/fuchsia/flutter/surface.cc +++ b/shell/platform/fuchsia/flutter/surface.cc @@ -14,8 +14,11 @@ namespace flutter_runner { Surface::Surface(std::string debug_label, - flutter::ExternalViewEmbedder* view_embedder) - : debug_label_(std::move(debug_label)), view_embedder_(view_embedder) {} + flutter::ExternalViewEmbedder* view_embedder, + GrDirectContext* gr_context) + : debug_label_(std::move(debug_label)), + view_embedder_(view_embedder), + gr_context_(gr_context) {} Surface::~Surface() = default; @@ -36,7 +39,7 @@ std::unique_ptr Surface::AcquireFrame( // |flutter::Surface| GrDirectContext* Surface::GetContext() { - return nullptr; + return gr_context_; } static zx_status_t DriverWatcher(int dirfd, diff --git a/shell/platform/fuchsia/flutter/surface.h b/shell/platform/fuchsia/flutter/surface.h index 7040f0b742663..8ebb89de0da24 100644 --- a/shell/platform/fuchsia/flutter/surface.h +++ b/shell/platform/fuchsia/flutter/surface.h @@ -16,7 +16,8 @@ namespace flutter_runner { class Surface final : public flutter::Surface { public: Surface(std::string debug_label, - flutter::ExternalViewEmbedder* view_embedder); + flutter::ExternalViewEmbedder* view_embedder, + GrDirectContext* gr_context); ~Surface() override; @@ -24,6 +25,7 @@ class Surface final : public flutter::Surface { const bool valid_ = CanConnectToDisplay(); const std::string debug_label_; flutter::ExternalViewEmbedder* view_embedder_; + GrDirectContext* gr_context_; // |flutter::Surface| bool IsValid() override; diff --git a/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc b/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc index 87efa11e95f2a..5a5463266faa8 100644 --- a/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc @@ -2,18 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "gtest/gtest.h" - +#include +#include +#include #include #include -#include - -#include -#include #include "flutter/shell/platform/fuchsia/flutter/logging.h" #include "flutter/shell/platform/fuchsia/flutter/runner.h" #include "flutter/shell/platform/fuchsia/flutter/session_connection.h" +#include "gtest/gtest.h" using namespace flutter_runner; @@ -30,12 +28,8 @@ class SessionConnectionTest : public ::testing::Test { loop_.StartThread("SessionConnectionTestThread", &fidl_thread_)); auto session_listener_request = session_listener_.NewRequest(); - auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); - view_token_ = std::move(view_token); scenic_->CreateSession(session_.NewRequest(), session_listener_.Bind()); - presenter_->PresentOrReplaceView(std::move(view_holder_token), nullptr); - FML_CHECK(zx::event::create(0, &vsync_event_) == ZX_OK); // Ensure Scenic has had time to wake up before the test logic begins. @@ -60,7 +54,6 @@ class SessionConnectionTest : public ::testing::Test { fidl::InterfaceHandle session_; fidl::InterfaceHandle session_listener_; - fuchsia::ui::views::ViewToken view_token_; zx::event vsync_event_; thrd_t fidl_thread_; }; @@ -76,12 +69,11 @@ TEST_F(SessionConnectionTest, SimplePresentTest) { }; flutter_runner::SessionConnection session_connection( - "debug label", std::move(view_token_), scenic::ViewRefPair::New(), - std::move(session_), on_session_error_callback, + "debug label", std::move(session_), on_session_error_callback, on_frame_presented_callback, vsync_event_.get()); for (int i = 0; i < 200; ++i) { - session_connection.Present(nullptr); + session_connection.Present(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } @@ -99,12 +91,11 @@ TEST_F(SessionConnectionTest, BatchedPresentTest) { }; flutter_runner::SessionConnection session_connection( - "debug label", std::move(view_token_), scenic::ViewRefPair::New(), - std::move(session_), on_session_error_callback, + "debug label", std::move(session_), on_session_error_callback, on_frame_presented_callback, vsync_event_.get()); for (int i = 0; i < 200; ++i) { - session_connection.Present(nullptr); + session_connection.Present(); if (i % 10 == 9) { std::this_thread::sleep_for(std::chrono::milliseconds(20)); } diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.cc b/shell/platform/fuchsia/flutter/vulkan_surface.cc index 0d117047e7ea2..1b9961ceca0e7 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface.cc @@ -332,14 +332,19 @@ bool VulkanSurface::SetupSkiaSurface(sk_sp context, return false; } - const GrVkImageInfo image_info = { - vulkan_image_.vk_image, // image - {vk_memory_, 0, memory_reqs.size, 0}, // alloc - image_create_info.tiling, // tiling - image_create_info.initialLayout, // layout - image_create_info.format, // format - image_create_info.mipLevels, // level count - }; + GrVkAlloc alloc; + alloc.fMemory = vk_memory_; + alloc.fOffset = 0; + alloc.fSize = memory_reqs.size; + alloc.fFlags = 0; + + GrVkImageInfo image_info; + image_info.fImage = vulkan_image_.vk_image; + image_info.fAlloc = alloc; + image_info.fImageTiling = image_create_info.tiling; + image_info.fImageLayout = image_create_info.initialLayout; + image_info.fFormat = image_create_info.format; + image_info.fLevelCount = image_create_info.mipLevels; GrBackendRenderTarget sk_render_target(size.width(), size.height(), 0, image_info); diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.h b/shell/platform/fuchsia/flutter/vulkan_surface.h index e6b7eb4e8943a..bff2713a011c9 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -12,17 +13,46 @@ #include #include "flutter/flow/raster_cache_key.h" -#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" #include "flutter/vulkan/vulkan_command_buffer.h" #include "flutter/vulkan/vulkan_handle.h" #include "flutter/vulkan/vulkan_proc_table.h" #include "flutter/vulkan/vulkan_provider.h" -#include "lib/ui/scenic/cpp/resources.h" #include "third_party/skia/include/core/SkSurface.h" namespace flutter_runner { +class SurfaceProducerSurface { + public: + virtual ~SurfaceProducerSurface() = default; + + virtual size_t AdvanceAndGetAge() = 0; + + virtual bool FlushSessionAcquireAndReleaseEvents() = 0; + + virtual bool IsValid() const = 0; + + virtual SkISize GetSize() const = 0; + + virtual void SignalWritesFinished( + const std::function& on_writes_committed) = 0; + + virtual scenic::Image* GetImage() = 0; + + virtual sk_sp GetSkiaSurface() const = 0; +}; + +class SurfaceProducer { + public: + virtual ~SurfaceProducer() = default; + + virtual std::unique_ptr ProduceSurface( + const SkISize& size) = 0; + + virtual void SubmitSurface( + std::unique_ptr surface) = 0; +}; + // A |VkImage| and its relevant metadata. struct VulkanImage { VulkanImage() = default; @@ -44,8 +74,7 @@ bool CreateVulkanImage(vulkan::VulkanProvider& vulkan_provider, const SkISize& size, VulkanImage* out_vulkan_image); -class VulkanSurface final - : public flutter::SceneUpdateContext::SurfaceProducerSurface { +class VulkanSurface final : public SurfaceProducerSurface { public: VulkanSurface(vulkan::VulkanProvider& vulkan_provider, sk_sp context, @@ -54,16 +83,16 @@ class VulkanSurface final ~VulkanSurface() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| size_t AdvanceAndGetAge() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| bool FlushSessionAcquireAndReleaseEvents() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| bool IsValid() const override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| SkISize GetSize() const override; // Note: It is safe for the caller to collect the surface in the @@ -71,10 +100,10 @@ class VulkanSurface final void SignalWritesFinished( const std::function& on_writes_committed) override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| scenic::Image* GetImage() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| sk_sp GetSkiaSurface() const override; const vulkan::VulkanHandle& GetVkImage() { @@ -119,41 +148,6 @@ class VulkanSurface final // if the swap was not successful. bool BindToImage(sk_sp context, VulkanImage vulkan_image); - // Flutter may retain a |VulkanSurface| for a |flutter::Layer| subtree to - // improve the performance. The |retained_key_| identifies which layer subtree - // this |VulkanSurface| is retained for. The key has two parts. One is the - // pointer to the root of that layer subtree: |retained_key_.id()|. Another is - // the transformation matrix: |retained_key_.matrix()|. We need the matrix - // part because a different matrix would invalidate the pixels (raster cache) - // in this |VulkanSurface|. - const flutter::LayerRasterCacheKey& GetRetainedKey() const { - return retained_key_; - } - - // For better safety in retained rendering, Flutter uses a retained - // |EntityNode| associated with the retained surface instead of using the - // retained surface directly. Hence Flutter can't modify the surface during - // retained rendering. However, the node itself is modifiable to be able - // to adjust its position. - scenic::EntityNode* GetRetainedNode() { - used_in_retained_rendering_ = true; - return retained_node_.get(); - } - - // Check whether the retained surface (and its associated |EntityNode|) is - // used in the current frame or not. If unused, the |VulkanSurfacePool| will - // try to recycle the surface. This flag is reset after each frame. - bool IsUsedInRetainedRendering() const { return used_in_retained_rendering_; } - void ResetIsUsedInRetainedRendering() { used_in_retained_rendering_ = false; } - - // Let this surface own the retained EntityNode associated with it (see - // |GetRetainedNode|), and set the retained key (see |GetRetainedKey|). - void SetRetainedInfo(const flutter::LayerRasterCacheKey& key, - std::unique_ptr node) { - retained_key_ = key; - retained_node_ = std::move(node); - } - private: static constexpr int kSizeHistorySize = 4; @@ -202,11 +196,6 @@ class VulkanSurface final size_t age_ = 0; bool valid_ = false; - flutter::LayerRasterCacheKey retained_key_ = {0, SkMatrix::Scale(1, 1)}; - std::unique_ptr retained_node_ = nullptr; - - std::atomic used_in_retained_rendering_ = {false}; - FML_DISALLOW_COPY_AND_ASSIGN(VulkanSurface); }; diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc b/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc index cb40dd25ddbb3..1b85a957898bd 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc @@ -112,8 +112,7 @@ std::unique_ptr VulkanSurfacePool::GetCachedOrCreateSurface( } void VulkanSurfacePool::SubmitSurface( - std::unique_ptr - p_surface) { + std::unique_ptr p_surface) { TRACE_EVENT0("flutter", "VulkanSurfacePool::SubmitSurface"); // This cast is safe because |VulkanSurface| is the only implementation of @@ -126,43 +125,14 @@ void VulkanSurfacePool::SubmitSurface( return; } - const flutter::LayerRasterCacheKey& retained_key = - vulkan_surface->GetRetainedKey(); - - // TODO(https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=44141): Re-enable - // retained surfaces after we find out why textures are being prematurely - // recycled. - const bool kUseRetainedSurfaces = false; - if (kUseRetainedSurfaces && retained_key.id() != 0) { - // Add the surface to |retained_surfaces_| if its retained key has a valid - // layer id (|retained_key.id()|). - // - // We have to add the entry to |retained_surfaces_| map early when it's - // still pending (|is_pending| = true). Otherwise (if we add the surface - // later when |SignalRetainedReady| is called), Flutter would fail to find - // the retained node before the painting is done (which could take multiple - // frames). Flutter would then create a new |VulkanSurface| for the layer - // upon the failed lookup. The new |VulkanSurface| would invalidate this - // surface, and before the new |VulkanSurface| is done painting, another - // newer |VulkanSurface| is likely to be created to replace the new - // |VulkanSurface|. That would make the retained rendering much less useful - // in improving the performance. - auto insert_iterator = retained_surfaces_.insert(std::make_pair( - retained_key, RetainedSurface({true, std::move(vulkan_surface)}))); - if (insert_iterator.second) { - insert_iterator.first->second.vk_surface->SignalWritesFinished(std::bind( - &VulkanSurfacePool::SignalRetainedReady, this, retained_key)); - } - } else { - uintptr_t surface_key = reinterpret_cast(vulkan_surface.get()); - auto insert_iterator = pending_surfaces_.insert(std::make_pair( - surface_key, // key - std::move(vulkan_surface) // value - )); - if (insert_iterator.second) { - insert_iterator.first->second->SignalWritesFinished(std::bind( - &VulkanSurfacePool::RecyclePendingSurface, this, surface_key)); - } + uintptr_t surface_key = reinterpret_cast(vulkan_surface.get()); + auto insert_iterator = pending_surfaces_.insert(std::make_pair( + surface_key, // key + std::move(vulkan_surface) // value + )); + if (insert_iterator.second) { + insert_iterator.first->second->SignalWritesFinished(std::bind( + &VulkanSurfacePool::RecyclePendingSurface, this, surface_key)); } } @@ -213,25 +183,6 @@ void VulkanSurfacePool::RecycleSurface(std::unique_ptr surface) { TraceStats(); } -void VulkanSurfacePool::RecycleRetainedSurface( - const flutter::LayerRasterCacheKey& key) { - auto it = retained_surfaces_.find(key); - if (it == retained_surfaces_.end()) { - return; - } - - // The surface should not be pending. - FML_DCHECK(!it->second.is_pending); - - auto surface_to_recycle = std::move(it->second.vk_surface); - retained_surfaces_.erase(it); - RecycleSurface(std::move(surface_to_recycle)); -} - -void VulkanSurfacePool::SignalRetainedReady(flutter::LayerRasterCacheKey key) { - retained_surfaces_[key].is_pending = false; -} - void VulkanSurfacePool::AgeAndCollectOldBuffers() { TRACE_EVENT0("flutter", "VulkanSurfacePool::AgeAndCollectOldBuffers"); @@ -268,25 +219,6 @@ void VulkanSurfacePool::AgeAndCollectOldBuffers() { } } - // Recycle retained surfaces that are not used and not pending in this frame. - // - // It's safe to recycle any retained surfaces that are not pending no matter - // whether they're used or not. Hence if there's memory pressure, feel free to - // recycle all retained surfaces that are not pending. - std::vector recycle_keys; - for (auto& [key, retained_surface] : retained_surfaces_) { - if (retained_surface.is_pending || - retained_surface.vk_surface->IsUsedInRetainedRendering()) { - // Reset the flag for the next frame - retained_surface.vk_surface->ResetIsUsedInRetainedRendering(); - } else { - recycle_keys.push_back(key); - } - } - for (auto& key : recycle_keys) { - RecycleRetainedSurface(key); - } - TraceStats(); } @@ -320,15 +252,9 @@ void VulkanSurfacePool::ShrinkToFit() { void VulkanSurfacePool::TraceStats() { // Resources held in cached buffers. size_t cached_surfaces_bytes = 0; - size_t retained_surfaces_bytes = 0; - for (const auto& surface : available_surfaces_) { cached_surfaces_bytes += surface->GetAllocationSize(); } - for (const auto& retained_entry : retained_surfaces_) { - retained_surfaces_bytes += - retained_entry.second.vk_surface->GetAllocationSize(); - } // Resources held by Skia. int skia_resources = 0; @@ -342,13 +268,13 @@ void VulkanSurfacePool::TraceStats() { "Created", trace_surfaces_created_, // "Reused", trace_surfaces_reused_, // "PendingInCompositor", pending_surfaces_.size(), // - "Retained", retained_surfaces_.size(), // + "Retained", 0, // "SkiaCacheResources", skia_resources // ); TRACE_COUNTER("flutter", "SurfacePoolBytes", 0u, // "CachedBytes", cached_surfaces_bytes, // - "RetainedBytes", retained_surfaces_bytes, // + "RetainedBytes", 0, // "SkiaCacheBytes", skia_bytes, // "SkiaCachePurgeable", skia_cache_purgeable // ); diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_pool.h b/shell/platform/fuchsia/flutter/vulkan_surface_pool.h index 302be9bac7b8a..0667db88d6c0c 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_pool.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface_pool.h @@ -28,9 +28,7 @@ class VulkanSurfacePool final { std::unique_ptr AcquireSurface(const SkISize& size); - void SubmitSurface( - std::unique_ptr - surface); + void SubmitSurface(std::unique_ptr surface); void AgeAndCollectOldBuffers(); @@ -38,26 +36,7 @@ class VulkanSurfacePool final { // small as they can be. void ShrinkToFit(); - // For |VulkanSurfaceProducer::HasRetainedNode|. - bool HasRetainedNode(const flutter::LayerRasterCacheKey& key) const { - return retained_surfaces_.find(key) != retained_surfaces_.end(); - } - // For |VulkanSurfaceProducer::GetRetainedNode|. - scenic::EntityNode* GetRetainedNode(const flutter::LayerRasterCacheKey& key) { - FML_DCHECK(HasRetainedNode(key)); - return retained_surfaces_[key].vk_surface->GetRetainedNode(); - } - private: - // Struct for retained_surfaces_ map. - struct RetainedSurface { - // If |is_pending| is true, the |vk_surface| is still under painting - // (similar to those in |pending_surfaces_|) so we can't recycle the - // |vk_surface| yet. - bool is_pending; - std::unique_ptr vk_surface; - }; - vulkan::VulkanProvider& vulkan_provider_; sk_sp context_; scenic::Session* scenic_session_; @@ -65,9 +44,6 @@ class VulkanSurfacePool final { std::unordered_map> pending_surfaces_; - // Retained surfaces keyed by the layer that created and used the surface. - flutter::LayerRasterCacheKey::Map retained_surfaces_; - size_t trace_surfaces_created_ = 0; size_t trace_surfaces_reused_ = 0; @@ -79,13 +55,6 @@ class VulkanSurfacePool final { void RecyclePendingSurface(uintptr_t surface_key); - // Clear the |is_pending| flag of the retained surface. - void SignalRetainedReady(flutter::LayerRasterCacheKey key); - - // Remove the corresponding surface from |retained_surfaces| and recycle it. - // The surface must not be pending. - void RecycleRetainedSurface(const flutter::LayerRasterCacheKey& key); - void TraceStats(); FML_DISALLOW_COPY_AND_ASSIGN(VulkanSurfacePool); diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc b/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc index 1e0d03df847a8..36eeadd85afa3 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc @@ -155,9 +155,7 @@ bool VulkanSurfaceProducer::Initialize(scenic::Session* scenic_session) { } void VulkanSurfaceProducer::OnSurfacesPresented( - std::vector< - std::unique_ptr> - surfaces) { + std::vector> surfaces) { TRACE_EVENT0("flutter", "VulkanSurfaceProducer::OnSurfacesPresented"); // Do a single flush for all canvases derived from the context. @@ -197,11 +195,12 @@ void VulkanSurfaceProducer::OnSurfacesPresented( } bool VulkanSurfaceProducer::TransitionSurfacesToExternal( - const std::vector< - std::unique_ptr>& - surfaces) { + const std::vector>& surfaces) { for (auto& surface : surfaces) { auto vk_surface = static_cast(surface.get()); + if (!vk_surface) { + continue; + } vulkan::VulkanCommandBuffer* command_buffer = vk_surface->GetCommandBuffer(logical_device_->GetCommandPool()); @@ -259,21 +258,15 @@ bool VulkanSurfaceProducer::TransitionSurfacesToExternal( return true; } -std::unique_ptr -VulkanSurfaceProducer::ProduceSurface( - const SkISize& size, - const flutter::LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) { +std::unique_ptr VulkanSurfaceProducer::ProduceSurface( + const SkISize& size) { FML_DCHECK(valid_); last_produce_time_ = async::Now(async_get_default_dispatcher()); - auto surface = surface_pool_->AcquireSurface(size); - surface->SetRetainedInfo(layer_key, std::move(entity_node)); - return surface; + return surface_pool_->AcquireSurface(size); } void VulkanSurfaceProducer::SubmitSurface( - std::unique_ptr - surface) { + std::unique_ptr surface) { FML_DCHECK(valid_ && surface != nullptr); surface_pool_->SubmitSurface(std::move(surface)); } diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h index 403ecd19bebbe..2caaf64a36d7f 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h @@ -8,8 +8,8 @@ #include #include -#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" +#include "flutter/fml/memory/weak_ptr.h" #include "flutter/vulkan/vulkan_application.h" #include "flutter/vulkan/vulkan_device.h" #include "flutter/vulkan/vulkan_proc_table.h" @@ -22,9 +22,8 @@ namespace flutter_runner { -class VulkanSurfaceProducer final - : public flutter::SceneUpdateContext::SurfaceProducer, - public vulkan::VulkanProvider { +class VulkanSurfaceProducer final : public SurfaceProducer, + public vulkan::VulkanProvider { public: VulkanSurfaceProducer(scenic::Session* scenic_session); @@ -32,39 +31,15 @@ class VulkanSurfaceProducer final bool IsValid() const { return valid_; } - // |flutter::SceneUpdateContext::SurfaceProducer| - std::unique_ptr - ProduceSurface(const SkISize& size, - const flutter::LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) override; + // |SurfaceProducer| + std::unique_ptr ProduceSurface( + const SkISize& size) override; - // |flutter::SceneUpdateContext::SurfaceProducer| - void SubmitSurface( - std::unique_ptr - surface) override; - - // |flutter::SceneUpdateContext::HasRetainedNode| - bool HasRetainedNode(const flutter::LayerRasterCacheKey& key) const override { - return surface_pool_->HasRetainedNode(key); - } - - // |flutter::SceneUpdateContext::GetRetainedNode| - scenic::EntityNode* GetRetainedNode( - const flutter::LayerRasterCacheKey& key) override { - return surface_pool_->GetRetainedNode(key); - } + // |SurfaceProducer| + void SubmitSurface(std::unique_ptr surface) override; void OnSurfacesPresented( - std::vector< - std::unique_ptr> - surfaces); - - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - FX_LOGF(INFO, LOG_TAG, - "VulkanSurfaceProducer:OnSessionSizeChangeHint %f, %f", - width_change_factor, height_change_factor); - } + std::vector> surfaces); GrDirectContext* gr_context() { return context_.get(); } @@ -76,9 +51,7 @@ class VulkanSurfaceProducer final } bool TransitionSurfacesToExternal( - const std::vector< - std::unique_ptr>& - surfaces); + const std::vector>& surfaces); // Note: the order here is very important. The proctable must be destroyed // last because it contains the function pointers for VkDestroyDevice and diff --git a/shell/platform/glfw/client_wrapper/BUILD.gn b/shell/platform/glfw/client_wrapper/BUILD.gn index d5115de50d84c..3a4f57263ef6f 100644 --- a/shell/platform/glfw/client_wrapper/BUILD.gn +++ b/shell/platform/glfw/client_wrapper/BUILD.gn @@ -78,6 +78,8 @@ executable("client_wrapper_glfw_unittests") { "flutter_window_unittests.cc", ] + defines = [ "FLUTTER_DESKTOP_LIBRARY" ] + deps = [ ":client_wrapper_glfw", ":client_wrapper_glfw_fixtures", diff --git a/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h b/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h index a2a9da9ef3f8b..aa0f03deed16a 100644 --- a/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h +++ b/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h @@ -5,10 +5,10 @@ #ifndef FLUTTER_SHELL_PLATFORM_GLFW_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_GLFW_H_ #define FLUTTER_SHELL_PLATFORM_GLFW_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_GLFW_H_ -#include - #include +#include + #include "flutter_window.h" #include "plugin_registrar.h" @@ -34,6 +34,14 @@ class PluginRegistrarGlfw : public PluginRegistrar { FlutterWindow* window() { return window_.get(); } + // Enables input blocking on the given channel name. + // + // If set, then the parent window should disable input callbacks + // while waiting for the handler for messages on that channel to run. + void EnableInputBlockingForChannel(const std::string& channel) { + FlutterDesktopRegistrarEnableInputBlocking(registrar(), channel.c_str()); + } + private: // The owned FlutterWindow, if any. std::unique_ptr window_; diff --git a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc index 38614fc15c554..8875a1b51d23d 100644 --- a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc +++ b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc @@ -182,3 +182,11 @@ FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(2); } + +void FlutterDesktopRegistrarEnableInputBlocking( + FlutterDesktopPluginRegistrarRef registrar, + const char* channel) { + if (s_stub_implementation) { + s_stub_implementation->RegistrarEnableInputBlocking(channel); + } +} diff --git a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h index 25792e8c14be1..765a02e45f7f5 100644 --- a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h +++ b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h @@ -88,6 +88,9 @@ class StubFlutterGlfwApi { // Called for FlutterDesktopShutDownEngine. virtual bool ShutDownEngine() { return true; } + + // Called for FlutterDesktopRegistrarEnableInputBlocking. + virtual void RegistrarEnableInputBlocking(const char* channel) {} }; // A test helper that owns a stub implementation, making it the test stub for diff --git a/shell/platform/glfw/flutter_glfw.cc b/shell/platform/glfw/flutter_glfw.cc index 802a1d9a73126..6ed101449e9ca 100644 --- a/shell/platform/glfw/flutter_glfw.cc +++ b/shell/platform/glfw/flutter_glfw.cc @@ -184,6 +184,9 @@ static FlutterDesktopMessage ConvertToDesktopMessage( // that a screen coordinate is one dp. static double GetScreenCoordinatesPerInch() { auto* primary_monitor = glfwGetPrimaryMonitor(); + if (primary_monitor == nullptr) { + return kDpPerInch; + } auto* primary_monitor_mode = glfwGetVideoMode(primary_monitor); int primary_monitor_width_mm; glfwGetMonitorPhysicalSize(primary_monitor, &primary_monitor_width_mm, diff --git a/shell/platform/glfw/public/flutter_glfw.h b/shell/platform/glfw/public/flutter_glfw.h index 45a6420f93c87..e8b4513877852 100644 --- a/shell/platform/glfw/public/flutter_glfw.h +++ b/shell/platform/glfw/public/flutter_glfw.h @@ -219,10 +219,26 @@ FLUTTER_EXPORT bool FlutterDesktopShutDownEngine( FlutterDesktopEngineRef engine); // Returns the window associated with this registrar's engine instance. +// // This is a GLFW shell-specific extension to flutter_plugin_registrar.h FLUTTER_EXPORT FlutterDesktopWindowRef FlutterDesktopRegistrarGetWindow(FlutterDesktopPluginRegistrarRef registrar); +// Enables input blocking on the given channel. +// +// If set, then the Flutter window will disable input callbacks +// while waiting for the handler for messages on that channel to run. This is +// useful if handling the message involves showing a modal window, for instance. +// +// This must be called after FlutterDesktopSetMessageHandler, as setting a +// handler on a channel will reset the input blocking state back to the +// default of disabled. +// +// This is a GLFW shell-specific extension to flutter_plugin_registrar.h +FLUTTER_EXPORT void FlutterDesktopRegistrarEnableInputBlocking( + FlutterDesktopPluginRegistrarRef registrar, + const char* channel); + #if defined(__cplusplus) } // extern "C" #endif diff --git a/shell/platform/linux/fl_basic_message_channel.cc b/shell/platform/linux/fl_basic_message_channel.cc index 405ae0e0427f1..5411c4a6d139f 100644 --- a/shell/platform/linux/fl_basic_message_channel.cc +++ b/shell/platform/linux/fl_basic_message_channel.cc @@ -117,8 +117,9 @@ static void channel_closed_cb(gpointer user_data) { self->channel_closed = TRUE; // Disconnect handler. - if (self->message_handler_destroy_notify != nullptr) + if (self->message_handler_destroy_notify != nullptr) { self->message_handler_destroy_notify(self->message_handler_data); + } self->message_handler = nullptr; self->message_handler_data = nullptr; self->message_handler_destroy_notify = nullptr; @@ -136,8 +137,9 @@ static void fl_basic_message_channel_dispose(GObject* object) { g_clear_pointer(&self->name, g_free); g_clear_object(&self->codec); - if (self->message_handler_destroy_notify != nullptr) + if (self->message_handler_destroy_notify != nullptr) { self->message_handler_destroy_notify(self->message_handler_data); + } self->message_handler = nullptr; self->message_handler_data = nullptr; self->message_handler_destroy_notify = nullptr; @@ -187,13 +189,15 @@ G_MODULE_EXPORT void fl_basic_message_channel_set_message_handler( g_warning( "Attempted to set message handler on a closed FlBasicMessageChannel"); } - if (destroy_notify != nullptr) + if (destroy_notify != nullptr) { destroy_notify(user_data); + } return; } - if (self->message_handler_destroy_notify != nullptr) + if (self->message_handler_destroy_notify != nullptr) { self->message_handler_destroy_notify(self->message_handler_data); + } self->message_handler = handler; self->message_handler_data = user_data; @@ -211,8 +215,9 @@ G_MODULE_EXPORT gboolean fl_basic_message_channel_respond( g_autoptr(GBytes) data = fl_message_codec_encode_message(self->codec, message, error); - if (data == nullptr) + if (data == nullptr) { return FALSE; + } gboolean result = fl_binary_messenger_send_response( self->messenger, response_handle->response_handle, data, error); @@ -237,8 +242,9 @@ G_MODULE_EXPORT void fl_basic_message_channel_send(FlBasicMessageChannel* self, g_autoptr(GBytes) data = fl_message_codec_encode_message(self->codec, message, &error); if (data == nullptr) { - if (task != nullptr) + if (task != nullptr) { g_task_return_error(task, error); + } return; } @@ -260,8 +266,9 @@ G_MODULE_EXPORT FlValue* fl_basic_message_channel_send_finish( g_autoptr(GBytes) message = fl_binary_messenger_send_on_channel_finish(self->messenger, r, error); - if (message == nullptr) + if (message == nullptr) { return nullptr; + } return fl_message_codec_decode_message(self->codec, message, error); } diff --git a/shell/platform/linux/fl_binary_messenger.cc b/shell/platform/linux/fl_binary_messenger.cc index 5a743fa2b9267..e817fd36225ba 100644 --- a/shell/platform/linux/fl_binary_messenger.cc +++ b/shell/platform/linux/fl_binary_messenger.cc @@ -43,8 +43,9 @@ static void fl_binary_messenger_response_handle_dispose(GObject* object) { FlBinaryMessengerResponseHandle* self = FL_BINARY_MESSENGER_RESPONSE_HANDLE(object); - if (self->response_handle != nullptr && self->messenger->engine != nullptr) + if (self->response_handle != nullptr && self->messenger->engine != nullptr) { g_critical("FlBinaryMessengerResponseHandle was not responded to"); + } g_clear_object(&self->messenger); self->response_handle = nullptr; @@ -93,8 +94,9 @@ static PlatformMessageHandler* platform_message_handler_new( static void platform_message_handler_free(gpointer data) { PlatformMessageHandler* self = static_cast(data); - if (self->message_handler_destroy_notify) + if (self->message_handler_destroy_notify) { self->message_handler_destroy_notify(self->message_handler_data); + } g_free(self); } @@ -116,8 +118,9 @@ static gboolean fl_binary_messenger_platform_message_cb( PlatformMessageHandler* handler = static_cast( g_hash_table_lookup(self->platform_message_handlers, channel)); - if (handler == nullptr) + if (handler == nullptr) { return FALSE; + } g_autoptr(FlBinaryMessengerResponseHandle) handle = fl_binary_messenger_response_handle_new(self, response_handle); @@ -180,8 +183,9 @@ G_MODULE_EXPORT void fl_binary_messenger_set_message_handler_on_channel( "Attempted to set message handler on an FlBinaryMessenger without an " "engine"); } - if (destroy_notify != nullptr) + if (destroy_notify != nullptr) { destroy_notify(user_data); + } return; } @@ -204,8 +208,9 @@ G_MODULE_EXPORT gboolean fl_binary_messenger_send_response( g_return_val_if_fail(response_handle->messenger == self, FALSE); g_return_val_if_fail(response_handle->response_handle != nullptr, FALSE); - if (self->engine == nullptr) + if (self->engine == nullptr) { return TRUE; + } if (response_handle->response_handle == nullptr) { g_set_error( @@ -239,8 +244,9 @@ G_MODULE_EXPORT void fl_binary_messenger_send_on_channel( g_return_if_fail(FL_IS_BINARY_MESSENGER(self)); g_return_if_fail(channel != nullptr); - if (self->engine == nullptr) + if (self->engine == nullptr) { return; + } fl_engine_send_platform_message( self->engine, channel, message, cancellable, @@ -259,8 +265,9 @@ G_MODULE_EXPORT GBytes* fl_binary_messenger_send_on_channel_finish( g_autoptr(GTask) task = G_TASK(result); GAsyncResult* r = G_ASYNC_RESULT(g_task_propagate_pointer(task, nullptr)); - if (self->engine == nullptr) + if (self->engine == nullptr) { return nullptr; + } return fl_engine_send_platform_message_finish(self->engine, r, error); } diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc index 13e5c22088d7c..7308bb792c64a 100644 --- a/shell/platform/linux/fl_engine.cc +++ b/shell/platform/linux/fl_engine.cc @@ -94,34 +94,42 @@ static void parse_locale(const gchar* locale, // Passes locale information to the Flutter engine. static void setup_locales(FlEngine* self) { const gchar* const* languages = g_get_language_names(); - g_autoptr(GPtrArray) locales = g_ptr_array_new_with_free_func(g_free); + g_autoptr(GPtrArray) locales_array = g_ptr_array_new_with_free_func(g_free); // Helper array to take ownership of the strings passed to Flutter. g_autoptr(GPtrArray) locale_strings = g_ptr_array_new_with_free_func(g_free); for (int i = 0; languages[i] != nullptr; i++) { gchar *language, *territory, *codeset, *modifier; parse_locale(languages[i], &language, &territory, &codeset, &modifier); - if (language != nullptr) + if (language != nullptr) { g_ptr_array_add(locale_strings, language); - if (territory != nullptr) + } + if (territory != nullptr) { g_ptr_array_add(locale_strings, territory); - if (codeset != nullptr) + } + if (codeset != nullptr) { g_ptr_array_add(locale_strings, codeset); - if (modifier != nullptr) + } + if (modifier != nullptr) { g_ptr_array_add(locale_strings, modifier); + } FlutterLocale* locale = static_cast(g_malloc0(sizeof(FlutterLocale))); - g_ptr_array_add(locales, locale); + g_ptr_array_add(locales_array, locale); locale->struct_size = sizeof(FlutterLocale); locale->language_code = language; locale->country_code = territory; locale->script_code = codeset; locale->variant_code = modifier; } + FlutterLocale** locales = + reinterpret_cast(locales_array->pdata); FlutterEngineResult result = FlutterEngineUpdateLocales( - self->engine, (const FlutterLocale**)locales->pdata, locales->len); - if (result != kSuccess) + self->engine, const_cast(locales), + locales_array->len); + if (result != kSuccess) { g_warning("Failed to set up Flutter locales"); + } } // Callback to run a Flutter task in the GLib main loop. @@ -133,8 +141,9 @@ static gboolean flutter_source_dispatch(GSource* source, FlutterEngineResult result = FlutterEngineRunTask(self->engine, &fl_source->task); - if (result != kSuccess) + if (result != kSuccess) { g_warning("Failed to run Flutter task\n"); + } return G_SOURCE_REMOVE; } @@ -160,8 +169,9 @@ static bool fl_engine_gl_make_current(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_make_current(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -169,8 +179,9 @@ static bool fl_engine_gl_clear_current(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_clear_current(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -183,8 +194,9 @@ static bool fl_engine_gl_present(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_present(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -192,8 +204,9 @@ static bool fl_engine_gl_make_resource_current(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_make_resource_current(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -246,7 +259,7 @@ static void fl_engine_platform_message_response_cb(const uint8_t* data, void* user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_task_return_pointer(task, g_bytes_new(data, data_length), - (GDestroyNotify)g_bytes_unref); + reinterpret_cast(g_bytes_unref)); } // Implements FlPluginRegistry::get_registrar_for_plugin. @@ -318,8 +331,9 @@ G_MODULE_EXPORT FlEngine* fl_engine_new_headless(FlDartProject* project) { gboolean fl_engine_start(FlEngine* self, GError** error) { g_return_val_if_fail(FL_IS_ENGINE(self), FALSE); - if (!fl_renderer_start(self->renderer, error)) + if (!fl_renderer_start(self->renderer, error)) { return FALSE; + } FlutterRendererConfig config = {}; config.type = kOpenGL; @@ -499,8 +513,9 @@ void fl_engine_send_platform_message(FlEngine* self, g_object_unref(task); } - if (response_handle != nullptr) + if (response_handle != nullptr) { FlutterPlatformMessageReleaseResponseHandle(self->engine, response_handle); + } } GBytes* fl_engine_send_platform_message_finish(FlEngine* self, @@ -518,8 +533,9 @@ void fl_engine_send_window_metrics_event(FlEngine* self, double pixel_ratio) { g_return_if_fail(FL_IS_ENGINE(self)); - if (self->engine == nullptr) + if (self->engine == nullptr) { return; + } FlutterWindowMetricsEvent event = {}; event.struct_size = sizeof(FlutterWindowMetricsEvent); @@ -539,8 +555,9 @@ void fl_engine_send_mouse_pointer_event(FlEngine* self, int64_t buttons) { g_return_if_fail(FL_IS_ENGINE(self)); - if (self->engine == nullptr) + if (self->engine == nullptr) { return; + } FlutterPointerEvent fl_event = {}; fl_event.struct_size = sizeof(fl_event); @@ -548,8 +565,9 @@ void fl_engine_send_mouse_pointer_event(FlEngine* self, fl_event.timestamp = timestamp; fl_event.x = x; fl_event.y = y; - if (scroll_delta_x != 0 || scroll_delta_y != 0) + if (scroll_delta_x != 0 || scroll_delta_y != 0) { fl_event.signal_kind = kFlutterPointerSignalKindScroll; + } fl_event.scroll_delta_x = scroll_delta_x; fl_event.scroll_delta_y = scroll_delta_y; fl_event.device_kind = kFlutterPointerDeviceKindMouse; diff --git a/shell/platform/linux/fl_json_method_codec.cc b/shell/platform/linux/fl_json_method_codec.cc index 63e04582a3993..7ea1b2c74e992 100644 --- a/shell/platform/linux/fl_json_method_codec.cc +++ b/shell/platform/linux/fl_json_method_codec.cc @@ -56,8 +56,9 @@ static gboolean fl_json_method_codec_decode_method_call(FlMethodCodec* codec, g_autoptr(FlValue) value = fl_message_codec_decode_message( FL_MESSAGE_CODEC(self->codec), message, error); - if (value == nullptr) + if (value == nullptr) { return FALSE; + } if (fl_value_get_type(value) != FL_VALUE_TYPE_MAP) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -131,8 +132,9 @@ static FlMethodResponse* fl_json_method_codec_decode_response( g_autoptr(FlValue) value = fl_message_codec_decode_message( FL_MESSAGE_CODEC(self->codec), message, error); - if (value == nullptr) + if (value == nullptr) { return nullptr; + } if (fl_value_get_type(value) != FL_VALUE_TYPE_LIST) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -167,8 +169,9 @@ static FlMethodResponse* fl_json_method_codec_decode_response( : nullptr; FlValue* args = fl_value_get_list_value(value, 2); - if (fl_value_get_type(args) == FL_VALUE_TYPE_NULL) + if (fl_value_get_type(args) == FL_VALUE_TYPE_NULL) { args = nullptr; + } return FL_METHOD_RESPONSE( fl_method_error_response_new(code, message, args)); diff --git a/shell/platform/linux/fl_method_channel.cc b/shell/platform/linux/fl_method_channel.cc index 70ab771d3aa00..c276c233765a6 100644 --- a/shell/platform/linux/fl_method_channel.cc +++ b/shell/platform/linux/fl_method_channel.cc @@ -44,8 +44,9 @@ static void message_cb(FlBinaryMessenger* messenger, gpointer user_data) { FlMethodChannel* self = FL_METHOD_CHANNEL(user_data); - if (self->method_call_handler == nullptr) + if (self->method_call_handler == nullptr) { return; + } g_autofree gchar* method = nullptr; g_autoptr(FlValue) args = nullptr; @@ -76,8 +77,9 @@ static void channel_closed_cb(gpointer user_data) { self->channel_closed = TRUE; // Disconnect handler. - if (self->method_call_handler_destroy_notify != nullptr) + if (self->method_call_handler_destroy_notify != nullptr) { self->method_call_handler_destroy_notify(self->method_call_handler_data); + } self->method_call_handler = nullptr; self->method_call_handler_data = nullptr; self->method_call_handler_destroy_notify = nullptr; @@ -95,8 +97,9 @@ static void fl_method_channel_dispose(GObject* object) { g_clear_pointer(&self->name, g_free); g_clear_object(&self->codec); - if (self->method_call_handler_destroy_notify != nullptr) + if (self->method_call_handler_destroy_notify != nullptr) { self->method_call_handler_destroy_notify(self->method_call_handler_data); + } self->method_call_handler = nullptr; self->method_call_handler_data = nullptr; self->method_call_handler_destroy_notify = nullptr; @@ -145,13 +148,15 @@ G_MODULE_EXPORT void fl_method_channel_set_method_call_handler( g_warning( "Attempted to set method call handler on a closed FlMethodChannel"); } - if (destroy_notify != nullptr) + if (destroy_notify != nullptr) { destroy_notify(user_data); + } return; } - if (self->method_call_handler_destroy_notify != nullptr) + if (self->method_call_handler_destroy_notify != nullptr) { self->method_call_handler_destroy_notify(self->method_call_handler_data); + } self->method_call_handler = handler; self->method_call_handler_data = user_data; @@ -176,8 +181,9 @@ G_MODULE_EXPORT void fl_method_channel_invoke_method( g_autoptr(GBytes) message = fl_method_codec_encode_method_call(self->codec, method, args, &error); if (message == nullptr) { - if (task != nullptr) + if (task != nullptr) { g_task_return_error(task, error); + } return; } @@ -199,8 +205,9 @@ G_MODULE_EXPORT FlMethodResponse* fl_method_channel_invoke_method_finish( g_autoptr(GBytes) response = fl_binary_messenger_send_on_channel_finish(self->messenger, r, error); - if (response == nullptr) + if (response == nullptr) { return nullptr; + } return fl_method_codec_decode_response(self->codec, response, error); } diff --git a/shell/platform/linux/fl_method_codec.cc b/shell/platform/linux/fl_method_codec.cc index fd9d667b15316..b51979f88eb1a 100644 --- a/shell/platform/linux/fl_method_codec.cc +++ b/shell/platform/linux/fl_method_codec.cc @@ -68,8 +68,9 @@ FlMethodResponse* fl_method_codec_decode_response(FlMethodCodec* self, g_return_val_if_fail(FL_IS_METHOD_CODEC(self), nullptr); g_return_val_if_fail(message != nullptr, nullptr); - if (g_bytes_get_size(message) == 0) + if (g_bytes_get_size(message) == 0) { return FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } return FL_METHOD_CODEC_GET_CLASS(self)->decode_response(self, message, error); } diff --git a/shell/platform/linux/fl_renderer.cc b/shell/platform/linux/fl_renderer.cc index d74395fa0bde3..4baf50df92424 100644 --- a/shell/platform/linux/fl_renderer.cc +++ b/shell/platform/linux/fl_renderer.cc @@ -21,29 +21,6 @@ typedef struct { G_DEFINE_TYPE_WITH_PRIVATE(FlRenderer, fl_renderer, G_TYPE_OBJECT) -// Creates a resource surface. -static void create_resource_surface(FlRenderer* self, EGLConfig config) { - FlRendererPrivate* priv = - static_cast(fl_renderer_get_instance_private(self)); - - EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; - const EGLint resource_context_attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, - EGL_NONE}; - priv->resource_surface = eglCreatePbufferSurface(priv->egl_display, config, - resource_context_attribs); - if (priv->resource_surface == EGL_NO_SURFACE) { - g_warning("Failed to create EGL resource surface: %s", - egl_error_to_string(eglGetError())); - return; - } - - priv->resource_context = eglCreateContext( - priv->egl_display, config, priv->egl_context, context_attributes); - if (priv->resource_context == EGL_NO_CONTEXT) - g_warning("Failed to create EGL resource context: %s", - egl_error_to_string(eglGetError())); -} - static void fl_renderer_class_init(FlRendererClass* klass) {} static void fl_renderer_init(FlRenderer* self) {} @@ -52,12 +29,7 @@ gboolean fl_renderer_setup(FlRenderer* self, GError** error) { FlRendererPrivate* priv = static_cast(fl_renderer_get_instance_private(self)); - // Note the use of EGL_DEFAULT_DISPLAY rather than sharing an existing - // display connection (e.g. an X11 connection from GTK). This is because - // this EGL display is going to be accessed by a thread from Flutter. In the - // case of GTK/X11 the display connection is not thread safe and this would - // cause a crash. - priv->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + priv->egl_display = FL_RENDERER_GET_CLASS(self)->create_display(self); if (!eglInitialize(priv->egl_display, nullptr, nullptr)) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, @@ -93,7 +65,7 @@ gboolean fl_renderer_setup(FlRenderer* self, GError** error) { } if (!eglBindAPI(EGL_OPENGL_ES_API)) { - EGLint egl_error = eglGetError(); // must be before egl_config_to_string(). + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). g_autofree gchar* config_string = egl_config_to_string(priv->egl_display, priv->egl_config); g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, @@ -130,40 +102,63 @@ GdkVisual* fl_renderer_get_visual(FlRenderer* self, return visual; } +void fl_renderer_set_window(FlRenderer* self, GdkWindow* window) { + g_return_if_fail(FL_IS_RENDERER(self)); + g_return_if_fail(GDK_IS_WINDOW(window)); + + if (FL_RENDERER_GET_CLASS(self)->set_window) { + FL_RENDERER_GET_CLASS(self)->set_window(self, window); + } +} + gboolean fl_renderer_start(FlRenderer* self, GError** error) { FlRendererPrivate* priv = static_cast(fl_renderer_get_instance_private(self)); - priv->egl_surface = FL_RENDERER_GET_CLASS(self)->create_surface( - self, priv->egl_display, priv->egl_config); - if (priv->egl_surface == EGL_NO_SURFACE) { - EGLint egl_error = eglGetError(); // must be before egl_config_to_string(). - g_autofree gchar* config_string = - egl_config_to_string(priv->egl_display, priv->egl_config); + if (priv->egl_surface != EGL_NO_SURFACE || + priv->resource_surface != EGL_NO_SURFACE) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, - "Failed to create EGL surface using configuration (%s): %s", - config_string, egl_error_to_string(egl_error)); + "fl_renderer_start() called after surfaces already created"); + return FALSE; + } + + if (!FL_RENDERER_GET_CLASS(self)->create_surfaces( + self, priv->egl_display, priv->egl_config, &priv->egl_surface, + &priv->resource_surface, error)) { return FALSE; } EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; + priv->egl_context = eglCreateContext(priv->egl_display, priv->egl_config, EGL_NO_CONTEXT, context_attributes); - if (priv->egl_context == EGL_NO_CONTEXT) { - EGLint egl_error = eglGetError(); // must be before egl_config_to_string(). + priv->resource_context = + eglCreateContext(priv->egl_display, priv->egl_config, priv->egl_context, + context_attributes); + if (priv->egl_context == EGL_NO_CONTEXT || + priv->resource_context == EGL_NO_CONTEXT) { + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). g_autofree gchar* config_string = egl_config_to_string(priv->egl_display, priv->egl_config); g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, - "Failed to create EGL context using configuration (%s): %s", + "Failed to create EGL contexts using configuration (%s): %s", config_string, egl_error_to_string(egl_error)); return FALSE; } - create_resource_surface(self, priv->egl_config); - return TRUE; } +void fl_renderer_set_geometry(FlRenderer* self, + GdkRectangle* geometry, + gint scale) { + g_return_if_fail(FL_IS_RENDERER(self)); + + if (FL_RENDERER_GET_CLASS(self)->set_geometry) { + FL_RENDERER_GET_CLASS(self)->set_geometry(self, geometry, scale); + } +} + void* fl_renderer_get_proc_address(FlRenderer* self, const char* name) { return reinterpret_cast(eglGetProcAddress(name)); } @@ -172,6 +167,13 @@ gboolean fl_renderer_make_current(FlRenderer* self, GError** error) { FlRendererPrivate* priv = static_cast(fl_renderer_get_instance_private(self)); + if (priv->egl_surface == EGL_NO_SURFACE || + priv->egl_context == EGL_NO_CONTEXT) { + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to make EGL context current: No surface created"); + return FALSE; + } + if (!eglMakeCurrent(priv->egl_display, priv->egl_surface, priv->egl_surface, priv->egl_context)) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, @@ -188,13 +190,17 @@ gboolean fl_renderer_make_resource_current(FlRenderer* self, GError** error) { static_cast(fl_renderer_get_instance_private(self)); if (priv->resource_surface == EGL_NO_SURFACE || - priv->resource_context == EGL_NO_CONTEXT) + priv->resource_context == EGL_NO_CONTEXT) { + g_set_error( + error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to make EGL resource context current: No surface created"); return FALSE; + } if (!eglMakeCurrent(priv->egl_display, priv->resource_surface, priv->resource_surface, priv->resource_context)) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, - "Failed to make EGL context current: %s", + "Failed to make EGL resource context current: %s", egl_error_to_string(eglGetError())); return FALSE; } diff --git a/shell/platform/linux/fl_renderer.h b/shell/platform/linux/fl_renderer.h index 25042f9666986..d355bfd565996 100644 --- a/shell/platform/linux/fl_renderer.h +++ b/shell/platform/linux/fl_renderer.h @@ -40,10 +40,42 @@ struct _FlRendererClass { GdkScreen* screen, EGLint visual_id); - // Virtual method called when Flutter needs a surface to render to. - EGLSurface (*create_surface)(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config); + /** + * Virtual method called after a GDK window has been created. + * This is called once. Does not need to be implemented. + */ + void (*set_window)(FlRenderer* renderer, GdkWindow* window); + + /** + * Virtual method to create a new EGL display. + */ + EGLDisplay (*create_display)(FlRenderer* renderer); + + /** + * Virtual method called when Flutter needs surfaces to render to. + * @renderer: an #FlRenderer. + * @display: display to create surfaces on. + * @visible: (out): the visible surface that is created. + * @resource: (out): the resource surface that is created. + * @error: (allow-none): #GError location to store the error occurring, or + * %NULL to ignore. + * + * Returns: %TRUE if both surfaces were created, %FALSE if there was an error. + */ + gboolean (*create_surfaces)(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error); + + /** + * Virtual method called when the EGL window needs to be resized. + * Does not need to be implemented. + */ + void (*set_geometry)(FlRenderer* renderer, + GdkRectangle* geometry, + gint scale); }; /** @@ -56,7 +88,7 @@ struct _FlRendererClass { * * Returns: %TRUE if successfully setup. */ -gboolean fl_renderer_setup(FlRenderer* self, GError** error); +gboolean fl_renderer_setup(FlRenderer* renderer, GError** error); /** * fl_renderer_get_visual: @@ -69,10 +101,19 @@ gboolean fl_renderer_setup(FlRenderer* self, GError** error); * * Returns: a #GdkVisual. */ -GdkVisual* fl_renderer_get_visual(FlRenderer* self, +GdkVisual* fl_renderer_get_visual(FlRenderer* renderer, GdkScreen* screen, GError** error); +/** + * fl_renderer_set_window: + * @renderer: an #FlRenderer. + * @window: the GDK Window this renderer will render to. + * + * Set the window this renderer will use. + */ +void fl_renderer_set_window(FlRenderer* renderer, GdkWindow* window); + /** * fl_renderer_start: * @renderer: an #FlRenderer. @@ -83,7 +124,17 @@ GdkVisual* fl_renderer_get_visual(FlRenderer* self, * * Returns: %TRUE if successfully started. */ -gboolean fl_renderer_start(FlRenderer* self, GError** error); +gboolean fl_renderer_start(FlRenderer* renderer, GError** error); + +/** + * fl_renderer_set_geometry: + * @renderer: an #FlRenderer. + * @geometry: New size and position (unscaled) of the EGL window. + * @scale: Scale of the window. + */ +void fl_renderer_set_geometry(FlRenderer* renderer, + GdkRectangle* geometry, + gint scale); /** * fl_renderer_get_proc_address: diff --git a/shell/platform/linux/fl_renderer_headless.cc b/shell/platform/linux/fl_renderer_headless.cc index 90f917a2b316f..e7880fb7b8b9d 100644 --- a/shell/platform/linux/fl_renderer_headless.cc +++ b/shell/platform/linux/fl_renderer_headless.cc @@ -10,15 +10,18 @@ struct _FlRendererHeadless { G_DEFINE_TYPE(FlRendererHeadless, fl_renderer_headless, fl_renderer_get_type()) -static EGLSurface fl_renderer_headless_create_surface(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config) { - return EGL_NO_SURFACE; +static gboolean fl_renderer_headless_create_surfaces(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error) { + return FALSE; } static void fl_renderer_headless_class_init(FlRendererHeadlessClass* klass) { - FL_RENDERER_CLASS(klass)->create_surface = - fl_renderer_headless_create_surface; + FL_RENDERER_CLASS(klass)->create_surfaces = + fl_renderer_headless_create_surfaces; } static void fl_renderer_headless_init(FlRendererHeadless* self) {} diff --git a/shell/platform/linux/fl_renderer_x11.cc b/shell/platform/linux/fl_renderer_x11.cc index f4e8dfdfc3ca2..16bd1dca6aa7f 100644 --- a/shell/platform/linux/fl_renderer_x11.cc +++ b/shell/platform/linux/fl_renderer_x11.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "fl_renderer_x11.h" +#include "flutter/shell/platform/linux/egl_utils.h" struct _FlRendererX11 { FlRenderer parent_instance; @@ -20,26 +21,77 @@ static void fl_renderer_x11_dispose(GObject* object) { G_OBJECT_CLASS(fl_renderer_x11_parent_class)->dispose(object); } -// Implments FlRenderer::get_visual. +// Implements FlRenderer::get_visual. static GdkVisual* fl_renderer_x11_get_visual(FlRenderer* renderer, GdkScreen* screen, EGLint visual_id) { return gdk_x11_screen_lookup_visual(GDK_X11_SCREEN(screen), visual_id); } -// Implments FlRenderer::create_surface. -static EGLSurface fl_renderer_x11_create_surface(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config) { +// Implements FlRenderer::set_window. +static void fl_renderer_x11_set_window(FlRenderer* renderer, + GdkWindow* window) { FlRendererX11* self = FL_RENDERER_X11(renderer); - return eglCreateWindowSurface(display, config, - gdk_x11_window_get_xid(self->window), nullptr); + g_return_if_fail(GDK_IS_X11_WINDOW(window)); + g_assert_null(self->window); + self->window = GDK_X11_WINDOW(g_object_ref(window)); +} + +// Implements FlRenderer::create_display. +static EGLDisplay fl_renderer_x11_create_display(FlRenderer* renderer) { + // Note the use of EGL_DEFAULT_DISPLAY rather than sharing the existing + // display connection from GTK. This is because this EGL display is going to + // be accessed by a thread from Flutter. The GTK/X11 display connection is not + // thread safe and would cause a crash. + return eglGetDisplay(EGL_DEFAULT_DISPLAY); +} + +// Implements FlRenderer::create_surfaces. +static gboolean fl_renderer_x11_create_surfaces(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error) { + FlRendererX11* self = FL_RENDERER_X11(renderer); + + if (!self->window) { + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Can not create EGL surface: FlRendererX11::window not set"); + return FALSE; + } + + *visible = eglCreateWindowSurface( + display, config, gdk_x11_window_get_xid(self->window), nullptr); + if (*visible == EGL_NO_SURFACE) { + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). + g_autofree gchar* config_string = egl_config_to_string(display, config); + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to create EGL surface using configuration (%s): %s", + config_string, egl_error_to_string(egl_error)); + return FALSE; + } + + const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + *resource = eglCreatePbufferSurface(display, config, attribs); + if (*resource == EGL_NO_SURFACE) { + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). + g_autofree gchar* config_string = egl_config_to_string(display, config); + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to create EGL resource using configuration (%s): %s", + config_string, egl_error_to_string(egl_error)); + return FALSE; + } + + return TRUE; } static void fl_renderer_x11_class_init(FlRendererX11Class* klass) { G_OBJECT_CLASS(klass)->dispose = fl_renderer_x11_dispose; FL_RENDERER_CLASS(klass)->get_visual = fl_renderer_x11_get_visual; - FL_RENDERER_CLASS(klass)->create_surface = fl_renderer_x11_create_surface; + FL_RENDERER_CLASS(klass)->set_window = fl_renderer_x11_set_window; + FL_RENDERER_CLASS(klass)->create_display = fl_renderer_x11_create_display; + FL_RENDERER_CLASS(klass)->create_surfaces = fl_renderer_x11_create_surfaces; } static void fl_renderer_x11_init(FlRendererX11* self) {} @@ -47,8 +99,3 @@ static void fl_renderer_x11_init(FlRendererX11* self) {} FlRendererX11* fl_renderer_x11_new() { return FL_RENDERER_X11(g_object_new(fl_renderer_x11_get_type(), nullptr)); } - -void fl_renderer_x11_set_window(FlRendererX11* self, GdkX11Window* window) { - g_return_if_fail(FL_IS_RENDERER_X11(self)); - self->window = GDK_X11_WINDOW(g_object_ref(window)); -} diff --git a/shell/platform/linux/fl_renderer_x11.h b/shell/platform/linux/fl_renderer_x11.h index 92d808591da70..8c32dcd0bc31b 100644 --- a/shell/platform/linux/fl_renderer_x11.h +++ b/shell/platform/linux/fl_renderer_x11.h @@ -33,15 +33,6 @@ G_DECLARE_FINAL_TYPE(FlRendererX11, */ FlRendererX11* fl_renderer_x11_new(); -/** - * fl_renderer_x11_set_window: - * @renderer: an #FlRendererX11. - * @window: the X window being rendered to. - * - * Sets the X11 window that is being rendered to. - */ -void fl_renderer_x11_set_window(FlRendererX11* renderer, GdkX11Window* window); - G_END_DECLS #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_RENDERER_X11_H_ diff --git a/shell/platform/linux/fl_standard_message_codec.cc b/shell/platform/linux/fl_standard_message_codec.cc index d06f3ad9454d7..61d7193dc0edc 100644 --- a/shell/platform/linux/fl_standard_message_codec.cc +++ b/shell/platform/linux/fl_standard_message_codec.cc @@ -66,8 +66,9 @@ static void write_float64(GByteArray* buffer, double value) { // Write padding bytes to align to @align multiple of bytes. static void write_align(GByteArray* buffer, guint align) { - while (buffer->len % align != 0) + while (buffer->len % align != 0) { write_uint8(buffer, 0); + } } // Checks there is enough data in @buffer to be read. @@ -88,12 +89,14 @@ static gboolean read_align(GBytes* buffer, size_t* offset, size_t align, GError** error) { - if ((*offset) % align == 0) + if ((*offset) % align == 0) { return TRUE; + } size_t required = align - (*offset) % align; - if (!check_size(buffer, *offset, required, error)) + if (!check_size(buffer, *offset, required, error)) { return FALSE; + } (*offset) += required; return TRUE; @@ -111,8 +114,9 @@ static gboolean read_uint8(GBytes* buffer, size_t* offset, uint8_t* value, GError** error) { - if (!check_size(buffer, *offset, sizeof(uint8_t), error)) + if (!check_size(buffer, *offset, sizeof(uint8_t), error)) { return FALSE; + } *value = get_data(buffer, offset)[0]; (*offset)++; @@ -125,8 +129,9 @@ static gboolean read_uint16(GBytes* buffer, size_t* offset, uint16_t* value, GError** error) { - if (!check_size(buffer, *offset, sizeof(uint16_t), error)) + if (!check_size(buffer, *offset, sizeof(uint16_t), error)) { return FALSE; + } *value = reinterpret_cast(get_data(buffer, offset))[0]; *offset += sizeof(uint16_t); @@ -139,8 +144,9 @@ static gboolean read_uint32(GBytes* buffer, size_t* offset, uint32_t* value, GError** error) { - if (!check_size(buffer, *offset, sizeof(uint32_t), error)) + if (!check_size(buffer, *offset, sizeof(uint32_t), error)) { return FALSE; + } *value = reinterpret_cast(get_data(buffer, offset))[0]; *offset += sizeof(uint32_t); @@ -153,8 +159,9 @@ static gboolean read_uint32(GBytes* buffer, static FlValue* read_int32_value(GBytes* buffer, size_t* offset, GError** error) { - if (!check_size(buffer, *offset, sizeof(int32_t), error)) + if (!check_size(buffer, *offset, sizeof(int32_t), error)) { return nullptr; + } FlValue* value = fl_value_new_int( reinterpret_cast(get_data(buffer, offset))[0]); @@ -168,8 +175,9 @@ static FlValue* read_int32_value(GBytes* buffer, static FlValue* read_int64_value(GBytes* buffer, size_t* offset, GError** error) { - if (!check_size(buffer, *offset, sizeof(int64_t), error)) + if (!check_size(buffer, *offset, sizeof(int64_t), error)) { return nullptr; + } FlValue* value = fl_value_new_int( reinterpret_cast(get_data(buffer, offset))[0]); @@ -183,10 +191,12 @@ static FlValue* read_int64_value(GBytes* buffer, static FlValue* read_float64_value(GBytes* buffer, size_t* offset, GError** error) { - if (!read_align(buffer, offset, 8, error)) + if (!read_align(buffer, offset, 8, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(double), error)) + } + if (!check_size(buffer, *offset, sizeof(double), error)) { return nullptr; + } FlValue* value = fl_value_new_float( reinterpret_cast(get_data(buffer, offset))[0]); @@ -203,10 +213,12 @@ static FlValue* read_string_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!check_size(buffer, *offset, length, error)) + } + if (!check_size(buffer, *offset, length, error)) { return nullptr; + } FlValue* value = fl_value_new_string_sized( reinterpret_cast(get_data(buffer, offset)), length); *offset += length; @@ -222,10 +234,12 @@ static FlValue* read_uint8_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(uint8_t) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(uint8_t) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_uint8_list(get_data(buffer, offset), length); *offset += length; return value; @@ -240,12 +254,15 @@ static FlValue* read_int32_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!read_align(buffer, offset, 4, error)) + } + if (!read_align(buffer, offset, 4, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(int32_t) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(int32_t) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_int32_list( reinterpret_cast(get_data(buffer, offset)), length); *offset += sizeof(int32_t) * length; @@ -261,12 +278,15 @@ static FlValue* read_int64_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!read_align(buffer, offset, 8, error)) + } + if (!read_align(buffer, offset, 8, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(int64_t) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(int64_t) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_int64_list( reinterpret_cast(get_data(buffer, offset)), length); *offset += sizeof(int64_t) * length; @@ -282,12 +302,15 @@ static FlValue* read_float64_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!read_align(buffer, offset, 8, error)) + } + if (!read_align(buffer, offset, 8, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(double) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(double) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_float_list( reinterpret_cast(get_data(buffer, offset)), length); *offset += sizeof(double) * length; @@ -303,15 +326,17 @@ static FlValue* read_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; + } g_autoptr(FlValue) list = fl_value_new_list(); for (size_t i = 0; i < length; i++) { g_autoptr(FlValue) child = fl_standard_message_codec_read_value(self, buffer, offset, error); - if (child == nullptr) + if (child == nullptr) { return nullptr; + } fl_value_append(list, child); } @@ -327,19 +352,22 @@ static FlValue* read_map_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; + } g_autoptr(FlValue) map = fl_value_new_map(); for (size_t i = 0; i < length; i++) { g_autoptr(FlValue) key = fl_standard_message_codec_read_value(self, buffer, offset, error); - if (key == nullptr) + if (key == nullptr) { return nullptr; + } g_autoptr(FlValue) value = fl_standard_message_codec_read_value(self, buffer, offset, error); - if (value == nullptr) + if (value == nullptr) { return nullptr; + } fl_value_set(map, key, value); } @@ -354,8 +382,9 @@ static GBytes* fl_standard_message_codec_encode_message(FlMessageCodec* codec, reinterpret_cast(codec); g_autoptr(GByteArray) buffer = g_byte_array_new(); - if (!fl_standard_message_codec_write_value(self, buffer, message, error)) + if (!fl_standard_message_codec_write_value(self, buffer, message, error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); } @@ -370,8 +399,9 @@ static FlValue* fl_standard_message_codec_decode_message(FlMessageCodec* codec, size_t offset = 0; g_autoptr(FlValue) value = fl_standard_message_codec_read_value(self, message, &offset, error); - if (value == nullptr) + if (value == nullptr) { return nullptr; + } if (offset != g_bytes_get_size(message)) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, @@ -419,16 +449,19 @@ gboolean fl_standard_message_codec_read_size(FlStandardMessageCodec* codec, uint32_t* value, GError** error) { uint8_t value8; - if (!read_uint8(buffer, offset, &value8, error)) + if (!read_uint8(buffer, offset, &value8, error)) { return FALSE; + } if (value8 == 255) { - if (!read_uint32(buffer, offset, value, error)) + if (!read_uint32(buffer, offset, value, error)) { return FALSE; + } } else if (value8 == 254) { uint16_t value16; - if (!read_uint16(buffer, offset, &value16, error)) + if (!read_uint16(buffer, offset, &value16, error)) { return FALSE; + } *value = value16; } else { *value = value8; @@ -451,10 +484,11 @@ gboolean fl_standard_message_codec_write_value(FlStandardMessageCodec* self, write_uint8(buffer, kValueNull); return TRUE; case FL_VALUE_TYPE_BOOL: - if (fl_value_get_bool(value)) + if (fl_value_get_bool(value)) { write_uint8(buffer, kValueTrue); - else + } else { write_uint8(buffer, kValueFalse); + } return TRUE; case FL_VALUE_TYPE_INT: { int64_t v = fl_value_get_int(value); @@ -528,8 +562,9 @@ gboolean fl_standard_message_codec_write_value(FlStandardMessageCodec* self, fl_value_get_length(value)); for (size_t i = 0; i < fl_value_get_length(value); i++) { if (!fl_standard_message_codec_write_value( - self, buffer, fl_value_get_list_value(value, i), error)) + self, buffer, fl_value_get_list_value(value, i), error)) { return FALSE; + } } return TRUE; case FL_VALUE_TYPE_MAP: @@ -540,8 +575,9 @@ gboolean fl_standard_message_codec_write_value(FlStandardMessageCodec* self, if (!fl_standard_message_codec_write_value( self, buffer, fl_value_get_map_key(value, i), error) || !fl_standard_message_codec_write_value( - self, buffer, fl_value_get_map_value(value, i), error)) + self, buffer, fl_value_get_map_value(value, i), error)) { return FALSE; + } } return TRUE; } @@ -557,8 +593,9 @@ FlValue* fl_standard_message_codec_read_value(FlStandardMessageCodec* self, size_t* offset, GError** error) { uint8_t type; - if (!read_uint8(buffer, offset, &type, error)) + if (!read_uint8(buffer, offset, &type, error)) { return nullptr; + } g_autoptr(FlValue) value = nullptr; if (type == kValueNull) { diff --git a/shell/platform/linux/fl_standard_method_codec.cc b/shell/platform/linux/fl_standard_method_codec.cc index be191d029ce85..32eff323fed0e 100644 --- a/shell/platform/linux/fl_standard_method_codec.cc +++ b/shell/platform/linux/fl_standard_method_codec.cc @@ -44,10 +44,13 @@ static GBytes* fl_standard_method_codec_encode_method_call(FlMethodCodec* codec, g_autoptr(GByteArray) buffer = g_byte_array_new(); g_autoptr(FlValue) name_value = fl_value_new_string(name); if (!fl_standard_message_codec_write_value(self->codec, buffer, name_value, - error)) + error)) { return nullptr; - if (!fl_standard_message_codec_write_value(self->codec, buffer, args, error)) + } + if (!fl_standard_message_codec_write_value(self->codec, buffer, args, + error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); @@ -65,8 +68,9 @@ static gboolean fl_standard_method_codec_decode_method_call( size_t offset = 0; g_autoptr(FlValue) name_value = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (name_value == nullptr) + if (name_value == nullptr) { return FALSE; + } if (fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, "Method call name wrong type"); @@ -75,8 +79,9 @@ static gboolean fl_standard_method_codec_decode_method_call( g_autoptr(FlValue) args_value = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (args_value == nullptr) + if (args_value == nullptr) { return FALSE; + } if (offset != g_bytes_get_size(message)) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -101,8 +106,9 @@ static GBytes* fl_standard_method_codec_encode_success_envelope( guint8 type = kEnvelopeTypeSuccess; g_byte_array_append(buffer, &type, 1); if (!fl_standard_message_codec_write_value(self->codec, buffer, result, - error)) + error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); @@ -122,16 +128,19 @@ static GBytes* fl_standard_method_codec_encode_error_envelope( g_byte_array_append(buffer, &type, 1); g_autoptr(FlValue) code_value = fl_value_new_string(code); if (!fl_standard_message_codec_write_value(self->codec, buffer, code_value, - error)) + error)) { return nullptr; + } g_autoptr(FlValue) message_value = message != nullptr ? fl_value_new_string(message) : nullptr; if (!fl_standard_message_codec_write_value(self->codec, buffer, message_value, - error)) + error)) { return nullptr; + } if (!fl_standard_message_codec_write_value(self->codec, buffer, details, - error)) + error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); @@ -160,8 +169,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( if (type == kEnvelopeTypeError) { g_autoptr(FlValue) code = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (code == nullptr) + if (code == nullptr) { return nullptr; + } if (fl_value_get_type(code) != FL_VALUE_TYPE_STRING) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, "Error code wrong type"); @@ -170,8 +180,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( g_autoptr(FlValue) error_message = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (error_message == nullptr) + if (error_message == nullptr) { return nullptr; + } if (fl_value_get_type(error_message) != FL_VALUE_TYPE_STRING && fl_value_get_type(error_message) != FL_VALUE_TYPE_NULL) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -181,8 +192,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( g_autoptr(FlValue) details = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (details == nullptr) + if (details == nullptr) { return nullptr; + } response = FL_METHOD_RESPONSE(fl_method_error_response_new( fl_value_get_string(code), @@ -194,8 +206,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( g_autoptr(FlValue) result = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (result == nullptr) + if (result == nullptr) { return nullptr; + } response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } else { diff --git a/shell/platform/linux/fl_string_codec.cc b/shell/platform/linux/fl_string_codec.cc index 68ea4fbdb52cc..75ddbe9215542 100644 --- a/shell/platform/linux/fl_string_codec.cc +++ b/shell/platform/linux/fl_string_codec.cc @@ -14,7 +14,7 @@ struct _FlStringCodec { G_DEFINE_TYPE(FlStringCodec, fl_string_codec, fl_message_codec_get_type()) -// Implments FlMessageCodec::encode_message. +// Implements FlMessageCodec::encode_message. static GBytes* fl_string_codec_encode_message(FlMessageCodec* codec, FlValue* value, GError** error) { @@ -29,7 +29,7 @@ static GBytes* fl_string_codec_encode_message(FlMessageCodec* codec, return g_bytes_new(text, strlen(text)); } -// Implments FlMessageCodec::decode_message. +// Implements FlMessageCodec::decode_message. static FlValue* fl_string_codec_decode_message(FlMessageCodec* codec, GBytes* message, GError** error) { diff --git a/shell/platform/linux/fl_text_input_plugin.cc b/shell/platform/linux/fl_text_input_plugin.cc index f62203f71bfc1..89580fa798b10 100644 --- a/shell/platform/linux/fl_text_input_plugin.cc +++ b/shell/platform/linux/fl_text_input_plugin.cc @@ -61,8 +61,9 @@ static gboolean finish_method(GObject* object, GError** error) { g_autoptr(FlMethodResponse) response = fl_method_channel_invoke_method_finish( FL_METHOD_CHANNEL(object), result, error); - if (response == nullptr) + if (response == nullptr) { return FALSE; + } return fl_method_response_get_result(response, error) != nullptr; } @@ -113,8 +114,9 @@ static void perform_action_response_cb(GObject* object, GAsyncResult* result, gpointer user_data) { g_autoptr(GError) error = nullptr; - if (!finish_method(object, result, &error)) + if (!finish_method(object, result, &error)) { g_warning("Failed to call %s: %s", kPerformActionMethod, error->message); + } } // Inform Flutter that the input has been activated. @@ -150,8 +152,9 @@ static gboolean im_retrieve_surrounding_cb(FlTextInputPlugin* self) { static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self, gint offset, gint n_chars) { - if (self->text_model->DeleteSurrounding(offset, n_chars)) + if (self->text_model->DeleteSurrounding(offset, n_chars)) { update_editing_state(self); + } return TRUE; } @@ -168,8 +171,9 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { g_free(self->input_action); FlValue* input_action_value = fl_value_lookup_string(config_value, kInputActionKey); - if (fl_value_get_type(input_action_value) == FL_VALUE_TYPE_STRING) + if (fl_value_get_type(input_action_value) == FL_VALUE_TYPE_STRING) { self->input_action = g_strdup(fl_value_get_string(input_action_value)); + } return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } @@ -224,22 +228,24 @@ static void method_call_cb(FlMethodChannel* channel, FlValue* args = fl_method_call_get_args(method_call); g_autoptr(FlMethodResponse) response = nullptr; - if (strcmp(method, kSetClientMethod) == 0) + if (strcmp(method, kSetClientMethod) == 0) { response = set_client(self, args); - else if (strcmp(method, kShowMethod) == 0) + } else if (strcmp(method, kShowMethod) == 0) { response = show(self); - else if (strcmp(method, kSetEditingStateMethod) == 0) + } else if (strcmp(method, kSetEditingStateMethod) == 0) { response = set_editing_state(self, args); - else if (strcmp(method, kClearClientMethod) == 0) + } else if (strcmp(method, kClearClientMethod) == 0) { response = clear_client(self); - else if (strcmp(method, kHideMethod) == 0) + } else if (strcmp(method, kHideMethod) == 0) { response = hide(self); - else + } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } g_autoptr(GError) error = nullptr; - if (!fl_method_call_respond(method_call, response, &error)) + if (!fl_method_call_respond(method_call, response, &error)) { g_warning("Failed to send method call response: %s", error->message); + } } static void fl_text_input_plugin_dispose(GObject* object) { @@ -293,11 +299,13 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, GdkEventKey* event) { g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), FALSE); - if (self->client_id == kClientIdUnset) + if (self->client_id == kClientIdUnset) { return FALSE; + } - if (gtk_im_context_filter_keypress(self->im_context, event)) + if (gtk_im_context_filter_keypress(self->im_context, event)) { return TRUE; + } // Handle navigation keys. gboolean changed = FALSE; @@ -334,8 +342,9 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, } } - if (changed) + if (changed) { update_editing_state(self); + } return FALSE; } diff --git a/shell/platform/linux/fl_text_input_plugin.h b/shell/platform/linux/fl_text_input_plugin.h index f44dbe3a17c6b..14ab5cfc04417 100644 --- a/shell/platform/linux/fl_text_input_plugin.h +++ b/shell/platform/linux/fl_text_input_plugin.h @@ -37,14 +37,14 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger); /** * fl_text_input_plugin_filter_keypress - * @self: an #FlTextInputPlugin. + * @plugin: an #FlTextInputPlugin. * @event: a #GdkEventKey * * Process a Gdk key event. * * Returns: %TRUE if the event was used. */ -gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, +gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* plugin, GdkEventKey* event); G_END_DECLS diff --git a/shell/platform/linux/fl_value.cc b/shell/platform/linux/fl_value.cc index 5b0a4bf8bf66a..d65f02ced05c0 100644 --- a/shell/platform/linux/fl_value.cc +++ b/shell/platform/linux/fl_value.cc @@ -86,8 +86,9 @@ static ssize_t fl_value_lookup_index(FlValue* self, FlValue* key) { for (size_t i = 0; i < fl_value_get_length(self); i++) { FlValue* k = fl_value_get_map_key(self, i); - if (fl_value_equal(k, key)) + if (fl_value_equal(k, key)) { return i; + } } return -1; } @@ -109,8 +110,9 @@ static void float_to_string(double value, GString* buffer) { zero_count = zero_count == 0 ? 0 : zero_count - 1; break; } - if (buffer->str[i] != '0') + if (buffer->str[i] != '0') { break; + } zero_count++; } g_string_truncate(buffer, buffer->len - zero_count); @@ -122,10 +124,11 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "null"); return; case FL_VALUE_TYPE_BOOL: - if (fl_value_get_bool(value)) + if (fl_value_get_bool(value)) { g_string_append(buffer, "true"); - else + } else { g_string_append(buffer, "false"); + } return; case FL_VALUE_TYPE_INT: int_to_string(fl_value_get_int(value), buffer); @@ -141,8 +144,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const uint8_t* values = fl_value_get_uint8_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } int_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -152,8 +156,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const int32_t* values = fl_value_get_int32_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } int_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -163,8 +168,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const int64_t* values = fl_value_get_int64_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } int_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -174,8 +180,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const double* values = fl_value_get_float_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } float_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -184,8 +191,9 @@ static void value_to_string(FlValue* value, GString* buffer) { case FL_VALUE_TYPE_LIST: { g_string_append(buffer, "["); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } value_to_string(fl_value_get_list_value(value, i), buffer); } g_string_append(buffer, "]"); @@ -194,8 +202,9 @@ static void value_to_string(FlValue* value, GString* buffer) { case FL_VALUE_TYPE_MAP: { g_string_append(buffer, "{"); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } value_to_string(fl_value_get_map_key(value, i), buffer); g_string_append(buffer, ": "); value_to_string(fl_value_get_map_value(value, i), buffer); @@ -307,8 +316,9 @@ G_MODULE_EXPORT FlValue* fl_value_new_list_from_strv( const gchar* const* str_array) { g_return_val_if_fail(str_array != nullptr, nullptr); g_autoptr(FlValue) value = fl_value_new_list(); - for (int i = 0; str_array[i] != nullptr; i++) + for (int i = 0; str_array[i] != nullptr; i++) { fl_value_append_take(value, fl_value_new_string(str_array[i])); + } return fl_value_ref(value); } @@ -330,8 +340,9 @@ G_MODULE_EXPORT void fl_value_unref(FlValue* self) { g_return_if_fail(self != nullptr); g_return_if_fail(self->ref_count > 0); self->ref_count--; - if (self->ref_count != 0) + if (self->ref_count != 0) { return; + } switch (self->type) { case FL_VALUE_TYPE_STRING: { @@ -388,8 +399,9 @@ G_MODULE_EXPORT bool fl_value_equal(FlValue* a, FlValue* b) { g_return_val_if_fail(a != nullptr, false); g_return_val_if_fail(b != nullptr, false); - if (a->type != b->type) + if (a->type != b->type) { return false; + } switch (a->type) { case FL_VALUE_TYPE_NULL: @@ -406,70 +418,83 @@ G_MODULE_EXPORT bool fl_value_equal(FlValue* a, FlValue* b) { return g_strcmp0(a_->value, b_->value) == 0; } case FL_VALUE_TYPE_UINT8_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const uint8_t* values_a = fl_value_get_uint8_list(a); const uint8_t* values_b = fl_value_get_uint8_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_INT32_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const int32_t* values_a = fl_value_get_int32_list(a); const int32_t* values_b = fl_value_get_int32_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_INT64_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const int64_t* values_a = fl_value_get_int64_list(a); const int64_t* values_b = fl_value_get_int64_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_FLOAT_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const double* values_a = fl_value_get_float_list(a); const double* values_b = fl_value_get_float_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } for (size_t i = 0; i < fl_value_get_length(a); i++) { if (!fl_value_equal(fl_value_get_list_value(a, i), - fl_value_get_list_value(b, i))) + fl_value_get_list_value(b, i))) { return false; + } } return true; } case FL_VALUE_TYPE_MAP: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } for (size_t i = 0; i < fl_value_get_length(a); i++) { FlValue* key = fl_value_get_map_key(a, i); FlValue* value_b = fl_value_lookup(b, key); - if (value_b == nullptr) + if (value_b == nullptr) { return false; + } FlValue* value_a = fl_value_get_map_value(a, i); - if (!fl_value_equal(value_a, value_b)) + if (!fl_value_equal(value_a, value_b)) { return false; + } } return true; } @@ -676,16 +701,21 @@ G_MODULE_EXPORT FlValue* fl_value_lookup(FlValue* self, FlValue* key) { g_return_val_if_fail(self->type == FL_VALUE_TYPE_MAP, nullptr); ssize_t index = fl_value_lookup_index(self, key); - if (index < 0) + if (index < 0) { return nullptr; + } return fl_value_get_map_value(self, index); } G_MODULE_EXPORT FlValue* fl_value_lookup_string(FlValue* self, const gchar* key) { g_return_val_if_fail(self != nullptr, nullptr); - g_autoptr(FlValue) string_key = fl_value_new_string(key); - return fl_value_lookup(self, string_key); + FlValue* string_key = fl_value_new_string(key); + FlValue* value = fl_value_lookup(self, string_key); + // Explicit unref used because the g_autoptr is triggering a false positive + // with clang-tidy. + fl_value_unref(string_key); + return value; } G_MODULE_EXPORT gchar* fl_value_to_string(FlValue* value) { diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 2abfc39e2dfac..fa9bea13f00a6 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -70,25 +70,28 @@ static gboolean fl_view_send_pointer_button_event(FlView* self, return FALSE; } int old_button_state = self->button_state; - FlutterPointerPhase phase; + FlutterPointerPhase phase = kMove; if (event->type == GDK_BUTTON_PRESS) { // Drop the event if Flutter already thinks the button is down. - if ((self->button_state & button) != 0) + if ((self->button_state & button) != 0) { return FALSE; + } self->button_state ^= button; phase = old_button_state == 0 ? kDown : kMove; } else if (event->type == GDK_BUTTON_RELEASE) { // Drop the event if Flutter already thinks the button is up. - if ((self->button_state & button) == 0) + if ((self->button_state & button) == 0) { return FALSE; + } self->button_state ^= button; phase = self->button_state == 0 ? kUp : kMove; } - if (self->engine == nullptr) + if (self->engine == nullptr) { return FALSE; + } gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_engine_send_mouse_pointer_event( @@ -100,13 +103,14 @@ static gboolean fl_view_send_pointer_button_event(FlView* self, } // Updates the engine with the current window metrics. -static void fl_view_send_window_metrics(FlView* self) { +static void fl_view_geometry_changed(FlView* self) { GtkAllocation allocation; gtk_widget_get_allocation(GTK_WIDGET(self), &allocation); gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_engine_send_window_metrics_event( self->engine, allocation.width * scale_factor, allocation.height * scale_factor, scale_factor); + fl_renderer_set_geometry(self->renderer, &allocation, scale_factor); } // Implements FlPluginRegistry::get_registrar_for_plugin. @@ -175,11 +179,12 @@ static void fl_view_notify(GObject* object, GParamSpec* pspec) { FlView* self = FL_VIEW(object); if (strcmp(pspec->name, "scale-factor") == 0) { - fl_view_send_window_metrics(self); + fl_view_geometry_changed(self); } - if (G_OBJECT_CLASS(fl_view_parent_class)->notify != nullptr) + if (G_OBJECT_CLASS(fl_view_parent_class)->notify != nullptr) { G_OBJECT_CLASS(fl_view_parent_class)->notify(object, pspec); + } } static void fl_view_dispose(GObject* object) { @@ -203,8 +208,9 @@ static void fl_view_realize(GtkWidget* widget) { gtk_widget_set_realized(widget, TRUE); g_autoptr(GError) error = nullptr; - if (!fl_renderer_setup(self->renderer, &error)) + if (!fl_renderer_setup(self->renderer, &error)) { g_warning("Failed to setup renderer: %s", error->message); + } GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); @@ -232,12 +238,11 @@ static void fl_view_realize(GtkWidget* widget) { gtk_widget_register_window(widget, window); gtk_widget_set_window(widget, window); - fl_renderer_x11_set_window( - FL_RENDERER_X11(self->renderer), - GDK_X11_WINDOW(gtk_widget_get_window(GTK_WIDGET(self)))); + fl_renderer_set_window(self->renderer, window); - if (!fl_engine_start(self->engine, &error)) + if (!fl_engine_start(self->engine, &error)) { g_warning("Failed to start Flutter engine: %s", error->message); + } } // Implements GtkWidget::size-allocate. @@ -253,7 +258,7 @@ static void fl_view_size_allocate(GtkWidget* widget, allocation->height); } - fl_view_send_window_metrics(self); + fl_view_geometry_changed(self); } // Implements GtkWidget::button_press_event. @@ -263,8 +268,9 @@ static gboolean fl_view_button_press_event(GtkWidget* widget, // Flutter doesn't handle double and triple click events. if (event->type == GDK_DOUBLE_BUTTON_PRESS || - event->type == GDK_TRIPLE_BUTTON_PRESS) + event->type == GDK_TRIPLE_BUTTON_PRESS) { return FALSE; + } return fl_view_send_pointer_button_event(self, event); } @@ -298,8 +304,8 @@ static gboolean fl_view_scroll_event(GtkWidget* widget, GdkEventScroll* event) { scroll_delta_x = 1; } - // TODO: See if this can be queried from the OS; this value is chosen - // arbitrarily to get something that feels reasonable. + // TODO(robert-ancell): See if this can be queried from the OS; this value is + // chosen arbitrarily to get something that feels reasonable. const int kScrollOffsetMultiplier = 20; scroll_delta_x *= kScrollOffsetMultiplier; scroll_delta_y *= kScrollOffsetMultiplier; @@ -319,8 +325,9 @@ static gboolean fl_view_motion_notify_event(GtkWidget* widget, GdkEventMotion* event) { FlView* self = FL_VIEW(widget); - if (self->engine == nullptr) + if (self->engine == nullptr) { return FALSE; + } gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_engine_send_mouse_pointer_event( diff --git a/shell/platform/linux/testing/mock_renderer.cc b/shell/platform/linux/testing/mock_renderer.cc index 0112c38faa9bc..8793491778951 100644 --- a/shell/platform/linux/testing/mock_renderer.cc +++ b/shell/platform/linux/testing/mock_renderer.cc @@ -17,16 +17,28 @@ static GdkVisual* fl_mock_renderer_get_visual(FlRenderer* renderer, return static_cast(g_object_new(GDK_TYPE_VISUAL, nullptr)); } -// Implements FlRenderer::create_surface. -static EGLSurface fl_mock_renderer_create_surface(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config) { - return eglCreateWindowSurface(display, config, 0, nullptr); +// Implements FlRenderer::create_display. +static EGLDisplay fl_mock_renderer_create_display(FlRenderer* renderer) { + return eglGetDisplay(EGL_DEFAULT_DISPLAY); +} + +// Implements FlRenderer::create_surfaces. +static gboolean fl_mock_renderer_create_surfaces(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error) { + *visible = eglCreateWindowSurface(display, config, 0, nullptr); + const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + *resource = eglCreatePbufferSurface(display, config, attribs); + return TRUE; } static void fl_mock_renderer_class_init(FlMockRendererClass* klass) { FL_RENDERER_CLASS(klass)->get_visual = fl_mock_renderer_get_visual; - FL_RENDERER_CLASS(klass)->create_surface = fl_mock_renderer_create_surface; + FL_RENDERER_CLASS(klass)->create_display = fl_mock_renderer_create_display; + FL_RENDERER_CLASS(klass)->create_surfaces = fl_mock_renderer_create_surfaces; } static void fl_mock_renderer_init(FlMockRenderer* self) {} diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 14adddc19c51f..9ecb0d2f5e863 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -42,7 +42,11 @@ source_set("flutter_windows_source") { "angle_surface_manager.h", "cursor_handler.cc", "cursor_handler.h", + "flutter_project_bundle.cc", + "flutter_project_bundle.h", "flutter_windows.cc", + "flutter_windows_engine.cc", + "flutter_windows_engine.h", "flutter_windows_view.cc", "flutter_windows_view.h", "key_event_handler.cc", @@ -50,6 +54,8 @@ source_set("flutter_windows_source") { "keyboard_hook_handler.h", "string_conversion.cc", "string_conversion.h", + "system_utils.h", + "system_utils_win32.cc", "text_input_plugin.cc", "text_input_plugin.h", "win32_dpi_utils.cc", @@ -62,6 +68,8 @@ source_set("flutter_windows_source") { "win32_task_runner.h", "win32_window.cc", "win32_window.h", + "win32_window_proc_delegate_manager.cc", + "win32_window_proc_delegate_manager.h", "window_binding_handler.h", "window_binding_handler_delegate.h", "window_state.h", @@ -114,12 +122,14 @@ executable("flutter_windows_unittests") { sources = [ "string_conversion_unittests.cc", + "system_utils_unittests.cc", "testing/win32_flutter_window_test.cc", "testing/win32_flutter_window_test.h", "testing/win32_window_test.cc", "testing/win32_window_test.h", "win32_dpi_utils_unittests.cc", "win32_flutter_window_unittests.cc", + "win32_window_proc_delegate_manager_unittests.cc", "win32_window_unittests.cc", ] diff --git a/shell/platform/windows/angle_surface_manager.cc b/shell/platform/windows/angle_surface_manager.cc index cc046281e8114..eda082c7c868d 100644 --- a/shell/platform/windows/angle_surface_manager.cc +++ b/shell/platform/windows/angle_surface_manager.cc @@ -169,14 +169,22 @@ void AngleSurfaceManager::CleanUp() { } } -bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target) { +bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height) { if (!render_target || !initialize_succeeded_) { return false; } EGLSurface surface = EGL_NO_SURFACE; - const EGLint surfaceAttributes[] = {EGL_NONE}; + // Disable Angle's automatic surface sizing logic and provide and exlicit + // size. AngleSurfaceManager is responsible for initiating Angle surface size + // changes to avoid race conditions with rendering when automatic mode is + // used. + const EGLint surfaceAttributes[] = { + EGL_FIXED_SIZE_ANGLE, EGL_TRUE, EGL_WIDTH, width, + EGL_HEIGHT, height, EGL_NONE}; surface = eglCreateWindowSurface( egl_display_, egl_config_, @@ -190,6 +198,26 @@ bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target) { return true; } +void AngleSurfaceManager::ResizeSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height) { + EGLint existing_width, existing_height; + GetSurfaceDimensions(&existing_width, &existing_height); + if (width != existing_width || height != existing_height) { + // Destroy existing surface with previous stale dimensions and create new + // surface at new size. Since the Windows compositor retains the front + // buffer until the new surface has been presented, no need to manually + // preserve the previous surface contents. This resize approach could be + // further optimized if Angle exposed a public entrypoint for + // SwapChain11::reset or SwapChain11::resize. + DestroySurface(); + if (!CreateSurface(render_target, width, height)) { + std::cerr << "AngleSurfaceManager::ResizeSurface failed to create surface" + << std::endl; + } + } +} + void AngleSurfaceManager::GetSurfaceDimensions(EGLint* width, EGLint* height) { if (render_surface_ == EGL_NO_SURFACE || !initialize_succeeded_) { width = 0; diff --git a/shell/platform/windows/angle_surface_manager.h b/shell/platform/windows/angle_surface_manager.h index a6a5fc213c3d4..60754c37cc5cf 100644 --- a/shell/platform/windows/angle_surface_manager.h +++ b/shell/platform/windows/angle_surface_manager.h @@ -23,6 +23,8 @@ namespace flutter { // destroy surfaces class AngleSurfaceManager { public: + // Creates a new surface manager retaining reference to the passed-in target + // for the lifetime of the manager. AngleSurfaceManager(); ~AngleSurfaceManager(); @@ -32,8 +34,19 @@ class AngleSurfaceManager { // Creates an EGLSurface wrapper and backing DirectX 11 SwapChain // asociated with window, in the appropriate format for display. - // Target represents the visual entity to bind to. - bool CreateSurface(WindowsRenderTarget* render_target); + // Target represents the visual entity to bind to. Width and + // height represent dimensions surface is created at. + bool CreateSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height); + + // Resizes backing surface from current size to newly requested size + // based on width and height for the specific case when width and height do + // not match current surface dimensions. Target represents the visual entity + // to bind to. + void ResizeSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height); // queries EGL for the dimensions of surface in physical // pixels returning width and height as out params. diff --git a/shell/platform/windows/client_wrapper/BUILD.gn b/shell/platform/windows/client_wrapper/BUILD.gn index 6f89a81a2dd8a..fe54c2ba2a484 100644 --- a/shell/platform/windows/client_wrapper/BUILD.gn +++ b/shell/platform/windows/client_wrapper/BUILD.gn @@ -7,19 +7,25 @@ import("//flutter/testing/testing.gni") _wrapper_includes = [ "include/flutter/dart_project.h", + "include/flutter/flutter_engine.h", "include/flutter/flutter_view_controller.h", "include/flutter/flutter_view.h", "include/flutter/plugin_registrar_windows.h", ] -_wrapper_sources = [ "flutter_view_controller.cc" ] +_wrapper_sources = [ + "flutter_engine.cc", + "flutter_view_controller.cc", +] # This code will be merged into .../common/cpp/client_wrapper for client use, # so uses header paths that assume the merged state. Include the header -# header directory of the core wrapper files so these includes will work. +# directories of the core wrapper files so these includes will work. config("relative_core_wrapper_headers") { - include_dirs = - [ "//flutter/shell/platform/common/cpp/client_wrapper/include/flutter" ] + include_dirs = [ + "//flutter/shell/platform/common/cpp/client_wrapper", + "//flutter/shell/platform/common/cpp/client_wrapper/include/flutter", + ] } # Windows client wrapper build for internal use by the shell implementation. @@ -69,11 +75,15 @@ executable("client_wrapper_windows_unittests") { sources = [ "dart_project_unittests.cc", + "flutter_engine_unittests.cc", "flutter_view_controller_unittests.cc", "flutter_view_unittests.cc", "plugin_registrar_windows_unittests.cc", ] + # Set embedder.h to export, not import, since the stubs are locally defined. + defines = [ "FLUTTER_DESKTOP_LIBRARY" ] + deps = [ ":client_wrapper_library_stubs_windows", ":client_wrapper_windows", diff --git a/shell/platform/windows/client_wrapper/flutter_engine.cc b/shell/platform/windows/client_wrapper/flutter_engine.cc new file mode 100644 index 0000000000000..8bf94420646c7 --- /dev/null +++ b/shell/platform/windows/client_wrapper/flutter_engine.cc @@ -0,0 +1,83 @@ +// 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. + +#include "include/flutter/flutter_engine.h" + +#include +#include + +#include "binary_messenger_impl.h" + +namespace flutter { + +FlutterEngine::FlutterEngine(const DartProject& project) { + FlutterDesktopEngineProperties c_engine_properties = {}; + c_engine_properties.assets_path = project.assets_path().c_str(); + c_engine_properties.icu_data_path = project.icu_data_path().c_str(); + c_engine_properties.aot_library_path = project.aot_library_path().c_str(); + std::vector engine_switches; + std::transform( + project.engine_switches().begin(), project.engine_switches().end(), + std::back_inserter(engine_switches), + [](const std::string& arg) -> const char* { return arg.c_str(); }); + if (engine_switches.size() > 0) { + c_engine_properties.switches = &engine_switches[0]; + c_engine_properties.switches_count = engine_switches.size(); + } + + engine_ = FlutterDesktopEngineCreate(c_engine_properties); + + auto core_messenger = FlutterDesktopEngineGetMessenger(engine_); + messenger_ = std::make_unique(core_messenger); +} + +FlutterEngine::~FlutterEngine() { + ShutDown(); +} + +bool FlutterEngine::Run(const char* entry_point) { + if (!engine_) { + std::cerr << "Cannot run an engine that failed creation." << std::endl; + return false; + } + if (has_been_run_) { + std::cerr << "Cannot run an engine more than once." << std::endl; + return false; + } + bool run_succeeded = FlutterDesktopEngineRun(engine_, entry_point); + if (!run_succeeded) { + std::cerr << "Failed to start engine." << std::endl; + } + has_been_run_ = true; + return run_succeeded; +} + +void FlutterEngine::ShutDown() { + if (engine_ && owns_engine_) { + FlutterDesktopEngineDestroy(engine_); + } + engine_ = nullptr; +} + +std::chrono::nanoseconds FlutterEngine::ProcessMessages() { + return std::chrono::nanoseconds(FlutterDesktopEngineProcessMessages(engine_)); +} + +FlutterDesktopPluginRegistrarRef FlutterEngine::GetRegistrarForPlugin( + const std::string& plugin_name) { + if (!engine_) { + std::cerr << "Cannot get plugin registrar on an engine that isn't running; " + "call Run first." + << std::endl; + return nullptr; + } + return FlutterDesktopEngineGetPluginRegistrar(engine_, plugin_name.c_str()); +} + +FlutterDesktopEngineRef FlutterEngine::RelinquishEngine() { + owns_engine_ = false; + return engine_; +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc new file mode 100644 index 0000000000000..8fe83225b114f --- /dev/null +++ b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc @@ -0,0 +1,106 @@ +// 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. + +#include +#include + +#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h" +#include "flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h" +#include "gtest/gtest.h" + +namespace flutter { + +namespace { + +// Stub implementation to validate calls to the API. +class TestFlutterWindowsApi : public testing::StubFlutterWindowsApi { + public: + // |flutter::testing::StubFlutterWindowsApi| + FlutterDesktopEngineRef EngineCreate( + const FlutterDesktopEngineProperties& engine_properties) { + create_called_ = true; + return reinterpret_cast(1); + } + + // |flutter::testing::StubFlutterWindowsApi| + bool EngineRun(const char* entry_point) override { + run_called_ = true; + return true; + } + + // |flutter::testing::StubFlutterWindowsApi| + bool EngineDestroy() override { + destroy_called_ = true; + return true; + } + + // |flutter::testing::StubFlutterWindowsApi| + uint64_t EngineProcessMessages() override { return 99; } + + bool create_called() { return create_called_; } + + bool run_called() { return run_called_; } + + bool destroy_called() { return destroy_called_; } + + private: + bool create_called_ = false; + bool run_called_ = false; + bool destroy_called_ = false; +}; + +} // namespace + +TEST(FlutterEngineTest, CreateDestroy) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + { + FlutterEngine engine(DartProject(L"fake/project/path")); + engine.Run(); + EXPECT_EQ(test_api->create_called(), true); + EXPECT_EQ(test_api->run_called(), true); + EXPECT_EQ(test_api->destroy_called(), false); + } + // Destroying should implicitly shut down if it hasn't been done manually. + EXPECT_EQ(test_api->destroy_called(), true); +} + +TEST(FlutterEngineTest, ExplicitShutDown) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + FlutterEngine engine(DartProject(L"fake/project/path")); + engine.Run(); + EXPECT_EQ(test_api->create_called(), true); + EXPECT_EQ(test_api->run_called(), true); + EXPECT_EQ(test_api->destroy_called(), false); + engine.ShutDown(); + EXPECT_EQ(test_api->destroy_called(), true); +} + +TEST(FlutterEngineTest, ProcessMessages) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + FlutterEngine engine(DartProject(L"fake/project/path")); + engine.Run(); + + std::chrono::nanoseconds next_event_time = engine.ProcessMessages(); + EXPECT_EQ(next_event_time.count(), 99); +} + +TEST(FlutterEngineTest, GetMessenger) { + DartProject project(L"data"); + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + FlutterEngine engine(DartProject(L"fake/project/path")); + EXPECT_NE(engine.messenger(), nullptr); +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller.cc b/shell/platform/windows/client_wrapper/flutter_view_controller.cc index 16dd49f8a24d4..e32c4e59ea6cb 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller.cc @@ -12,73 +12,41 @@ namespace flutter { FlutterViewController::FlutterViewController(int width, int height, const DartProject& project) { - std::vector switches; - std::transform( - project.engine_switches().begin(), project.engine_switches().end(), - std::back_inserter(switches), - [](const std::string& arg) -> const char* { return arg.c_str(); }); - size_t switch_count = switches.size(); - - FlutterDesktopEngineProperties properties = {}; - properties.assets_path = project.assets_path().c_str(); - properties.icu_data_path = project.icu_data_path().c_str(); - // It is harmless to pass this in non-AOT mode. - properties.aot_library_path = project.aot_library_path().c_str(); - properties.switches = switch_count > 0 ? switches.data() : nullptr; - properties.switches_count = switch_count; - controller_ = FlutterDesktopCreateViewController(width, height, properties); + engine_ = std::make_unique(project); + controller_ = FlutterDesktopViewControllerCreate(width, height, + engine_->RelinquishEngine()); if (!controller_) { std::cerr << "Failed to create view controller." << std::endl; return; } - view_ = std::make_unique(FlutterDesktopGetView(controller_)); + view_ = std::make_unique( + FlutterDesktopViewControllerGetView(controller_)); } -FlutterViewController::FlutterViewController( - const std::string& icu_data_path, - int width, - int height, - const std::string& assets_path, - const std::vector& arguments) { +FlutterViewController::~FlutterViewController() { if (controller_) { - std::cerr << "Only one Flutter view can exist at a time." << std::endl; + FlutterDesktopViewControllerDestroy(controller_); } - - std::vector engine_arguments; - std::transform( - arguments.begin(), arguments.end(), std::back_inserter(engine_arguments), - [](const std::string& arg) -> const char* { return arg.c_str(); }); - size_t arg_count = engine_arguments.size(); - - controller_ = FlutterDesktopCreateViewControllerLegacy( - width, height, assets_path.c_str(), icu_data_path.c_str(), - arg_count > 0 ? &engine_arguments[0] : nullptr, arg_count); - if (!controller_) { - std::cerr << "Failed to create view controller." << std::endl; - return; - } - view_ = std::make_unique(FlutterDesktopGetView(controller_)); } -FlutterViewController::~FlutterViewController() { - if (controller_) { - FlutterDesktopDestroyViewController(controller_); - } +std::optional FlutterViewController::HandleTopLevelWindowProc( + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + LRESULT result; + bool handled = FlutterDesktopViewControllerHandleTopLevelWindowProc( + controller_, hwnd, message, wparam, lparam, &result); + return handled ? result : std::optional(std::nullopt); } std::chrono::nanoseconds FlutterViewController::ProcessMessages() { - return std::chrono::nanoseconds(FlutterDesktopProcessMessages(controller_)); + return engine_->ProcessMessages(); } FlutterDesktopPluginRegistrarRef FlutterViewController::GetRegistrarForPlugin( const std::string& plugin_name) { - if (!controller_) { - std::cerr << "Cannot get plugin registrar without a window; call " - "CreateWindow first." - << std::endl; - return nullptr; - } - return FlutterDesktopGetPluginRegistrar(controller_, plugin_name.c_str()); + return engine_->GetRegistrarForPlugin(plugin_name); } } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc index ec1b7fb561eae..264a998c29138 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc @@ -15,32 +15,59 @@ namespace { // Stub implementation to validate calls to the API. class TestWindowsApi : public testing::StubFlutterWindowsApi { - FlutterDesktopViewControllerRef CreateViewController( + public: + // |flutter::testing::StubFlutterWindowsApi| + FlutterDesktopViewControllerRef ViewControllerCreate( int width, int height, + FlutterDesktopEngineRef engine) override { + return reinterpret_cast(2); + } + + // |flutter::testing::StubFlutterWindowsApi| + void ViewControllerDestroy() override { view_controller_destroyed_ = true; } + + // |flutter::testing::StubFlutterWindowsApi| + FlutterDesktopEngineRef EngineCreate( const FlutterDesktopEngineProperties& engine_properties) override { - return reinterpret_cast(1); + return reinterpret_cast(1); } + + // |flutter::testing::StubFlutterWindowsApi| + bool EngineDestroy() override { + engine_destroyed_ = true; + return true; + } + + bool engine_destroyed() { return engine_destroyed_; } + bool view_controller_destroyed() { return view_controller_destroyed_; } + + private: + bool engine_destroyed_ = false; + bool view_controller_destroyed_ = false; }; } // namespace -TEST(FlutterViewControllerTest, CreateDestroyLegacy) { +TEST(FlutterViewControllerTest, CreateDestroy) { + DartProject project(L"data"); testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); - { - FlutterViewController controller("", 100, 100, "", - std::vector{}); - } + { FlutterViewController controller(100, 100, project); } + EXPECT_TRUE(test_api->view_controller_destroyed()); + // Per the C API, once a view controller has taken ownership of an engine + // the engine destruction method should not be called. + EXPECT_FALSE(test_api->engine_destroyed()); } -TEST(FlutterViewControllerTest, CreateDestroy) { +TEST(FlutterViewControllerTest, GetEngine) { DartProject project(L"data"); testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); - { FlutterViewController controller(100, 100, project); } + FlutterViewController controller(100, 100, project); + EXPECT_NE(controller.engine(), nullptr); } TEST(FlutterViewControllerTest, GetView) { diff --git a/shell/platform/windows/client_wrapper/include/flutter/dart_project.h b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h index ae542aebf1d68..6c94ffa757215 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/dart_project.h +++ b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h @@ -45,6 +45,7 @@ class DartProject { // flexible options for project structures are needed later without it // being a breaking change. Provide access to internal classes that need // them. + friend class FlutterEngine; friend class FlutterViewController; friend class DartProjectTest; diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h new file mode 100644 index 0000000000000..94edb4064dd2c --- /dev/null +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h @@ -0,0 +1,94 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_ + +#define NOMINMAX +#include + +#include +#include +#include + +#include "binary_messenger.h" +#include "dart_project.h" +#include "plugin_registrar.h" +#include "plugin_registry.h" + +namespace flutter { + +// An instance of a Flutter engine. +// +// In the future, this will be the API surface used for all interactions with +// the engine, rather than having them duplicated on FlutterViewController. +// For now it is only used in the rare where you need a headless Flutter engine. +class FlutterEngine : public PluginRegistry { + public: + // Creates a new engine for running the given project. + explicit FlutterEngine(const DartProject& project); + + virtual ~FlutterEngine(); + + // Prevent copying. + FlutterEngine(FlutterEngine const&) = delete; + FlutterEngine& operator=(FlutterEngine const&) = delete; + + // Starts running the engine, with an optional entry point. + // + // If provided, entry_point must be the name of a top-level function from the + // same Dart library that contains the app's main() function, and must be + // decorated with `@pragma(vm:entry-point)` to ensure the method is not + // tree-shaken by the Dart compiler. If not provided, defaults to main(). + bool Run(const char* entry_point = nullptr); + + // Terminates the running engine. + void ShutDown(); + + // Processes any pending events in the Flutter engine, and returns the + // nanosecond delay until the next scheduled event (or max, if none). + // + // This should be called on every run of the application-level runloop, and + // a wait for native events in the runloop should never be longer than the + // last return value from this function. + std::chrono::nanoseconds ProcessMessages(); + + // flutter::PluginRegistry: + FlutterDesktopPluginRegistrarRef GetRegistrarForPlugin( + const std::string& plugin_name) override; + + // Returns the messenger to use for creating channels to communicate with the + // Flutter engine. + // + // This pointer will remain valid for the lifetime of this instance. + BinaryMessenger* messenger() { return messenger_.get(); } + + private: + // For access to RelinquishEngine. + friend class FlutterViewController; + + // Gives up ownership of |engine_|, but keeps a weak reference to it. + // + // This is intended to be used by FlutterViewController, since the underlying + // C API for view controllers takes over engine ownership. + FlutterDesktopEngineRef RelinquishEngine(); + + // Handle for interacting with the C API's engine reference. + FlutterDesktopEngineRef engine_ = nullptr; + + // Messenger for communicating with the engine. + std::unique_ptr messenger_; + + // Whether or not this wrapper owns |engine_|. + bool owns_engine_ = true; + + // Whether the engine has been run. This will be true if Run has been called, + // or if RelinquishEngine has been called (since the view controller will + // run the engine if it hasn't already been run). + bool has_been_run_ = false; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h index 607e01a72f247..32a1f2f8fd30c 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h @@ -5,13 +5,14 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_VIEW_CONTROLLER_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_VIEW_CONTROLLER_H_ +#include #include -#include -#include -#include +#include +#include #include "dart_project.h" +#include "flutter_engine.h" #include "flutter_view.h" #include "plugin_registrar.h" #include "plugin_registry.h" @@ -33,30 +34,32 @@ class FlutterViewController : public PluginRegistry { int height, const DartProject& project); - // DEPRECATED. Will be removed soon; use the version above. - explicit FlutterViewController(const std::string& icu_data_path, - int width, - int height, - const std::string& assets_path, - const std::vector& arguments); - virtual ~FlutterViewController(); // Prevent copying. FlutterViewController(FlutterViewController const&) = delete; FlutterViewController& operator=(FlutterViewController const&) = delete; + // Returns the engine running Flutter content in this view. + FlutterEngine* engine() { return engine_.get(); } + + // Returns the view managed by this controller. FlutterView* view() { return view_.get(); } - // Processes any pending events in the Flutter engine, and returns the - // nanosecond delay until the next scheduled event (or max, if none). + // Allows the Flutter engine and any interested plugins an opportunity to + // handle the given message. // - // This should be called on every run of the application-level runloop, and - // a wait for native events in the runloop should never be longer than the - // last return value from this function. + // If a result is returned, then the message was handled in such a way that + // further handling should not be done. + std::optional HandleTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam); + + // DEPRECATED. Call engine()->ProcessMessages() instead. std::chrono::nanoseconds ProcessMessages(); - // flutter::PluginRegistry: + // DEPRECATED. Call engine()->GetRegistrarForPlugin() instead. FlutterDesktopPluginRegistrarRef GetRegistrarForPlugin( const std::string& plugin_name) override; @@ -64,6 +67,9 @@ class FlutterViewController : public PluginRegistry { // Handle for interacting with the C API's view controller, if any. FlutterDesktopViewControllerRef controller_ = nullptr; + // The backing engine + std::unique_ptr engine_; + // The owned FlutterView. std::unique_ptr view_; }; diff --git a/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h b/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h index 052f22bbc8a80..16bab4891ef28 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h +++ b/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h @@ -5,15 +5,24 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_WINDOWS_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_WINDOWS_H_ +#include #include #include +#include #include "flutter_view.h" #include "plugin_registrar.h" namespace flutter { +// A delegate callback for WindowProc delegation. +// +// Implementations should return a value only if they have handled the message +// and want to stop all further handling. +using WindowProcDelegate = std::function(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)>; + // An extension to PluginRegistrar providing access to Windows-specific // functionality. class PluginRegistrarWindows : public PluginRegistrar { @@ -24,7 +33,7 @@ class PluginRegistrarWindows : public PluginRegistrar { FlutterDesktopPluginRegistrarRef core_registrar) : PluginRegistrar(core_registrar) { view_ = std::make_unique( - FlutterDesktopRegistrarGetView(core_registrar)); + FlutterDesktopPluginRegistrarGetView(core_registrar)); } virtual ~PluginRegistrarWindows() = default; @@ -35,9 +44,78 @@ class PluginRegistrarWindows : public PluginRegistrar { FlutterView* GetView() { return view_.get(); } + // Registers |delegate| to recieve WindowProc callbacks for the top-level + // window containing this Flutter instance. Returns an ID that can be used to + // unregister the handler. + // + // Delegates are not guaranteed to be called: + // - The application may choose not to delegate WindowProc calls. + // - If multiple plugins are registered, the first one that returns a value + // from the delegate message will "win", and others will not be called. + // The order of delegate calls is not defined. + // + // Delegates should be implemented as narrowly as possible, only returning + // a value in cases where it's important that other delegates not run, to + // minimize the chances of conflicts between plugins. + int RegisterTopLevelWindowProcDelegate(WindowProcDelegate delegate) { + if (window_proc_delegates_.empty()) { + FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + registrar(), PluginRegistrarWindows::OnTopLevelWindowProc, this); + } + int delegate_id = next_window_proc_delegate_id_++; + window_proc_delegates_.emplace(delegate_id, std::move(delegate)); + return delegate_id; + } + + // Unregisters a previously registered delegate. + void UnregisterTopLevelWindowProcDelegate(int proc_id) { + window_proc_delegates_.erase(proc_id); + if (window_proc_delegates_.empty()) { + FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + registrar(), PluginRegistrarWindows::OnTopLevelWindowProc); + } + } + private: + // A FlutterDesktopWindowProcCallback implementation that forwards back to + // a PluginRegistarWindows instance provided as |user_data|. + static bool OnTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + void* user_data, + LRESULT* result) { + const auto* registrar = static_cast(user_data); + std::optional optional_result = registrar->CallTopLevelWindowProcDelegates( + hwnd, message, wparam, lparam); + if (optional_result) { + *result = *optional_result; + } + return optional_result.has_value(); + } + + std::optional CallTopLevelWindowProcDelegates(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) const { + std::optional result; + for (const auto& pair : window_proc_delegates_) { + result = pair.second(hwnd, message, wparam, lparam); + // Stop as soon as any delegate indicates that it has handled the message. + if (result) { + break; + } + } + return result; + } + // The associated FlutterView, if any. std::unique_ptr view_; + + // The next ID to return from RegisterWindowProcDelegate. + int next_window_proc_delegate_id_ = 1; + + std::map window_proc_delegates_; }; } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc b/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc index 07eaff0c1e2be..63049a5eb82b9 100644 --- a/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc +++ b/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc @@ -14,7 +14,34 @@ namespace flutter { namespace { // Stub implementation to validate calls to the API. -class TestWindowsApi : public testing::StubFlutterWindowsApi {}; +class TestWindowsApi : public testing::StubFlutterWindowsApi { + public: + void PluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data) override { + ++registered_delegate_count_; + last_registered_delegate_ = delegate; + last_registered_user_data_ = user_data; + } + + void PluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate) override { + --registered_delegate_count_; + } + + int registered_delegate_count() { return registered_delegate_count_; } + + FlutterDesktopWindowProcCallback last_registered_delegate() { + return last_registered_delegate_; + } + + void* last_registered_user_data() { return last_registered_user_data_; } + + private: + int registered_delegate_count_ = 0; + FlutterDesktopWindowProcCallback last_registered_delegate_ = nullptr; + void* last_registered_user_data_ = nullptr; +}; } // namespace @@ -27,4 +54,104 @@ TEST(PluginRegistrarWindowsTest, GetView) { EXPECT_NE(registrar.GetView(), nullptr); } +TEST(PluginRegistrarWindowsTest, RegisterUnregister) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + PluginRegistrarWindows registrar( + reinterpret_cast(1)); + + WindowProcDelegate delegate = [](HWND hwnd, UINT message, WPARAM wparam, + LPARAM lparam) { + return std::optional(); + }; + int id_a = registrar.RegisterTopLevelWindowProcDelegate(delegate); + EXPECT_EQ(test_api->registered_delegate_count(), 1); + int id_b = registrar.RegisterTopLevelWindowProcDelegate(delegate); + // All the C++-level delegates are driven by a since C callback, so the + // registration count should stay the same. + EXPECT_EQ(test_api->registered_delegate_count(), 1); + + // Unregistering one of the two delegates shouldn't cause the underlying C + // callback to be unregistered. + registrar.UnregisterTopLevelWindowProcDelegate(id_a); + EXPECT_EQ(test_api->registered_delegate_count(), 1); + // Unregistering both should unregister it. + registrar.UnregisterTopLevelWindowProcDelegate(id_b); + EXPECT_EQ(test_api->registered_delegate_count(), 0); + + EXPECT_NE(id_a, id_b); +} + +TEST(PluginRegistrarWindowsTest, CallsRegisteredDelegates) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + PluginRegistrarWindows registrar( + reinterpret_cast(1)); + + HWND dummy_hwnd; + bool called_a = false; + WindowProcDelegate delegate_a = [&called_a, &dummy_hwnd]( + HWND hwnd, UINT message, WPARAM wparam, + LPARAM lparam) { + called_a = true; + EXPECT_EQ(hwnd, dummy_hwnd); + EXPECT_EQ(message, 2); + EXPECT_EQ(wparam, 3); + EXPECT_EQ(lparam, 4); + return std::optional(); + }; + bool called_b = false; + WindowProcDelegate delegate_b = [&called_b](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(); + }; + int id_a = registrar.RegisterTopLevelWindowProcDelegate(delegate_a); + int id_b = registrar.RegisterTopLevelWindowProcDelegate(delegate_b); + + LRESULT result = 0; + bool handled = test_api->last_registered_delegate()( + dummy_hwnd, 2, 3, 4, test_api->last_registered_user_data(), &result); + EXPECT_TRUE(called_a); + EXPECT_TRUE(called_b); + EXPECT_FALSE(handled); +} + +TEST(PluginRegistrarWindowsTest, StopsOnceHandled) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + PluginRegistrarWindows registrar( + reinterpret_cast(1)); + + bool called_a = false; + WindowProcDelegate delegate_a = [&called_a](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(7); + }; + bool called_b = false; + WindowProcDelegate delegate_b = [&called_b](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(7); + }; + int id_a = registrar.RegisterTopLevelWindowProcDelegate(delegate_a); + int id_b = registrar.RegisterTopLevelWindowProcDelegate(delegate_b); + + HWND dummy_hwnd; + LRESULT result = 0; + bool handled = test_api->last_registered_delegate()( + dummy_hwnd, 2, 3, 4, test_api->last_registered_user_data(), &result); + // Only one of the delegates should have been called, since each claims to + // have fully handled the message. + EXPECT_TRUE(called_a || called_b); + EXPECT_NE(called_a, called_b); + // The return value should propagate through. + EXPECT_TRUE(handled); + EXPECT_EQ(result, 7); +} + } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index b84d9195daf26..20613734f9005 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -35,86 +35,121 @@ ScopedStubFlutterWindowsApi::~ScopedStubFlutterWindowsApi() { // Forwarding dummy implementations of the C API. -FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( +FlutterDesktopViewControllerRef FlutterDesktopViewControllerCreate( int width, int height, - const FlutterDesktopEngineProperties& engine_properties) { + FlutterDesktopEngineRef engine) { if (s_stub_implementation) { - return s_stub_implementation->CreateViewController(width, height, - engine_properties); + return s_stub_implementation->ViewControllerCreate(width, height, engine); } return nullptr; } -FlutterDesktopViewControllerRef FlutterDesktopCreateViewControllerLegacy( - int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { +void FlutterDesktopViewControllerDestroy( + FlutterDesktopViewControllerRef controller) { if (s_stub_implementation) { - // This stub will be removed shortly, and the current tests don't need the - // arguments, so there's no need to translate them to engine_properties. - FlutterDesktopEngineProperties engine_properties; - return s_stub_implementation->CreateViewController( - initial_width, initial_height, engine_properties); + s_stub_implementation->ViewControllerDestroy(); } - return nullptr; } -void FlutterDesktopDestroyViewController( +FlutterDesktopEngineRef FlutterDesktopViewControllerGetEngine( FlutterDesktopViewControllerRef controller) { - if (s_stub_implementation) { - s_stub_implementation->DestroyViewController(); - } + // The stub ignores this, so just return an arbitrary non-zero value. + return reinterpret_cast(1); } -FlutterDesktopViewRef FlutterDesktopGetView( +FlutterDesktopViewRef FlutterDesktopViewControllerGetView( FlutterDesktopViewControllerRef controller) { // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(1); } -uint64_t FlutterDesktopProcessMessages( - FlutterDesktopViewControllerRef controller) { +bool FlutterDesktopViewControllerHandleTopLevelWindowProc( + FlutterDesktopViewControllerRef controller, + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { if (s_stub_implementation) { - return s_stub_implementation->ProcessMessages(); + return s_stub_implementation->ViewControllerHandleTopLevelWindowProc( + hwnd, message, wparam, lparam, result); } - return 0; + return false; } -HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef controller) { +FlutterDesktopEngineRef FlutterDesktopEngineCreate( + const FlutterDesktopEngineProperties& engine_properties) { if (s_stub_implementation) { - return s_stub_implementation->ViewGetHWND(); + return s_stub_implementation->EngineCreate(engine_properties); } - return reinterpret_cast(-1); + return nullptr; } -FlutterDesktopEngineRef FlutterDesktopRunEngine( - const FlutterDesktopEngineProperties& engine_properties) { +bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine_ref) { if (s_stub_implementation) { - return s_stub_implementation->RunEngine(engine_properties); + return s_stub_implementation->EngineDestroy(); } - return nullptr; + return true; } -bool FlutterDesktopShutDownEngine(FlutterDesktopEngineRef engine_ref) { +bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, + const char* entry_point) { if (s_stub_implementation) { - return s_stub_implementation->ShutDownEngine(); + return s_stub_implementation->EngineRun(entry_point); } return true; } -FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( - FlutterDesktopViewControllerRef controller, +uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { + if (s_stub_implementation) { + return s_stub_implementation->EngineProcessMessages(); + } + return 0; +} + +FlutterDesktopPluginRegistrarRef FlutterDesktopEngineGetPluginRegistrar( + FlutterDesktopEngineRef engine, const char* plugin_name) { // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(1); } -FlutterDesktopViewRef FlutterDesktopRegistrarGetView( +FlutterDesktopMessengerRef FlutterDesktopEngineGetMessenger( + FlutterDesktopEngineRef engine) { + // The stub ignores this, so just return an arbitrary non-zero value. + return reinterpret_cast(2); +} + +HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef controller) { + if (s_stub_implementation) { + return s_stub_implementation->ViewGetHWND(); + } + return reinterpret_cast(-1); +} + +FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView( FlutterDesktopPluginRegistrarRef controller) { // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(1); } + +void FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate, + void* user_data) { + if (s_stub_implementation) { + return s_stub_implementation + ->PluginRegistrarRegisterTopLevelWindowProcDelegate(delegate, + user_data); + } +} + +void FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate) { + if (s_stub_implementation) { + return s_stub_implementation + ->PluginRegistrarUnregisterTopLevelWindowProcDelegate(delegate); + } +} diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index 631d445802f28..532abff196776 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -29,31 +29,51 @@ class StubFlutterWindowsApi { virtual ~StubFlutterWindowsApi() {} - // Called for FlutterDesktopCreateViewController. - virtual FlutterDesktopViewControllerRef CreateViewController( - int width, - int height, + // Called for FlutterDesktopViewControllerCreate. + virtual FlutterDesktopViewControllerRef + ViewControllerCreate(int width, int height, FlutterDesktopEngineRef engine) { + return nullptr; + } + + // Called for FlutterDesktopViewControllerDestroy. + virtual void ViewControllerDestroy() {} + + // Called for FlutterDesktopViewControllerHandleTopLevelWindowProc. + virtual bool ViewControllerHandleTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { + return false; + } + + // Called for FlutterDesktopEngineCreate. + virtual FlutterDesktopEngineRef EngineCreate( const FlutterDesktopEngineProperties& engine_properties) { return nullptr; } - // Called for FlutterDesktopDestroyView. - virtual void DestroyViewController() {} + // Called for FlutterDesktopEngineDestroy. + virtual bool EngineDestroy() { return true; } + + // Called for FlutterDesktopEngineRun. + virtual bool EngineRun(const char* entry_point) { return true; } - // Called for FlutterDesktopProcessMessages. - virtual uint64_t ProcessMessages() { return 0; } + // Called for FlutterDesktopEngineProcessMessages. + virtual uint64_t EngineProcessMessages() { return 0; } // Called for FlutterDesktopViewGetHWND. virtual HWND ViewGetHWND() { return reinterpret_cast(1); } - // Called for FlutterDesktopRunEngine. - virtual FlutterDesktopEngineRef RunEngine( - const FlutterDesktopEngineProperties& engine_properties) { - return nullptr; - } + // Called for FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate. + virtual void PluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data) {} - // Called for FlutterDesktopShutDownEngine. - virtual bool ShutDownEngine() { return true; } + // Called for + // FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate. + virtual void PluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate) {} }; // A test helper that owns a stub implementation, making it the test stub for diff --git a/shell/platform/windows/cursor_handler.cc b/shell/platform/windows/cursor_handler.cc index cb696e5f63425..c740b4a1ee1c7 100644 --- a/shell/platform/windows/cursor_handler.cc +++ b/shell/platform/windows/cursor_handler.cc @@ -16,33 +16,32 @@ static constexpr char kKindKey[] = "kind"; namespace flutter { -CursorHandler::CursorHandler(flutter::BinaryMessenger* messenger, +CursorHandler::CursorHandler(BinaryMessenger* messenger, WindowBindingHandler* delegate) - : channel_(std::make_unique>( + : channel_(std::make_unique>( messenger, kChannelName, - &flutter::StandardMethodCodec::GetInstance())), + &StandardMethodCodec::GetInstance())), delegate_(delegate) { channel_->SetMethodCallHandler( - [this](const flutter::MethodCall& call, - std::unique_ptr> result) { + [this](const MethodCall& call, + std::unique_ptr> result) { HandleMethodCall(call, std::move(result)); }); } void CursorHandler::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { + const MethodCall& method_call, + std::unique_ptr> result) { const std::string& method = method_call.method_name(); if (method.compare(kActivateSystemCursorMethod) == 0) { - const flutter::EncodableMap& arguments = - method_call.arguments()->MapValue(); - auto kind_iter = arguments.find(EncodableValue(kKindKey)); + const auto& arguments = std::get(*method_call.arguments()); + auto kind_iter = arguments.find(EncodableValue(std::string(kKindKey))); if (kind_iter == arguments.end()) { result->Error("Argument error", "Missing argument while trying to activate system cursor"); } - const std::string& kind = kind_iter->second.StringValue(); + const auto& kind = std::get(kind_iter->second); delegate_->UpdateFlutterCursor(kind); result->Success(); } else { diff --git a/shell/platform/windows/flutter_project_bundle.cc b/shell/platform/windows/flutter_project_bundle.cc new file mode 100644 index 0000000000000..2459311524d40 --- /dev/null +++ b/shell/platform/windows/flutter_project_bundle.cc @@ -0,0 +1,81 @@ +// 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. + +#include "flutter/shell/platform/windows/flutter_project_bundle.h" + +#include +#include + +#include "flutter/shell/platform/common/cpp/path_utils.h" + +namespace flutter { + +FlutterProjectBundle::FlutterProjectBundle( + const FlutterDesktopEngineProperties& properties) + : assets_path_(properties.assets_path), + icu_path_(properties.icu_data_path) { + if (properties.aot_library_path != nullptr) { + aot_library_path_ = std::filesystem::path(properties.aot_library_path); + } + + // Resolve any relative paths. + if (assets_path_.is_relative() || icu_path_.is_relative() || + (!aot_library_path_.empty() && aot_library_path_.is_relative())) { + std::filesystem::path executable_location = GetExecutableDirectory(); + if (executable_location.empty()) { + std::cerr + << "Unable to find executable location to resolve resource paths." + << std::endl; + assets_path_ = std::filesystem::path(); + icu_path_ = std::filesystem::path(); + } else { + assets_path_ = std::filesystem::path(executable_location) / assets_path_; + icu_path_ = std::filesystem::path(executable_location) / icu_path_; + if (!aot_library_path_.empty()) { + aot_library_path_ = + std::filesystem::path(executable_location) / aot_library_path_; + } + } + } + + if (properties.switches_count > 0) { + switches_.insert(switches_.end(), &properties.switches[0], + &properties.switches[properties.switches_count]); + } +} + +bool FlutterProjectBundle::HasValidPaths() { + return !assets_path_.empty() && !icu_path_.empty(); +} + +// Attempts to load AOT data from the given path, which must be absolute and +// non-empty. Logs and returns nullptr on failure. +UniqueAotDataPtr FlutterProjectBundle::LoadAotData() { + if (aot_library_path_.empty()) { + std::cerr + << "Attempted to load AOT data, but no aot_library_path was provided." + << std::endl; + return nullptr; + } + if (!std::filesystem::exists(aot_library_path_)) { + std::cerr << "Can't load AOT data from " << aot_library_path_.u8string() + << "; no such file." << std::endl; + return nullptr; + } + std::string path_string = aot_library_path_.u8string(); + FlutterEngineAOTDataSource source = {}; + source.type = kFlutterEngineAOTDataSourceTypeElfPath; + source.elf_path = path_string.c_str(); + FlutterEngineAOTData data = nullptr; + auto result = FlutterEngineCreateAOTData(&source, &data); + if (result != kSuccess) { + std::cerr << "Failed to load AOT data from: " << path_string << std::endl; + return nullptr; + } + return UniqueAotDataPtr(data); +} + +FlutterProjectBundle::~FlutterProjectBundle() {} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_project_bundle.h b/shell/platform/windows/flutter_project_bundle.h new file mode 100644 index 0000000000000..f8ab57be1ee3c --- /dev/null +++ b/shell/platform/windows/flutter_project_bundle.h @@ -0,0 +1,65 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PROJECT_BUNDLE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PROJECT_BUNDLE_H_ + +#include +#include +#include + +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/public/flutter_windows.h" + +namespace flutter { + +struct AotDataDeleter { + void operator()(FlutterEngineAOTData aot_data) { + FlutterEngineCollectAOTData(aot_data); + } +}; +using UniqueAotDataPtr = std::unique_ptr<_FlutterEngineAOTData, AotDataDeleter>; + +// The data associated with a Flutter project needed to run it in an engine. +class FlutterProjectBundle { + public: + // Creates a new project bundle from the given properties. + // + // Treats any relative paths as relative to the directory of this executable. + explicit FlutterProjectBundle( + const FlutterDesktopEngineProperties& properties); + + ~FlutterProjectBundle(); + + // Whether or not the bundle is valid. This does not check that the paths + // exist, or contain valid data, just that paths were able to be constructed. + bool HasValidPaths(); + + // Returns the path to the assets directory. + const std::filesystem::path& assets_path() { return assets_path_; } + + // Returns the path to the ICU data file. + const std::filesystem::path& icu_path() { return icu_path_; } + + // Returns any switches that should be passed to the engine. + const std::vector& switches() { return switches_; } + + // Attempts to load AOT data for this bundle. The returned data must be + // retained until any engine instance it is passed to has been shut down. + // + // Logs and returns nullptr on failure. + UniqueAotDataPtr LoadAotData(); + + private: + std::filesystem::path assets_path_; + std::filesystem::path icu_path_; + std::vector switches_; + + // Path to the AOT library file, if any. + std::filesystem::path aot_library_path_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PROJECT_BUNDLE_H_ diff --git a/shell/platform/windows/flutter_windows.cc b/shell/platform/windows/flutter_windows.cc index 19bf9c97c77ec..1871021e40e9e 100644 --- a/shell/platform/windows/flutter_windows.cc +++ b/shell/platform/windows/flutter_windows.cc @@ -19,253 +19,160 @@ #include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" #include "flutter/shell/platform/common/cpp/path_utils.h" #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/flutter_project_bundle.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/win32_dpi_utils.h" #include "flutter/shell/platform/windows/win32_flutter_window.h" -#include "flutter/shell/platform/windows/win32_platform_handler.h" #include "flutter/shell/platform/windows/win32_task_runner.h" #include "flutter/shell/platform/windows/window_binding_handler.h" #include "flutter/shell/platform/windows/window_state.h" static_assert(FLUTTER_ENGINE_VERSION == 1, ""); -// Attempts to load AOT data from the given path, which must be absolute and -// non-empty. Logs and returns nullptr on failure. -UniqueAotDataPtr LoadAotData(std::filesystem::path aot_data_path) { - if (aot_data_path.empty()) { - std::cerr - << "Attempted to load AOT data, but no aot_library_path was provided." - << std::endl; - return nullptr; - } - if (!std::filesystem::exists(aot_data_path)) { - std::cerr << "Can't load AOT data from " << aot_data_path.u8string() - << "; no such file." << std::endl; - return nullptr; - } - std::string path_string = aot_data_path.u8string(); - FlutterEngineAOTDataSource source = {}; - source.type = kFlutterEngineAOTDataSourceTypeElfPath; - source.elf_path = path_string.c_str(); - FlutterEngineAOTData data = nullptr; - auto result = FlutterEngineCreateAOTData(&source, &data); - if (result != kSuccess) { - std::cerr << "Failed to load AOT data from: " << path_string << std::endl; - return nullptr; - } - return UniqueAotDataPtr(data); +// Returns the engine corresponding to the given opaque API handle. +static flutter::FlutterWindowsEngine* EngineFromHandle( + FlutterDesktopEngineRef ref) { + return reinterpret_cast(ref); } -// Spins up an instance of the Flutter Engine. -// -// This function launches the Flutter Engine in a background thread, supplying -// the necessary callbacks for rendering within a win32window (if one is -// provided). -// -// Returns the state object for the engine, or null on failure to start the -// engine. -static std::unique_ptr RunFlutterEngine( - flutter::FlutterWindowsView* view, - const FlutterDesktopEngineProperties& engine_properties) { - auto state = std::make_unique(); - - // FlutterProjectArgs is expecting a full argv, so when processing it for - // flags the first item is treated as the executable and ignored. Add a dummy - // value so that all provided arguments are used. - std::vector argv = {"placeholder"}; - if (engine_properties.switches_count > 0) { - argv.insert(argv.end(), &engine_properties.switches[0], - &engine_properties.switches[engine_properties.switches_count]); - } +// Returns the opaque API handle for the given engine instance. +static FlutterDesktopEngineRef HandleForEngine( + flutter::FlutterWindowsEngine* engine) { + return reinterpret_cast(engine); +} - view->CreateRenderSurface(); +// Returns the view corresponding to the given opaque API handle. +static flutter::FlutterWindowsView* ViewFromHandle(FlutterDesktopViewRef ref) { + return reinterpret_cast(ref); +} - // Provide the necessary callbacks for rendering within a win32 child window. - FlutterRendererConfig config = {}; - config.type = kOpenGL; - config.open_gl.struct_size = sizeof(config.open_gl); - config.open_gl.make_current = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->MakeCurrent(); - }; - config.open_gl.clear_current = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->ClearContext(); - }; - config.open_gl.present = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->SwapBuffers(); - }; - config.open_gl.fbo_callback = [](void* user_data) -> uint32_t { return 0; }; - config.open_gl.gl_proc_resolver = [](void* user_data, - const char* what) -> void* { - return reinterpret_cast(eglGetProcAddress(what)); - }; - config.open_gl.make_resource_current = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->MakeResourceCurrent(); - }; +// Returns the opaque API handle for the given view instance. +static FlutterDesktopViewRef HandleForView(flutter::FlutterWindowsView* view) { + return reinterpret_cast(view); +} - // Configure task runner interop. - auto state_ptr = state.get(); - state->task_runner = std::make_unique( - GetCurrentThreadId(), [state_ptr](const auto* task) { - if (FlutterEngineRunTask(state_ptr->engine, task) != kSuccess) { - std::cerr << "Could not post an engine task." << std::endl; - } - }); - FlutterTaskRunnerDescription platform_task_runner = {}; - platform_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription); - platform_task_runner.user_data = state->task_runner.get(); - platform_task_runner.runs_task_on_current_thread_callback = - [](void* user_data) -> bool { - return reinterpret_cast(user_data) - ->RunsTasksOnCurrentThread(); - }; - platform_task_runner.post_task_callback = [](FlutterTask task, - uint64_t target_time_nanos, - void* user_data) -> void { - reinterpret_cast(user_data)->PostTask( - task, target_time_nanos); - }; +FlutterDesktopViewControllerRef FlutterDesktopViewControllerCreate( + int width, + int height, + FlutterDesktopEngineRef engine) { + std::unique_ptr window_wrapper = + std::make_unique(width, height); - FlutterCustomTaskRunners custom_task_runners = {}; - custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); - custom_task_runners.platform_task_runner = &platform_task_runner; - - std::filesystem::path assets_path(engine_properties.assets_path); - std::filesystem::path icu_path(engine_properties.icu_data_path); - std::filesystem::path aot_library_path = - engine_properties.aot_library_path == nullptr - ? std::filesystem::path() - : std::filesystem::path(engine_properties.aot_library_path); - if (assets_path.is_relative() || icu_path.is_relative() || - (!aot_library_path.empty() && aot_library_path.is_relative())) { - // Treat relative paths as relative to the directory of this executable. - std::filesystem::path executable_location = - flutter::GetExecutableDirectory(); - if (executable_location.empty()) { - std::cerr - << "Unable to find executable location to resolve resource paths." - << std::endl; - return nullptr; - } - assets_path = std::filesystem::path(executable_location) / assets_path; - icu_path = std::filesystem::path(executable_location) / icu_path; - if (!aot_library_path.empty()) { - aot_library_path = - std::filesystem::path(executable_location) / aot_library_path; - } - } - std::string assets_path_string = assets_path.u8string(); - std::string icu_path_string = icu_path.u8string(); + auto state = std::make_unique(); + state->view = + std::make_unique(std::move(window_wrapper)); + state->view->CreateRenderSurface(); - if (FlutterEngineRunsAOTCompiledDartCode()) { - state->aot_data = LoadAotData(aot_library_path); - if (!state->aot_data) { - std::cerr << "Unable to start engine without AOT data." << std::endl; + // Take ownership of the engine, starting it if necessary. + state->view->SetEngine( + std::unique_ptr(EngineFromHandle(engine))); + if (!state->view->GetEngine()->running()) { + if (!state->view->GetEngine()->RunWithEntrypoint(nullptr)) { return nullptr; } } - FlutterProjectArgs args = {}; - args.struct_size = sizeof(FlutterProjectArgs); - args.assets_path = assets_path_string.c_str(); - args.icu_data_path = icu_path_string.c_str(); - args.command_line_argc = static_cast(argv.size()); - args.command_line_argv = &argv[0]; - args.platform_message_callback = - [](const FlutterPlatformMessage* engine_message, - void* user_data) -> void { - auto window = reinterpret_cast(user_data); - return window->HandlePlatformMessage(engine_message); - }; - args.custom_task_runners = &custom_task_runners; - if (state->aot_data) { - args.aot_data = state->aot_data.get(); - } - - FLUTTER_API_SYMBOL(FlutterEngine) engine = nullptr; - auto result = - FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, view, &engine); - if (result != kSuccess || engine == nullptr) { - std::cerr << "Failed to start Flutter engine: error " << result - << std::endl; - return nullptr; - } - state->engine = engine; - return state; + // Must happen after engine is running. + state->view->SendInitialBounds(); + return state.release(); } -FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( - int width, - int height, - const FlutterDesktopEngineProperties& engine_properties) { - std::unique_ptr window_wrapper = - std::make_unique(width, height); +void FlutterDesktopViewControllerDestroy( + FlutterDesktopViewControllerRef controller) { + delete controller; +} - FlutterDesktopViewControllerRef state = - flutter::FlutterWindowsView::CreateFlutterWindowsView( - std::move(window_wrapper)); +FlutterDesktopEngineRef FlutterDesktopViewControllerGetEngine( + FlutterDesktopViewControllerRef controller) { + return HandleForEngine(controller->view->GetEngine()); +} - auto engine_state = RunFlutterEngine(state->view.get(), engine_properties); +FlutterDesktopViewRef FlutterDesktopViewControllerGetView( + FlutterDesktopViewControllerRef controller) { + return HandleForView(controller->view.get()); +} - if (!engine_state) { - return nullptr; +bool FlutterDesktopViewControllerHandleTopLevelWindowProc( + FlutterDesktopViewControllerRef controller, + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { + std::optional delegate_result = + controller->view->GetEngine() + ->window_proc_delegate_manager() + ->OnTopLevelWindowProc(hwnd, message, wparam, lparam); + if (delegate_result) { + *result = *delegate_result; } - state->view->SetState(engine_state->engine); - state->engine_state = std::move(engine_state); - return state; + return delegate_result.has_value(); } -FlutterDesktopViewControllerRef FlutterDesktopCreateViewControllerLegacy( - int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { - std::filesystem::path assets_path_fs = std::filesystem::u8path(assets_path); - std::filesystem::path icu_data_path_fs = - std::filesystem::u8path(icu_data_path); - FlutterDesktopEngineProperties engine_properties = {}; - engine_properties.assets_path = assets_path_fs.c_str(); - engine_properties.icu_data_path = icu_data_path_fs.c_str(); - engine_properties.switches = arguments; - engine_properties.switches_count = argument_count; - - return FlutterDesktopCreateViewController(initial_width, initial_height, - engine_properties); +FlutterDesktopEngineRef FlutterDesktopEngineCreate( + const FlutterDesktopEngineProperties& engine_properties) { + flutter::FlutterProjectBundle project(engine_properties); + auto engine = std::make_unique(project); + return HandleForEngine(engine.release()); } -uint64_t FlutterDesktopProcessMessages( - FlutterDesktopViewControllerRef controller) { - return controller->engine_state->task_runner->ProcessTasks().count(); +bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine_ref) { + flutter::FlutterWindowsEngine* engine = EngineFromHandle(engine_ref); + bool result = true; + if (engine->running()) { + result = engine->Stop(); + } + delete engine; + return result; } -void FlutterDesktopDestroyViewController( - FlutterDesktopViewControllerRef controller) { - FlutterEngineShutdown(controller->engine_state->engine); - delete controller; +bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, + const char* entry_point) { + return EngineFromHandle(engine)->RunWithEntrypoint(entry_point); } -FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( - FlutterDesktopViewControllerRef controller, +uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { + return EngineFromHandle(engine)->task_runner()->ProcessTasks().count(); +} + +FlutterDesktopPluginRegistrarRef FlutterDesktopEngineGetPluginRegistrar( + FlutterDesktopEngineRef engine, const char* plugin_name) { // Currently, one registrar acts as the registrar for all plugins, so the // name is ignored. It is part of the API to reduce churn in the future when // aligning more closely with the Flutter registrar system. - return controller->view->GetRegistrar(); + return EngineFromHandle(engine)->GetRegistrar(); } -FlutterDesktopViewRef FlutterDesktopGetView( - FlutterDesktopViewControllerRef controller) { - return controller->view_wrapper.get(); +FlutterDesktopMessengerRef FlutterDesktopEngineGetMessenger( + FlutterDesktopEngineRef engine) { + return EngineFromHandle(engine)->messenger(); +} + +HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view) { + return std::get(*ViewFromHandle(view)->GetRenderTarget()); } -HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view_ref) { - return std::get(*view_ref->view->GetRenderTarget()); +FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView( + FlutterDesktopPluginRegistrarRef registrar) { + return HandleForView(registrar->engine->view()); +} + +void FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate, + void* user_data) { + registrar->engine->window_proc_delegate_manager() + ->RegisterTopLevelWindowProcDelegate(delegate, user_data); +} + +void FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate) { + registrar->engine->window_proc_delegate_manager() + ->UnregisterTopLevelWindowProcDelegate(delegate); } UINT FlutterDesktopGetDpiForHWND(HWND hwnd) { @@ -287,39 +194,17 @@ void FlutterDesktopResyncOutputStreams() { std::ios::sync_with_stdio(); } -FlutterDesktopEngineRef FlutterDesktopRunEngine( - const FlutterDesktopEngineProperties& engine_properties) { - auto engine = RunFlutterEngine(nullptr, engine_properties); - return engine.release(); -} - -bool FlutterDesktopShutDownEngine(FlutterDesktopEngineRef engine_ref) { - std::cout << "Shutting down flutter engine process." << std::endl; - auto result = FlutterEngineShutdown(engine_ref->engine); - delete engine_ref; - return (result == kSuccess); -} - -void FlutterDesktopRegistrarEnableInputBlocking( - FlutterDesktopPluginRegistrarRef registrar, - const char* channel) { - registrar->messenger->dispatcher->EnableInputBlockingForChannel(channel); -} +// Implementations of common/cpp/ API methods. FlutterDesktopMessengerRef FlutterDesktopRegistrarGetMessenger( FlutterDesktopPluginRegistrarRef registrar) { - return registrar->messenger.get(); + return registrar->engine->messenger(); } void FlutterDesktopRegistrarSetDestructionHandler( FlutterDesktopPluginRegistrarRef registrar, FlutterDesktopOnRegistrarDestroyed callback) { - registrar->destruction_handler = callback; -} - -FlutterDesktopViewRef FlutterDesktopRegistrarGetView( - FlutterDesktopPluginRegistrarRef registrar) { - return registrar->view; + registrar->engine->SetPluginRegistrarDestructionCallback(callback); } bool FlutterDesktopMessengerSendWithReply(FlutterDesktopMessengerRef messenger, @@ -331,7 +216,7 @@ bool FlutterDesktopMessengerSendWithReply(FlutterDesktopMessengerRef messenger, FlutterPlatformMessageResponseHandle* response_handle = nullptr; if (reply != nullptr && user_data != nullptr) { FlutterEngineResult result = FlutterPlatformMessageCreateResponseHandle( - messenger->engine, reply, user_data, &response_handle); + messenger->engine->engine(), reply, user_data, &response_handle); if (result != kSuccess) { std::cout << "Failed to create response handle\n"; return false; @@ -346,11 +231,11 @@ bool FlutterDesktopMessengerSendWithReply(FlutterDesktopMessengerRef messenger, response_handle, }; - FlutterEngineResult message_result = - FlutterEngineSendPlatformMessage(messenger->engine, &platform_message); + FlutterEngineResult message_result = FlutterEngineSendPlatformMessage( + messenger->engine->engine(), &platform_message); if (response_handle != nullptr) { - FlutterPlatformMessageReleaseResponseHandle(messenger->engine, + FlutterPlatformMessageReleaseResponseHandle(messenger->engine->engine(), response_handle); } @@ -370,13 +255,14 @@ void FlutterDesktopMessengerSendResponse( const FlutterDesktopMessageResponseHandle* handle, const uint8_t* data, size_t data_length) { - FlutterEngineSendPlatformMessageResponse(messenger->engine, handle, data, - data_length); + FlutterEngineSendPlatformMessageResponse(messenger->engine->engine(), handle, + data, data_length); } void FlutterDesktopMessengerSetCallback(FlutterDesktopMessengerRef messenger, const char* channel, FlutterDesktopMessageCallback callback, void* user_data) { - messenger->dispatcher->SetMessageCallback(channel, callback, user_data); + messenger->engine->message_dispatcher()->SetMessageCallback(channel, callback, + user_data); } diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc new file mode 100644 index 0000000000000..76af353ab0b52 --- /dev/null +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -0,0 +1,259 @@ +// 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. + +#include "flutter/shell/platform/windows/flutter_windows_engine.h" + +#include +#include +#include + +#include "flutter/shell/platform/common/cpp/path_utils.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" +#include "flutter/shell/platform/windows/string_conversion.h" +#include "flutter/shell/platform/windows/system_utils.h" + +namespace flutter { + +namespace { + +// Creates and returns a FlutterRendererConfig that renders to the view (if any) +// of a FlutterWindowsEngine, which should be the user_data received by the +// render callbacks. +FlutterRendererConfig GetRendererConfig() { + FlutterRendererConfig config = {}; + config.type = kOpenGL; + config.open_gl.struct_size = sizeof(config.open_gl); + config.open_gl.make_current = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->MakeCurrent(); + }; + config.open_gl.clear_current = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->ClearContext(); + }; + config.open_gl.present = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->SwapBuffers(); + }; + config.open_gl.fbo_callback = [](void* user_data) -> uint32_t { return 0; }; + config.open_gl.gl_proc_resolver = [](void* user_data, + const char* what) -> void* { + return reinterpret_cast(eglGetProcAddress(what)); + }; + config.open_gl.make_resource_current = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->MakeResourceCurrent(); + }; + return config; +} + +// Converts a FlutterPlatformMessage to an equivalent FlutterDesktopMessage. +static FlutterDesktopMessage ConvertToDesktopMessage( + const FlutterPlatformMessage& engine_message) { + FlutterDesktopMessage message = {}; + message.struct_size = sizeof(message); + message.channel = engine_message.channel; + message.message = engine_message.message; + message.message_size = engine_message.message_size; + message.response_handle = engine_message.response_handle; + return message; +} + +// Converts a LanguageInfo struct to a FlutterLocale struct. |info| must outlive +// the returned value, since the returned FlutterLocale has pointers into it. +FlutterLocale CovertToFlutterLocale(const LanguageInfo& info) { + FlutterLocale locale = {}; + locale.struct_size = sizeof(FlutterLocale); + locale.language_code = info.language.c_str(); + if (!info.region.empty()) { + locale.country_code = info.region.c_str(); + } + if (!info.script.empty()) { + locale.script_code = info.script.c_str(); + } + return locale; +} + +} // namespace + +FlutterWindowsEngine::FlutterWindowsEngine(const FlutterProjectBundle& project) + : project_(std::make_unique(project)) { + task_runner_ = std::make_unique( + GetCurrentThreadId(), [this](const auto* task) { + if (!engine_) { + std::cerr << "Cannot post an engine task when engine is not running." + << std::endl; + return; + } + if (FlutterEngineRunTask(engine_, task) != kSuccess) { + std::cerr << "Failed to post an engine task." << std::endl; + } + }); + + // Set up the legacy structs backing the API handles. + messenger_ = std::make_unique(); + messenger_->engine = this; + plugin_registrar_ = std::make_unique(); + plugin_registrar_->engine = this; + + message_dispatcher_ = + std::make_unique(messenger_.get()); + window_proc_delegate_manager_ = + std::make_unique(); +} + +FlutterWindowsEngine::~FlutterWindowsEngine() { + Stop(); +} + +bool FlutterWindowsEngine::RunWithEntrypoint(const char* entrypoint) { + if (!project_->HasValidPaths()) { + std::cerr << "Missing or unresolvable paths to assets." << std::endl; + return false; + } + std::string assets_path_string = project_->assets_path().u8string(); + std::string icu_path_string = project_->icu_path().u8string(); + if (FlutterEngineRunsAOTCompiledDartCode()) { + aot_data_ = project_->LoadAotData(); + if (!aot_data_) { + std::cerr << "Unable to start engine without AOT data." << std::endl; + return false; + } + } + + // FlutterProjectArgs is expecting a full argv, so when processing it for + // flags the first item is treated as the executable and ignored. Add a dummy + // value so that all provided arguments are used. + std::vector argv = {"placeholder"}; + std::transform( + project_->switches().begin(), project_->switches().end(), + std::back_inserter(argv), + [](const std::string& arg) -> const char* { return arg.c_str(); }); + + // Configure task runners. + FlutterTaskRunnerDescription platform_task_runner = {}; + platform_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription); + platform_task_runner.user_data = task_runner_.get(); + platform_task_runner.runs_task_on_current_thread_callback = + [](void* user_data) -> bool { + return static_cast(user_data)->RunsTasksOnCurrentThread(); + }; + platform_task_runner.post_task_callback = [](FlutterTask task, + uint64_t target_time_nanos, + void* user_data) -> void { + static_cast(user_data)->PostTask(task, target_time_nanos); + }; + FlutterCustomTaskRunners custom_task_runners = {}; + custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); + custom_task_runners.platform_task_runner = &platform_task_runner; + + FlutterProjectArgs args = {}; + args.struct_size = sizeof(FlutterProjectArgs); + args.assets_path = assets_path_string.c_str(); + args.icu_data_path = icu_path_string.c_str(); + args.command_line_argc = static_cast(argv.size()); + args.command_line_argv = argv.size() > 0 ? argv.data() : nullptr; + args.platform_message_callback = + [](const FlutterPlatformMessage* engine_message, + void* user_data) -> void { + auto host = static_cast(user_data); + return host->HandlePlatformMessage(engine_message); + }; + args.custom_task_runners = &custom_task_runners; + if (aot_data_) { + args.aot_data = aot_data_.get(); + } + if (entrypoint) { + args.custom_dart_entrypoint = entrypoint; + } + + FlutterRendererConfig renderer_config = GetRendererConfig(); + + auto result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &renderer_config, + &args, this, &engine_); + if (result != kSuccess || engine_ == nullptr) { + std::cerr << "Failed to start Flutter engine: error " << result + << std::endl; + return false; + } + + SendSystemSettings(); + + return true; +} + +bool FlutterWindowsEngine::Stop() { + if (engine_) { + if (plugin_registrar_destruction_callback_) { + plugin_registrar_destruction_callback_(plugin_registrar_.get()); + } + FlutterEngineResult result = FlutterEngineShutdown(engine_); + engine_ = nullptr; + return (result == kSuccess); + } + return false; +} + +void FlutterWindowsEngine::SetView(FlutterWindowsView* view) { + view_ = view; +} + +// Returns the currently configured Plugin Registrar. +FlutterDesktopPluginRegistrarRef FlutterWindowsEngine::GetRegistrar() { + return plugin_registrar_.get(); +} + +void FlutterWindowsEngine::SetPluginRegistrarDestructionCallback( + FlutterDesktopOnRegistrarDestroyed callback) { + plugin_registrar_destruction_callback_ = callback; +} + +void FlutterWindowsEngine::HandlePlatformMessage( + const FlutterPlatformMessage* engine_message) { + if (engine_message->struct_size != sizeof(FlutterPlatformMessage)) { + std::cerr << "Invalid message size received. Expected: " + << sizeof(FlutterPlatformMessage) << " but received " + << engine_message->struct_size << std::endl; + return; + } + + auto message = ConvertToDesktopMessage(*engine_message); + + message_dispatcher_->HandleMessage( + message, [this] {}, [this] {}); +} + +void FlutterWindowsEngine::SendSystemSettings() { + std::vector languages = GetPreferredLanguageInfo(); + std::vector flutter_locales; + flutter_locales.reserve(languages.size()); + for (const auto& info : languages) { + flutter_locales.push_back(CovertToFlutterLocale(info)); + } + // Convert the locale list to the locale pointer list that must be provided. + std::vector flutter_locale_list; + flutter_locale_list.reserve(flutter_locales.size()); + std::transform( + flutter_locales.begin(), flutter_locales.end(), + std::back_inserter(flutter_locale_list), + [](const auto& arg) -> const auto* { return &arg; }); + FlutterEngineUpdateLocales(engine_, flutter_locale_list.data(), + flutter_locale_list.size()); + + // TODO: Send 'flutter/settings' channel settings here as well. +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h new file mode 100644 index 0000000000000..d18a414ee5838 --- /dev/null +++ b/shell/platform/windows/flutter_windows_engine.h @@ -0,0 +1,126 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_ENGINE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_ENGINE_H_ + +#include +#include +#include +#include + +#include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" +#include "flutter/shell/platform/windows/flutter_project_bundle.h" +#include "flutter/shell/platform/windows/public/flutter_windows.h" +#include "flutter/shell/platform/windows/win32_task_runner.h" +#include "flutter/shell/platform/windows/win32_window_proc_delegate_manager.h" +#include "flutter/shell/platform/windows/window_state.h" + +namespace flutter { + +class FlutterWindowsView; + +// Manages state associated with the underlying FlutterEngine that isn't +// related to its display. +// +// In most cases this will be associated with a FlutterView, but if not will +// run in headless mode. +class FlutterWindowsEngine { + public: + // Creates a new Flutter engine object configured to run |project|. + explicit FlutterWindowsEngine(const FlutterProjectBundle& project); + + virtual ~FlutterWindowsEngine(); + + // Prevent copying. + FlutterWindowsEngine(FlutterWindowsEngine const&) = delete; + FlutterWindowsEngine& operator=(FlutterWindowsEngine const&) = delete; + + // Starts running the engine with the given entrypoint. If null, defaults to + // main(). + // + // Returns false if the engine couldn't be started. + bool RunWithEntrypoint(const char* entrypoint); + + // Returns true if the engine is currently running. + bool running() { return engine_ != nullptr; } + + // Stops the engine. This invalidates the pointer returned by engine(). + // + // Returns false if stopping the engine fails, or if it was not running. + bool Stop(); + + // Sets the view that is displaying this engine's content. + void SetView(FlutterWindowsView* view); + + // The view displaying this engine's content, if any. This will be null for + // headless engines. + FlutterWindowsView* view() { return view_; } + + // Returns the currently configured Plugin Registrar. + FlutterDesktopPluginRegistrarRef GetRegistrar(); + + // Sets |callback| to be called when the plugin registrar is destroyed. + void SetPluginRegistrarDestructionCallback( + FlutterDesktopOnRegistrarDestroyed callback); + + FLUTTER_API_SYMBOL(FlutterEngine) engine() { return engine_; } + + FlutterDesktopMessengerRef messenger() { return messenger_.get(); } + + IncomingMessageDispatcher* message_dispatcher() { + return message_dispatcher_.get(); + } + + Win32TaskRunner* task_runner() { return task_runner_.get(); } + + Win32WindowProcDelegateManager* window_proc_delegate_manager() { + return window_proc_delegate_manager_.get(); + } + + // Callback passed to Flutter engine for notifying window of platform + // messages. + void HandlePlatformMessage(const FlutterPlatformMessage*); + + private: + // Sends system settings (e.g., locale) to the engine. + // + // Should be called just after the engine is run, and after any relevant + // system changes. + void SendSystemSettings(); + + // The handle to the embedder.h engine instance. + FLUTTER_API_SYMBOL(FlutterEngine) engine_ = nullptr; + + std::unique_ptr project_; + + // AOT data, if any. + UniqueAotDataPtr aot_data_; + + // The view displaying the content running in this engine, if any. + FlutterWindowsView* view_ = nullptr; + + // Task runner for tasks posted from the engine. + std::unique_ptr task_runner_; + + // The plugin messenger handle given to API clients. + std::unique_ptr messenger_; + + // Message dispatch manager for messages from engine_. + std::unique_ptr message_dispatcher_; + + // The plugin registrar handle given to API clients. + std::unique_ptr plugin_registrar_; + + // A callback to be called when the engine (and thus the plugin registrar) + // is being destroyed. + FlutterDesktopOnRegistrarDestroyed plugin_registrar_destruction_callback_; + + // The manager for WindowProc delegate registration and callbacks. + std::unique_ptr window_proc_delegate_manager_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_ENGINE_H_ diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index db650a4e0ae7d..5736ce988f53e 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -1,63 +1,39 @@ +// 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. + #include "flutter/shell/platform/windows/flutter_windows_view.h" #include namespace flutter { -FlutterWindowsView::FlutterWindowsView() { +FlutterWindowsView::FlutterWindowsView( + std::unique_ptr window_binding) { surface_manager_ = std::make_unique(); + + // Take the binding handler, and give it a pointer back to self. + binding_handler_ = std::move(window_binding); + binding_handler_->SetView(this); + + render_target_ = std::make_unique( + binding_handler_->GetRenderTarget()); } FlutterWindowsView::~FlutterWindowsView() { DestroyRenderSurface(); - if (plugin_registrar_ && plugin_registrar_->destruction_handler) { - plugin_registrar_->destruction_handler(plugin_registrar_.get()); - } -} - -FlutterDesktopViewControllerRef FlutterWindowsView::CreateFlutterWindowsView( - std::unique_ptr windowbinding) { - auto state = std::make_unique(); - state->view = std::make_unique(); - - // FlutterWindowsView instance owns windowbinding - state->view->binding_handler_ = std::move(windowbinding); - - // a window wrapper for the state block, distinct from the - // window_wrapper handed to plugin_registrar. - state->view_wrapper = std::make_unique(); - - // Give the binding handler a pointer back to this | FlutterWindowsView | - state->view->binding_handler_->SetView(state->view.get()); - - // opaque pointer to FlutterWindowsView - state->view_wrapper->view = state->view.get(); - - state->view->render_target_ = std::make_unique( - state->view->binding_handler_->GetRenderTarget()); - - return state.release(); } -void FlutterWindowsView::SetState(FLUTTER_API_SYMBOL(FlutterEngine) eng) { - engine_ = eng; +void FlutterWindowsView::SetEngine( + std::unique_ptr engine) { + engine_ = std::move(engine); - auto messenger = std::make_unique(); - message_dispatcher_ = - std::make_unique(messenger.get()); - messenger->engine = engine_; - messenger->dispatcher = message_dispatcher_.get(); - - window_wrapper_ = std::make_unique(); - window_wrapper_->view = this; - plugin_registrar_ = std::make_unique(); - plugin_registrar_->messenger = std::move(messenger); - plugin_registrar_->view = window_wrapper_.get(); + engine_->SetView(this); internal_plugin_registrar_ = - std::make_unique(plugin_registrar_.get()); + std::make_unique(engine_->GetRegistrar()); - // Set up the keyboard handlers. + // Set up the system channel handlers. auto internal_plugin_messenger = internal_plugin_registrar_->messenger(); keyboard_hook_handlers_.push_back( std::make_unique(internal_plugin_messenger)); @@ -74,41 +50,9 @@ void FlutterWindowsView::SetState(FLUTTER_API_SYMBOL(FlutterEngine) eng) { binding_handler_->GetDpiScale()); } -FlutterDesktopPluginRegistrarRef FlutterWindowsView::GetRegistrar() { - return plugin_registrar_.get(); -} - -// Converts a FlutterPlatformMessage to an equivalent FlutterDesktopMessage. -static FlutterDesktopMessage ConvertToDesktopMessage( - const FlutterPlatformMessage& engine_message) { - FlutterDesktopMessage message = {}; - message.struct_size = sizeof(message); - message.channel = engine_message.channel; - message.message = engine_message.message; - message.message_size = engine_message.message_size; - message.response_handle = engine_message.response_handle; - return message; -} - -// The Flutter Engine calls out to this function when new platform messages -// are available. -void FlutterWindowsView::HandlePlatformMessage( - const FlutterPlatformMessage* engine_message) { - if (engine_message->struct_size != sizeof(FlutterPlatformMessage)) { - std::cerr << "Invalid message size received. Expected: " - << sizeof(FlutterPlatformMessage) << " but received " - << engine_message->struct_size << std::endl; - return; - } - - auto message = ConvertToDesktopMessage(*engine_message); - - message_dispatcher_->HandleMessage( - message, [this] {}, [this] {}); -} - void FlutterWindowsView::OnWindowSizeChanged(size_t width, size_t height) const { + surface_manager_->ResizeSurface(GetRenderTarget(), width, height); SendWindowMetrics(width, height, binding_handler_->GetDpiScale()); } @@ -162,17 +106,17 @@ void FlutterWindowsView::OnScroll(double x, } void FlutterWindowsView::OnFontChange() { - if (engine_ == nullptr) { + if (engine_->engine() == nullptr) { return; } - FlutterEngineReloadSystemFonts(engine_); + FlutterEngineReloadSystemFonts(engine_->engine()); } // Sends new size information to FlutterEngine. void FlutterWindowsView::SendWindowMetrics(size_t width, size_t height, double dpiScale) const { - if (engine_ == nullptr) { + if (engine_->engine() == nullptr) { return; } @@ -181,7 +125,14 @@ void FlutterWindowsView::SendWindowMetrics(size_t width, event.width = width; event.height = height; event.pixel_ratio = dpiScale; - auto result = FlutterEngineSendWindowMetricsEvent(engine_, &event); + auto result = FlutterEngineSendWindowMetricsEvent(engine_->engine(), &event); +} + +void FlutterWindowsView::SendInitialBounds() { + PhysicalWindowBounds bounds = binding_handler_->GetPhysicalWindowBounds(); + + SendWindowMetrics(bounds.width, bounds.height, + binding_handler_->GetDpiScale()); } // Set's |event_data|'s phase to either kMove or kHover depending on the current @@ -293,7 +244,7 @@ void FlutterWindowsView::SendPointerEventWithData( std::chrono::high_resolution_clock::now().time_since_epoch()) .count(); - FlutterEngineSendPointerEvent(engine_, &event, 1); + FlutterEngineSendPointerEvent(engine_->engine(), &event, 1); if (event_data.phase == FlutterPointerPhase::kAdd) { SetMouseFlutterStateAdded(true); @@ -320,7 +271,9 @@ bool FlutterWindowsView::SwapBuffers() { } void FlutterWindowsView::CreateRenderSurface() { - surface_manager_->CreateSurface(render_target_.get()); + PhysicalWindowBounds bounds = binding_handler_->GetPhysicalWindowBounds(); + surface_manager_->CreateSurface(GetRenderTarget(), bounds.width, + bounds.height); } void FlutterWindowsView::DestroyRenderSurface() { @@ -329,8 +282,12 @@ void FlutterWindowsView::DestroyRenderSurface() { } } -WindowsRenderTarget* FlutterWindowsView::GetRenderTarget() { +WindowsRenderTarget* FlutterWindowsView::GetRenderTarget() const { return render_target_.get(); } +FlutterWindowsEngine* FlutterWindowsView::GetEngine() { + return engine_.get(); +} + } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index f19e9f97ac02a..5df86e476299d 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -7,14 +7,15 @@ #include +#include #include #include #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h" -#include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/windows/angle_surface_manager.h" #include "flutter/shell/platform/windows/cursor_handler.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/key_event_handler.h" #include "flutter/shell/platform/windows/keyboard_hook_handler.h" #include "flutter/shell/platform/windows/public/flutter_windows.h" @@ -30,26 +31,18 @@ namespace flutter { // view that works with win32 hwnds and Windows::UI::Composition visuals. class FlutterWindowsView : public WindowBindingHandlerDelegate { public: - FlutterWindowsView(); + // Creates a FlutterWindowsView with the given implementator of + // WindowBindingHandler. + // + // In order for object to render Flutter content the SetEngine method must be + // called with a valid FlutterWindowsEngine instance. + FlutterWindowsView(std::unique_ptr window_binding); ~FlutterWindowsView(); - // Factory for creating FlutterWindowsView requiring an implementator of - // WindowBindingHandler. In order for object to render Flutter content - // the SetState method must be called with a valid FlutterEngine instance. - static FlutterDesktopViewControllerRef CreateFlutterWindowsView( - std::unique_ptr window_binding); - // Configures the window instance with an instance of a running Flutter // engine. - void SetState(FLUTTER_API_SYMBOL(FlutterEngine) state); - - // Returns the currently configured Plugin Registrar. - FlutterDesktopPluginRegistrarRef GetRegistrar(); - - // Callback passed to Flutter engine for notifying window of platform - // messages. - void HandlePlatformMessage(const FlutterPlatformMessage*); + void SetEngine(std::unique_ptr engine); // Creates rendering surface for Flutter engine to draw into. // Should be called before calling FlutterEngineRun using this view. @@ -59,7 +52,10 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { void DestroyRenderSurface(); // Return the currently configured WindowsRenderTarget. - WindowsRenderTarget* GetRenderTarget(); + WindowsRenderTarget* GetRenderTarget() const; + + // Returns the engine backing this view. + FlutterWindowsEngine* GetEngine(); // Callbacks for clearing context, settings context and swapping buffers. bool ClearContext(); @@ -67,6 +63,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { bool MakeResourceCurrent(); bool SwapBuffers(); + // Send initial bounds to embedder. Must occur after engine has initialized. + void SendInitialBounds(); + // |WindowBindingHandlerDelegate| void OnWindowSizeChanged(size_t width, size_t height) const override; @@ -189,21 +188,12 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { // surfaces. Surface creation functionality requires a valid render_target. std::unique_ptr surface_manager_; - // The handle to the Flutter engine instance. - FLUTTER_API_SYMBOL(FlutterEngine) engine_ = nullptr; + // The engine associated with this view. + std::unique_ptr engine_; // Keeps track of mouse state in relation to the window. MouseState mouse_state_; - // The window handle given to API clients. - std::unique_ptr window_wrapper_; - - // The plugin registrar handle given to API clients. - std::unique_ptr plugin_registrar_; - - // Message dispatch manager for messages from the Flutter engine. - std::unique_ptr message_dispatcher_; - // The plugin registrar managing internal plugins. std::unique_ptr internal_plugin_registrar_; diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h index 5915da7b32cae..3a636e60cb19f 100644 --- a/shell/platform/windows/public/flutter_windows.h +++ b/shell/platform/windows/public/flutter_windows.h @@ -22,10 +22,12 @@ typedef struct FlutterDesktopViewControllerState* FlutterDesktopViewControllerRef; // Opaque reference to a Flutter window. +struct FlutterDesktopView; typedef struct FlutterDesktopView* FlutterDesktopViewRef; // Opaque reference to a Flutter engine instance. -typedef struct FlutterDesktopEngineState* FlutterDesktopEngineRef; +struct FlutterDesktopEngine; +typedef struct FlutterDesktopEngine* FlutterDesktopEngineRef; // Properties for configuring a Flutter engine instance. typedef struct { @@ -55,64 +57,157 @@ typedef struct { size_t switches_count; } FlutterDesktopEngineProperties; -// Creates a View with the given dimensions running a Flutter Application. +// ========== View Controller ========== + +// Creates a view that hosts and displays the given engine instance. // -// This will set up and run an associated Flutter engine using the settings in -// |engine_properties|. +// This takes ownership of |engine|, so FlutterDesktopEngineDestroy should no +// longer be called on it, as it will be called internally when the view +// controller is destroyed. If creating the view controller fails, the engine +// will be destroyed immediately. // -// Returns a null pointer in the event of an error. -FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopCreateViewController( - int width, - int height, - const FlutterDesktopEngineProperties& engine_properties); - -// DEPRECATED. Will be removed soon; switch to the version above. +// If |engine| is not already running, the view controller will start running +// it automatically before displaying the window. +// +// The caller owns the returned reference, and is responsible for calling +// FlutterDesktopViewControllerDestroy. Returns a null pointer in the event of +// an error. FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopCreateViewControllerLegacy(int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count); +FlutterDesktopViewControllerCreate(int width, + int height, + FlutterDesktopEngineRef engine); // Shuts down the engine instance associated with |controller|, and cleans up // associated state. // // |controller| is no longer valid after this call. -FLUTTER_EXPORT void FlutterDesktopDestroyViewController( +FLUTTER_EXPORT void FlutterDesktopViewControllerDestroy( FlutterDesktopViewControllerRef controller); -// Returns the plugin registrar handle for the plugin with the given name. +// Returns the handle for the engine running in FlutterDesktopViewControllerRef. // -// The name must be unique across the application. -FLUTTER_EXPORT FlutterDesktopPluginRegistrarRef -FlutterDesktopGetPluginRegistrar(FlutterDesktopViewControllerRef controller, - const char* plugin_name); - +// Its lifetime is the same as the |controller|'s. +FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopViewControllerGetEngine( + FlutterDesktopViewControllerRef controller); // Returns the view managed by the given controller. + FLUTTER_EXPORT FlutterDesktopViewRef -FlutterDesktopGetView(FlutterDesktopViewControllerRef controller); +FlutterDesktopViewControllerGetView(FlutterDesktopViewControllerRef controller); + +// Allows the Flutter engine and any interested plugins an opportunity to +// handle the given message. +// +// If the WindowProc was handled and further handling should stop, this returns +// true and |result| will be populated. |result| is not set if returning false. +FLUTTER_EXPORT bool FlutterDesktopViewControllerHandleTopLevelWindowProc( + FlutterDesktopViewControllerRef controller, + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result); + +// ========== Engine ========== + +// Creates a Flutter engine with the given properties. +// +// The caller owns the returned reference, and is responsible for calling +// FlutterDesktopEngineDestroy. +FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopEngineCreate( + const FlutterDesktopEngineProperties& engine_properties); + +// Shuts down and destroys the given engine instance. Returns true if the +// shutdown was successful, or if the engine was not running. +// +// |engine| is no longer valid after this call. +FLUTTER_EXPORT bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine); + +// Starts running the given engine instance and optional entry point in the Dart +// project. If the entry point is null, defaults to main(). +// +// If provided, entry_point must be the name of a top-level function from the +// same Dart library that contains the app's main() function, and must be +// decorated with `@pragma(vm:entry-point)` to ensure the method is not +// tree-shaken by the Dart compiler. +// +// Returns false if running the engine failed. +FLUTTER_EXPORT bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, + const char* entry_point); // Processes any pending events in the Flutter engine, and returns the -// number of nanoseconds until the next scheduled event (or max, if none). +// number of nanoseconds until the next scheduled event (or max, if none). // // This should be called on every run of the application-level runloop, and // a wait for native events in the runloop should never be longer than the // last return value from this function. FLUTTER_EXPORT uint64_t -FlutterDesktopProcessMessages(FlutterDesktopViewControllerRef controller); +FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine); + +// Returns the plugin registrar handle for the plugin with the given name. +// +// The name must be unique across the application. +FLUTTER_EXPORT FlutterDesktopPluginRegistrarRef +FlutterDesktopEngineGetPluginRegistrar(FlutterDesktopEngineRef engine, + const char* plugin_name); + +// Returns the messenger associated with the engine. +FLUTTER_EXPORT FlutterDesktopMessengerRef +FlutterDesktopEngineGetMessenger(FlutterDesktopEngineRef engine); + +// ========== View ========== // Return backing HWND for manipulation in host application. FLUTTER_EXPORT HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view); +// ========== Plugin Registrar (extensions) ========== +// These are Windows-specific extensions to flutter_plugin_registrar.h + +// Function pointer type for top level WindowProc delegate registration. +// +// The user data will be whatever was passed to +// FlutterDesktopRegisterTopLevelWindowProcHandler. +// +// Implementations should populate |result| and return true if the WindowProc +// was handled and further handling should stop. |result| is ignored if the +// function returns false. +typedef bool (*FlutterDesktopWindowProcCallback)(HWND /* hwnd */, + UINT /* uMsg */, + WPARAM /*wParam*/, + LPARAM /* lParam*/, + void* /* user data */, + LRESULT* result); + +// Returns the view associated with this registrar's engine instance. +FLUTTER_EXPORT FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView( + FlutterDesktopPluginRegistrarRef registrar); + +FLUTTER_EXPORT void +FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate, + void* user_data); + +FLUTTER_EXPORT void +FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate); + +// ========== Freestanding Utilities ========== + // Gets the DPI for a given |hwnd|, depending on the supported APIs per // windows version and DPI awareness mode. If nullptr is passed, returns the DPI // of the primary monitor. +// +// This uses the same logic and fallback for older Windows versions that is used +// internally by Flutter to determine the DPI to use for displaying Flutter +// content, so should be used by any code (e.g., in plugins) that translates +// between Windows and Dart sizes/offsets. FLUTTER_EXPORT UINT FlutterDesktopGetDpiForHWND(HWND hwnd); // Gets the DPI for a given |monitor|. If the API is not available, a default // DPI of 96 is returned. +// +// See FlutterDesktopGetDpiForHWND for more information. FLUTTER_EXPORT UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor); // Reopens stdout and stderr and resysncs the standard library output streams. @@ -120,22 +215,6 @@ FLUTTER_EXPORT UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor); // (e.g., after an AllocConsole call). FLUTTER_EXPORT void FlutterDesktopResyncOutputStreams(); -// Runs an instance of a headless Flutter engine. -// -// Returns a null pointer in the event of an error. -FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopRunEngine( - const FlutterDesktopEngineProperties& engine_properties); - -// Shuts down the given engine instance. Returns true if the shutdown was -// successful. |engine_ref| is no longer valid after this call. -FLUTTER_EXPORT bool FlutterDesktopShutDownEngine( - FlutterDesktopEngineRef engine_ref); - -// Returns the view associated with this registrar's engine instance -// This is a Windows-specific extension to flutter_plugin_registrar.h. -FLUTTER_EXPORT FlutterDesktopViewRef -FlutterDesktopRegistrarGetView(FlutterDesktopPluginRegistrarRef registrar); - #if defined(__cplusplus) } // extern "C" #endif diff --git a/shell/platform/windows/system_utils.h b/shell/platform/windows/system_utils.h new file mode 100644 index 0000000000000..2585d4e8d5241 --- /dev/null +++ b/shell/platform/windows/system_utils.h @@ -0,0 +1,36 @@ +// 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. + +// This file contains utilities for system-level information/settings. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_SYSTEM_UTILS_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_SYSTEM_UTILS_H_ + +#include +#include + +namespace flutter { + +// Components of a system language/locale. +struct LanguageInfo { + std::string language; + std::string region; + std::string script; +}; + +// Returns the list of user-preferred languages, in preference order, +// parsed into LanguageInfo structures. +std::vector GetPreferredLanguageInfo(); + +// Returns the list of user-preferred languages, in preference order. +// The language names are as described at: +// https://docs.microsoft.com/en-us/windows/win32/intl/language-names +std::vector GetPreferredLanguages(); + +// Parses a Windows language name into its components. +LanguageInfo ParseLanguageName(std::wstring language_name); + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_SYSTEM_UTILS_H_ diff --git a/shell/platform/windows/system_utils_unittests.cc b/shell/platform/windows/system_utils_unittests.cc new file mode 100644 index 0000000000000..d784d5027645f --- /dev/null +++ b/shell/platform/windows/system_utils_unittests.cc @@ -0,0 +1,75 @@ +// 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. + +#include + +#include "flutter/shell/platform/windows/system_utils.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +TEST(SystemUtils, GetPreferredLanguageInfo) { + std::vector languages = GetPreferredLanguageInfo(); + // There should be at least one language. + ASSERT_GE(languages.size(), 1); + // The info should have a valid languge. + EXPECT_GE(languages[0].language.size(), 2); +} + +TEST(SystemUtils, GetPreferredLanguages) { + std::vector languages = GetPreferredLanguages(); + // There should be at least one language. + ASSERT_GE(languages.size(), 1); + // The language should be non-empty. + EXPECT_FALSE(languages[0].empty()); + // There should not be a trailing null from the parsing step. + EXPECT_EQ(languages[0].size(), wcslen(languages[0].c_str())); +} + +TEST(SystemUtils, ParseLanguageNameGeneric) { + LanguageInfo info = ParseLanguageName(L"en"); + EXPECT_EQ(info.language, "en"); + EXPECT_TRUE(info.region.empty()); + EXPECT_TRUE(info.script.empty()); +} + +TEST(SystemUtils, ParseLanguageNameWithRegion) { + LanguageInfo info = ParseLanguageName(L"hu-HU"); + EXPECT_EQ(info.language, "hu"); + EXPECT_EQ(info.region, "HU"); + EXPECT_TRUE(info.script.empty()); +} + +TEST(SystemUtils, ParseLanguageNameWithScript) { + LanguageInfo info = ParseLanguageName(L"us-Latn"); + EXPECT_EQ(info.language, "us"); + EXPECT_TRUE(info.region.empty()); + EXPECT_EQ(info.script, "Latn"); +} + +TEST(SystemUtils, ParseLanguageNameWithRegionAndScript) { + LanguageInfo info = ParseLanguageName(L"uz-Latn-UZ"); + EXPECT_EQ(info.language, "uz"); + EXPECT_EQ(info.region, "UZ"); + EXPECT_EQ(info.script, "Latn"); +} + +TEST(SystemUtils, ParseLanguageNameWithSuplementalLanguage) { + LanguageInfo info = ParseLanguageName(L"en-US-x-fabricam"); + EXPECT_EQ(info.language, "en"); + EXPECT_EQ(info.region, "US"); + EXPECT_TRUE(info.script.empty()); +} + +// Ensure that ISO 639-2/T codes are handled. +TEST(SystemUtils, ParseLanguageNameWithThreeCharacterLanguage) { + LanguageInfo info = ParseLanguageName(L"ale-ZZ"); + EXPECT_EQ(info.language, "ale"); + EXPECT_EQ(info.region, "ZZ"); + EXPECT_TRUE(info.script.empty()); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/system_utils_win32.cc b/shell/platform/windows/system_utils_win32.cc new file mode 100644 index 0000000000000..a198523bf536b --- /dev/null +++ b/shell/platform/windows/system_utils_win32.cc @@ -0,0 +1,93 @@ +// 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. + +#include "flutter/shell/platform/windows/system_utils.h" + +#include + +#include + +#include "flutter/shell/platform/windows/string_conversion.h" + +namespace flutter { + +std::vector GetPreferredLanguageInfo() { + std::vector languages = GetPreferredLanguages(); + std::vector language_info; + language_info.reserve(languages.size()); + + for (auto language : languages) { + language_info.push_back(ParseLanguageName(language)); + } + return language_info; +} + +std::vector GetPreferredLanguages() { + std::vector languages; + DWORD flags = MUI_LANGUAGE_NAME | MUI_UI_FALLBACK; + + // Get buffer length. + ULONG count = 0; + ULONG buffer_size = 0; + if (!::GetThreadPreferredUILanguages(flags, &count, nullptr, &buffer_size)) { + return languages; + } + + // Get the list of null-separated languages. + std::wstring buffer(buffer_size, '\0'); + if (!::GetThreadPreferredUILanguages(flags, &count, buffer.data(), + &buffer_size)) { + return languages; + } + + // Extract the individual languages from the buffer. + size_t start = 0; + while (true) { + // The buffer is terminated by an empty string (i.e., a double null). + if (buffer[start] == L'\0') { + break; + } + // Read the next null-terminated language. + std::wstring language(buffer.c_str() + start); + if (language.size() == 0) { + break; + } + languages.push_back(language); + // Skip past that language and its terminating null in the buffer. + start += language.size() + 1; + } + return languages; +} + +LanguageInfo ParseLanguageName(std::wstring language_name) { + LanguageInfo info; + + // Split by '-', discarding any suplemental language info (-x-foo). + std::vector components; + std::istringstream stream(Utf8FromUtf16(language_name)); + std::string component; + while (getline(stream, component, '-')) { + if (component == "x") { + break; + } + components.push_back(component); + } + + // Determine which components are which. + info.language = components[0]; + if (components.size() == 3) { + info.script = components[1]; + info.region = components[2]; + } else if (components.size() == 2) { + // A script code will always be four characters long. + if (components[1].size() == 4) { + info.script = components[1]; + } else { + info.region = components[1]; + } + } + return info; +} + +} // namespace flutter diff --git a/shell/platform/windows/win32_dpi_utils_unittests.cc b/shell/platform/windows/win32_dpi_utils_unittests.cc index 90a580bb95318..c580a9d55807c 100644 --- a/shell/platform/windows/win32_dpi_utils_unittests.cc +++ b/shell/platform/windows/win32_dpi_utils_unittests.cc @@ -1,3 +1,7 @@ +// 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. + #include #include "flutter/shell/platform/windows/win32_dpi_utils.h" diff --git a/shell/platform/windows/win32_flutter_window_unittests.cc b/shell/platform/windows/win32_flutter_window_unittests.cc index dece5a280f807..ff3a5d81f8cde 100644 --- a/shell/platform/windows/win32_flutter_window_unittests.cc +++ b/shell/platform/windows/win32_flutter_window_unittests.cc @@ -1,3 +1,7 @@ +// 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. + #include "flutter/shell/platform/windows/testing/win32_flutter_window_test.h" #include "gtest/gtest.h" diff --git a/shell/platform/windows/win32_platform_handler.cc b/shell/platform/windows/win32_platform_handler.cc index a9efc31f9a2d7..02a3d9c37faa2 100644 --- a/shell/platform/windows/win32_platform_handler.cc +++ b/shell/platform/windows/win32_platform_handler.cc @@ -228,12 +228,11 @@ void PlatformHandler::HandleMethodCall( if (!clipboard.Open(std::get(*view_->GetRenderTarget()))) { rapidjson::Document error_code; error_code.SetInt(::GetLastError()); - result->Error(kClipboardError, "Unable to open clipboard", &error_code); + result->Error(kClipboardError, "Unable to open clipboard", error_code); return; } if (!clipboard.HasString()) { - rapidjson::Document null; - result->Success(&null); + result->Success(rapidjson::Document()); return; } std::optional clipboard_string = clipboard.GetString(); @@ -241,7 +240,7 @@ void PlatformHandler::HandleMethodCall( rapidjson::Document error_code; error_code.SetInt(::GetLastError()); result->Error(kClipboardError, "Unable to get clipboard data", - &error_code); + error_code); return; } @@ -252,7 +251,7 @@ void PlatformHandler::HandleMethodCall( rapidjson::Value(kTextKey, allocator), rapidjson::Value(Utf8FromUtf16(*clipboard_string), allocator), allocator); - result->Success(&document); + result->Success(document); } else if (method.compare(kSetClipboardDataMethod) == 0) { const rapidjson::Value& document = *method_call.arguments(); rapidjson::Value::ConstMemberIterator itr = document.FindMember(kTextKey); @@ -266,14 +265,14 @@ void PlatformHandler::HandleMethodCall( if (!clipboard.Open(std::get(*view_->GetRenderTarget()))) { rapidjson::Document error_code; error_code.SetInt(::GetLastError()); - result->Error(kClipboardError, "Unable to open clipboard", &error_code); + result->Error(kClipboardError, "Unable to open clipboard", error_code); return; } if (!clipboard.SetString(Utf16FromUtf8(itr->value.GetString()))) { rapidjson::Document error_code; error_code.SetInt(::GetLastError()); result->Error(kClipboardError, "Unable to set clipboard data", - &error_code); + error_code); return; } result->Success(); diff --git a/shell/platform/windows/win32_window_proc_delegate_manager.cc b/shell/platform/windows/win32_window_proc_delegate_manager.cc new file mode 100644 index 0000000000000..3d70feb255658 --- /dev/null +++ b/shell/platform/windows/win32_window_proc_delegate_manager.cc @@ -0,0 +1,42 @@ +// 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. + +#include "flutter/shell/platform/windows/win32_window_proc_delegate_manager.h" + +#include "flutter/shell/platform/embedder/embedder.h" + +namespace flutter { + +Win32WindowProcDelegateManager::Win32WindowProcDelegateManager() = default; +Win32WindowProcDelegateManager::~Win32WindowProcDelegateManager() = default; + +void Win32WindowProcDelegateManager::RegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data) { + top_level_window_proc_handlers_[delegate] = user_data; +} + +void Win32WindowProcDelegateManager::UnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate) { + top_level_window_proc_handlers_.erase(delegate); +} + +std::optional Win32WindowProcDelegateManager::OnTopLevelWindowProc( + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + std::optional result; + for (const auto& [handler, user_data] : top_level_window_proc_handlers_) { + LPARAM handler_result; + // Stop as soon as any delegate indicates that it has handled the message. + if (handler(hwnd, message, wparam, lparam, user_data, &handler_result)) { + result = handler_result; + break; + } + } + return result; +} + +} // namespace flutter diff --git a/shell/platform/windows/win32_window_proc_delegate_manager.h b/shell/platform/windows/win32_window_proc_delegate_manager.h new file mode 100644 index 0000000000000..a87aad239671c --- /dev/null +++ b/shell/platform/windows/win32_window_proc_delegate_manager.h @@ -0,0 +1,58 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WIN32_WINDOW_PROC_DELEGATE_MANAGER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_WIN32_WINDOW_PROC_DELEGATE_MANAGER_H_ + +#include + +#include +#include + +#include "flutter/shell/platform/windows/public/flutter_windows.h" + +namespace flutter { + +// Handles registration, unregistration, and dispatching for WindowProc +// delegation. +class Win32WindowProcDelegateManager { + public: + explicit Win32WindowProcDelegateManager(); + ~Win32WindowProcDelegateManager(); + + // Prevent copying. + Win32WindowProcDelegateManager(Win32WindowProcDelegateManager const&) = + delete; + Win32WindowProcDelegateManager& operator=( + Win32WindowProcDelegateManager const&) = delete; + + // Adds |delegate| as a delegate to be called for |OnTopLevelWindowProc|. + // + // Multiple calls with the same |delegate| will replace the previous + // registration, even if |user_data| is different. + void RegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data); + + // Unregisters |delegate| as a delate for |OnTopLevelWindowProc|. + void UnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate); + + // Calls any registered WindowProc delegates. + // + // If a result is returned, then the message was handled in such a way that + // further handling should not be done. + std::optional OnTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam); + + private: + std::map + top_level_window_proc_handlers_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WIN32_WINDOW_PROC_DELEGATE_MANAGER_H_ diff --git a/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc b/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc new file mode 100644 index 0000000000000..27e5c77ae0a1c --- /dev/null +++ b/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc @@ -0,0 +1,168 @@ +// 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. + +#include "flutter/shell/platform/windows/win32_window_proc_delegate_manager.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { + +using TestWindowProcDelegate = std::function(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)>; + +// A FlutterDesktopWindowProcCallback that forwards to a std::function provided +// as user_data. +bool TestWindowProcCallback(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + void* user_data, + LRESULT* result) { + TestWindowProcDelegate& delegate = + *static_cast(user_data); + auto delegate_result = delegate(hwnd, message, wparam, lparam); + if (delegate_result) { + *result = *delegate_result; + } + return delegate_result.has_value(); +} + +// Same as the above, but with a different address, to test multiple +// registration. +bool TestWindowProcCallback2(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + void* user_data, + LRESULT* result) { + return TestWindowProcCallback(hwnd, message, wparam, lparam, user_data, + result); +} + +} // namespace + +TEST(Win32WindowProcDelegateManagerTest, CallsCorrectly) { + Win32WindowProcDelegateManager manager; + HWND dummy_hwnd; + + bool called = false; + TestWindowProcDelegate delegate = [&called, &dummy_hwnd]( + HWND hwnd, UINT message, WPARAM wparam, + LPARAM lparam) { + called = true; + EXPECT_EQ(hwnd, dummy_hwnd); + EXPECT_EQ(message, 2); + EXPECT_EQ(wparam, 3); + EXPECT_EQ(lparam, 4); + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, &delegate); + auto result = manager.OnTopLevelWindowProc(dummy_hwnd, 2, 3, 4); + + EXPECT_TRUE(called); + EXPECT_FALSE(result); +} + +TEST(Win32WindowProcDelegateManagerTest, ReplacementRegister) { + Win32WindowProcDelegateManager manager; + + bool called_a = false; + TestWindowProcDelegate delegate_a = + [&called_a](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(); + }; + bool called_b = false; + TestWindowProcDelegate delegate_b = + [&called_b](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_a); + // The function pointer is the same, so this should replace, not add. + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_b); + manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_FALSE(called_a); + EXPECT_TRUE(called_b); +} + +TEST(Win32WindowProcDelegateManagerTest, RegisterMultiple) { + Win32WindowProcDelegateManager manager; + + bool called_a = false; + TestWindowProcDelegate delegate_a = + [&called_a](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(); + }; + bool called_b = false; + TestWindowProcDelegate delegate_b = + [&called_b](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_a); + // Function pointer is different, so both should be called. + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback2, + &delegate_b); + manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_TRUE(called_a); + EXPECT_TRUE(called_b); +} + +TEST(Win32WindowProcDelegateManagerTest, ConflictingDelegates) { + Win32WindowProcDelegateManager manager; + + bool called_a = false; + TestWindowProcDelegate delegate_a = + [&called_a](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(1); + }; + bool called_b = false; + TestWindowProcDelegate delegate_b = + [&called_b](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(1); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_a); + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback2, + &delegate_b); + auto result = manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_TRUE(result); + // Exactly one of the handlers should be called since each will claim to have + // handled the message. Which one is unspecified, since the calling order is + // unspecified. + EXPECT_TRUE(called_a || called_b); + EXPECT_NE(called_a, called_b); +} + +TEST(Win32WindowProcDelegateManagerTest, Unregister) { + Win32WindowProcDelegateManager manager; + + bool called = false; + TestWindowProcDelegate delegate = [&called](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called = true; + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, &delegate); + manager.UnregisterTopLevelWindowProcDelegate(TestWindowProcCallback); + auto result = manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_FALSE(result); + EXPECT_FALSE(called); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/win32_window_unittests.cc b/shell/platform/windows/win32_window_unittests.cc index 0adc0b0388389..fea9faa4547d7 100644 --- a/shell/platform/windows/win32_window_unittests.cc +++ b/shell/platform/windows/win32_window_unittests.cc @@ -1,3 +1,7 @@ +// 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. + #include "flutter/shell/platform/windows/testing/win32_window_test.h" #include "gtest/gtest.h" diff --git a/shell/platform/windows/window_state.h b/shell/platform/windows/window_state.h index d1eceb866f507..3092a7bc0b90e 100644 --- a/shell/platform/windows/window_state.h +++ b/shell/platform/windows/window_state.h @@ -8,76 +8,36 @@ #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h" #include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" #include "flutter/shell/platform/embedder/embedder.h" -#include "flutter/shell/platform/windows/key_event_handler.h" -#include "flutter/shell/platform/windows/keyboard_hook_handler.h" -#include "flutter/shell/platform/windows/text_input_plugin.h" -#include "flutter/shell/platform/windows/win32_platform_handler.h" -#include "flutter/shell/platform/windows/win32_task_runner.h" + +// Structs backing the opaque references used in the C API. +// +// DO NOT ADD ANY NEW CODE HERE. These are legacy, and are being phased out +// in favor of objects that own and manage the relevant functionality. namespace flutter { +struct FlutterWindowsEngine; struct FlutterWindowsView; -} +} // namespace flutter -// Struct for storing state within an instance of the windows native (HWND or -// CoreWindow) Window. +// Wrapper to distinguish the view controller ref from the view ref given out +// in the C API. struct FlutterDesktopViewControllerState { - // The view that owns this state object. + // The view that backs this state object. std::unique_ptr view; - - // The state associate with the engine backing the view. - std::unique_ptr engine_state; - - // The window handle given to API clients. - std::unique_ptr view_wrapper; -}; - -// Opaque reference for the native windows itself. This is separate from the -// controller so that it can be provided to plugins without giving them access -// to all of the controller-based functionality. -struct FlutterDesktopView { - // The view that (indirectly) owns this state object. - flutter::FlutterWindowsView* view; -}; - -struct AotDataDeleter { - void operator()(FlutterEngineAOTData aot_data) { - FlutterEngineCollectAOTData(aot_data); - } }; -using UniqueAotDataPtr = std::unique_ptr<_FlutterEngineAOTData, AotDataDeleter>; - -// Struct for storing state of a Flutter engine instance. -struct FlutterDesktopEngineState { - // The handle to the Flutter engine instance. - FLUTTER_API_SYMBOL(FlutterEngine) engine; - - // Task runner for tasks posted from the engine. - std::unique_ptr task_runner; - - // AOT data, if any. - UniqueAotDataPtr aot_data; -}; - -// State associated with the plugin registrar. +// Wrapper to distinguish the plugin registrar ref from the engine ref given out +// in the C API. struct FlutterDesktopPluginRegistrar { - // The plugin messenger handle given to API clients. - std::unique_ptr messenger; - - // The handle for the view associated with this registrar. - FlutterDesktopView* view; - - // Callback to be called on registrar destruction. - FlutterDesktopOnRegistrarDestroyed destruction_handler; + // The engine that owns this state object. + flutter::FlutterWindowsEngine* engine = nullptr; }; -// State associated with the messenger used to communicate with the engine. +// Wrapper to distinguish the messenger ref from the engine ref given out +// in the C API. struct FlutterDesktopMessenger { - // The Flutter engine this messenger sends outgoing messages to. - FLUTTER_API_SYMBOL(FlutterEngine) engine; - - // The message dispatcher for handling incoming messages. - flutter::IncomingMessageDispatcher* dispatcher; + // The engine that owns this state object. + flutter::FlutterWindowsEngine* engine = nullptr; }; #endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOW_STATE_H_ diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 8d14e6376e4a8..7029e5f3bc724 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -140,8 +140,7 @@ int RunTester(const flutter::Settings& settings, }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); + return std::make_unique(shell); }; auto shell = Shell::Create(task_runners, // @@ -235,10 +234,10 @@ int RunTester(const flutter::Settings& settings, } }); - flutter::ViewportMetrics metrics; + flutter::ViewportMetrics metrics{}; metrics.device_pixel_ratio = 3.0; - metrics.physical_width = 2400; // 800 at 3x resolution - metrics.physical_height = 1800; // 600 at 3x resolution + metrics.physical_width = 2400.0; // 800 at 3x resolution. + metrics.physical_height = 1800.0; // 600 at 3x resolution. shell->GetPlatformView()->SetViewportMetrics(metrics); // Run the message loop and wait for the script to do its thing. diff --git a/sky/packages/sky_engine/LICENSE b/sky/packages/sky_engine/LICENSE index 817331779fd1e..1bd8c4561a569 100644 --- a/sky/packages/sky_engine/LICENSE +++ b/sky/packages/sky_engine/LICENSE @@ -6158,6 +6158,30 @@ freely, subject to the following restrictions: -------------------------------------------------------------------------------- harfbuzz +Copyright (C) 2011 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + Copyright (C) 2012 Grigori Goronzy Permission to use, copy, modify, and/or distribute this software for any @@ -6174,6 +6198,30 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- harfbuzz +Copyright (C) 2013 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + Copyright © 1998-2004 David Turner and Werner Lemberg Copyright © 2004,2007,2009 Red Hat, Inc. Copyright © 2011,2012 Google, Inc. @@ -6454,6 +6502,32 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- harfbuzz +Copyright © 2007,2008,2009 Red Hat, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2018 Khaled Hosny + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + Copyright © 2007,2008,2009,2010 Red Hat, Inc. Copyright © 2010,2012 Google, Inc. @@ -7839,7 +7913,7 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. harfbuzz Copyright © 2018 Ebrahim Byagowi -Copyright © 2018 Khaled Hosny +Copyright © 2020 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -8105,12 +8179,85 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- harfbuzz +Copyright © 2019-2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + +Copyright © 2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + +Copyright © 2020 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. For parts of HarfBuzz that are licensed under different licenses see individual files names COPYING in subdirectories where applicable. -Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019 Google, Inc. -Copyright © 2019 Facebook, Inc. +Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020 Google, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. Copyright © 2012 Mozilla Foundation Copyright © 2011 Codethink Limited Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) @@ -12893,7 +13040,7 @@ wasmer MIT License -Copyright (c) 2019 Wasmer, Inc. and its affiliates. +Copyright (c) 2019-present Wasmer, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/testing/BUILD.gn b/testing/BUILD.gn index db338fb42d646..71fc74b08b501 100644 --- a/testing/BUILD.gn +++ b/testing/BUILD.gn @@ -39,7 +39,7 @@ source_set("testing") { public_deps = [ ":testing_lib" ] } -source_set_maybe_fuchsia_legacy("dart") { +source_set("dart") { testonly = true sources = [ @@ -54,13 +54,12 @@ source_set_maybe_fuchsia_legacy("dart") { public_deps = [ ":testing_lib", "//flutter/common", + "//flutter/runtime", "//flutter/runtime:libdart", "//flutter/third_party/tonic", "//third_party/dart/runtime/bin:elf_loader", "//third_party/skia", ] - - public_deps_legacy_and_next = [ "//flutter/runtime:runtime" ] } source_set("skia") { @@ -80,7 +79,7 @@ source_set("skia") { ] } -source_set_maybe_fuchsia_legacy("fixture_test") { +source_set("fixture_test") { testonly = true sources = [ @@ -88,11 +87,10 @@ source_set_maybe_fuchsia_legacy("fixture_test") { "fixture_test.h", ] - public_deps = [ "//flutter/common" ] - - public_deps_legacy_and_next = [ + public_deps = [ ":dart", - "//flutter/runtime:runtime", + "//flutter/common", + "//flutter/runtime", ] } diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 7bf507206d0b1..8640cc86bf63d 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -38,6 +38,35 @@ void testCanvas(CanvasCallback callback) { } catch (error) { } // ignore: empty_catches } +void expectAssertion(Function callback) { + bool assertsEnabled = false; + assert(() { + assertsEnabled = true; + return true; + }()); + if (assertsEnabled) { + bool threw = false; + try { + callback(); + } catch (e) { + expect(e is AssertionError, true); + threw = true; + } + expect(threw, true); + } +} + +void expectArgumentError(Function callback) { + bool threw = false; + try { + callback(); + } catch (e) { + expect(e is ArgumentError, true); + threw = true; + } + expect(threw, true); +} + void testNoCrashes() { test('canvas APIs should not crash', () async { final Paint paint = Paint(); @@ -218,32 +247,54 @@ void main() { expect(areEqual, true); }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 - test('Image size reflected in picture size for image*, drawAtlas, and drawPicture methods', () async { + test('Null values allowed for drawAtlas methods', () async { final Image image = await createImage(100, 100); final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); const Rect rect = Rect.fromLTWH(0, 0, 100, 100); - canvas.drawImage(image, Offset.zero, Paint()); - canvas.drawImageRect(image, rect, rect, Paint()); - canvas.drawImageNine(image, rect, rect, Paint()); - canvas.drawAtlas(image, [], [], [], BlendMode.src, rect, Paint()); - final Picture picture = recorder.endRecording(); - - // Some of the numbers here appear to utilize sharing/reuse of common items, - // e.g. of the Paint() or same `Rect` usage, etc. - // The raw utilization of a 100x100 picture here should be 53333: - // 100 * 100 * 4 * (4/3) = 53333.333333.... - // To avoid platform specific idiosyncrasies and brittleness against changes - // to Skia, we just assert this is _at least_ 4x the image size. - const int minimumExpected = 53333 * 4; - expect(picture.approximateBytesUsed, greaterThan(minimumExpected)); - - final PictureRecorder recorder2 = PictureRecorder(); - final Canvas canvas2 = Canvas(recorder2); - canvas2.drawPicture(picture); - final Picture picture2 = recorder2.endRecording(); + final RSTransform transform = RSTransform(1, 0, 0, 0); + const Color color = Color(0); + final Paint paint = Paint(); + canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, rect, paint); + canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, null, paint); + canvas.drawAtlas(image, [transform], [rect], [], null, rect, paint); + canvas.drawAtlas(image, [transform], [rect], null, null, rect, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), BlendMode.src, rect, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), BlendMode.src, null, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), null, null, rect, paint); + + expectAssertion(() => canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, rect, null)); + expectAssertion(() => canvas.drawAtlas(image, [transform], [rect], [color], null, rect, paint)); + expectAssertion(() => canvas.drawAtlas(image, [transform], null, [color], BlendMode.src, rect, paint)); + expectAssertion(() => canvas.drawAtlas(image, null, [rect], [color], BlendMode.src, rect, paint)); + expectAssertion(() => canvas.drawAtlas(null, [transform], [rect], [color], BlendMode.src, rect, paint)); + }); - expect(picture2.approximateBytesUsed, greaterThan(minimumExpected)); + test('Data lengths must match for drawAtlas methods', () async { + final Image image = await createImage(100, 100); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + const Rect rect = Rect.fromLTWH(0, 0, 100, 100); + final RSTransform transform = RSTransform(1, 0, 0, 0); + const Color color = Color(0); + final Paint paint = Paint(); + canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, rect, paint); + canvas.drawAtlas(image, [transform, transform], [rect, rect], [color, color], BlendMode.src, rect, paint); + canvas.drawAtlas(image, [transform], [rect], [], null, rect, paint); + canvas.drawAtlas(image, [transform], [rect], null, null, rect, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), BlendMode.src, rect, paint); + canvas.drawRawAtlas(image, Float32List(4), Float32List(4), Int32List(1), BlendMode.src, rect, paint); + canvas.drawRawAtlas(image, Float32List(4), Float32List(4), null, null, rect, paint); + + expectArgumentError(() => canvas.drawAtlas(image, [transform], [], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [], [rect], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [transform], [rect], [color, color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [transform], [rect, rect], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [transform, transform], [rect], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(3), Float32List(3), null, null, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(4), Float32List(0), null, null, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(0), Float32List(4), null, null, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(4), Float32List(4), Int32List(2), BlendMode.src, rect, paint)); }); test('Vertex buffer size reflected in picture size for drawVertices', () async { diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index 186ca928a1c3e..21eda23623664 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -46,7 +46,6 @@ void main() { double? oldDPR; Size? oldSize; - double? oldDepth; WindowPadding? oldPadding; WindowPadding? oldInsets; WindowPadding? oldSystemGestureInsets; @@ -54,7 +53,6 @@ void main() { void setUp() { oldDPR = window.devicePixelRatio; oldSize = window.physicalSize; - oldDepth = window.physicalDepth; oldPadding = window.viewPadding; oldInsets = window.viewInsets; oldSystemGestureInsets = window.systemGestureInsets; @@ -76,7 +74,6 @@ void main() { oldDPR!, // DPR oldSize!.width, // width oldSize!.height, // height - oldDepth!, // depth oldPadding!.top, // padding top oldPadding!.right, // padding right oldPadding!.bottom, // padding bottom @@ -161,7 +158,6 @@ void main() { 0.1234, // DPR 0.0, // width 0.0, // height - 0.0, // depth 0.0, // padding top 0.0, // padding right 0.0, // padding bottom @@ -378,7 +374,6 @@ void main() { 1.0, // DPR 800.0, // width 600.0, // height - 100.0, // depth 50.0, // padding top 0.0, // padding right 40.0, // padding bottom @@ -396,14 +391,12 @@ void main() { expectEquals(window.viewInsets.bottom, 0.0); expectEquals(window.viewPadding.bottom, 40.0); expectEquals(window.padding.bottom, 40.0); - expectEquals(window.physicalDepth, 100.0); expectEquals(window.systemGestureInsets.bottom, 0.0); _updateWindowMetrics( 1.0, // DPR 800.0, // width 600.0, // height - 100.0, // depth 50.0, // padding top 0.0, // padding right 40.0, // padding bottom @@ -421,7 +414,6 @@ void main() { expectEquals(window.viewInsets.bottom, 400.0); expectEquals(window.viewPadding.bottom, 40.0); expectEquals(window.padding.bottom, 0.0); - expectEquals(window.physicalDepth, 100.0); expectEquals(window.systemGestureInsets.bottom, 44.0); }); } diff --git a/testing/dart/window_test.dart b/testing/dart/window_test.dart index a434482024d69..79767f881e7bc 100644 --- a/testing/dart/window_test.dart +++ b/testing/dart/window_test.dart @@ -22,8 +22,14 @@ void main() { }); test('FrameTiming.toString has the correct format', () { - final FrameTiming timing = FrameTiming([1000, 8000, 9000, 19500]); - expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, totalSpan: 18.5ms)'); + final FrameTiming timing = FrameTiming( + vsyncStart: 500, + buildStart: 1000, + buildFinish: 8000, + rasterStart: 9000, + rasterFinish: 19500 + ); + expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, vsyncOverhead: 0.5ms, totalSpan: 19.0ms)'); }); test('computePlatformResolvedLocale basic', () { diff --git a/testing/fuchsia/run_tests.sh b/testing/fuchsia/run_tests.sh index 93ec4bc0b2e63..61c2de7b8dd27 100755 --- a/testing/fuchsia/run_tests.sh +++ b/testing/fuchsia/run_tests.sh @@ -50,11 +50,11 @@ reboot() { --timeout-seconds $ssh_timeout_seconds \ --identity-file $pkey - echo "$(date) START:REBOOT ------------------------------------------" + echo "$(date) START:REBOOT ----------------------------------------" # note: this will set an exit code of 255, which we can ignore. ./fuchsia_ctl -d $device_name ssh -c "dm reboot-recovery" \ --identity-file $pkey || true - echo "$(date) END:REBOOT --------------------------------------------" + echo "$(date) END:REBOOT ------------------------------------------" } trap reboot EXIT @@ -103,7 +103,7 @@ echo "$(date) DONE:txt_tests ----------------------------------------" # once it passes on Fuchsia. # TODO(https://github.com/flutter/flutter/issues/58211): Re-enable MessageLoop # test once it passes on Fuchsia. -echo "$(date) START:fml_tests ----------------------------------------" +echo "$(date) START:fml_tests ---------------------------------------" ./fuchsia_ctl -d $device_name test \ -f fml_tests-0.far \ -t fml_tests \ @@ -121,13 +121,6 @@ echo "$(date) START:flow_tests --------------------------------------" --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f flow_tests_next-0.far \ - -t flow_tests_next \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages echo "$(date) DONE:flow_tests ---------------------------------------" @@ -138,13 +131,6 @@ echo "$(date) START:runtime_tests -----------------------------------" --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f runtime_tests_next-0.far \ - -t runtime_tests_next \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages echo "$(date) DONE:runtime_tests ------------------------------------" echo "$(date) START:ui_tests ----------------------------------------" @@ -154,31 +140,12 @@ echo "$(date) START:ui_tests ----------------------------------------" --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f ui_tests_next-0.far \ - -t ui_tests_next \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages echo "$(date) DONE:ui_tests -----------------------------------------" -# TODO(https://github.com/flutter/flutter/issues/53399): Re-enable -# OnServiceProtocolGetSkSLsWorks, CanLoadSkSLsFromAsset, and -# CanRemoveOldPersistentCache once they pass on Fuchsia. echo "$(date) START:shell_tests -------------------------------------" ./fuchsia_ctl -d $device_name test \ -f shell_tests-0.far \ -t shell_tests \ - -a "--gtest_filter=-ShellTest.CacheSkSLWorks:ShellTest.SetResourceCacheSize*:ShellTest.OnServiceProtocolGetSkSLsWorks:ShellTest.CanLoadSkSLsFromAsset:ShellTest.CanRemoveOldPersistentCache" \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f shell_tests_next-0.far \ - -t shell_tests_next \ - -a "--gtest_filter=-ShellTest.CacheSkSLWorks:ShellTest.SetResourceCacheSize*:ShellTest.OnServiceProtocolGetSkSLsWorks:ShellTest.CanLoadSkSLsFromAsset:ShellTest.CanRemoveOldPersistentCache" \ --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages @@ -194,13 +161,14 @@ echo "$(date) START:flutter_runner_tests ----------------------------" --timeout-seconds $test_timeout_seconds \ --packages-directory packages -./fuchsia_ctl -d $device_name test \ - -f flutter_aot_runner-0.far \ - -f flutter_runner_scenic_tests-0.far \ - -t flutter_runner_scenic_tests \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages +# TODO(https://github.com/flutter/flutter/issues/61768): De-flake and re-enable +# ./fuchsia_ctl -d $device_name test \ +# -f flutter_aot_runner-0.far \ +# -f flutter_runner_scenic_tests-0.far \ +# -t flutter_runner_scenic_tests \ +# --identity-file $pkey \ +# --timeout-seconds $test_timeout_seconds \ +# --packages-directory packages ./fuchsia_ctl -d $device_name test \ -f flutter_aot_runner-0.far \ diff --git a/testing/fuchsia/test_fars b/testing/fuchsia/test_fars index b26051c1ea870..e5b84456afbb7 100644 --- a/testing/fuchsia/test_fars +++ b/testing/fuchsia/test_fars @@ -8,7 +8,3 @@ shell_tests-0.far testing_tests-0.far txt_tests-0.far ui_tests-0.far -flow_tests_next-0.far -runtime_tests_next-0.far -shell_tests_next-0.far -ui_tests_next-0.far diff --git a/testing/ios/IosUnitTests/App/Info.plist b/testing/ios/IosUnitTests/App/Info.plist index 16be3b681122d..52b6c2050105b 100644 --- a/testing/ios/IosUnitTests/App/Info.plist +++ b/testing/ios/IosUnitTests/App/Info.plist @@ -24,6 +24,26 @@ LaunchScreen UIMainStoryboardFile Main + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + invalid-site.com + + NSIncludesSubdomains + + NSThirdPartyExceptionAllowsInsecureHTTPLoads + + + sub.invalid-site.com + + NSThirdPartyExceptionAllowsInsecureHTTPLoads + + + + UIRequiredDeviceCapabilities armv7 diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 598624564fe53..eeaef513460a9 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -28,6 +28,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SemanticsObjectTest.mm; sourceTree = ""; }; + 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEngineTest.mm; sourceTree = ""; }; + 0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = accessibility_bridge_test.mm; sourceTree = ""; }; + 0AC2330B24BA71D300A85907 /* FlutterTextInputPluginTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterTextInputPluginTest.m; sourceTree = ""; }; + 0AC2330F24BA71D300A85907 /* FlutterBinaryMessengerRelayTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterBinaryMessengerRelayTest.mm; sourceTree = ""; }; + 0AC2331024BA71D300A85907 /* connection_collection_test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = connection_collection_test.mm; sourceTree = ""; }; + 0AC2331224BA71D300A85907 /* FlutterEnginePlatformViewTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEnginePlatformViewTest.mm; sourceTree = ""; }; + 0AC2331924BA71D300A85907 /* FlutterPluginAppLifeCycleDelegateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterPluginAppLifeCycleDelegateTest.m; sourceTree = ""; }; + 0AC2332124BA71D300A85907 /* FlutterViewControllerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterViewControllerTest.mm; sourceTree = ""; }; 0D1CE5D7233430F400E5D880 /* FlutterChannelsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterChannelsTest.m; sourceTree = ""; }; 0D6AB6B122BB05E100EEE540 /* IosUnitTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IosUnitTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6B422BB05E100EEE540 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -62,6 +71,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0AC232E924BA71D300A85907 /* Source */ = { + isa = PBXGroup; + children = ( + 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */, + 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */, + 0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */, + 0AC2330B24BA71D300A85907 /* FlutterTextInputPluginTest.m */, + 0AC2330F24BA71D300A85907 /* FlutterBinaryMessengerRelayTest.mm */, + 0AC2331024BA71D300A85907 /* connection_collection_test.mm */, + 0AC2331224BA71D300A85907 /* FlutterEnginePlatformViewTest.mm */, + 0AC2331924BA71D300A85907 /* FlutterPluginAppLifeCycleDelegateTest.m */, + 0AC2332124BA71D300A85907 /* FlutterViewControllerTest.mm */, + ); + name = Source; + path = ../../../shell/platform/darwin/ios/framework/Source; + sourceTree = ""; + }; 0D1CE5D62334309900E5D880 /* Source-Common */ = { isa = PBXGroup; children = ( @@ -74,6 +100,7 @@ 0D6AB6A822BB05E100EEE540 = { isa = PBXGroup; children = ( + 0AC232E924BA71D300A85907 /* Source */, 0D6AB6B322BB05E100EEE540 /* App */, 0D6AB6CC22BB05E200EEE540 /* Tests */, 0D6AB6B222BB05E100EEE540 /* Products */, diff --git a/testing/ios/IosUnitTests/run_tests.sh b/testing/ios/IosUnitTests/run_tests.sh index 6c44de0aef00f..a06335283ac17 100755 --- a/testing/ios/IosUnitTests/run_tests.sh +++ b/testing/ios/IosUnitTests/run_tests.sh @@ -8,9 +8,6 @@ if [ $# -eq 1 ]; then FLUTTER_ENGINE=$1 fi -set -o pipefail && xcodebuild -sdk iphonesimulator \ - -scheme IosUnitTests \ - -destination 'platform=iOS Simulator,name=iPhone 8' \ - test \ - FLUTTER_ENGINE=$FLUTTER_ENGINE +../../run_tests.py --variant $FLUTTER_ENGINE --type objc --ios-variant $FLUTTER_ENGINE + popd diff --git a/testing/run_tests.py b/testing/run_tests.py index 7df1757ee80b0..a5c7d833fb279 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -331,10 +331,11 @@ def AssertExpectedJavaVersion(): def AssertExpectedXcodeVersion(): """Checks that the user has a recent version of Xcode installed""" - EXPECTED_MAJOR_VERSION = '11' + EXPECTED_MAJOR_VERSION = ['11', '12'] version_output = subprocess.check_output(['xcodebuild', '-version']) + match = re.match("Xcode (\d+)", version_output) message = "Xcode must be installed to run the iOS embedding unit tests" - assert "Xcode %s." % EXPECTED_MAJOR_VERSION in version_output, message + assert match.group(1) in EXPECTED_MAJOR_VERSION, message def RunJavaTests(filter, android_variant='android_debug_unopt'): """Runs the Java JUnit unit tests for the Android embedding""" @@ -344,7 +345,7 @@ def RunJavaTests(filter, android_variant='android_debug_unopt'): embedding_deps_dir = os.path.join(buildroot_dir, 'third_party', 'android_embedding_dependencies', 'lib') classpath = map(str, [ - os.path.join(buildroot_dir, 'third_party', 'android_tools', 'sdk', 'platforms', 'android-29', 'android.jar'), + os.path.join(buildroot_dir, 'third_party', 'android_tools', 'sdk', 'platforms', 'android-30', 'android.jar'), os.path.join(embedding_deps_dir, '*'), # Wildcard for all jars in the directory os.path.join(android_out_dir, 'flutter.jar'), os.path.join(android_out_dir, 'robolectric_tests.jar') @@ -369,17 +370,19 @@ def RunObjcTests(ios_variant='ios_debug_sim_unopt'): ios_out_dir = os.path.join(out_dir, ios_variant) EnsureIosTestsAreBuilt(ios_out_dir) - pretty = "cat" if subprocess.call(["which", "xcpretty"]) else "xcpretty" ios_unit_test_dir = os.path.join(buildroot_dir, 'flutter', 'testing', 'ios', 'IosUnitTests') + # Avoid using xcpretty unless the following can be addressed: + # - Make sure all relevant failure output is printed on a failure. + # - Make sure that a failing exit code is set for CI. + # See https://github.com/flutter/flutter/issues/63742 command = [ 'xcodebuild ' '-sdk iphonesimulator ' '-scheme IosUnitTests ' "-destination platform='iOS Simulator,name=iPhone 8' " 'test ' - 'FLUTTER_ENGINE=' + ios_variant + - ' | ' + pretty + 'FLUTTER_ENGINE=' + ios_variant ] RunCmd(command, cwd=ios_unit_test_dir, shell=True) diff --git a/testing/scenario_app/.gitignore b/testing/scenario_app/.gitignore index 6dc8f6e23d125..b9de2ce97b175 100644 --- a/testing/scenario_app/.gitignore +++ b/testing/scenario_app/.gitignore @@ -5,3 +5,6 @@ build/ ios/Scenarios/*.framework/ android/app/libs/flutter.jar android/app/src/main/jniLibs/arm64-v8a/libapp.so +android/gradle-home/.cache + +!.vpython diff --git a/testing/scenario_app/README.md b/testing/scenario_app/README.md index 6f78ad9afdaca..131f7186feaf5 100644 --- a/testing/scenario_app/README.md +++ b/testing/scenario_app/README.md @@ -42,6 +42,19 @@ compared against golden reside. ## Running for Android +The test is run on a x86 emulator. To run the test locally, you must create an emulator running API level 28, using an x86_64 ABI, and set the following screen settings in the avd's `config.ini` file: + +``` +hw.lcd.density = 480 +hw.lcd.height = 1920 +hw.lcd.width = 1080 +lcd.depth = 16 +``` + +This file is typically located in your `$HOME/.android/avd/` folder. + +Once the emulator is up, you can run the test by running: + ```bash ./build_and_run_android_tests.sh ``` diff --git a/testing/scenario_app/android/app/build.gradle b/testing/scenario_app/android/app/build.gradle index 4438a32d79ac1..44915f7d7050e 100644 --- a/testing/scenario_app/android/app/build.gradle +++ b/testing/scenario_app/android/app/build.gradle @@ -7,7 +7,7 @@ screenshots { } android { - compileSdkVersion 28 + compileSdkVersion 30 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -26,6 +26,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + sourceSets { + main.assets.srcDirs += "${project.buildDir}/assets" + } } dependencies { diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java index 85dcfa5051e7b..d060bf6ccb907 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java @@ -13,7 +13,6 @@ import android.util.Xml; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnitRunner; -import androidx.test.runner.screenshot.Screenshot; import com.facebook.testing.screenshot.ScreenshotRunner; import com.facebook.testing.screenshot.internal.AlbumImpl; import com.facebook.testing.screenshot.internal.Registry; @@ -164,8 +163,6 @@ public static void capture(TestableFlutterActivity activity) // This method is called from the runner thread, // so block the UI thread while taking the screenshot. - // UiThreadLocker locker = new UiThreadLocker(); - // locker.lock(); // Screenshot.capture(view or activity) does not capture the Flutter UI. // Unfortunately, it doesn't work with Android's `Surface` or `TextureSurface`. @@ -182,7 +179,8 @@ public static void capture(TestableFlutterActivity activity) new Callable() { @Override public Void call() { - Bitmap bitmap = Screenshot.capture().getBitmap(); + Bitmap bitmap = + InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot(); // Remove the status and action bars from the screenshot capture. bitmap = Bitmap.createBitmap( diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java index c00042ade9a5a..457d980c5ecae 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java @@ -4,27 +4,34 @@ package dev.flutter.scenarios; -import android.os.Bundle; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import java.util.concurrent.atomic.AtomicBoolean; public class TestableFlutterActivity extends FlutterActivity { - private Object flutterUiRenderedLock; + private Object flutterUiRenderedLock = new Object(); + private AtomicBoolean isScenarioReady = new AtomicBoolean(false); @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Reset the lock. - flutterUiRenderedLock = new Object(); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + flutterEngine + .getDartExecutor() + .setMessageHandler("take_screenshot", (byteBuffer, binaryReply) -> notifyFlutterRendered()); } protected void notifyFlutterRendered() { synchronized (flutterUiRenderedLock) { + isScenarioReady.set(true); flutterUiRenderedLock.notifyAll(); } } public void waitUntilFlutterRendered() { try { + if (isScenarioReady.get()) { + return; + } synchronized (flutterUiRenderedLock) { flutterUiRenderedLock.wait(); } diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java index 486d848e1697c..1fd0609df3bc1 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java @@ -12,7 +12,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.view.Choreographer; import androidx.annotation.NonNull; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; @@ -71,6 +70,7 @@ public FlutterShellArgs getFlutterShellArgs() { @Override public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); flutterEngine .getPlatformViewsController() .getRegistry() @@ -89,22 +89,6 @@ public void onFlutterUiDisplayed() { test.put("name", launchIntent.getStringExtra("scenario")); test.put("use_android_view", launchIntent.getBooleanExtra("use_android_view", false)); channel.invokeMethod("set_scenario", test); - - notifyFlutterRenderedAfterVsync(); - } - - private void notifyFlutterRenderedAfterVsync() { - // Wait 1s after the next frame, so the Android texture are rendered. - Choreographer.getInstance() - .postFrameCallbackDelayed( - new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - reportFullyDrawn(); - notifyFlutterRendered(); - } - }, - 1000L); } private void writeTimelineData(Uri logFile) { diff --git a/testing/scenario_app/android/build.gradle b/testing/scenario_app/android/build.gradle index 7f9bc60362ffa..11451c5bbf298 100644 --- a/testing/scenario_app/android/build.gradle +++ b/testing/scenario_app/android/build.gradle @@ -4,12 +4,11 @@ buildscript { repositories { google() jcenter() - } dependencies { classpath 'com.android.tools.build:gradle:3.6.0' classpath 'com.facebook.testing.screenshot:plugin:0.12.0' - + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/testing/scenario_app/android/gradle-home/.vpython b/testing/scenario_app/android/gradle-home/.vpython new file mode 100644 index 0000000000000..741711e4e06bc --- /dev/null +++ b/testing/scenario_app/android/gradle-home/.vpython @@ -0,0 +1,11 @@ +# +# "vpython" specification file for build +# +# Pillow is required by com.facebook.testing.screenshot. + +python_version: "2.7" + +wheel: < + name: "infra/python/wheels/pillow/${vpython_platform}" + version: "version:6.0.0" +> diff --git a/testing/scenario_app/android/gradle-home/bin/python b/testing/scenario_app/android/gradle-home/bin/python new file mode 100755 index 0000000000000..367e9b679d413 --- /dev/null +++ b/testing/scenario_app/android/gradle-home/bin/python @@ -0,0 +1,14 @@ +#!/bin/bash + +# This script is added to the PATH on LUCI when executing the junit tests. +# The Python scripts used to compare the screenshots are contained in a jar file. +# As a result, vpython cannot find the .vpython spec file because it's outside +# of the jar file. + +set -e + +SCRIPT_DIR="$( dirname "${BASH_SOURCE[0]}" )" + +echo "Running from $SCRIPT_DIR/bin/python" + +vpython -vpython-spec "$SCRIPT_DIR/../.vpython" "$@" diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformView.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformView.png index f3379e547e5fdfd3001ba9645444876bbb9f5848..731fa2cef4e5bc3d6cc81415571f6fe9d5e5da8b 100644 GIT binary patch literal 29557 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 z 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 KBN_j)zWqOZJ8BL9 literal 30028 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprect.png index 831086188cc152fedddf6690ef1e2b09bb0a8eea..fa335e53e51ccd6b316e2b8b20ba403037897013 100644 GIT binary patch literal 18389 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 literal 21585 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 Q5$`>B=8{hSDf_?v7X!F(^Z)<= diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprrect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprrect.png index d6a5eede8c1981c997c5c59ac7a387b87f502b9b..d827032a9b2392cfb85f4811805d35db71fb0cc0 100644 GIT binary patch literal 20209 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 literal 23520 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 literal 53141 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 QXc!ngUHx3vIVCg!0OlPPqW}N^ diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleBackgroundForeground.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleBackgroundForeground.png index 46f3bc5f784040d64f82b86a340c49a2fe3409a6..03df55e98afda985eddc81de5c64e67d911b57cc 100644 GIT binary patch literal 11452 zcmeHN>r0bS9DbIUnWkL?nOY=CMsz`uN~hB_Q^bRzz83 z8XEW^uf3%UH(Ld+dbfnca%64c2O;OB3uD@*op*i6{~&Yc{dNu0=VDZmr0k<= zwOU=GR3?-M!XSNW15y$b6T^*0BXb;3-7)Y$&=Q2gi00;IZpsx(_I+k(c6L^)({cE+ z6CNmrVM>aNWB)=84-cE_fvIuVUO!@nSD_wbB$L&PeUL*U!V-;&E%RdHjUTZ$u5BPb z21<#==z!XQ#sSR*dLGa-k=6xT4rxn3+oA>A8g{qHTbMM$XtE2I1Gi1InWe${kAt;) zM>)`;Kb?0x!x_dt;;5#Wqp!esLXm-}jDUw*x#`S6S`g=Nm>?FJF zEo3a!LZYcqlCm-(x6M>eO{g57oYb%^ z%i(v$a8Saeq)0xWKM%@mHrtm1*0<6u;*<(+p#Oh_4xM6+{GMnDhThG@*Z)vt7i0j9 zxs?7>34|&?RQaLG4^@6hJzV5nWI1d1vmVR)U9>n zSXV9uF=CEKS%qd*`{R^Gpo4&drSb(7E^f7ZwJESLU`r{R|Iu8<~OB z31Og_3=KA;oY62CO$VcSVYECLEfYuUh0*G8v`H}9EE;VZjy8`;=pyU}b~V0mFg%!R z3+!Md%wT5+>Yboo03;bInMOIIVL2|}9%KY2$0wXJ zK+<8}fluTa(Y!EP9*mZWqxHgQbvU$}1b3^e4!WPSyete%$_bP0l+XkKQQw&% diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleWithoutOverlays.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleWithoutOverlays.png index a39406e1f5917eb01dd30d413101dae4ff01f529..24c9c9d3278e2500cd127bcfe1660541be1c03f6 100644 GIT binary patch literal 43916 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 literal 62517 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewOpacity.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewOpacity.png index 19269308da7e002c036d2717a358d2effa9b4827..30c42c9ea7d2e8c96414e002f6b7e45988033eb9 100644 GIT binary patch literal 22231 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 !O1S!Ts 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 literal 31570 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewRotate.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewRotate.png index f3379e547e5fdfd3001ba9645444876bbb9f5848..183f24f433da5000607e53e4cc0a33452655f4f5 100644 GIT binary patch literal 22559 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 L+UIl5UJd*oc7~lM literal 33464 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 z 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 literal 24157 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� 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
%)Mv}Hu*8e9a!pya2& zO#gw4$VD?px1d~?`Jh44m4%SU$|T23wiUq(X5rcRM?u1H6;j=&M3jKxGzbEyhyfY1fS%=R$C;;WNy*_)~U$)@m^#4J8h1z zBb2^?!bl%%!SeCmL=>EhjS9H_^|;0s{d$U^%kssSnM%;P9(4mhky3>>O#^rGOFkDp z1b2#|Hj+7-&sw8?V>cuu=aF?x0C5}benc!ud65M2x}un zpowmYh2~z&)Vj7GI?*Fzq}sFMe;)_?)0uDDdl}k0-K_i5ZLp_Dzq*4f& z6~jN;li(*qAN!xL{#fIWZ~O^|Kgr=wKKxT1{uIPN@Zb+3`h$r6Afi8r=no?LgNXhh zqCbe}4LmEd zu*wK}o(vy^>ARxz2?&4;^UP4%`|}7e!XJzMahN~x@_%Z&*oQmLGZSsGe+EYK^=n#c K#aHe<{(k`YgE!*< diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewTwoIntersectingOverlays.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewTwoIntersectingOverlays.png index eec5596f8859c15d22d12366743b0c266628d2f4..d0679ad960c1a09489b771473a1b75305bb83626 100644 GIT binary patch literal 30760 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�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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformView.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformView.png index 57431b17fd0bbfb9a704d2e626e57d7490505e59..1cc75de30df9b81e0a6bd512b7a268ca65bbf438 100644 GIT binary patch literal 32713 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 vh(Zw)BT*-38@NBM5n;gs5?TG&e7Y9wP literal 39235 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
` and `

` elements /// to draw shapes and text. -abstract class PersistedPicture extends PersistedLeafSurface { +class PersistedPicture extends PersistedLeafSurface { PersistedPicture(this.dx, this.dy, this.picture, this.hints) : localPaintBounds = picture.recordingCanvas!.pictureBounds; @@ -553,7 +270,14 @@ abstract class PersistedPicture extends PersistedLeafSurface { /// /// If the implementation does not paint onto a bitmap canvas, it should /// return zero. - int get bitmapPixelCount; + int get bitmapPixelCount { + if (_canvas is! BitmapCanvas) { + return 0; + } + + final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; + return bitmapCanvas.bitmapPixelCount; + } void _applyPaint(PersistedPicture? oldSurface) { final EngineCanvas? oldCanvas = oldSurface?._canvas; @@ -575,8 +299,193 @@ abstract class PersistedPicture extends PersistedLeafSurface { applyPaint(oldCanvas); } - /// Concrete implementations implement this method to do actual painting. - void applyPaint(EngineCanvas? oldCanvas); + @override + double matchForUpdate(PersistedPicture existingSurface) { + if (existingSurface.picture == picture) { + // Picture is the same, return perfect score. + return 0.0; + } + + if (!existingSurface.picture.recordingCanvas!.didDraw) { + // The previous surface didn't draw anything and therefore has no + // resources to reuse. + return 1.0; + } + + final bool didRequireBitmap = + existingSurface.picture.recordingCanvas!.hasArbitraryPaint; + final bool requiresBitmap = picture.recordingCanvas!.hasArbitraryPaint; + if (didRequireBitmap != requiresBitmap) { + // Switching canvas types is always expensive. + return 1.0; + } else if (!requiresBitmap) { + // Currently DomCanvas is always expensive to repaint, as we always throw + // out all the DOM we rendered before. This may change in the future, at + // which point we may return other values here. + return 1.0; + } else { + final BitmapCanvas? oldCanvas = existingSurface._canvas as BitmapCanvas?; + if (oldCanvas == null) { + // We did not allocate a canvas last time. This can happen when the + // picture is completely clipped out of the view. + return 1.0; + } else if (!oldCanvas.doesFitBounds(_exactLocalCullRect!)) { + // The canvas needs to be resized before painting. + return 1.0; + } else { + final int newPixelCount = BitmapCanvas._widthToPhysical(_exactLocalCullRect!.width) + * BitmapCanvas._heightToPhysical(_exactLocalCullRect!.height); + final int oldPixelCount = + oldCanvas._widthInBitmapPixels * oldCanvas._heightInBitmapPixels; + + if (oldPixelCount == 0) { + return 1.0; + } + + final double pixelCountRatio = newPixelCount / oldPixelCount; + assert(0 <= pixelCountRatio && pixelCountRatio <= 1.0, + 'Invalid pixel count ratio $pixelCountRatio'); + return 1.0 - pixelCountRatio; + } + } + } + + @override + Matrix4? get localTransformInverse => null; + + void applyPaint(EngineCanvas? oldCanvas) { + if (picture.recordingCanvas!.hasArbitraryPaint) { + _applyBitmapPaint(oldCanvas); + } else { + _applyDomPaint(oldCanvas); + } + } + + void _applyDomPaint(EngineCanvas? oldCanvas) { + _recycleCanvas(oldCanvas); + _canvas = DomCanvas(); + domRenderer.clearDom(rootElement!); + rootElement!.append(_canvas!.rootElement); + picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); + } + + void _applyBitmapPaint(EngineCanvas? oldCanvas) { + if (oldCanvas is BitmapCanvas && + oldCanvas.doesFitBounds(_optimalLocalCullRect!) && + oldCanvas.isReusable()) { + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.keptCount++; + } + oldCanvas.bounds = _optimalLocalCullRect!; + _canvas = oldCanvas; + oldCanvas.setElementCache(_elementCache); + _canvas!.clear(); + picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); + } else { + // We can't use the old canvas because the size has changed, so we put + // it in a cache for later reuse. + _recycleCanvas(oldCanvas); + // We cannot paint immediately because not all canvases that we may be + // able to reuse have been released yet. So instead we enqueue this + // picture to be painted after the update cycle is done syncing the layer + // tree then reuse canvases that were freed up. + _paintQueue.add(_PaintRequest( + canvasSize: _optimalLocalCullRect!.size, + paintCallback: () { + _canvas = _findOrCreateCanvas(_optimalLocalCullRect!); + assert(_canvas is BitmapCanvas + && (_canvas as BitmapCanvas?)!._elementCache == _elementCache); + if (_debugExplainSurfaceStats) { + final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; + _surfaceStatsFor(this).paintPixelCount += + bitmapCanvas.bitmapPixelCount; + } + domRenderer.clearDom(rootElement!); + rootElement!.append(_canvas!.rootElement); + _canvas!.clear(); + picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); + }, + )); + } + } + + /// Attempts to reuse a canvas from the [_recycledCanvases]. Allocates a new + /// one if unable to reuse. + /// + /// The best recycled canvas is one that: + /// + /// - Fits the requested [canvasSize]. This is a hard requirement. Otherwise + /// we risk clipping the picture. + /// - Is the smallest among all possible reusable canvases. This makes canvas + /// reuse more efficient. + /// - Contains no more than twice the number of requested pixels. This makes + /// sure we do not use too much memory for small canvases. + BitmapCanvas _findOrCreateCanvas(ui.Rect bounds) { + final ui.Size canvasSize = bounds.size; + BitmapCanvas? bestRecycledCanvas; + double lastPixelCount = double.infinity; + for (int i = 0; i < _recycledCanvases.length; i++) { + final BitmapCanvas candidate = _recycledCanvases[i]; + if (!candidate.isReusable()) { + continue; + } + + final ui.Size candidateSize = candidate.size; + final double candidatePixelCount = + candidateSize.width * candidateSize.height; + + final bool fits = candidate.doesFitBounds(bounds); + final bool isSmaller = candidatePixelCount < lastPixelCount; + if (fits && isSmaller) { + // [isTooSmall] is used to make sure that a small picture doesn't + // reuse and hold onto memory of a large canvas. + final double requestedPixelCount = bounds.width * bounds.height; + final bool isTooSmall = isSmaller && + requestedPixelCount > 1 && + (candidatePixelCount / requestedPixelCount) > 4; + if (!isTooSmall) { + bestRecycledCanvas = candidate; + lastPixelCount = candidatePixelCount; + final bool fitsExactly = candidateSize.width == canvasSize.width && + candidateSize.height == canvasSize.height; + if (fitsExactly) { + // No need to keep looking any more. + break; + } + } + } + } + + if (bestRecycledCanvas != null) { + if (_debugExplainSurfaceStats) { + _surfaceStatsFor(this).reuseCanvasCount++; + } + _recycledCanvases.remove(bestRecycledCanvas); + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.inRecycleCount = + _recycledCanvases.length; + } + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.reusedCount++; + } + bestRecycledCanvas.bounds = bounds; + bestRecycledCanvas.setElementCache(_elementCache); + return bestRecycledCanvas; + } + + if (_debugShowCanvasReuseStats) { + DebugCanvasReuseOverlay.instance.createdCount++; + } + final BitmapCanvas canvas = BitmapCanvas(bounds); + canvas.setElementCache(_elementCache); + if (_debugExplainSurfaceStats) { + _surfaceStatsFor(this) + ..allocateBitmapCanvasCount += 1 + ..allocatedBitmapSizeInPixels = + canvas._widthInBitmapPixels * canvas._heightInBitmapPixels; + } + return canvas; + } void _applyTranslate() { rootElement!.style.transform = 'translate(${dx}px, ${dy}px)'; diff --git a/lib/web_ui/lib/src/engine/surface/platform_view.dart b/lib/web_ui/lib/src/engine/html/platform_view.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/platform_view.dart rename to lib/web_ui/lib/src/engine/html/platform_view.dart diff --git a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart b/lib/web_ui/lib/src/engine/html/recording_canvas.dart similarity index 87% rename from lib/web_ui/lib/src/engine/surface/recording_canvas.dart rename to lib/web_ui/lib/src/engine/html/recording_canvas.dart index c9288b7aca27b..a7c994990ad5d 100644 --- a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/recording_canvas.dart @@ -608,8 +608,6 @@ abstract class PaintCommand { const PaintCommand(); void apply(EngineCanvas? canvas); - - void serializeToCssPaint(List> serializedCommands); } /// A [PaintCommand] that affect pixels on the screen (unlike, for example, the @@ -665,11 +663,6 @@ class PaintSave extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add(const [1]); - } } class PaintRestore extends PaintCommand { @@ -688,11 +681,6 @@ class PaintRestore extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add(const [2]); - } } class PaintTranslate extends PaintCommand { @@ -714,11 +702,6 @@ class PaintTranslate extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([3, dx, dy]); - } } class PaintScale extends PaintCommand { @@ -740,11 +723,6 @@ class PaintScale extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([4, sx, sy]); - } } class PaintRotate extends PaintCommand { @@ -765,11 +743,6 @@ class PaintRotate extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([5, radians]); - } } class PaintTransform extends PaintCommand { @@ -790,11 +763,6 @@ class PaintTransform extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([6]..addAll(matrix4)); - } } class PaintSkew extends PaintCommand { @@ -816,11 +784,6 @@ class PaintSkew extends PaintCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([7, sx, sy]); - } } class PaintClipRect extends DrawCommand { @@ -841,11 +804,6 @@ class PaintClipRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([8, _serializeRectToCssPaint(rect)]); - } } class PaintClipRRect extends DrawCommand { @@ -866,14 +824,6 @@ class PaintClipRRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 9, - _serializeRRectToCssPaint(rrect), - ]); - } } class PaintClipPath extends DrawCommand { @@ -894,11 +844,6 @@ class PaintClipPath extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([10, path.webOnlySerializeToCssPaint()]); - } } class PaintDrawColor extends DrawCommand { @@ -920,12 +865,6 @@ class PaintDrawColor extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands - .add([11, colorToCssString(color), blendMode.index]); - } } class PaintDrawLine extends DrawCommand { @@ -948,18 +887,6 @@ class PaintDrawLine extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 12, - p1.dx, - p1.dy, - p2.dx, - p2.dy, - _serializePaintToCssPaint(paint) - ]); - } } class PaintDrawPaint extends DrawCommand { @@ -980,11 +907,6 @@ class PaintDrawPaint extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([13, _serializePaintToCssPaint(paint)]); - } } class PaintDrawVertices extends DrawCommand { @@ -1006,11 +928,6 @@ class PaintDrawVertices extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - throw UnimplementedError(); - } } class PaintDrawPoints extends DrawCommand { @@ -1032,11 +949,6 @@ class PaintDrawPoints extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - throw UnimplementedError(); - } } class PaintDrawRect extends DrawCommand { @@ -1058,15 +970,6 @@ class PaintDrawRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 14, - _serializeRectToCssPaint(rect), - _serializePaintToCssPaint(paint) - ]); - } } class PaintDrawRRect extends DrawCommand { @@ -1088,15 +991,6 @@ class PaintDrawRRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 15, - _serializeRRectToCssPaint(rrect), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawDRRect extends DrawCommand { @@ -1125,16 +1019,6 @@ class PaintDrawDRRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 16, - _serializeRRectToCssPaint(outer), - _serializeRRectToCssPaint(inner), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawOval extends DrawCommand { @@ -1156,15 +1040,6 @@ class PaintDrawOval extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 17, - _serializeRectToCssPaint(rect), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawCircle extends DrawCommand { @@ -1187,17 +1062,6 @@ class PaintDrawCircle extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 18, - c.dx, - c.dy, - radius, - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawPath extends DrawCommand { @@ -1219,15 +1083,6 @@ class PaintDrawPath extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 19, - path.webOnlySerializeToCssPaint(), - _serializePaintToCssPaint(paint), - ]); - } } class PaintDrawShadow extends DrawCommand { @@ -1252,22 +1107,6 @@ class PaintDrawShadow extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - serializedCommands.add([ - 20, - path.webOnlySerializeToCssPaint(), - [ - color.alpha, - color.red, - color.green, - color.blue, - ], - elevation, - transparentOccluder, - ]); - } } class PaintDrawImage extends DrawCommand { @@ -1290,13 +1129,6 @@ class PaintDrawImage extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - if (assertionsEnabled) { - throw UnsupportedError('drawImage not serializable'); - } - } } class PaintDrawImageRect extends DrawCommand { @@ -1320,13 +1152,6 @@ class PaintDrawImageRect extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - if (assertionsEnabled) { - throw UnsupportedError('drawImageRect not serializable'); - } - } } class PaintDrawParagraph extends DrawCommand { @@ -1348,55 +1173,6 @@ class PaintDrawParagraph extends DrawCommand { return super.toString(); } } - - @override - void serializeToCssPaint(List> serializedCommands) { - if (assertionsEnabled) { - throw UnsupportedError('drawParagraph not serializable'); - } - } -} - -List _serializePaintToCssPaint(SurfacePaintData paint) { - final EngineGradient? engineShader = paint.shader as EngineGradient?; - return [ - paint.blendMode?.index, - paint.style?.index, - paint.strokeWidth, - paint.strokeCap?.index, - paint.isAntiAlias, - colorToCssString(paint.color), - engineShader?.webOnlySerializeToCssPaint(), - paint.maskFilter?.webOnlySerializeToCssPaint(), - paint.filterQuality?.index, - paint.colorFilter?.webOnlySerializeToCssPaint(), - ]; -} - -List _serializeRectToCssPaint(ui.Rect rect) { - return [ - rect.left, - rect.top, - rect.right, - rect.bottom, - ]; -} - -List _serializeRRectToCssPaint(ui.RRect rrect) { - return [ - rrect.left, - rrect.top, - rrect.right, - rrect.bottom, - rrect.tlRadiusX, - rrect.tlRadiusY, - rrect.trRadiusX, - rrect.trRadiusY, - rrect.brRadiusX, - rrect.brRadiusY, - rrect.blRadiusX, - rrect.blRadiusY, - ]; } class Subpath { @@ -1421,14 +1197,6 @@ class Subpath { return result; } - List serializeToCssPaint() { - final List serialization = []; - for (int i = 0; i < commands.length; i++) { - serialization.add(commands[i].serializeToCssPaint()); - } - return serialization; - } - @override String toString() { if (assertionsEnabled) { @@ -1439,26 +1207,11 @@ class Subpath { } } -/// ! Houdini implementation relies on indices here. Keep in sync. -class PathCommandTypes { - static const int moveTo = 0; - static const int lineTo = 1; - static const int ellipse = 2; - static const int close = 3; - static const int quadraticCurveTo = 4; - static const int bezierCurveTo = 5; - static const int rect = 6; - static const int rRect = 7; -} - abstract class PathCommand { - final int type; - const PathCommand(this.type); + const PathCommand(); PathCommand shifted(ui.Offset offset); - List serializeToCssPaint(); - /// Transform the command and add to targetPath. void transform(Float32List matrix4, SurfacePath targetPath); @@ -1472,18 +1225,13 @@ class MoveTo extends PathCommand { final double x; final double y; - const MoveTo(this.x, this.y) : super(PathCommandTypes.moveTo); + const MoveTo(this.x, this.y); @override MoveTo shifted(ui.Offset offset) { return MoveTo(x + offset.dx, y + offset.dy); } - @override - List serializeToCssPaint() { - return [1, x, y]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); @@ -1504,18 +1252,13 @@ class LineTo extends PathCommand { final double x; final double y; - const LineTo(this.x, this.y) : super(PathCommandTypes.lineTo); + const LineTo(this.x, this.y); @override LineTo shifted(ui.Offset offset) { return LineTo(x + offset.dx, y + offset.dy); } - @override - List serializeToCssPaint() { - return [2, x, y]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); @@ -1543,8 +1286,7 @@ class Ellipse extends PathCommand { final bool anticlockwise; const Ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotation, - this.startAngle, this.endAngle, this.anticlockwise) - : super(PathCommandTypes.ellipse); + this.startAngle, this.endAngle, this.anticlockwise); @override Ellipse shifted(ui.Offset offset) { @@ -1552,21 +1294,6 @@ class Ellipse extends PathCommand { startAngle, endAngle, anticlockwise); } - @override - List serializeToCssPaint() { - return [ - 3, - x, - y, - radiusX, - radiusY, - rotation, - startAngle, - endAngle, - anticlockwise, - ]; - } - @override void transform(Float32List matrix4, SurfacePath targetPath) { final ui.Path bezierPath = ui.Path(); @@ -1686,8 +1413,7 @@ class QuadraticCurveTo extends PathCommand { final double x2; final double y2; - const QuadraticCurveTo(this.x1, this.y1, this.x2, this.y2) - : super(PathCommandTypes.quadraticCurveTo); + const QuadraticCurveTo(this.x1, this.y1, this.x2, this.y2); @override QuadraticCurveTo shifted(ui.Offset offset) { @@ -1695,11 +1421,6 @@ class QuadraticCurveTo extends PathCommand { x1 + offset.dx, y1 + offset.dy, x2 + offset.dx, y2 + offset.dy); } - @override - List serializeToCssPaint() { - return [4, x1, y1, x2, y2]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final double m0 = matrix4[0]; @@ -1734,8 +1455,7 @@ class BezierCurveTo extends PathCommand { final double x3; final double y3; - const BezierCurveTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3) - : super(PathCommandTypes.bezierCurveTo); + const BezierCurveTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); @override BezierCurveTo shifted(ui.Offset offset) { @@ -1743,11 +1463,6 @@ class BezierCurveTo extends PathCommand { y2 + offset.dy, x3 + offset.dx, y3 + offset.dy); } - @override - List serializeToCssPaint() { - return [5, x1, y1, x2, y2, x3, y3]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { final double s0 = matrix4[0]; @@ -1782,8 +1497,7 @@ class RectCommand extends PathCommand { final double width; final double height; - const RectCommand(this.x, this.y, this.width, this.height) - : super(PathCommandTypes.rect); + const RectCommand(this.x, this.y, this.width, this.height); @override RectCommand shifted(ui.Offset offset) { @@ -1824,11 +1538,6 @@ class RectCommand extends PathCommand { } } - @override - List serializeToCssPaint() { - return [6, x, y, width, height]; - } - @override String toString() { if (assertionsEnabled) { @@ -1842,18 +1551,13 @@ class RectCommand extends PathCommand { class RRectCommand extends PathCommand { final ui.RRect rrect; - const RRectCommand(this.rrect) : super(PathCommandTypes.rRect); + const RRectCommand(this.rrect); @override RRectCommand shifted(ui.Offset offset) { return RRectCommand(rrect.shift(offset)); } - @override - List serializeToCssPaint() { - return [7, _serializeRRectToCssPaint(rrect)]; - } - @override void transform(Float32List matrix4, SurfacePath targetPath) { final ui.Path roundRectPath = ui.Path(); @@ -1872,18 +1576,11 @@ class RRectCommand extends PathCommand { } class CloseCommand extends PathCommand { - const CloseCommand() : super(PathCommandTypes.close); - @override CloseCommand shifted(ui.Offset offset) { return this; } - @override - List serializeToCssPaint() { - return [8]; - } - @override void transform(Float32List matrix4, ui.Path targetPath) { targetPath.close(); diff --git a/lib/web_ui/lib/src/engine/surface/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/render_vertices.dart rename to lib/web_ui/lib/src/engine/html/render_vertices.dart index e5ce5d1044e88..11d400bfc5f5b 100644 --- a/lib/web_ui/lib/src/engine/surface/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -163,8 +163,7 @@ class _WebGlRenderer implements _GlRenderer { } _GlContext gl = _OffscreenCanvas.createGlContext(widthInPixels, heightInPixels)!; - final bool isWebKit = (browserEngine == BrowserEngine.webkit); - _GlProgram glProgram = isWebKit + _GlProgram glProgram = webGLVersion == 1 ? gl.useAndCacheProgram( _vertexShaderTriangleEs1, _fragmentShaderTriangleEs1)! : gl.useAndCacheProgram( @@ -499,13 +498,10 @@ class _GlContext { switch (mode) { case ui.VertexMode.triangles: return kTriangles; - break; case ui.VertexMode.triangleFan: return kTriangleFan; - break; case ui.VertexMode.triangleStrip: return kTriangleStrip; - break; } } diff --git a/lib/web_ui/lib/src/engine/surface/scene.dart b/lib/web_ui/lib/src/engine/html/scene.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/scene.dart rename to lib/web_ui/lib/src/engine/html/scene.dart diff --git a/lib/web_ui/lib/src/engine/surface/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart similarity index 98% rename from lib/web_ui/lib/src/engine/surface/scene_builder.dart rename to lib/web_ui/lib/src/engine/html/scene_builder.dart index fd38e403e38b8..e6cb5d185f7b0 100644 --- a/lib/web_ui/lib/src/engine/surface/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -78,9 +78,6 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { Float64List matrix4, { ui.TransformEngineLayer? oldLayer, }) { - if (matrix4 == null) { // ignore: unnecessary_null_comparison - throw ArgumentError('"matrix4" argument cannot be null'); - } if (matrix4.length != 16) { throw ArgumentError('"matrix4" must have 16 entries.'); } @@ -363,7 +360,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { if (willChangeHint) { hints |= 2; } - _addSurface(persistedPictureFactory(offset.dx, offset.dy, picture, hints)); + _addSurface(PersistedPicture(offset.dx, offset.dy, picture as EnginePicture, hints)); } /// Adds a backend texture to the scene. diff --git a/lib/web_ui/lib/src/engine/shader.dart b/lib/web_ui/lib/src/engine/html/shader.dart similarity index 64% rename from lib/web_ui/lib/src/engine/shader.dart rename to lib/web_ui/lib/src/engine/html/shader.dart index 46d7153f9b4ec..4dae88f674bf9 100644 --- a/lib/web_ui/lib/src/engine/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shader.dart @@ -5,34 +5,12 @@ // @dart = 2.10 part of engine; -bool _offsetIsValid(ui.Offset offset) { - assert(offset != null, 'Offset argument was null.'); // ignore: unnecessary_null_comparison - assert(!offset.dx.isNaN && !offset.dy.isNaN, - 'Offset argument contained a NaN value.'); - return true; -} - -bool _matrix4IsValid(Float32List matrix4) { - assert(matrix4 != null, 'Matrix4 argument was null.'); // ignore: unnecessary_null_comparison - assert(matrix4.length == 16, 'Matrix4 must have 16 entries.'); - return true; -} - -abstract class EngineShader { - /// Create a shader for use in the Skia backend. - SkShader createSkiaShader(); -} - -abstract class EngineGradient implements ui.Gradient, EngineShader { +abstract class EngineGradient implements ui.Gradient { /// Hidden constructor to prevent subclassing. EngineGradient._(); /// Creates a fill style to be used in painting. Object createPaintStyle(html.CanvasRenderingContext2D? ctx); - - List webOnlySerializeToCssPaint() { - throw UnsupportedError('CSS paint not implemented for this shader type'); - } } class GradientSweep extends EngineGradient { @@ -61,23 +39,6 @@ class GradientSweep extends EngineGradient { final double startAngle; final double endAngle; final Float32List? matrix4; - - @override - SkShader createSkiaShader() { - throw UnimplementedError(); - } -} - -void _validateColorStops(List colors, List? colorStops) { - if (colorStops == null) { - if (colors.length != 2) - throw ArgumentError( - '"colors" must have length 2 if "colorStops" is omitted.'); - } else { - if (colors.length != colorStops.length) - throw ArgumentError( - '"colors" and "colorStops" arguments must have equal length.'); - } } class GradientLinear extends EngineGradient { @@ -135,37 +96,6 @@ class GradientLinear extends EngineGradient { } return gradient; } - - @override - List webOnlySerializeToCssPaint() { - final List serializedColors = []; - for (int i = 0; i < colors.length; i++) { - serializedColors.add(colorToCssString(colors[i])); - } - return [ - 1, - from.dx, - from.dy, - to.dx, - to.dy, - serializedColors, - colorStops, - tileMode.index - ]; - } - - @override - SkShader createSkiaShader() { - assert(experimentalUseSkia); - - return canvasKit.SkShader.MakeLinearGradient( - toSkPoint(from), - toSkPoint(to), - toSkFloatColorList(colors), - toSkColorStops(colorStops), - toSkTileMode(tileMode), - ); - } } // TODO(flutter_web): For transforms and tile modes implement as webgl @@ -207,21 +137,6 @@ class GradientRadial extends EngineGradient { } return gradient; } - - @override - SkShader createSkiaShader() { - assert(experimentalUseSkia); - - return canvasKit.SkShader.MakeRadialGradient( - toSkPoint(center), - radius, - toSkFloatColorList(colors), - toSkColorStops(colorStops), - toSkTileMode(tileMode), - matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, - 0, - ); - } } class GradientConical extends EngineGradient { @@ -242,22 +157,6 @@ class GradientConical extends EngineGradient { Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { throw UnimplementedError(); } - - @override - SkShader createSkiaShader() { - assert(experimentalUseSkia); - return canvasKit.SkShader.MakeTwoPointConicalGradient( - toSkPoint(focal), - focalRadius, - toSkPoint(center), - radius, - toSkFloatColorList(colors), - toSkColorStops(colorStops), - toSkTileMode(tileMode), - matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, - 0, - ); - } } /// Backend implementation of [ui.ImageFilter]. @@ -284,20 +183,3 @@ class EngineImageFilter implements ui.ImageFilter { return 'ImageFilter.blur($sigmaX, $sigmaY)'; } } - -/// Backend implementation of [ui.ImageShader]. -class EngineImageShader implements ui.ImageShader, EngineShader { - EngineImageShader( - ui.Image image, this.tileModeX, this.tileModeY, this.matrix4) - : _skImage = image as CkImage; - - final ui.TileMode tileModeX; - final ui.TileMode tileModeY; - final Float64List matrix4; - final CkImage _skImage; - - SkShader createSkiaShader() => _skImage.skImage.makeShader( - toSkTileMode(tileModeX), - toSkTileMode(tileModeY), - ); -} diff --git a/lib/web_ui/lib/src/engine/surface/surface.dart b/lib/web_ui/lib/src/engine/html/surface.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/surface.dart rename to lib/web_ui/lib/src/engine/html/surface.dart diff --git a/lib/web_ui/lib/src/engine/surface/surface_stats.dart b/lib/web_ui/lib/src/engine/html/surface_stats.dart similarity index 98% rename from lib/web_ui/lib/src/engine/surface/surface_stats.dart rename to lib/web_ui/lib/src/engine/html/surface_stats.dart index 2fc0487f1a123..911a825ad03ab 100644 --- a/lib/web_ui/lib/src/engine/surface/surface_stats.dart +++ b/lib/web_ui/lib/src/engine/html/surface_stats.dart @@ -237,7 +237,7 @@ void _debugPrintSurfaceStats(PersistedScene scene, int frameNumber) { elementReuseCount += stats.reuseElementCount; totalAllocatedDomNodeCount += stats.allocatedDomNodeCount; - if (surface is PersistedStandardPicture) { + if (surface is PersistedPicture) { pictureCount += 1; paintCount += stats.paintCount; @@ -291,8 +291,8 @@ void _debugPrintSurfaceStats(PersistedScene scene, int frameNumber) { final int pixelCount = canvasElements .cast() .map((html.CanvasElement e) { - final int pixels = e.width * e.height; - canvasInfo.writeln(' - ${e.width} x ${e.height} = $pixels pixels'); + final int pixels = e.width! * e.height!; + canvasInfo.writeln(' - ${e.width!} x ${e.height!} = $pixels pixels'); return pixels; }).fold(0, (int total, int pixels) => total + pixels); final double physicalScreenWidth = diff --git a/lib/web_ui/lib/src/engine/surface/transform.dart b/lib/web_ui/lib/src/engine/html/transform.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/transform.dart rename to lib/web_ui/lib/src/engine/html/transform.dart 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 cc8b30c1d27ef..6a0595d66ae50 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -32,17 +32,13 @@ class HtmlCodec implements ui.Codec { // Currently there is no way to watch decode progress, so // we add 0/100 , 100/100 progress callbacks to enable loading progress // builders to create UI. - if (chunkCallback != null) { - chunkCallback!(0, 100); - } + chunkCallback?.call(0, 100); if (_supportsDecode) { final html.ImageElement imgElement = html.ImageElement(); imgElement.src = src; js_util.setProperty(imgElement, 'decoding', 'async'); imgElement.decode().then((dynamic _) { - if (chunkCallback != null) { - chunkCallback!(100, 100); - } + chunkCallback?.call(100, 100); final HtmlImage image = HtmlImage( imgElement, imgElement.naturalWidth, @@ -133,13 +129,13 @@ class HtmlImage implements ui.Image { final int height; @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - return futurize((Callback callback) { - return _toByteData(format.index, (Uint8List? encoded) { - callback(encoded?.buffer.asByteData()); - }); - }); + Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + if (imgElement.src?.startsWith('data:') == true) { + final data = UriData.fromUri(Uri.parse(imgElement.src!)); + return Future.value(data.contentAsBytes().buffer.asByteData()); + } else { + return Future.value(null); + } } // Returns absolutely positioned actual image element on first call and @@ -153,12 +149,4 @@ class HtmlImage implements ui.Image { return imgElement; } } - - // TODO(het): Support this for asset images and images generated from - // `Picture`s. - /// Returns an error message on failure, null on success. - String _toByteData(int format, Callback callback) { - callback(null); - return 'Image.toByteData is not supported in Flutter for Web'; - } } diff --git a/lib/web_ui/lib/src/engine/rrect_renderer.dart b/lib/web_ui/lib/src/engine/rrect_renderer.dart index 767c0666c6649..3ccc054331d41 100644 --- a/lib/web_ui/lib/src/engine/rrect_renderer.dart +++ b/lib/web_ui/lib/src/engine/rrect_renderer.dart @@ -7,7 +7,6 @@ part of engine; /// Renders an RRect using path primitives. abstract class _RRectRenderer { - // TODO(mdebbar): Backport the overlapping corners fix to houdini_painter.js // To draw the rounded rectangle, perform the following steps: // 0. Ensure border radius don't overlap // 1. Flip left,right top,bottom since web doesn't support flipped diff --git a/lib/web_ui/lib/src/engine/text/line_breaker.dart b/lib/web_ui/lib/src/engine/text/line_breaker.dart index df4049f653888..50970980bf518 100644 --- a/lib/web_ui/lib/src/engine/text/line_breaker.dart +++ b/lib/web_ui/lib/src/engine/text/line_breaker.dart @@ -21,11 +21,93 @@ enum LineBreakType { } /// Acts as a tuple that encapsulates information about a line break. +/// +/// It contains multiple indices that are helpful when it comes to measuring the +/// width of a line of text. +/// +/// [indexWithoutTrailingSpaces] <= [indexWithoutTrailingNewlines] <= [index] +/// +/// Example: for the string "foo \nbar " here are the indices: +/// ``` +/// f o o \n b a r +/// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ +/// 0 1 2 3 4 5 6 7 8 9 +/// ``` +/// It contains two line breaks: +/// ``` +/// // The first line break: +/// LineBreakResult(5, 4, 3, LineBreakType.mandatory) +/// +/// // Second line break: +/// LineBreakResult(9, 9, 8, LineBreakType.mandatory) +/// ``` class LineBreakResult { - LineBreakResult(this.index, this.type); - + const LineBreakResult( + this.index, + this.indexWithoutTrailingNewlines, + this.indexWithoutTrailingSpaces, + this.type, + ): assert(indexWithoutTrailingSpaces <= indexWithoutTrailingNewlines), + assert(indexWithoutTrailingNewlines <= index); + + /// Creates a [LineBreakResult] where all indices are the same (i.e. there are + /// no trailing spaces or new lines). + const LineBreakResult.sameIndex(this.index, this.type) + : indexWithoutTrailingNewlines = index, + indexWithoutTrailingSpaces = index; + + /// The true index at which the line break should occur, including all spaces + /// and new lines. final int index; + + /// The index of the line break excluding any trailing new lines. + final int indexWithoutTrailingNewlines; + + /// The index of the line break excluding any trailing spaces. + final int indexWithoutTrailingSpaces; + + /// The type of line break is useful to determine the behavior in text + /// measurement. + /// + /// For example, a mandatory line break always causes a line break regardless + /// of width constraints. But a line break opportunity requires further checks + /// to decide whether to take the line break or not. final LineBreakType type; + + @override + int get hashCode => ui.hashValues( + index, + indexWithoutTrailingNewlines, + indexWithoutTrailingSpaces, + type, + ); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is LineBreakResult && + other.index == index && + other.indexWithoutTrailingNewlines == indexWithoutTrailingNewlines && + other.indexWithoutTrailingSpaces == indexWithoutTrailingSpaces && + other.type == type; + } + + @override + String toString() { + if (assertionsEnabled) { + return 'LineBreakResult(index: $index, ' + 'without new lines: $indexWithoutTrailingNewlines, ' + 'without spaces: $indexWithoutTrailingSpaces, ' + 'type: $type)'; + } else { + return super.toString(); + } + } } bool _isHardBreak(LineCharProperty? prop) { @@ -49,7 +131,7 @@ bool _isKoreanSyllable(LineCharProperty? prop) { prop == LineCharProperty.H3; } -/// Whether the given char code has an Easter Asian width property of F, W or H. +/// Whether the given char code has an Eastern Asian width property of F, W or H. /// /// See: /// - https://www.unicode.org/reports/tr14/tr14-45.html#LB30 @@ -62,6 +144,18 @@ bool _hasEastAsianWidthFWH(int charCode) { /// Finds the next line break in the given [text] starting from [index]. /// +/// Wethink about indices as pointing between characters, and they go all the +/// way from 0 to the string length. For example, here are the indices for the +/// string "foo bar": +/// +/// ``` +/// f o o b a r +/// ^ ^ ^ ^ ^ ^ ^ ^ +/// 0 1 2 3 4 5 6 7 +/// ``` +/// +/// This way the indices work well with [String.substring()]. +/// /// Useful resources: /// /// * https://www.unicode.org/reports/tr14/tr14-45.html#Algorithm @@ -80,6 +174,12 @@ LineBreakResult nextLineBreak(String text, int index) { // sequence. LineCharProperty? baseOfSpaceSequence; + /// The index of the last character that wasn't a space. + int lastNonSpaceIndex = index; + + /// The index of the last character that wasn't a new line. + int lastNonNewlineIndex = index; + // When the text/line starts with SP, we should treat the begining of text/line // as if it were a WJ (word joiner). if (curr == LineCharProperty.SP) { @@ -131,12 +231,15 @@ LineBreakResult nextLineBreak(String text, int index) { // LB4: BK ! // // Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks. - // LB5: CR × LF - // CR ! - // LF ! + // LB5: LF ! // NL ! if (_isHardBreak(prev1)) { - return LineBreakResult(index, LineBreakType.mandatory); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.mandatory, + ); } if (prev1 == LineCharProperty.CR) { @@ -145,10 +248,21 @@ LineBreakResult nextLineBreak(String text, int index) { continue; } else { // LB5: CR ! - return LineBreakResult(index, LineBreakType.mandatory); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.mandatory, + ); } } + // At this point, we know for sure the prev character wasn't a new line. + lastNonNewlineIndex = index; + if (prev1 != LineCharProperty.SP) { + lastNonSpaceIndex = index; + } + // Do not break before hard line breaks. // LB6: × ( BK | CR | LF | NL ) if (_isHardBreak(curr) || curr == LineCharProperty.CR) { @@ -158,7 +272,12 @@ LineBreakResult nextLineBreak(String text, int index) { // Always break at the end of text. // LB3: ! eot if (index >= text.length) { - return LineBreakResult(text.length, LineBreakType.endOfText); + return LineBreakResult( + text.length, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.endOfText, + ); } // Do not break before spaces or zero width space. @@ -186,7 +305,12 @@ LineBreakResult nextLineBreak(String text, int index) { // LB8: ZW SP* ÷ if (prev1 == LineCharProperty.ZW || baseOfSpaceSequence == LineCharProperty.ZW) { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } // Do not break a combining character sequence; treat it as if it has the @@ -292,7 +416,12 @@ LineBreakResult nextLineBreak(String text, int index) { // Break after spaces. // LB18: SP ÷ if (prev1 == LineCharProperty.SP) { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } // Do not break before or after quotation marks, such as ‘”’. @@ -306,7 +435,12 @@ LineBreakResult nextLineBreak(String text, int index) { // LB20: ÷ CB // CB ÷ if (prev1 == LineCharProperty.CB || curr == LineCharProperty.CB) { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } // Do not break before hyphen-minus, other hyphens, fixed-width spaces, @@ -471,7 +605,12 @@ LineBreakResult nextLineBreak(String text, int index) { if (regionalIndicatorCount.isOdd) { continue; } else { - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } } @@ -484,7 +623,17 @@ LineBreakResult nextLineBreak(String text, int index) { // Break everywhere else. // LB31: ALL ÷ // ÷ ALL - return LineBreakResult(index, LineBreakType.opportunity); + return LineBreakResult( + index, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.opportunity, + ); } - return LineBreakResult(text.length, LineBreakType.endOfText); + return LineBreakResult( + text.length, + lastNonNewlineIndex, + lastNonSpaceIndex, + LineBreakType.endOfText, + ); } diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart index c4fae1e0af282..ac2de9edc3110 100644 --- a/lib/web_ui/lib/src/engine/text/measurement.dart +++ b/lib/web_ui/lib/src/engine/text/measurement.dart @@ -16,19 +16,13 @@ const double _baselineRatioHack = 1.1662499904632568; /// Signature of a function that takes a character and returns true or false. typedef CharPredicate = bool Function(int char); -bool _whitespacePredicate(int char) { +bool _newlinePredicate(int char) { final LineCharProperty prop = lineLookup.findForChar(char); - return prop == LineCharProperty.SP || - prop == LineCharProperty.BK || + return prop == LineCharProperty.BK || prop == LineCharProperty.LF || prop == LineCharProperty.CR; } -bool _newlinePredicate(int char) { - final LineCharProperty prop = lineLookup.findForChar(char); - return prop == LineCharProperty.BK || prop == LineCharProperty.LF || prop == LineCharProperty.CR; -} - /// Manages [ParagraphRuler] instances and caches them per unique /// [ParagraphGeometricStyle]. /// @@ -435,6 +429,7 @@ class DomTextMeasurementService extends TextMeasurementService { _excludeTrailing(text, 0, text.length, _newlinePredicate), hardBreak: true, width: lineWidth, + widthWithTrailingSpaces: lineWidth, left: alignOffset, lineNumber: 0, ), @@ -453,6 +448,7 @@ class DomTextMeasurementService extends TextMeasurementService { alphabeticBaseline: alphabeticBaseline, ideographicBaseline: ideographicBaseline, lines: lines, + placeholderBoxes: ruler.measurePlaceholderBoxes(), textAlign: paragraph._textAlign, textDirection: paragraph._textDirection, ); @@ -503,6 +499,7 @@ class DomTextMeasurementService extends TextMeasurementService { alphabeticBaseline: alphabeticBaseline, ideographicBaseline: ideographicBaseline, lines: null, + placeholderBoxes: ruler.measurePlaceholderBoxes(), textAlign: paragraph._textAlign, textDirection: paragraph._textDirection, ); @@ -614,6 +611,7 @@ class CanvasTextMeasurementService extends TextMeasurementService { maxIntrinsicWidth: maxIntrinsicCalculator.value, width: constraints.width, lines: linesCalculator.lines, + placeholderBoxes: [], textAlign: paragraph._textAlign, textDirection: paragraph._textDirection, ); @@ -688,8 +686,8 @@ double _measureSubstring( final double letterSpacing = style.letterSpacing ?? 0.0; final String sub = start == 0 && end == text.length ? text : text.substring(start, end); - final double width = - _canvasContext.measureText(sub).width! + letterSpacing * sub.length as double; + final double width = _canvasContext.measureText(sub).width! + + letterSpacing * sub.length as double; // What we are doing here is we are rounding to the nearest 2nd decimal // point. So 39.999423 becomes 40, and 11.243982 becomes 11.24. @@ -736,8 +734,17 @@ class LinesCalculator { /// The lines that have been consumed so far. List lines = []; - int _lineStart = 0; - int _chunkStart = 0; + /// The last line break regardless of whether it was optional or mandatory, or + /// whether we took it or not. + LineBreakResult _lastBreak = + const LineBreakResult.sameIndex(0, LineBreakType.mandatory); + + /// The last line break that actually caused a new line to exist. + LineBreakResult _lastTakenBreak = + const LineBreakResult.sameIndex(0, LineBreakType.mandatory); + + int get _lineStart => _lastTakenBreak.index; + int get _chunkStart => _lastBreak.index; bool _reachedMaxLines = false; double? _cachedEllipsisWidth; @@ -755,8 +762,8 @@ class LinesCalculator { final bool isHardBreak = brk.type == LineBreakType.mandatory || brk.type == LineBreakType.endOfText; final int chunkEnd = brk.index; - final int chunkEndWithoutSpace = - _excludeTrailing(_text!, _chunkStart, chunkEnd, _whitespacePredicate); + final int chunkEndWithoutNewlines = brk.indexWithoutTrailingNewlines; + final int chunkEndWithoutSpace = brk.indexWithoutTrailingSpaces; // A single chunk of text could be force-broken into multiple lines if it // doesn't fit in a single line. That's why we need a loop. @@ -800,10 +807,10 @@ class LinesCalculator { _text!.substring(_lineStart, breakingPoint) + _style.ellipsis!, startIndex: _lineStart, endIndex: chunkEnd, - endIndexWithoutNewlines: - _excludeTrailing(_text!, _chunkStart, chunkEnd, _newlinePredicate), + endIndexWithoutNewlines: chunkEndWithoutNewlines, hardBreak: false, width: widthOfResultingLine, + widthWithTrailingSpaces: widthOfResultingLine, left: alignOffset, lineNumber: lines.length, )); @@ -821,12 +828,14 @@ class LinesCalculator { // [isHardBreak] check below) to break the line. break; } - _addLineBreak(lineEnd: breakingPoint, isHardBreak: false); - _chunkStart = breakingPoint; + _addLineBreak(LineBreakResult.sameIndex( + breakingPoint, + LineBreakType.opportunity, + )); } else { // The control case of current line exceeding [_maxWidth], we break the // line. - _addLineBreak(lineEnd: _chunkStart, isHardBreak: false); + _addLineBreak(_lastBreak); } } @@ -835,46 +844,38 @@ class LinesCalculator { } if (isHardBreak) { - _addLineBreak(lineEnd: chunkEnd, isHardBreak: true); + _addLineBreak(brk); } - _chunkStart = chunkEnd; + _lastBreak = brk; } - void _addLineBreak({ - required int lineEnd, - required bool isHardBreak, - }) { - final int endWithoutNewlines = _excludeTrailing( - _text!, - _lineStart, - lineEnd, - _newlinePredicate, - ); - final int endWithoutSpace = _excludeTrailing( - _text!, - _lineStart, - endWithoutNewlines, - _whitespacePredicate, - ); + void _addLineBreak(LineBreakResult brk) { final int lineNumber = lines.length; - final double lineWidth = measureSubstring(_lineStart, endWithoutSpace); + final double lineWidth = + measureSubstring(_lineStart, brk.indexWithoutTrailingSpaces); + final double lineWidthWithTrailingSpaces = + measureSubstring(_lineStart, brk.indexWithoutTrailingNewlines); final double alignOffset = _calculateAlignOffsetForLine( paragraph: _paragraph, lineWidth: lineWidth, maxWidth: _maxWidth, ); + final bool isHardBreak = brk.type == LineBreakType.mandatory || + brk.type == LineBreakType.endOfText; + final EngineLineMetrics metrics = EngineLineMetrics.withText( - _text!.substring(_lineStart, endWithoutNewlines), + _text!.substring(_lineStart, brk.indexWithoutTrailingNewlines), startIndex: _lineStart, - endIndex: lineEnd, - endIndexWithoutNewlines: endWithoutNewlines, + endIndex: brk.index, + endIndexWithoutNewlines: brk.indexWithoutTrailingNewlines, hardBreak: isHardBreak, width: lineWidth, + widthWithTrailingSpaces: lineWidthWithTrailingSpaces, left: alignOffset, lineNumber: lineNumber, ); lines.add(metrics); - _lineStart = lineEnd; + _lastTakenBreak = _lastBreak = brk; if (lines.length == _style.maxLines) { _reachedMaxLines = true; } @@ -943,10 +944,13 @@ class MinIntrinsicCalculator { /// [value] will contain the final minimum intrinsic width. void update(LineBreakResult brk) { final int chunkEnd = brk.index; - final int chunkEndWithoutSpace = - _excludeTrailing(_text, _lastChunkEnd, chunkEnd, _whitespacePredicate); final double width = _measureSubstring( - _canvasContext, _style, _text, _lastChunkEnd, chunkEndWithoutSpace); + _canvasContext, + _style, + _text, + _lastChunkEnd, + brk.indexWithoutTrailingSpaces, + ); if (width > value) { value = width; } @@ -977,24 +981,17 @@ class MaxIntrinsicCalculator { return; } - final int hardLineEnd = brk.index; - final int hardLineEndWithoutNewlines = _excludeTrailing( - _text, - _lastHardLineEnd, - hardLineEnd, - _newlinePredicate, - ); final double lineWidth = _measureSubstring( _canvasContext, _style, _text, _lastHardLineEnd, - hardLineEndWithoutNewlines, + brk.indexWithoutTrailingNewlines, ); if (lineWidth > value) { value = lineWidth; } - _lastHardLineEnd = hardLineEnd; + _lastHardLineEnd = brk.index; } } diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index a3d09e9c57795..862f6789a4d35 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -7,6 +7,8 @@ part of engine; const ui.Color _defaultTextColor = ui.Color(0xFFFF0000); +const String _placeholderClass = 'paragraph-placeholder'; + class EngineLineMetrics implements ui.LineMetrics { EngineLineMetrics({ required this.hardBreak, @@ -21,7 +23,8 @@ class EngineLineMetrics implements ui.LineMetrics { }) : displayText = null, startIndex = -1, endIndex = -1, - endIndexWithoutNewlines = -1; + endIndexWithoutNewlines = -1, + widthWithTrailingSpaces = width; EngineLineMetrics.withText( String this.displayText, { @@ -30,6 +33,7 @@ class EngineLineMetrics implements ui.LineMetrics { required this.endIndexWithoutNewlines, required this.hardBreak, required this.width, + required this.widthWithTrailingSpaces, required this.left, required this.lineNumber, }) : assert(displayText != null), // ignore: unnecessary_null_comparison @@ -80,6 +84,18 @@ class EngineLineMetrics implements ui.LineMetrics { @override final double width; + /// The full width of the line including all trailing space but not new lines. + /// + /// The difference between [width] and [widthWithTrailingSpaces] is that + /// [widthWithTrailingSpaces] includes trailing spaces in the width + /// calculation while [width] doesn't. + /// + /// For alignment purposes for example, the [width] property is the right one + /// to use because trailing spaces shouldn't affect the centering of text. + /// But for placing cursors in text fields, we do care about trailing + /// spaces so [widthWithTrailingSpaces] is more suitable. + final double widthWithTrailingSpaces; + @override final double left; @@ -160,6 +176,7 @@ class EngineParagraph implements ui.Paragraph { required ui.TextAlign textAlign, required ui.TextDirection textDirection, required ui.Paint? background, + required this.placeholderCount, }) : assert((plainText == null && paint == null) || (plainText != null && paint != null)), _paragraphElement = paragraphElement, @@ -178,6 +195,8 @@ class EngineParagraph implements ui.Paragraph { final ui.TextDirection _textDirection; final SurfacePaint? _background; + final int placeholderCount; + @visibleForTesting String? get plainText => _plainText; @@ -318,7 +337,8 @@ class EngineParagraph implements ui.Paragraph { @override List getBoxesForPlaceholders() { - return const []; + assert(_isLaidOut); + return _measurementResult!.placeholderBoxes; } /// Returns `true` if this paragraph can be directly painted to the canvas. @@ -453,7 +473,7 @@ class EngineParagraph implements ui.Paragraph { return ui.TextBox.fromLTRBD( line.left + widthBeforeBox, top, - line.left + line.width - widthAfterBox, + line.left + line.widthWithTrailingSpaces - widthAfterBox, top + _lineHeight, _textDirection, ); @@ -468,6 +488,7 @@ class EngineParagraph implements ui.Paragraph { textAlign: _textAlign, textDirection: _textDirection, background: _background, + placeholderCount: placeholderCount, ); } @@ -1044,11 +1065,11 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { @override int get placeholderCount => _placeholderCount; - late int _placeholderCount; + int _placeholderCount = 0; @override List get placeholderScales => _placeholderScales; - List _placeholderScales = []; + final List _placeholderScales = []; @override void addPlaceholder( @@ -1059,8 +1080,20 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { double? baselineOffset, ui.TextBaseline? baseline, }) { - // TODO(garyq): Implement stub_ui version of this. - throw UnimplementedError(); + // Require a baseline to be specified if using a baseline-based alignment. + assert((alignment == ui.PlaceholderAlignment.aboveBaseline || + alignment == ui.PlaceholderAlignment.belowBaseline || + alignment == ui.PlaceholderAlignment.baseline) ? baseline != null : true); + + _placeholderCount++; + _placeholderScales.add(scale); + _ops.add(ParagraphPlaceholder( + width * scale, + height * scale, + alignment, + baselineOffset: (baselineOffset ?? height) * scale, + baseline: baseline ?? ui.TextBaseline.alphabetic, + )); } // TODO(yjbanov): do we need to do this? @@ -1239,6 +1272,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { textAlign: textAlign, textDirection: textDirection, background: cumulativeStyle._background, + placeholderCount: placeholderCount, ); } @@ -1293,6 +1327,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { textAlign: textAlign, textDirection: textDirection, background: cumulativeStyle._background, + placeholderCount: placeholderCount, ); } @@ -1301,6 +1336,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { final List elementStack = []; dynamic currentElement() => elementStack.isNotEmpty ? elementStack.last : _paragraphElement; + for (int i = 0; i < _ops.length; i++) { final dynamic op = _ops[i]; if (op is EngineTextStyle) { @@ -1313,6 +1349,11 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { elementStack.add(span); } else if (op is String) { domRenderer.appendText(currentElement(), op); + } else if (op is ParagraphPlaceholder) { + domRenderer.append( + currentElement(), + _createPlaceholderElement(placeholder: op), + ); } else if (identical(op, _paragraphBuilderPop)) { elementStack.removeLast(); } else { @@ -1336,10 +1377,42 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { textAlign: _paragraphStyle._effectiveTextAlign, textDirection: _paragraphStyle._effectiveTextDirection, background: null, + placeholderCount: placeholderCount, ); } } +/// Holds information for a placeholder in a paragraph. +/// +/// [width], [height] and [baselineOffset] are expected to be already scaled. +class ParagraphPlaceholder { + ParagraphPlaceholder( + this.width, + this.height, + this.alignment, { + required this.baselineOffset, + required this.baseline, + }); + + /// The scaled width of the placeholder. + final double width; + + /// The scaled height of the placeholder. + final double height; + + /// Specifies how the placeholder rectangle will be vertically aligned with + /// the surrounding text. + final ui.PlaceholderAlignment alignment; + + /// When the [alignment] value is [ui.PlaceholderAlignment.baseline], the + /// [baselineOffset] indicates the distance from the baseline to the top of + /// the placeholder rectangle. + final double baselineOffset; + + /// Dictates whether to use alphabetic or ideographic baseline. + final ui.TextBaseline baseline; +} + /// Converts [fontWeight] to its CSS equivalent value. String? fontWeightToCss(ui.FontWeight? fontWeight) { if (fontWeight == null) { @@ -1554,6 +1627,52 @@ void _applyTextStyleToElement({ } } +html.Element _createPlaceholderElement({ + required ParagraphPlaceholder placeholder, +}) { + final html.Element element = domRenderer.createElement('span'); + element.className = _placeholderClass; + final html.CssStyleDeclaration style = element.style; + style + ..display = 'inline-block' + ..width = '${placeholder.width}px' + ..height = '${placeholder.height}px' + ..verticalAlign = _placeholderAlignmentToCssVerticalAlign(placeholder); + + return element; +} + +String _placeholderAlignmentToCssVerticalAlign( + ParagraphPlaceholder placeholder, +) { + // For more details about the vertical-align CSS property, see: + // - https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align + switch (placeholder.alignment) { + case ui.PlaceholderAlignment.top: + return 'top'; + + case ui.PlaceholderAlignment.middle: + return 'middle'; + + case ui.PlaceholderAlignment.bottom: + return 'bottom'; + + case ui.PlaceholderAlignment.aboveBaseline: + return 'baseline'; + + case ui.PlaceholderAlignment.belowBaseline: + return '-${placeholder.height}px'; + + case ui.PlaceholderAlignment.baseline: + // In CSS, the placeholder is already placed above the baseline. But + // Flutter's `baselineOffset` assumes the placeholder is placed below the + // baseline. That's why we need to subtract the placeholder's height from + // `baselineOffset`. + final double offset = placeholder.baselineOffset - placeholder.height; + return '${offset}px'; + } +} + String _shadowListToCss(List shadows) { if (shadows.isEmpty) { return ''; @@ -1689,7 +1808,6 @@ String? textAlignToCssValue(ui.TextAlign? align, ui.TextDirection textDirection) case ui.TextDirection.rtl: return 'left'; } - break; default: // including ui.TextAlign.start switch (textDirection) { case ui.TextDirection.ltr: @@ -1697,7 +1815,6 @@ String? textAlignToCssValue(ui.TextAlign? align, ui.TextDirection textDirection) case ui.TextDirection.rtl: return 'right'; } - break; } } diff --git a/lib/web_ui/lib/src/engine/text/ruler.dart b/lib/web_ui/lib/src/engine/text/ruler.dart index 74500dc9d2acb..32018f2515917 100644 --- a/lib/web_ui/lib/src/engine/text/ruler.dart +++ b/lib/web_ui/lib/src/engine/text/ruler.dart @@ -628,6 +628,31 @@ class ParagraphRuler { ); } + List measurePlaceholderBoxes() { + assert(!_debugIsDisposed); + assert(_paragraph != null); + + if (_paragraph!.placeholderCount == 0) { + return const []; + } + + final List placeholderElements = + constrainedDimensions._element.querySelectorAll('.$_placeholderClass'); + final List boxes = []; + + for (final html.Element element in placeholderElements) { + final html.Rectangle rect = element.getBoundingClientRect(); + boxes.add(ui.TextBox.fromLTRBD( + rect.left as double, + rect.top as double, + rect.right as double, + rect.bottom as double, + _paragraph!._textDirection, + )); + } + return boxes; + } + /// Returns text position in a paragraph that contains multiple /// nested spans given an offset. int hitTest(ui.ParagraphConstraints constraints, ui.Offset offset) { @@ -905,6 +930,8 @@ class MeasurementResult { /// of each laid out line. final List? lines; + final List placeholderBoxes; + /// The text align value of the paragraph. final ui.TextAlign textAlign; @@ -923,6 +950,7 @@ class MeasurementResult { required this.alphabeticBaseline, required this.ideographicBaseline, required this.lines, + required this.placeholderBoxes, required ui.TextAlign? textAlign, required ui.TextDirection? textDirection, }) : assert(constraintWidth != null), // ignore: unnecessary_null_comparison diff --git a/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart b/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart index 9b8007ee0599d..a9f04137bb0bb 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart @@ -37,7 +37,7 @@ class TextCapitalizationConfig { const TextCapitalizationConfig.defaultCapitalization() : textCapitalization = TextCapitalization.none; - TextCapitalizationConfig.fromInputConfiguration(String inputConfiguration) + const TextCapitalizationConfig.fromInputConfiguration(String inputConfiguration) : this.textCapitalization = inputConfiguration == 'TextCapitalization.words' ? TextCapitalization.words 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 e29d24f1c3134..c46e15c22d3a4 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 @@ -6,7 +6,7 @@ part of engine; /// Make the content editable span visible to facilitate debugging. -const bool _debugVisibleTextEditing = false; +bool _debugVisibleTextEditing = false; /// The `keyCode` of the "Enter" key. const int _kReturnKeyCode = 13; @@ -55,7 +55,8 @@ void _setStaticStyleAttributes(html.HtmlElement domElement) { /// element. /// /// They are assigned once during the creation of the DOM element. -void _hideAutofillElements(html.HtmlElement domElement) { +void _hideAutofillElements(html.HtmlElement domElement, + {bool isOffScreen = false}) { final html.CssStyleDeclaration elementStyle = domElement.style; elementStyle ..whiteSpace = 'pre-wrap' @@ -73,6 +74,12 @@ void _hideAutofillElements(html.HtmlElement domElement) { ..textShadow = 'transparent' ..transformOrigin = '0 0 0'; + if (isOffScreen) { + elementStyle + ..top = '-9999px' + ..left = '-9999px'; + } + /// This property makes the input's blinking cursor transparent. elementStyle.setProperty('caret-color', 'transparent'); } @@ -82,14 +89,27 @@ void _hideAutofillElements(html.HtmlElement domElement) { /// These values are to be used when autofill is enabled and there is a group of /// text fields with more than one text field. class EngineAutofillForm { - EngineAutofillForm({this.formElement, this.elements, this.items}); + EngineAutofillForm( + {required this.formElement, + this.elements, + this.items, + this.formIdentifier = ''}); - final html.FormElement? formElement; + final html.FormElement formElement; final Map? elements; final Map? items; + /// Identifier for the form. + /// + /// It is constructed by concatenating unique ids of input elements on the + /// form. + /// + /// It is used for storing the form until submission. + /// See [formsOnTheDom]. + final String formIdentifier; + static EngineAutofillForm? fromFrameworkMessage( Map? focusedElementAutofill, List? fields, @@ -109,9 +129,22 @@ class EngineAutofillForm { // Validation is in the framework side. formElement.noValidate = true; + formElement.method = 'post'; + formElement.action = '#'; + formElement.addEventListener('submit', (e) { + e.preventDefault(); + }); _hideAutofillElements(formElement); + // We keep the ids in a list then sort them later, in case the text fields' + // locations are re-ordered on the framework side. + final List ids = List.empty(growable: true); + + // The focused text editing element will not be created here. + final AutofillInfo focusedElement = + AutofillInfo.fromFrameworkMessage(focusedElementAutofill); + if (fields != null) { for (Map field in fields.cast>()) { final Map autofillInfo = field['autofill']; @@ -120,9 +153,8 @@ class EngineAutofillForm { textCapitalization: TextCapitalizationConfig.fromInputConfiguration( field['textCapitalization'])); - // The focused text editing element will not be created here. - final AutofillInfo focusedElement = - AutofillInfo.fromFrameworkMessage(focusedElementAutofill); + ids.add(autofill.uniqueIdentifier); + if (autofill.uniqueIdentifier != focusedElement.uniqueIdentifier) { EngineInputType engineInputType = EngineInputType.fromName(field['inputType']['name']); @@ -137,22 +169,57 @@ class EngineAutofillForm { formElement.append(htmlElement); } } + } else { + // There is one input element in the form. + ids.add(focusedElement.uniqueIdentifier); } + ids.sort(); + final StringBuffer idBuffer = StringBuffer(); + + // Add a seperator between element identifiers. + for (final String id in ids) { + if (idBuffer.length > 0) { + idBuffer.write('*'); + } + idBuffer.write(id); + } + + final String formIdentifier = idBuffer.toString(); + + // If a form with the same Autofill elements is already on the dom, remove + // it from DOM. + if (formsOnTheDom[formIdentifier] != null) { + final html.FormElement form = + formsOnTheDom[formIdentifier] as html.FormElement; + form.remove(); + } + + // In order to submit the form when Framework sends a `TextInput.commit` + // message, we add a submit button to the form. + final html.InputElement submitButton = html.InputElement(); + _hideAutofillElements(submitButton, isOffScreen: true); + submitButton.className = 'submitBtn'; + submitButton.type = 'submit'; + + formElement.append(submitButton); + return EngineAutofillForm( formElement: formElement, elements: elements, items: items, + formIdentifier: formIdentifier, ); } void placeForm(html.HtmlElement mainTextEditingElement) { - formElement!.append(mainTextEditingElement); - domRenderer.glassPaneElement!.append(formElement!); + formElement.append(mainTextEditingElement); + domRenderer.glassPaneElement!.append(formElement); } - void removeForm() { - formElement!.remove(); + void storeForm() { + formsOnTheDom[formIdentifier] = this.formElement; + _hideAutofillElements(formElement, isOffScreen: true); } /// Listens to `onInput` event on the form fields. @@ -226,9 +293,11 @@ class AutofillInfo { /// The current text and selection state of a text field. final EditingState editingState; - /// Unique value set by the developer. + /// Unique value set by the developer or generated by the framework. /// /// Used as id of the text field. + /// + /// An example an id generated by the framework: `EditableText-285283643`. final String uniqueIdentifier; /// Information on how should autofilled text capitalized. @@ -272,20 +341,17 @@ class AutofillInfo { if (domElement is html.InputElement) { html.InputElement element = domElement; element.name = hint; - element.id = uniqueIdentifier; + element.id = hint; element.autocomplete = hint; - // Do not change the element type for the focused element. - if (focusedElement == false) { - if (hint.contains('password')) { - element.type = 'password'; - } else { - element.type = 'text'; - } + if (hint.contains('password')) { + element.type = 'password'; + } else { + element.type = 'text'; } } else if (domElement is html.TextAreaElement) { html.TextAreaElement element = domElement; element.name = hint; - element.id = uniqueIdentifier; + element.id = hint; element.setAttribute('autocomplete', hint); } } @@ -424,11 +490,13 @@ class EditingState { /// This corresponds to Flutter's [TextInputConfiguration]. class InputConfiguration { InputConfiguration({ - required this.inputType, - required this.inputAction, - required this.obscureText, - required this.autocorrect, - required this.textCapitalization, + this.inputType = EngineInputType.text, + this.inputAction = 'TextInputAction.done', + this.obscureText = false, + this.readOnly = false, + this.autocorrect = true, + this.textCapitalization = + const TextCapitalizationConfig.defaultCapitalization(), this.autofill, this.autofillGroup, }); @@ -436,14 +504,17 @@ class InputConfiguration { InputConfiguration.fromFrameworkMessage( Map flutterInputConfiguration) : inputType = EngineInputType.fromName( - flutterInputConfiguration['inputType']['name'], - isDecimal: - flutterInputConfiguration['inputType']['decimal'] ?? false), - inputAction = flutterInputConfiguration['inputAction'], - obscureText = flutterInputConfiguration['obscureText'], - autocorrect = flutterInputConfiguration['autocorrect'], + flutterInputConfiguration['inputType']['name'], + isDecimal: flutterInputConfiguration['inputType']['decimal'] ?? false, + ), + inputAction = + flutterInputConfiguration['inputAction'] ?? 'TextInputAction.done', + obscureText = flutterInputConfiguration['obscureText'] ?? false, + readOnly = flutterInputConfiguration['readOnly'] ?? false, + autocorrect = flutterInputConfiguration['autocorrect'] ?? true, textCapitalization = TextCapitalizationConfig.fromInputConfiguration( - flutterInputConfiguration['textCapitalization']), + flutterInputConfiguration['textCapitalization'], + ), autofill = flutterInputConfiguration.containsKey('autofill') ? AutofillInfo.fromFrameworkMessage( flutterInputConfiguration['autofill']) @@ -456,7 +527,12 @@ class InputConfiguration { final EngineInputType inputType; /// The default action for the input field. - final String? inputAction; + final String inputAction; + + /// Whether the text field can be edited or not. + /// + /// Defaults to false. + final bool readOnly; /// Whether to hide the text being edited. final bool? obscureText; @@ -468,7 +544,7 @@ class InputConfiguration { /// /// For future manual tests, note that autocorrect is an attribute only /// supported by Safari. - final bool? autocorrect; + final bool autocorrect; final AutofillInfo? autofill; @@ -538,12 +614,17 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy { @override void placeElement() { - super.placeElement(); if (hasAutofillGroup) { _geometry?.applyToDomElement(focusedFormElement!); placeForm(); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } // On Chrome, when a form is focused, it opens an autofill menu - // immeddiately. + // immediately. // Flutter framework sends `setEditableSizeAndTransform` for informing // the engine about the location of the text field. This call will // arrive after `show` call. @@ -551,13 +632,68 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy { // `setEditableSizeAndTransform` method is called and focus on the form // only after placing it to the correct position. Hence autofill menu // does not appear on top-left of the page. + // Refocus on the elements after applying the geometry. focusedFormElement!.focus(); + domElement.focus(); } else { _geometry?.applyToDomElement(domElement); } } } +/// A [TextEditingStrategy] for Safari Desktop Browser. +/// +/// It places its [domElement] assuming no prior transform or sizing is applied +/// to it. +/// +/// In case of an autofill enabled form, it does not append the form element +/// to the DOM, until the geometry information is updated. +/// +/// This implementation is used by text editables when semantics is not +/// enabled. With semantics enabled the placement is provided by the semantics +/// tree. +class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy { + SafariDesktopTextEditingStrategy(HybridTextEditing owner) : super(owner); + + /// Appending an element on the DOM for Safari Desktop Browser. + /// + /// This method is only called when geometry information is updated by + /// 'TextInput.setEditableSizeAndTransform' message. + /// + /// This method is similar to the [GloballyPositionedTextEditingStrategy]. + /// The only part different: this method does not call `super.placeElement()`, + /// which in current state calls `domElement.focus()`. + /// + /// Making an extra `focus` request causes flickering in Safari. + @override + void placeElement() { + _geometry?.applyToDomElement(domElement); + if (hasAutofillGroup) { + placeForm(); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } + // On Safari Desktop, when a form is focused, it opens an autofill menu + // immediately. + // Flutter framework sends `setEditableSizeAndTransform` for informing + // the engine about the location of the text field. This call will + // arrive after `show` call. Therefore form is placed, when + // `setEditableSizeAndTransform` method is called and focus called on the + // form only after placing it to the correct position and only once after + // that. Calling focus multiple times causes flickering. + focusedFormElement!.focus(); + } + } + + @override + void initializeElementPlacement() { + domElement.focus(); + } +} + /// Class implementing the default editing strategies for text editing. /// /// This class uses a DOM element to provide text editing capabilities. @@ -611,6 +747,10 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { bool get hasAutofillGroup => _inputConfiguration.autofillGroup != null; + /// Whether the focused input element is part of a form. + bool get appendedToForm => _appendedToForm; + bool _appendedToForm = false; + html.FormElement? get focusedFormElement => _inputConfiguration.autofillGroup?.formElement; @@ -625,6 +765,9 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { this._inputConfiguration = inputConfig; _domElement = inputConfig.inputType.createDomElement(); + if (inputConfig.readOnly) { + domElement.setAttribute('readonly', 'readonly'); + } if (inputConfig.obscureText!) { domElement.setAttribute('type', 'password'); } @@ -636,12 +779,14 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _setStaticStyleAttributes(domElement); _style?.applyToDomElement(domElement); + if (!hasAutofillGroup) { // If there is an Autofill Group the `FormElement`, it will be appended to the // DOM later, when the first location information arrived. // Otherwise, on Blink based Desktop browsers, the autofill menu appears // on top left of the screen. domRenderer.glassPaneElement!.append(domElement); + _appendedToForm = false; } initializeElementPlacement(); @@ -709,9 +854,19 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _subscriptions[i].cancel(); } _subscriptions.clear(); - domElement.remove(); + // If focused element is a part of a form, it needs to stay on the DOM + // until the autofill context of the form is finalized. + // More details on `TextInput.finishAutofillContext` call. + if (_appendedToForm && + _inputConfiguration.autofillGroup?.formElement != null) { + // Subscriptions are removed, listeners won't be triggered. + domElement.blur(); + _hideAutofillElements(domElement, isOffScreen: true); + _inputConfiguration.autofillGroup?.storeForm(); + } else { + domElement.remove(); + } _domElement = null; - _inputConfiguration.autofillGroup?.removeForm(); } @mustCallSuper @@ -730,6 +885,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void placeForm() { _inputConfiguration.autofillGroup!.placeForm(domElement); + _appendedToForm = true; } void _handleChange(html.Event event) { @@ -857,8 +1013,6 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy { inputConfig.inputType.configureInputMode(domElement); if (hasAutofillGroup) { placeForm(); - } else { - domRenderer.glassPaneElement!.append(domElement); } inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement); } @@ -1045,8 +1199,6 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy { onChange: onChange, onAction: onAction); if (hasAutofillGroup) { placeForm(); - } else { - domRenderer.glassPaneElement!.append(domElement); } } @@ -1097,6 +1249,12 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy { void placeElement() { domElement.focus(); _geometry?.applyToDomElement(domElement); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } } } @@ -1156,8 +1314,14 @@ class TextEditingChannel { break; case 'TextInput.finishAutofillContext': - // TODO(nurhan): Handle saving autofill information on web. - // https://github.com/flutter/flutter/issues/59378 + final bool saveForm = call.arguments as bool; + // Close the text editing connection. Form is finalizing. + implementation.sendTextConnectionClosedToFrameworkIfAny(); + if (saveForm) { + saveForms(); + } + // Clean the forms from DOM after submitting them. + cleanForms(); break; default: @@ -1167,6 +1331,30 @@ class TextEditingChannel { window._replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); } + /// Used for submitting the forms attached on the DOM. + /// + /// Browser will save the information entered to the form. + /// + /// Called when the form is finalized with save option `true`. + /// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277 + void saveForms() { + formsOnTheDom.forEach((String identifier, html.FormElement form) { + final html.InputElement submitBtn = + form.getElementsByClassName('submitBtn').first as html.InputElement; + submitBtn.click(); + }); + } + + /// Used for removing the forms on the DOM. + /// + /// Called when the form is finalized. + void cleanForms() { + for (html.FormElement form in formsOnTheDom.values) { + form.remove(); + } + formsOnTheDom.clear(); + } + /// Sends the 'TextInputClient.updateEditingState' message to the framework. void updateEditingState(int? clientId, EditingState? editingState) { if (window._onPlatformMessage != null) { @@ -1219,6 +1407,15 @@ class TextEditingChannel { /// Text editing singleton. final HybridTextEditing textEditing = HybridTextEditing(); +/// Map for storing forms left attached on the DOM. +/// +/// Used for keeping the form elements on the DOM until user confirms to +/// save or cancel them. +/// +/// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277 +final Map formsOnTheDom = + Map(); + /// Should be used as a singleton to provide support for text editing in /// Flutter Web. /// @@ -1236,6 +1433,8 @@ class HybridTextEditing { if (browserEngine == BrowserEngine.webkit && operatingSystem == OperatingSystem.iOs) { this._defaultEditingElement = IOSTextEditingStrategy(this); + } else if (browserEngine == BrowserEngine.webkit) { + this._defaultEditingElement = SafariDesktopTextEditingStrategy(this); } else if (browserEngine == BrowserEngine.blink && operatingSystem == OperatingSystem.android) { this._defaultEditingElement = AndroidTextEditingStrategy(this); diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index ba8061fc061a6..b747583c6d3fd 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -527,3 +527,28 @@ double convertSigmaToRadius(double sigma) { bool isUnsoundNull(dynamic object) { return object == null; } + +bool _offsetIsValid(ui.Offset offset) { + assert(offset != null, 'Offset argument was null.'); // ignore: unnecessary_null_comparison + assert(!offset.dx.isNaN && !offset.dy.isNaN, + 'Offset argument contained a NaN value.'); + return true; +} + +bool _matrix4IsValid(Float32List matrix4) { + assert(matrix4 != null, 'Matrix4 argument was null.'); // ignore: unnecessary_null_comparison + assert(matrix4.length == 16, 'Matrix4 must have 16 entries.'); + return true; +} + +void _validateColorStops(List colors, List? colorStops) { + if (colorStops == null) { + if (colors.length != 2) + throw ArgumentError( + '"colors" must have length 2 if "colorStops" is omitted.'); + } else { + if (colors.length != colorStops.length) + throw ArgumentError( + '"colors" and "colorStops" arguments must have equal length.'); + } +} diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 6463e6043502b..7dbb1497c716d 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -123,14 +123,19 @@ class EngineWindow extends ui.Window { height = html.window.innerHeight! * devicePixelRatio; width = html.window.innerWidth! * devicePixelRatio; } - // First confirm both heught and width is effected. - if (_physicalSize!.height != height && _physicalSize!.width != width) { - // If prior to rotation height is bigger than width it should be the - // opposite after the rotation and vice versa. - if ((_physicalSize!.height > _physicalSize!.width && height < width) || - (_physicalSize!.width > _physicalSize!.height && width < height)) { - // Rotation detected - return true; + + // This method compares the new dimensions with the previous ones. + // Return false if the previous dimensions are not set. + if(_physicalSize != null) { + // First confirm both height and width are effected. + if (_physicalSize!.height != height && _physicalSize!.width != width) { + // If prior to rotation height is bigger than width it should be the + // opposite after the rotation and vice versa. + if ((_physicalSize!.height > _physicalSize!.width && height < width) || + (_physicalSize!.width > _physicalSize!.height && width < height)) { + // Rotation detected + return true; + } } } return false; @@ -146,9 +151,6 @@ class EngineWindow extends ui.Window { /// Overrides the value of [physicalSize] in tests. ui.Size? webOnlyDebugPhysicalSizeOverride; - @override - double get physicalDepth => double.maxFinite; - /// Handles the browser history integration to allow users to use the back /// button, etc. final BrowserHistory _browserHistory = BrowserHistory(); diff --git a/lib/web_ui/lib/src/ui/annotations.dart b/lib/web_ui/lib/src/ui/annotations.dart index 7dac0c5670212..c2670d5282cc7 100644 --- a/lib/web_ui/lib/src/ui/annotations.dart +++ b/lib/web_ui/lib/src/ui/annotations.dart @@ -9,36 +9,6 @@ part of ui; // TODO(dnfield): Update this if/when we default this to on in the tool, // see: https://github.com/flutter/flutter/issues/52759 -/// Annotation used by Flutter's Dart compiler to indicate that an -/// [Object.toString] override should not be replaced with a supercall. -/// -/// Since `dart:ui` and `package:flutter` override `toString` purely for -/// debugging purposes, the frontend compiler is instructed to replace all -/// `toString` bodies with `return super.toString()` during compilation. This -/// significantly reduces release code size, and would make it impossible to -/// implement a meaningful override of `toString` for release mode without -/// disabling the feature and losing the size savings. If a package uses this -/// feature and has some unavoidable need to keep the `toString` implementation -/// for a specific class, applying this annotation will direct the compiler -/// to leave the method body as-is. -/// -/// For example, in the following class the `toString` method will remain as -/// `return _buffer.toString();`, even if the `--delete-tostring-package-uri` -/// option would otherwise apply and replace it with `return super.toString()`. -/// -/// ```dart -/// class MyStringBuffer { -/// StringBuffer _buffer = StringBuffer(); -/// -/// // ... -/// -/// @keepToString -/// @override -/// String toString() { -/// return _buffer.toString(); -/// } -/// } -/// ``` const _KeepToString keepToString = _KeepToString(); class _KeepToString { diff --git a/lib/web_ui/lib/src/ui/canvas.dart b/lib/web_ui/lib/src/ui/canvas.dart index ee185ef29ce7d..1699a1a287410 100644 --- a/lib/web_ui/lib/src/ui/canvas.dart +++ b/lib/web_ui/lib/src/ui/canvas.dart @@ -5,70 +5,24 @@ // @dart = 2.10 part of ui; -/// Defines how a list of points is interpreted when drawing a set of points. -/// -/// Used by [Canvas.drawPoints]. enum PointMode { - /// Draw each point separately. - /// - /// If the [Paint.strokeCap] is [StrokeCap.round], then each point is drawn - /// as a circle with the diameter of the [Paint.strokeWidth], filled as - /// described by the [Paint] (ignoring [Paint.style]). - /// - /// Otherwise, each point is drawn as an axis-aligned square with sides of - /// length [Paint.strokeWidth], filled as described by the [Paint] (ignoring - /// [Paint.style]). points, - - /// Draw each sequence of two points as a line segment. - /// - /// If the number of points is odd, then the last point is ignored. - /// - /// The lines are stroked as described by the [Paint] (ignoring - /// [Paint.style]). lines, - - /// Draw the entire sequence of point as one line. - /// - /// The lines are stroked as described by the [Paint] (ignoring - /// [Paint.style]). polygon, } -/// Defines how a new clip region should be merged with the existing clip -/// region. -/// -/// Used by [Canvas.clipRect]. enum ClipOp { - /// Subtract the new region from the existing region. difference, - - /// Intersect the new region from the existing region. intersect, } enum VertexMode { - /// Draw each sequence of three points as the vertices of a triangle. triangles, - - /// Draw each sliding window of three points as the vertices of a triangle. triangleStrip, - - /// Draw the first point and each sliding window of two points as the vertices of a triangle. triangleFan, } -/// A set of vertex data used by [Canvas.drawVertices]. class Vertices { - /// Creates a set of vertex data for use with [Canvas.drawVertices]. - /// - /// The [mode] and [positions] parameters must not be null. - /// - /// If the [textureCoordinates] or [colors] parameters are provided, they must - /// be the same length as [positions]. - /// - /// If the [indices] parameter is provided, all values in the list must be - /// valid index values for [positions]. factory Vertices( VertexMode mode, List positions, { @@ -77,34 +31,21 @@ class Vertices { List? indices, }) { if (engine.experimentalUseSkia) { - return engine.CkVertices(mode, positions, - textureCoordinates: textureCoordinates, - colors: colors, - indices: indices); - } - return engine.SurfaceVertices(mode, positions, + return engine.CkVertices( + mode, + positions, + textureCoordinates: textureCoordinates, colors: colors, - indices: indices); + indices: indices, + ); + } + return engine.SurfaceVertices( + mode, + positions, + colors: colors, + indices: indices, + ); } - - /// Creates a set of vertex data for use with [Canvas.drawVertices], directly - /// using the encoding methods of [new Vertices]. - /// - /// The [mode] parameter must not be null. - /// - /// The [positions] list is interpreted as a list of repeated pairs of x,y - /// coordinates. It must not be null. - /// - /// The [textureCoordinates] list is interpreted as a list of repeated pairs - /// of x,y coordinates, and must be the same length of [positions] if it - /// is not null. - /// - /// The [colors] list is interpreted as a list of RGBA encoded colors, similar - /// to [Color.value]. It must be half length of [positions] if it is not - /// null. - /// - /// If the [indices] list is provided, all values in the list must be - /// valid index values for [positions]. factory Vertices.raw( VertexMode mode, Float32List positions, { @@ -113,25 +54,24 @@ class Vertices { Uint16List? indices, }) { if (engine.experimentalUseSkia) { - return engine.CkVertices.raw(mode, positions, - textureCoordinates: textureCoordinates, - colors: colors, - indices: indices); - } - return engine.SurfaceVertices.raw(mode, positions, + return engine.CkVertices.raw( + mode, + positions, + textureCoordinates: textureCoordinates, colors: colors, - indices: indices); + indices: indices, + ); + } + return engine.SurfaceVertices.raw( + mode, + positions, + colors: colors, + indices: indices, + ); } } -/// Records a [Picture] containing a sequence of graphical operations. -/// -/// To begin recording, construct a [Canvas] to record the commands. -/// To end recording, use the [PictureRecorder.endRecording] method. abstract class PictureRecorder { - /// Creates a new idle PictureRecorder. To associate it with a - /// [Canvas] and begin recording, pass this [PictureRecorder] to the - /// [Canvas] constructor. factory PictureRecorder() { if (engine.experimentalUseSkia) { return engine.CkPictureRecorder(); @@ -139,41 +79,10 @@ abstract class PictureRecorder { return engine.EnginePictureRecorder(); } } - - /// Whether this object is currently recording commands. - /// - /// Specifically, this returns true if a [Canvas] object has been - /// created to record commands and recording has not yet ended via a - /// call to [endRecording], and false if either this - /// [PictureRecorder] has not yet been associated with a [Canvas], - /// or the [endRecording] method has already been called. bool get isRecording; - - /// Finishes recording graphical operations. - /// - /// Returns a picture containing the graphical operations that have been - /// recorded thus far. After calling this function, both the picture recorder - /// and the canvas objects are invalid and cannot be used further. Picture endRecording(); } -/// An interface for recording graphical operations. -/// -/// [Canvas] objects are used in creating [Picture] objects, which can -/// themselves be used with a [SceneBuilder] to build a [Scene]. In -/// normal usage, however, this is all handled by the framework. -/// -/// A canvas has a current transformation matrix which is applied to all -/// operations. Initially, the transformation matrix is the identity transform. -/// It can be modified using the [translate], [scale], [rotate], [skew], -/// and [transform] methods. -/// -/// A canvas also has a current clip region which is applied to all operations. -/// Initially, the clip region is infinite. It can be modified using the -/// [clipRect], [clipRRect], and [clipPath] methods. -/// -/// The current transform and clip can be saved and restored using the stack -/// managed by the [save], [saveLayer], and [restore] methods. abstract class Canvas { factory Canvas(PictureRecorder recorder, [Rect? cullRect]) { if (engine.experimentalUseSkia) { @@ -182,403 +91,55 @@ abstract class Canvas { return engine.SurfaceCanvas(recorder as engine.EnginePictureRecorder, cullRect); } } - - /// Saves a copy of the current transform and clip on the save stack. - /// - /// Call [restore] to pop the save stack. - /// - /// See also: - /// - /// * [saveLayer], which does the same thing but additionally also groups the - /// commands done until the matching [restore]. void save(); - - /// Saves a copy of the current transform and clip on the save stack, and then - /// creates a new group which subsequent calls will become a part of. When the - /// save stack is later popped, the group will be flattened into a layer and - /// have the given `paint`'s [Paint.colorFilter] and [Paint.blendMode] - /// applied. - /// - /// This lets you create composite effects, for example making a group of - /// drawing commands semi-transparent. Without using [saveLayer], each part of - /// the group would be painted individually, so where they overlap would be - /// darker than where they do not. By using [saveLayer] to group them - /// together, they can be drawn with an opaque color at first, and then the - /// entire group can be made transparent using the [saveLayer]'s paint. - /// - /// Call [restore] to pop the save stack and apply the paint to the group. - /// - /// ## Using saveLayer with clips - /// - /// When a rectangular clip operation (from [clipRect]) is not axis-aligned - /// with the raster buffer, or when the clip operation is not rectalinear (e.g. - /// because it is a rounded rectangle clip created by [clipRRect] or an - /// arbitrarily complicated path clip created by [clipPath]), the edge of the - /// clip needs to be anti-aliased. - /// - /// If two draw calls overlap at the edge of such a clipped region, without - /// using [saveLayer], the first drawing will be anti-aliased with the - /// background first, and then the second will be anti-aliased with the result - /// of blending the first drawing and the background. On the other hand, if - /// [saveLayer] is used immediately after establishing the clip, the second - /// drawing will cover the first in the layer, and thus the second alone will - /// be anti-aliased with the background when the layer is clipped and - /// composited (when [restore] is called). - /// - /// For example, this [CustomPainter.paint] method paints a clean white - /// rounded rectangle: - /// - /// ```dart - /// void paint(Canvas canvas, Size size) { - /// Rect rect = Offset.zero & size; - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(rect, 100.0, 100.0)); - /// canvas.saveLayer(rect, new Paint()); - /// canvas.drawPaint(new Paint()..color = Colors.red); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// canvas.restore(); - /// } - /// ``` - /// - /// On the other hand, this one renders a red outline, the result of the red - /// paint being anti-aliased with the background at the clip edge, then the - /// white paint being similarly anti-aliased with the background _including - /// the clipped red paint_: - /// - /// ```dart - /// void paint(Canvas canvas, Size size) { - /// // (this example renders poorly, prefer the example above) - /// Rect rect = Offset.zero & size; - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(rect, 100.0, 100.0)); - /// canvas.drawPaint(new Paint()..color = Colors.red); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// } - /// ``` - /// - /// This point is moot if the clip only clips one draw operation. For example, - /// the following paint method paints a pair of clean white rounded - /// rectangles, even though the clips are not done on a separate layer: - /// - /// ```dart - /// void paint(Canvas canvas, Size size) { - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(Offset.zero & (size / 2.0), 50.0, 50.0)); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// canvas.save(); - /// canvas.clipRRect(new RRect.fromRectXY(size.center(Offset.zero) & (size / 2.0), 50.0, 50.0)); - /// canvas.drawPaint(new Paint()..color = Colors.white); - /// canvas.restore(); - /// } - /// ``` - /// - /// (Incidentally, rather than using [clipRRect] and [drawPaint] to draw - /// rounded rectangles like this, prefer the [drawRRect] method. These - /// examples are using [drawPaint] as a proxy for "complicated draw operations - /// that will get clipped", to illustrate the point.) - /// - /// ## Performance considerations - /// - /// Generally speaking, [saveLayer] is relatively expensive. - /// - /// There are a several different hardware architectures for GPUs (graphics - /// processing units, the hardware that handles graphics), but most of them - /// involve batching commands and reordering them for performance. When layers - /// are used, they cause the rendering pipeline to have to switch render - /// target (from one layer to another). Render target switches can flush the - /// GPU's command buffer, which typically means that optimizations that one - /// could get with larger batching are lost. Render target switches also - /// generate a lot of memory churn because the GPU needs to copy out the - /// current frame buffer contents from the part of memory that's optimized for - /// writing, and then needs to copy it back in once the previous render target - /// (layer) is restored. - /// - /// See also: - /// - /// * [save], which saves the current state, but does not create a new layer - /// for subsequent commands. - /// * [BlendMode], which discusses the use of [Paint.blendMode] with - /// [saveLayer]. void saveLayer(Rect? bounds, Paint paint); - - /// Pops the current save stack, if there is anything to pop. - /// Otherwise, does nothing. - /// - /// Use [save] and [saveLayer] to push state onto the stack. - /// - /// If the state was pushed with with [saveLayer], then this call will also - /// cause the new layer to be composited into the previous layer. void restore(); - - /// Returns the number of items on the save stack, including the - /// initial state. This means it returns 1 for a clean canvas, and - /// that each call to [save] and [saveLayer] increments it, and that - /// each matching call to [restore] decrements it. - /// - /// This number cannot go below 1. int getSaveCount(); - - /// Add a translation to the current transform, shifting the coordinate space - /// horizontally by the first argument and vertically by the second argument. void translate(double dx, double dy); - - /// Add an axis-aligned scale to the current transform, scaling by the first - /// argument in the horizontal direction and the second in the vertical - /// direction. - /// - /// If [sy] is unspecified, [sx] will be used for the scale in both - /// directions. void scale(double sx, [double? sy]); - - /// Add a rotation to the current transform. The argument is in radians clockwise. void rotate(double radians); - - /// Add an axis-aligned skew to the current transform, with the first argument - /// being the horizontal skew in radians clockwise around the origin, and the - /// second argument being the vertical skew in radians clockwise around the - /// origin. void skew(double sx, double sy); - - /// Multiply the current transform by the specified 4⨉4 transformation matrix - /// specified as a list of values in column-major order. void transform(Float64List matrix4); - - /// Reduces the clip region to the intersection of the current clip and the - /// given rectangle. - /// - /// If [doAntiAlias] is true, then the clip will be anti-aliased. - /// - /// If multiple draw commands intersect with the clip boundary, this can result - /// in incorrect blending at the clip boundary. See [saveLayer] for a - /// discussion of how to address that. - /// - /// Use [ClipOp.difference] to subtract the provided rectangle from the - /// current clip. - void clipRect(Rect rect, - {ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true}); - - /// Reduces the clip region to the intersection of the current clip and the - /// given rounded rectangle. - /// - /// If [doAntiAlias] is true, then the clip will be anti-aliased. - /// - /// If multiple draw commands intersect with the clip boundary, this can result - /// in incorrect blending at the clip boundary. See [saveLayer] for a - /// discussion of how to address that and some examples of using [clipRRect]. + void clipRect(Rect rect, {ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true}); void clipRRect(RRect rrect, {bool doAntiAlias = true}); - - /// Reduces the clip region to the intersection of the current clip and the - /// given [Path]. - /// - /// If [doAntiAlias] is true, then the clip will be anti-aliased. - /// - /// If multiple draw commands intersect with the clip boundary, this can result - /// multiple draw commands intersect with the clip boundary, this can result - /// in incorrect blending at the clip boundary. See [saveLayer] for a - /// discussion of how to address that. void clipPath(Path path, {bool doAntiAlias = true}); - - /// Paints the given [Color] onto the canvas, applying the given - /// [BlendMode], with the given color being the source and the background - /// being the destination. void drawColor(Color color, BlendMode blendMode); - - /// Draws a line between the given points using the given paint. The line is - /// stroked, the value of the [Paint.style] is ignored for this call. - /// - /// The `p1` and `p2` arguments are interpreted as offsets from the origin. void drawLine(Offset p1, Offset p2, Paint paint); - - /// Fills the canvas with the given [Paint]. - /// - /// To fill the canvas with a solid color and blend mode, consider - /// [drawColor] instead. void drawPaint(Paint paint); - - /// Draws a rectangle with the given [Paint]. Whether the rectangle is filled - /// or stroked (or both) is controlled by [Paint.style]. void drawRect(Rect rect, Paint paint); - - /// Draws a rounded rectangle with the given [Paint]. Whether the rectangle is - /// filled or stroked (or both) is controlled by [Paint.style]. void drawRRect(RRect rrect, Paint paint); - - /// Draws a shape consisting of the difference between two rounded rectangles - /// with the given [Paint]. Whether this shape is filled or stroked (or both) - /// is controlled by [Paint.style]. - /// - /// This shape is almost but not quite entirely unlike an annulus. void drawDRRect(RRect outer, RRect inner, Paint paint); - - /// Draws an axis-aligned oval that fills the given axis-aligned rectangle - /// with the given [Paint]. Whether the oval is filled or stroked (or both) is - /// controlled by [Paint.style]. void drawOval(Rect rect, Paint paint); - - /// Draws a circle centered at the point given by the first argument and - /// that has the radius given by the second argument, with the [Paint] given in - /// the third argument. Whether the circle is filled or stroked (or both) is - /// controlled by [Paint.style]. void drawCircle(Offset c, double radius, Paint paint); - - /// Draw an arc scaled to fit inside the given rectangle. It starts from - /// startAngle radians around the oval up to startAngle + sweepAngle - /// radians around the oval, with zero radians being the point on - /// the right hand side of the oval that crosses the horizontal line - /// that intersects the center of the rectangle and with positive - /// angles going clockwise around the oval. If useCenter is true, the arc is - /// closed back to the center, forming a circle sector. Otherwise, the arc is - /// not closed, forming a circle segment. - /// - /// This method is optimized for drawing arcs and should be faster than [Path.arcTo]. - void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, - Paint paint); - - /// Draws the given [Path] with the given [Paint]. Whether this shape is - /// filled or stroked (or both) is controlled by [Paint.style]. If the path is - /// filled, then subpaths within it are implicitly closed (see [Path.close]). + void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint); void drawPath(Path path, Paint paint); - - /// Draws the given [Image] into the canvas with its top-left corner at the - /// given [Offset]. The image is composited into the canvas using the given [Paint]. void drawImage(Image image, Offset offset, Paint paint); - - /// Draws the subset of the given image described by the `src` argument into - /// the canvas in the axis-aligned rectangle given by the `dst` argument. - /// - /// This might sample from outside the `src` rect by up to half the width of - /// an applied filter. - /// - /// Multiple calls to this method with different arguments (from the same - /// image) can be batched into a single call to [drawAtlas] to improve - /// performance. void drawImageRect(Image image, Rect src, Rect dst, Paint paint); - - /// Draws the given [Image] into the canvas using the given [Paint]. - /// - /// The image is drawn in nine portions described by splitting the image by - /// drawing two horizontal lines and two vertical lines, where the `center` - /// argument describes the rectangle formed by the four points where these - /// four lines intersect each other. (This forms a 3-by-3 grid of regions, - /// the center region being described by the `center` argument.) - /// - /// The four regions in the corners are drawn, without scaling, in the four - /// corners of the destination rectangle described by `dst`. The remaining - /// five regions are drawn by stretching them to fit such that they exactly - /// cover the destination rectangle while maintaining their relative - /// positions. void drawImageNine(Image image, Rect center, Rect dst, Paint paint); - - /// Draw the given picture onto the canvas. To create a picture, see - /// [PictureRecorder]. void drawPicture(Picture picture); - - /// Draws the text in the given [Paragraph] into this canvas at the given - /// [Offset]. - /// - /// The [Paragraph] object must have had [Paragraph.layout] called on it - /// first. - /// - /// To align the text, set the `textAlign` on the [ParagraphStyle] object - /// passed to the [new ParagraphBuilder] constructor. For more details see - /// [TextAlign] and the discussion at [new ParagraphStyle]. - /// - /// If the text is left aligned or justified, the left margin will be at the - /// position specified by the `offset` argument's [Offset.dx] coordinate. - /// - /// If the text is right aligned or justified, the right margin will be at the - /// position described by adding the [ParagraphConstraints.width] given to - /// [Paragraph.layout], to the `offset` argument's [Offset.dx] coordinate. - /// - /// If the text is centered, the centering axis will be at the position - /// described by adding half of the [ParagraphConstraints.width] given to - /// [Paragraph.layout], to the `offset` argument's [Offset.dx] coordinate. void drawParagraph(Paragraph paragraph, Offset offset); - - /// Draws a sequence of points according to the given [PointMode]. - /// - /// The `points` argument is interpreted as offsets from the origin. - /// - /// See also: - /// - /// * [drawRawPoints], which takes `points` as a [Float32List] rather than a - /// [List]. void drawPoints(PointMode pointMode, List points, Paint paint); - - /// Draws a sequence of points according to the given [PointMode]. - /// - /// The `points` argument is interpreted as a list of pairs of floating point - /// numbers, where each pair represents an x and y offset from the origin. - /// - /// See also: - /// - /// * [drawPoints], which takes `points` as a [List] rather than a - /// [List]. void drawRawPoints(PointMode pointMode, Float32List points, Paint paint); void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint); - - /// Draws part of an image - the [atlas] - onto the canvas. - /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. - /// - /// All parameters must not be null. - /// - /// See also: - /// - /// * [drawRawAtlas], which takes its arguments as typed data lists rather - /// than objects. void drawAtlas( Image atlas, List transforms, List rects, - List colors, - BlendMode blendMode, + List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint, ); - - /// Draws part of an image - the [atlas] - onto the canvas. - /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. - /// - /// The [rstTransforms] argument is interpreted as a list of four-tuples, with - /// each tuple being ([RSTransform.scos], [RSTransform.ssin], - /// [RSTransform.tx], [RSTransform.ty]). - /// - /// The [rects] argument is interpreted as a list of four-tuples, with each - /// tuple being ([Rect.left], [Rect.top], [Rect.right], [Rect.bottom]). - /// - /// The [colors] argument, which can be null, is interpreted as a list of - /// 32-bit colors, with the same packing as [Color.value]. - /// - /// See also: - /// - /// * [drawAtlas], which takes its arguments as objects rather than typed - /// data lists. void drawRawAtlas( Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - BlendMode blendMode, + Int32List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint, ); - - /// Draws a shadow for a [Path] representing the given material elevation. - /// - /// The `transparentOccluder` argument should be true if the occluding object - /// is not opaque. - /// - /// The arguments must not be null. void drawShadow( Path path, Color color, @@ -587,116 +148,22 @@ abstract class Canvas { ); } -/// An object representing a sequence of recorded graphical operations. -/// -/// To create a [Picture], use a [PictureRecorder]. -/// -/// A [Picture] can be placed in a [Scene] using a [SceneBuilder], via -/// the [SceneBuilder.addPicture] method. A [Picture] can also be -/// drawn into a [Canvas], using the [Canvas.drawPicture] method. abstract class Picture { - /// Creates an image from this picture. - /// - /// The returned image will be `width` pixels wide and `height` pixels high. - /// The picture is rasterized within the 0 (left), 0 (top), `width` (right), - /// `height` (bottom) bounds. Content outside these bounds is clipped. - /// - /// Although the image is returned synchronously, the picture is actually - /// rasterized the first time the image is drawn and then cached. Future toImage(int width, int height); - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. void dispose(); - - /// Returns the approximate number of bytes allocated for this object. - /// - /// The actual size of this picture may be larger, particularly if it contains - /// references to image or other large objects. int get approximateBytesUsed; } -/// Determines the winding rule that decides how the interior of a [Path] is -/// calculated. -/// -/// This enum is used by the [Path.fillType] property. enum PathFillType { - /// The interior is defined by a non-zero sum of signed edge crossings. - /// - /// For a given point, the point is considered to be on the inside of the path - /// if a line drawn from the point to infinity crosses lines going clockwise - /// around the point a different number of times than it crosses lines going - /// counter-clockwise around that point. - /// - /// See: nonZero, - - /// The interior is defined by an odd number of edge crossings. - /// - /// For a given point, the point is considered to be on the inside of the path - /// if a line drawn from the point to infinity crosses an odd number of lines. - /// - /// See: evenOdd, } - -/// Strategies for combining paths. -/// -/// See also: -/// -/// * [Path.combine], which uses this enum to decide how to combine two paths. // Must be kept in sync with SkPathOp + enum PathOperation { - /// Subtract the second path from the first path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be a crescent portion of the - /// first circle that was not overlapped by the second circle. - /// - /// See also: - /// - /// * [reverseDifference], which is the same but subtracting the first path - /// from the second. difference, - - /// Create a new path that is the intersection of the two paths, leaving the - /// overlapping pieces of the path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be only the overlapping portion - /// of the two circles. - /// - /// See also: - /// * [xor], which is the inverse of this operation intersect, - - /// Create a new path that is the union (inclusive-or) of the two paths. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be a figure-eight like shape - /// matching the outer boundaries of both circles. union, - - /// Create a new path that is the exclusive-or of the two paths, leaving - /// everything but the overlapping pieces of the path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the figure-eight like shape less the overlapping - /// parts - /// - /// See also: - /// * [intersect], which is the inverse of this operation xor, - - /// Subtract the first path from the second path. - /// - /// For example, if the two paths are overlapping circles of equal diameter - /// but differing centers, the result would be a crescent portion of the - /// second circle that was not overlapped by the first circle. - /// - /// See also: - /// - /// * [difference], which is the same but subtracting the second path - /// from the first. reverseDifference, } diff --git a/lib/web_ui/lib/src/ui/channel_buffers.dart b/lib/web_ui/lib/src/ui/channel_buffers.dart index 6ce0bc962f1f4..2dc98120bd11d 100644 --- a/lib/web_ui/lib/src/ui/channel_buffers.dart +++ b/lib/web_ui/lib/src/ui/channel_buffers.dart @@ -5,50 +5,27 @@ // @dart = 2.10 part of ui; -/// A saved platform message for a channel with its callback. class _StoredMessage { - /// Default constructor, takes in a [ByteData] that represents the - /// payload of the message and a [PlatformMessageResponseCallback] - /// that represents the callback that will be called when the message - /// is handled. _StoredMessage(this._data, this._callback); - - /// Representation of the message's payload. final ByteData? _data; ByteData? get data => _data; - - /// Callback to be called when the message is received. final PlatformMessageResponseCallback _callback; PlatformMessageResponseCallback get callback => _callback; } -/// A fixed-size circular queue. class _RingBuffer { - /// The underlying data for the RingBuffer. ListQueue's dynamically resize, - /// [_RingBuffer]s do not. final collection.ListQueue _queue; - _RingBuffer(this._capacity) - : _queue = collection.ListQueue(_capacity); - - /// Returns the number of items in the [_RingBuffer]. + _RingBuffer(this._capacity) : _queue = collection.ListQueue(_capacity); int get length => _queue.length; - - /// The number of items that can be stored in the [_RingBuffer]. int _capacity; int get capacity => _capacity; - - /// Returns true if there are no items in the [_RingBuffer]. bool get isEmpty => _queue.isEmpty; - - /// A callback that get's called when items are ejected from the [_RingBuffer] - /// by way of an overflow or a resizing. Function(T)? _dropItemCallback; set dropItemCallback(Function(T) callback) { _dropItemCallback = callback; } - /// Returns true on overflow. bool push(T val) { if (_capacity <= 0) { return true; @@ -59,13 +36,10 @@ class _RingBuffer { } } - /// Returns null when empty. T? pop() { return _queue.isEmpty ? null : _queue.removeFirst(); } - /// Removes items until then length reaches [lengthLimit] and returns - /// the number of items removed. int _dropOverflowItems(int lengthLimit) { int result = 0; while (_queue.length > lengthLimit) { @@ -76,46 +50,20 @@ class _RingBuffer { return result; } - /// Returns the number of discarded items resulting from resize. int resize(int newSize) { _capacity = newSize; return _dropOverflowItems(newSize); } } -/// Signature for [ChannelBuffers.drain]. typedef DrainChannelCallback = Future Function(ByteData?, PlatformMessageResponseCallback); -/// Storage of channel messages until the channels are completely routed, -/// i.e. when a message handler is attached to the channel on the framework side. -/// -/// Each channel has a finite buffer capacity and in a FIFO manner messages will -/// be deleted if the capacity is exceeded. The intention is that these buffers -/// will be drained once a callback is setup on the BinaryMessenger in the -/// Flutter framework. -/// -/// Clients of Flutter shouldn't need to allocate their own ChannelBuffers -/// and should only access this package's [channelBuffers] if they are writing -/// their own custom [BinaryMessenger]. class ChannelBuffers { - /// By default we store one message per channel. There are tradeoffs associated - /// with any size. The correct size should be chosen for the semantics of your - /// channel. - /// - /// Size 0 implies you want to ignore any message that gets sent before the engine - /// is ready (keeping in mind there is no way to know when the engine is ready). - /// - /// Size 1 implies that you only care about the most recent value. - /// - /// Size >1 means you want to process every single message and want to chose a - /// buffer size that will avoid any overflows. static const int kDefaultBufferSize = 1; static const String kControlChannelName = 'dev.flutter/channel-buffers'; - - /// A mapping between a channel name and its associated [_RingBuffer]. final Map?> _messages = - ?>{}; + ?>{}; _RingBuffer<_StoredMessage> _makeRingBuffer(int size) { final _RingBuffer<_StoredMessage> result = _RingBuffer<_StoredMessage>(size); @@ -127,7 +75,6 @@ class ChannelBuffers { message.callback(null); } - /// Returns true on overflow. bool push(String channel, ByteData? data, PlatformMessageResponseCallback callback) { _RingBuffer<_StoredMessage>? queue = _messages[channel]; if (queue == null) { @@ -147,7 +94,6 @@ class ChannelBuffers { return didOverflow; } - /// Returns null on underflow. _StoredMessage? _pop(String channel) { final _RingBuffer<_StoredMessage>? queue = _messages[channel]; final _StoredMessage? result = queue?.pop(); @@ -159,10 +105,6 @@ class ChannelBuffers { return (queue == null) ? true : queue.isEmpty; } - /// Changes the capacity of the queue associated with the given channel. - /// - /// This could result in the dropping of messages if newSize is less - /// than the current length of the queue. void _resize(String channel, int newSize) { _RingBuffer<_StoredMessage>? queue = _messages[channel]; if (queue == null) { @@ -176,10 +118,6 @@ class ChannelBuffers { } } - /// Remove and process all stored messages for a given channel. - /// - /// This should be called once a channel is prepared to handle messages - /// (i.e. when a message handler is setup in the framework). Future drain(String channel, DrainChannelCallback callback) async { while (!_isEmpty(channel)) { final _StoredMessage message = _pop(channel)!; @@ -193,15 +131,6 @@ class ChannelBuffers { return utf8.decode(list); } - /// Handle a control message. - /// - /// This is intended to be called by the platform messages dispatcher. - /// - /// Available messages: - /// - Name: resize - /// Arity: 2 - /// Format: `resize\r\r` - /// Description: Allows you to set the size of a channel's buffer. void handleMessage(ByteData data) { final List command = _getString(data).split('\r'); if (command.length == /*arity=*/2 + 1 && command[0] == 'resize') { @@ -212,10 +141,4 @@ class ChannelBuffers { } } -/// [ChannelBuffer]s that allow the storage of messages between the -/// Engine and the Framework. Typically messages that can't be delivered -/// are stored here until the Framework is able to process them. -/// -/// See also: -/// * [BinaryMessenger] - The place where ChannelBuffers are typically read. final ChannelBuffers channelBuffers = ChannelBuffers(); diff --git a/lib/web_ui/lib/src/ui/compositing.dart b/lib/web_ui/lib/src/ui/compositing.dart index 11a0e1199d63a..665ebbe14c68d 100644 --- a/lib/web_ui/lib/src/ui/compositing.dart +++ b/lib/web_ui/lib/src/ui/compositing.dart @@ -5,113 +5,34 @@ // @dart = 2.10 part of ui; -/// An opaque object representing a composited scene. -/// -/// To create a Scene object, use a [SceneBuilder]. -/// -/// Scene objects can be displayed on the screen using the -/// [Window.render] method. abstract class Scene { - /// Creates a raster image representation of the current state of the scene. - /// This is a slow operation that is performed on a background thread. Future toImage(int width, int height); - - /// Releases the resources used by this scene. - /// - /// After calling this function, the scene is cannot be used further. void dispose(); } -/// An opaque handle to a transform engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushTransform]. -/// -/// {@template dart.ui.sceneBuilder.oldLayerCompatibility} -/// `oldLayer` parameter in [SceneBuilder] methods only accepts objects created -/// by the engine. [SceneBuilder] will throw an [AssertionError] if you pass it -/// a custom implementation of this class. -/// {@endtemplate} abstract class TransformEngineLayer implements EngineLayer {} -/// An opaque handle to an offset engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushOffset]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class OffsetEngineLayer implements EngineLayer {} -/// An opaque handle to a clip rect engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushClipRect]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ClipRectEngineLayer implements EngineLayer {} -/// An opaque handle to a clip rounded rect engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushClipRRect]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ClipRRectEngineLayer implements EngineLayer {} -/// An opaque handle to a clip path engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushClipPath]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ClipPathEngineLayer implements EngineLayer {} -/// An opaque handle to an opacity engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushOpacity]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class OpacityEngineLayer implements EngineLayer {} -/// An opaque handle to a color filter engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushColorFilter]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ColorFilterEngineLayer implements EngineLayer {} -/// An opaque handle to an image filter engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushImageFilter]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ImageFilterEngineLayer implements EngineLayer {} -/// An opaque handle to a backdrop filter engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushBackdropFilter]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class BackdropFilterEngineLayer implements EngineLayer {} -/// An opaque handle to a shader mask engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushShaderMask]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class ShaderMaskEngineLayer implements EngineLayer {} -/// An opaque handle to a physical shape engine layer. -/// -/// Instances of this class are created by [SceneBuilder.pushPhysicalShape]. -/// -/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} abstract class PhysicalShapeEngineLayer implements EngineLayer {} -/// Builds a [Scene] containing the given visuals. -/// -/// A [Scene] can then be rendered using [Window.render]. -/// -/// To draw graphical operations onto a [Scene], first create a -/// [Picture] using a [PictureRecorder] and a [Canvas], and then add -/// it to the scene using [addPicture]. abstract class SceneBuilder { - /// Creates an empty [SceneBuilder] object. factory SceneBuilder() { if (engine.experimentalUseSkia) { return engine.LayerSceneBuilder(); @@ -119,142 +40,53 @@ abstract class SceneBuilder { return engine.SurfaceSceneBuilder(); } } - - /// Pushes an offset operation onto the operation stack. - /// - /// This is equivalent to [pushTransform] with a matrix with only translation. - /// - /// See [pop] for details about the operation stack. OffsetEngineLayer? pushOffset( double dx, double dy, { OffsetEngineLayer? oldLayer, }); - - /// Pushes a transform operation onto the operation stack. - /// - /// The objects are transformed by the given matrix before rasterization. - /// - /// See [pop] for details about the operation stack. TransformEngineLayer? pushTransform( Float64List matrix4, { TransformEngineLayer? oldLayer, }); - - /// Pushes a rectangular clip operation onto the operation stack. - /// - /// Rasterization outside the given rectangle is discarded. - /// - /// See [pop] for details about the operation stack, and [Clip] for different clip modes. - /// By default, the clip will be anti-aliased (clip = [Clip.antiAlias]). ClipRectEngineLayer? pushClipRect( Rect rect, { Clip clipBehavior = Clip.antiAlias, ClipRectEngineLayer? oldLayer, }); - - /// Pushes a rounded-rectangular clip operation onto the operation stack. - /// - /// Rasterization outside the given rounded rectangle is discarded. - /// - /// See [pop] for details about the operation stack. ClipRRectEngineLayer? pushClipRRect( RRect rrect, { required Clip clipBehavior, ClipRRectEngineLayer? oldLayer, }); - - /// Pushes a path clip operation onto the operation stack. - /// - /// Rasterization outside the given path is discarded. - /// - /// See [pop] for details about the operation stack. ClipPathEngineLayer? pushClipPath( Path path, { Clip clipBehavior = Clip.antiAlias, ClipPathEngineLayer? oldLayer, }); - - /// Pushes an opacity operation onto the operation stack. - /// - /// The given alpha value is blended into the alpha value of the objects' - /// rasterization. An alpha value of 0 makes the objects entirely invisible. - /// An alpha value of 255 has no effect (i.e., the objects retain the current - /// opacity). - /// - /// See [pop] for details about the operation stack. OpacityEngineLayer? pushOpacity( int alpha, { Offset offset = Offset.zero, OpacityEngineLayer? oldLayer, }); - - /// Pushes a color filter operation onto the operation stack. - /// - /// The given color is applied to the objects' rasterization using the given - /// blend mode. - /// - /// {@macro dart.ui.sceneBuilder.oldLayer} - /// - /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} - /// - /// See [pop] for details about the operation stack. ColorFilterEngineLayer? pushColorFilter( ColorFilter filter, { ColorFilterEngineLayer? oldLayer, }); - - /// Pushes an image filter operation onto the operation stack. - /// - /// The given filter is applied to the children's rasterization before compositing them into - /// the scene. - /// - /// {@macro dart.ui.sceneBuilder.oldLayer} - /// - /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} - /// - /// See [pop] for details about the operation stack. ImageFilterEngineLayer? pushImageFilter( ImageFilter filter, { ImageFilterEngineLayer? oldLayer, }); - - /// Pushes a backdrop filter operation onto the operation stack. - /// - /// The given filter is applied to the current contents of the scene prior to - /// rasterizing the given objects. - /// - /// See [pop] for details about the operation stack. BackdropFilterEngineLayer? pushBackdropFilter( ImageFilter filter, { BackdropFilterEngineLayer? oldLayer, }); - - /// Pushes a shader mask operation onto the operation stack. - /// - /// The given shader is applied to the object's rasterization in the given - /// rectangle using the given blend mode. - /// - /// See [pop] for details about the operation stack. ShaderMaskEngineLayer? pushShaderMask( Shader shader, Rect maskRect, BlendMode blendMode, { ShaderMaskEngineLayer? oldLayer, }); - - /// Pushes a physical layer operation for an arbitrary shape onto the - /// operation stack. - /// - /// By default, the layer's content will not be clipped (clip = [Clip.none]). - /// If clip equals [Clip.hardEdge], [Clip.antiAlias], or [Clip.antiAliasWithSaveLayer], - /// then the content is clipped to the given shape defined by [path]. - /// - /// If [elevation] is greater than 0.0, then a shadow is drawn around the layer. - /// [shadowColor] defines the color of the shadow if present and [color] defines the - /// color of the layer background. - /// - /// See [pop] for details about the operation stack, and [Clip] for different clip modes. PhysicalShapeEngineLayer? pushPhysicalShape({ required Path path, required double elevation, @@ -263,64 +95,15 @@ abstract class SceneBuilder { Clip clipBehavior = Clip.none, PhysicalShapeEngineLayer? oldLayer, }); - - /// Add a retained engine layer subtree from previous frames. - /// - /// All the engine layers that are in the subtree of the retained layer will - /// be automatically appended to the current engine layer tree. - /// - /// Therefore, when implementing a subclass of the [Layer] concept defined in - /// the rendering layer of Flutter's framework, once this is called, there's - /// no need to call [addToScene] for its children layers. void addRetained(EngineLayer retainedLayer); - - /// Ends the effect of the most recently pushed operation. - /// - /// Internally the scene builder maintains a stack of operations. Each of the - /// operations in the stack applies to each of the objects added to the scene. - /// Calling this function removes the most recently added operation from the - /// stack. void pop(); - - /// Adds an object to the scene that displays performance statistics. - /// - /// Useful during development to assess the performance of the application. - /// The enabledOptions controls which statistics are displayed. The bounds - /// controls where the statistics are displayed. - /// - /// enabledOptions is a bit field with the following bits defined: - /// - 0x01: displayRasterizerStatistics - show raster thread frame time - /// - 0x02: visualizeRasterizerStatistics - graph raster thread frame times - /// - 0x04: displayEngineStatistics - show UI thread frame time - /// - 0x08: visualizeEngineStatistics - graph UI thread frame times - /// Set enabledOptions to 0x0F to enable all the currently defined features. - /// - /// The "UI thread" is the thread that includes all the execution of - /// the main Dart isolate (the isolate that can call - /// [Window.render]). The UI thread frame time is the total time - /// spent executing the [Window.onBeginFrame] callback. The "raster - /// thread" is the thread (running on the CPU) that subsequently - /// processes the [Scene] provided by the Dart code to turn it into - /// GPU commands and send it to the GPU. - /// - /// See also the [PerformanceOverlayOption] enum in the rendering library. - /// for more details. void addPerformanceOverlay(int enabledOptions, Rect bounds); - - /// Adds a [Picture] to the scene. - /// - /// The picture is rasterized at the given offset. void addPicture( Offset offset, Picture picture, { bool isComplexHint = false, bool willChangeHint = false, }); - - /// Adds a backend texture to the scene. - /// - /// The texture is scaled to the given size and rasterized at the given - /// offset. void addTexture( int textureId, { Offset offset = Offset.zero, @@ -329,32 +112,12 @@ abstract class SceneBuilder { bool freeze = false, FilterQuality filterQuality = FilterQuality.low, }); - - /// Adds a platform view (e.g an iOS UIView) to the scene. - /// - /// Only supported on iOS, this is currently a no-op on other platforms. - /// - /// On iOS this layer splits the current output surface into two surfaces, one for the scene nodes - /// preceding the platform view, and one for the scene nodes following the platform view. - /// - /// ## Performance impact - /// - /// Adding an additional surface doubles the amount of graphics memory directly used by Flutter - /// for output buffers. Quartz might allocated extra buffers for compositing the Flutter surfaces - /// and the platform view. - /// - /// With a platform view in the scene, Quartz has to composite the two Flutter surfaces and the - /// embedded UIView. In addition to that, on iOS versions greater than 9, the Flutter frames are - /// synchronized with the UIView frames adding additional performance overhead. void addPlatformView( int viewId, { Offset offset = Offset.zero, double width = 0.0, double height = 0.0, }); - - /// (Fuchsia-only) Adds a scene rendered by another application to the scene - /// for this application. void addChildScene({ Offset offset = Offset.zero, double width = 0.0, @@ -362,52 +125,10 @@ abstract class SceneBuilder { required SceneHost sceneHost, bool hitTestable = true, }); - - /// Sets a threshold after which additional debugging information should be - /// recorded. - /// - /// Currently this interface is difficult to use by end-developers. If you're - /// interested in using this feature, please contact [flutter-dev](https://groups.google.com/forum/#!forum/flutter-dev). - /// We'll hopefully be able to figure out how to make this feature more useful - /// to you. void setRasterizerTracingThreshold(int frameInterval); - - /// Sets whether the raster cache should checkerboard cached entries. This is - /// only useful for debugging purposes. - /// - /// The compositor can sometimes decide to cache certain portions of the - /// widget hierarchy. Such portions typically don't change often from frame to - /// frame and are expensive to render. This can speed up overall rendering. - /// However, there is certain upfront cost to constructing these cache - /// entries. And, if the cache entries are not used very often, this cost may - /// not be worth the speedup in rendering of subsequent frames. If the - /// developer wants to be certain that populating the raster cache is not - /// causing stutters, this option can be set. Depending on the observations - /// made, hints can be provided to the compositor that aid it in making better - /// decisions about caching. - /// - /// Currently this interface is difficult to use by end-developers. If you're - /// interested in using this feature, please contact [flutter-dev](https://groups.google.com/forum/#!forum/flutter-dev). void setCheckerboardRasterCacheImages(bool checkerboard); - - /// Sets whether the compositor should checkerboard layers that are rendered - /// to offscreen bitmaps. - /// - /// This is only useful for debugging purposes. void setCheckerboardOffscreenLayers(bool checkerboard); - - /// Finishes building the scene. - /// - /// Returns a [Scene] containing the objects that have been added to - /// this scene builder. The [Scene] can then be displayed on the - /// screen with [Window.render]. - /// - /// After calling this function, the scene builder object is invalid and - /// cannot be used further. Scene build(); - - /// Set properties on the linked scene. These properties include its bounds, - /// as well as whether it can be the target of focus events or not. void setProperties( double width, double height, @@ -419,36 +140,16 @@ abstract class SceneBuilder { ); } -/// A handle for the framework to hold and retain an engine layer across frames. class EngineLayer {} -//// (Fuchsia-only) Hosts content provided by another application. class SceneHost { - /// Creates a host for a child scene's content. - /// - /// The ViewHolder token is bound to a ViewHolder scene graph node which acts - /// as a container for the child's content. The creator of the SceneHost is - /// responsible for sending the corresponding ViewToken to the child. - /// - /// The ViewHolder token is a dart:zircon Handle, but that type isn't - /// available here. This is called by ChildViewConnection in - /// //topaz/public/dart/fuchsia_scenic_flutter/. - /// - /// The SceneHost takes ownership of the provided ViewHolder token. SceneHost( dynamic viewHolderToken, void Function() viewConnectedCallback, void Function() viewDisconnectedCallback, void Function(bool) viewStateChangedCallback, ); - - /// Releases the resources associated with the SceneHost. - /// - /// After calling this function, the SceneHost cannot be used further. void dispose() {} - - /// Set properties on the linked scene. These properties include its bounds, - /// as well as whether it can be the target of focus events or not. void setProperties( double width, double height, diff --git a/lib/web_ui/lib/src/ui/geometry.dart b/lib/web_ui/lib/src/ui/geometry.dart index c528c8c73db93..0ec3c06550a20 100644 --- a/lib/web_ui/lib/src/ui/geometry.dart +++ b/lib/web_ui/lib/src/ui/geometry.dart @@ -5,83 +5,19 @@ // @dart = 2.10 part of ui; -/// Base class for [Size] and [Offset], which are both ways to describe -/// a distance as a two-dimensional axis-aligned vector. abstract class OffsetBase { - /// Abstract const constructor. This constructor enables subclasses to provide - /// const constructors so that they can be used in const expressions. - /// - /// The first argument sets the horizontal component, and the second the - /// vertical component. const OffsetBase(this._dx, this._dy) : assert(_dx != null), // ignore: unnecessary_null_comparison assert(_dy != null); // ignore: unnecessary_null_comparison final double _dx; final double _dy; - - /// Returns true if either component is [double.infinity], and false if both - /// are finite (or negative infinity, or NaN). - /// - /// This is different than comparing for equality with an instance that has - /// _both_ components set to [double.infinity]. - /// - /// See also: - /// - /// * [isFinite], which is true if both components are finite (and not NaN). bool get isInfinite => _dx >= double.infinity || _dy >= double.infinity; - - /// Whether both components are finite (neither infinite nor NaN). - /// - /// See also: - /// - /// * [isInfinite], which returns true if either component is equal to - /// positive infinity. bool get isFinite => _dx.isFinite && _dy.isFinite; - - /// Less-than operator. Compares an [Offset] or [Size] to another [Offset] or - /// [Size], and returns true if both the horizontal and vertical values of the - /// left-hand-side operand are smaller than the horizontal and vertical values - /// of the right-hand-side operand respectively. Returns false otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator <(OffsetBase other) => _dx < other._dx && _dy < other._dy; - - /// Less-than-or-equal-to operator. Compares an [Offset] or [Size] to another - /// [Offset] or [Size], and returns true if both the horizontal and vertical - /// values of the left-hand-side operand are smaller than or equal to the - /// horizontal and vertical values of the right-hand-side operand - /// respectively. Returns false otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator <=(OffsetBase other) => _dx <= other._dx && _dy <= other._dy; - - /// Greater-than operator. Compares an [Offset] or [Size] to another [Offset] - /// or [Size], and returns true if both the horizontal and vertical values of - /// the left-hand-side operand are bigger than the horizontal and vertical - /// values of the right-hand-side operand respectively. Returns false - /// otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator >(OffsetBase other) => _dx > other._dx && _dy > other._dy; - - /// Greater-than-or-equal-to operator. Compares an [Offset] or [Size] to - /// another [Offset] or [Size], and returns true if both the horizontal and - /// vertical values of the left-hand-side operand are bigger than or equal to - /// the horizontal and vertical values of the right-hand-side operand - /// respectively. Returns false otherwise. - /// - /// This is a partial ordering. It is possible for two values to be neither - /// less, nor greater than, nor equal to, another. bool operator >=(OffsetBase other) => _dx >= other._dx && _dy >= other._dy; - - /// Equality operator. Compares an [Offset] or [Size] to another [Offset] or - /// [Size], and returns true if the horizontal and vertical values of the - /// left-hand-side operand are equal to the horizontal and vertical values of - /// the right-hand-side operand respectively. Returns false otherwise. @override bool operator ==(Object other) { return other is OffsetBase @@ -96,224 +32,29 @@ abstract class OffsetBase { String toString() => 'OffsetBase(${_dx.toStringAsFixed(1)}, ${_dy.toStringAsFixed(1)})'; } -/// An immutable 2D floating-point offset. -/// -/// Generally speaking, Offsets can be interpreted in two ways: -/// -/// 1. As representing a point in Cartesian space a specified distance from a -/// separately-maintained origin. For example, the top-left position of -/// children in the [RenderBox] protocol is typically represented as an -/// [Offset] from the top left of the parent box. -/// -/// 2. As a vector that can be applied to coordinates. For example, when -/// painting a [RenderObject], the parent is passed an [Offset] from the -/// screen's origin which it can add to the offsets of its children to find -/// the [Offset] from the screen's origin to each of the children. -/// -/// Because a particular [Offset] can be interpreted as one sense at one time -/// then as the other sense at a later time, the same class is used for both -/// senses. -/// -/// See also: -/// -/// * [Size], which represents a vector describing the size of a rectangle. class Offset extends OffsetBase { - /// Creates an offset. The first argument sets [dx], the horizontal component, - /// and the second sets [dy], the vertical component. const Offset(double dx, double dy) : super(dx, dy); - - /// Creates an offset from its [direction] and [distance]. - /// - /// The direction is in radians clockwise from the positive x-axis. - /// - /// The distance can be omitted, to create a unit vector (distance = 1.0). factory Offset.fromDirection(double direction, [ double distance = 1.0 ]) { return Offset(distance * math.cos(direction), distance * math.sin(direction)); } - - /// The x component of the offset. - /// - /// The y component is given by [dy]. double get dx => _dx; - - /// The y component of the offset. - /// - /// The x component is given by [dx]. double get dy => _dy; - - /// The magnitude of the offset. - /// - /// If you need this value to compare it to another [Offset]'s distance, - /// consider using [distanceSquared] instead, since it is cheaper to compute. double get distance => math.sqrt(dx * dx + dy * dy); - - /// The square of the magnitude of the offset. - /// - /// This is cheaper than computing the [distance] itself. double get distanceSquared => dx * dx + dy * dy; - - /// The angle of this offset as radians clockwise from the positive x-axis, in - /// the range -[pi] to [pi], assuming positive values of the x-axis go to the - /// left and positive values of the y-axis go down. - /// - /// Zero means that [dy] is zero and [dx] is zero or positive. - /// - /// Values from zero to [pi]/2 indicate positive values of [dx] and [dy], the - /// bottom-right quadrant. - /// - /// Values from [pi]/2 to [pi] indicate negative values of [dx] and positive - /// values of [dy], the bottom-left quadrant. - /// - /// Values from zero to -[pi]/2 indicate positive values of [dx] and negative - /// values of [dy], the top-right quadrant. - /// - /// Values from -[pi]/2 to -[pi] indicate negative values of [dx] and [dy], - /// the top-left quadrant. - /// - /// When [dy] is zero and [dx] is negative, the [direction] is [pi]. - /// - /// When [dx] is zero, [direction] is [pi]/2 if [dy] is positive and -[pi]/2 - /// if [dy] is negative. - /// - /// See also: - /// - /// * [distance], to compute the magnitude of the vector. - /// * [Canvas.rotate], which uses the same convention for its angle. double get direction => math.atan2(dy, dx); - - /// An offset with zero magnitude. - /// - /// This can be used to represent the origin of a coordinate space. static const Offset zero = Offset(0.0, 0.0); - - /// An offset with infinite x and y components. - /// - /// See also: - /// - /// * [isInfinite], which checks whether either component is infinite. - /// * [isFinite], which checks whether both components are finite. // This is included for completeness, because [Size.infinite] exists. static const Offset infinite = Offset(double.infinity, double.infinity); - - /// Returns a new offset with the x component scaled by `scaleX` and the y - /// component scaled by `scaleY`. - /// - /// If the two scale arguments are the same, consider using the `*` operator - /// instead: - /// - /// ```dart - /// Offset a = const Offset(10.0, 10.0); - /// Offset b = a * 2.0; // same as: a.scale(2.0, 2.0) - /// ``` - /// - /// If the two arguments are -1, consider using the unary `-` operator - /// instead: - /// - /// ```dart - /// Offset a = const Offset(10.0, 10.0); - /// Offset b = -a; // same as: a.scale(-1.0, -1.0) - /// ``` Offset scale(double scaleX, double scaleY) => Offset(dx * scaleX, dy * scaleY); - - /// Returns a new offset with translateX added to the x component and - /// translateY added to the y component. - /// - /// If the arguments come from another [Offset], consider using the `+` or `-` - /// operators instead: - /// - /// ```dart - /// Offset a = const Offset(10.0, 10.0); - /// Offset b = const Offset(10.0, 10.0); - /// Offset c = a + b; // same as: a.translate(b.dx, b.dy) - /// Offset d = a - b; // same as: a.translate(-b.dx, -b.dy) - /// ``` Offset translate(double translateX, double translateY) => Offset(dx + translateX, dy + translateY); - - /// Unary negation operator. - /// - /// Returns an offset with the coordinates negated. - /// - /// If the [Offset] represents an arrow on a plane, this operator returns the - /// same arrow but pointing in the reverse direction. Offset operator -() => Offset(-dx, -dy); - - /// Binary subtraction operator. - /// - /// Returns an offset whose [dx] value is the left-hand-side operand's [dx] - /// minus the right-hand-side operand's [dx] and whose [dy] value is the - /// left-hand-side operand's [dy] minus the right-hand-side operand's [dy]. - /// - /// See also [translate]. Offset operator -(Offset other) => Offset(dx - other.dx, dy - other.dy); - - /// Binary addition operator. - /// - /// Returns an offset whose [dx] value is the sum of the [dx] values of the - /// two operands, and whose [dy] value is the sum of the [dy] values of the - /// two operands. - /// - /// See also [translate]. Offset operator +(Offset other) => Offset(dx + other.dx, dy + other.dy); - - /// Multiplication operator. - /// - /// Returns an offset whose coordinates are the coordinates of the - /// left-hand-side operand (an Offset) multiplied by the scalar - /// right-hand-side operand (a double). - /// - /// See also [scale]. Offset operator *(double operand) => Offset(dx * operand, dy * operand); - - /// Division operator. - /// - /// Returns an offset whose coordinates are the coordinates of the - /// left-hand-side operand (an Offset) divided by the scalar right-hand-side - /// operand (a double). - /// - /// See also [scale]. Offset operator /(double operand) => Offset(dx / operand, dy / operand); - - /// Integer (truncating) division operator. - /// - /// Returns an offset whose coordinates are the coordinates of the - /// left-hand-side operand (an Offset) divided by the scalar right-hand-side - /// operand (a double), rounded towards zero. Offset operator ~/(double operand) => Offset((dx ~/ operand).toDouble(), (dy ~/ operand).toDouble()); - - /// Modulo (remainder) operator. - /// - /// Returns an offset whose coordinates are the remainder of dividing the - /// coordinates of the left-hand-side operand (an Offset) by the scalar - /// right-hand-side operand (a double). Offset operator %(double operand) => Offset(dx % operand, dy % operand); - - /// Rectangle constructor operator. - /// - /// Combines an [Offset] and a [Size] to form a [Rect] whose top-left - /// coordinate is the point given by adding this offset, the left-hand-side - /// operand, to the origin, and whose size is the right-hand-side operand. - /// - /// ```dart - /// Rect myRect = Offset.zero & const Size(100.0, 100.0); - /// // same as: Rect.fromLTWH(0.0, 0.0, 100.0, 100.0) - /// ``` Rect operator &(Size other) => Rect.fromLTWH(dx, dy, other.width, other.height); - - /// Linearly interpolate between two offsets. - /// - /// If either offset is null, this function interpolates from [Offset.zero]. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Offset? lerp(Offset? a, Offset? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -331,7 +72,6 @@ class Offset extends OffsetBase { } } - /// Compares two Offsets for equality. @override bool operator ==(Object other) { return other is Offset @@ -346,61 +86,16 @@ class Offset extends OffsetBase { String toString() => 'Offset(${dx.toStringAsFixed(1)}, ${dy.toStringAsFixed(1)})'; } -/// Holds a 2D floating-point size. -/// -/// You can think of this as an [Offset] from the origin. class Size extends OffsetBase { - /// Creates a [Size] with the given [width] and [height]. const Size(double width, double height) : super(width, height); - - /// Creates an instance of [Size] that has the same values as another. // Used by the rendering library's _DebugSize hack. Size.copy(Size source) : super(source.width, source.height); - - /// Creates a square [Size] whose [width] and [height] are the given dimension. - /// - /// See also: - /// - /// * [Size.fromRadius], which is more convenient when the available size - /// is the radius of a circle. const Size.square(double dimension) : super(dimension, dimension); - - /// Creates a [Size] with the given [width] and an infinite [height]. const Size.fromWidth(double width) : super(width, double.infinity); - - /// Creates a [Size] with the given [height] and an infinite [width]. const Size.fromHeight(double height) : super(double.infinity, height); - - /// Creates a square [Size] whose [width] and [height] are twice the given - /// dimension. - /// - /// This is a square that contains a circle with the given radius. - /// - /// See also: - /// - /// * [Size.square], which creates a square with the given dimension. const Size.fromRadius(double radius) : super(radius * 2.0, radius * 2.0); - - /// The horizontal extent of this size. double get width => _dx; - - /// The vertical extent of this size. double get height => _dy; - - /// The aspect ratio of this size. - /// - /// This returns the [width] divided by the [height]. - /// - /// If the [width] is zero, the result will be zero. If the [height] is zero - /// (and the [width] is not), the result will be [double.infinity] or - /// [double.negativeInfinity] as determined by the sign of [width]. - /// - /// See also: - /// - /// * [AspectRatio], a widget for giving a child widget a specific aspect - /// ratio. - /// * [FittedBox], a widget that (in most modes) attempts to maintain a - /// child widget's aspect ratio while changing its size. double get aspectRatio { if (height != 0.0) return width / height; @@ -411,38 +106,9 @@ class Size extends OffsetBase { return 0.0; } - /// An empty size, one with a zero width and a zero height. static const Size zero = Size(0.0, 0.0); - - /// A size whose [width] and [height] are infinite. - /// - /// See also: - /// - /// * [isInfinite], which checks whether either dimension is infinite. - /// * [isFinite], which checks whether both dimensions are finite. static const Size infinite = Size(double.infinity, double.infinity); - - /// Whether this size encloses a non-zero area. - /// - /// Negative areas are considered empty. bool get isEmpty => width <= 0.0 || height <= 0.0; - - /// Binary subtraction operator for [Size]. - /// - /// Subtracting a [Size] from a [Size] returns the [Offset] that describes how - /// much bigger the left-hand-side operand is than the right-hand-side - /// operand. Adding that resulting [Offset] to the [Size] that was the - /// right-hand-side operand would return a [Size] equal to the [Size] that was - /// the left-hand-side operand. (i.e. if `sizeA - sizeB -> offsetA`, then - /// `offsetA + sizeB -> sizeA`) - /// - /// Subtracting an [Offset] from a [Size] returns the [Size] that is smaller than - /// the [Size] operand by the difference given by the [Offset] operand. In other - /// words, the returned [Size] has a [width] consisting of the [width] of the - /// left-hand-side operand minus the [Offset.dx] dimension of the - /// right-hand-side operand, and a [height] consisting of the [height] of the - /// left-hand-side operand minus the [Offset.dy] dimension of the - /// right-hand-side operand. OffsetBase operator -(OffsetBase other) { if (other is Size) return Offset(width - other.width, height - other.height); @@ -451,140 +117,30 @@ class Size extends OffsetBase { throw ArgumentError(other); } - /// Binary addition operator for adding an [Offset] to a [Size]. - /// - /// Returns a [Size] whose [width] is the sum of the [width] of the - /// left-hand-side operand, a [Size], and the [Offset.dx] dimension of the - /// right-hand-side operand, an [Offset], and whose [height] is the sum of the - /// [height] of the left-hand-side operand and the [Offset.dy] dimension of - /// the right-hand-side operand. Size operator +(Offset other) => Size(width + other.dx, height + other.dy); - - /// Multiplication operator. - /// - /// Returns a [Size] whose dimensions are the dimensions of the left-hand-side - /// operand (a [Size]) multiplied by the scalar right-hand-side operand (a - /// [double]). Size operator *(double operand) => Size(width * operand, height * operand); - - /// Division operator. - /// - /// Returns a [Size] whose dimensions are the dimensions of the left-hand-side - /// operand (a [Size]) divided by the scalar right-hand-side operand (a - /// [double]). Size operator /(double operand) => Size(width / operand, height / operand); - - /// Integer (truncating) division operator. - /// - /// Returns a [Size] whose dimensions are the dimensions of the left-hand-side - /// operand (a [Size]) divided by the scalar right-hand-side operand (a - /// [double]), rounded towards zero. Size operator ~/(double operand) => Size((width ~/ operand).toDouble(), (height ~/ operand).toDouble()); - - /// Modulo (remainder) operator. - /// - /// Returns a [Size] whose dimensions are the remainder of dividing the - /// left-hand-side operand (a [Size]) by the scalar right-hand-side operand (a - /// [double]). Size operator %(double operand) => Size(width % operand, height % operand); - - /// The lesser of the magnitudes of the [width] and the [height]. double get shortestSide => math.min(width.abs(), height.abs()); - - /// The greater of the magnitudes of the [width] and the [height]. double get longestSide => math.max(width.abs(), height.abs()); // Convenience methods that do the equivalent of calling the similarly named // methods on a Rect constructed from the given origin and this size. - - /// The offset to the intersection of the top and left edges of the rectangle - /// described by the given [Offset] (which is interpreted as the top-left corner) - /// and this [Size]. - /// - /// See also [Rect.topLeft]. Offset topLeft(Offset origin) => origin; - - /// The offset to the center of the top edge of the rectangle described by the - /// given offset (which is interpreted as the top-left corner) and this size. - /// - /// See also [Rect.topCenter]. Offset topCenter(Offset origin) => Offset(origin.dx + width / 2.0, origin.dy); - - /// The offset to the intersection of the top and right edges of the rectangle - /// described by the given offset (which is interpreted as the top-left corner) - /// and this size. - /// - /// See also [Rect.topRight]. Offset topRight(Offset origin) => Offset(origin.dx + width, origin.dy); - - /// The offset to the center of the left edge of the rectangle described by the - /// given offset (which is interpreted as the top-left corner) and this size. - /// - /// See also [Rect.centerLeft]. Offset centerLeft(Offset origin) => Offset(origin.dx, origin.dy + height / 2.0); - - /// The offset to the point halfway between the left and right and the top and - /// bottom edges of the rectangle described by the given offset (which is - /// interpreted as the top-left corner) and this size. - /// - /// See also [Rect.center]. Offset center(Offset origin) => Offset(origin.dx + width / 2.0, origin.dy + height / 2.0); - - /// The offset to the center of the right edge of the rectangle described by the - /// given offset (which is interpreted as the top-left corner) and this size. - /// - /// See also [Rect.centerLeft]. Offset centerRight(Offset origin) => Offset(origin.dx + width, origin.dy + height / 2.0); - - /// The offset to the intersection of the bottom and left edges of the - /// rectangle described by the given offset (which is interpreted as the - /// top-left corner) and this size. - /// - /// See also [Rect.bottomLeft]. Offset bottomLeft(Offset origin) => Offset(origin.dx, origin.dy + height); - - /// The offset to the center of the bottom edge of the rectangle described by - /// the given offset (which is interpreted as the top-left corner) and this - /// size. - /// - /// See also [Rect.bottomLeft]. Offset bottomCenter(Offset origin) => Offset(origin.dx + width / 2.0, origin.dy + height); - - /// The offset to the intersection of the bottom and right edges of the - /// rectangle described by the given offset (which is interpreted as the - /// top-left corner) and this size. - /// - /// See also [Rect.bottomRight]. Offset bottomRight(Offset origin) => Offset(origin.dx + width, origin.dy + height); - - /// Whether the point specified by the given offset (which is assumed to be - /// relative to the top left of the size) lies between the left and right and - /// the top and bottom edges of a rectangle of this size. - /// - /// Rectangles include their top and left edges but exclude their bottom and - /// right edges. bool contains(Offset offset) { return offset.dx >= 0.0 && offset.dx < width && offset.dy >= 0.0 && offset.dy < height; } - /// A [Size] with the [width] and [height] swapped. Size get flipped => Size(height, width); - - /// Linearly interpolate between two sizes - /// - /// If either size is null, this function interpolates from [Size.zero]. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Size? lerp(Size? a, Size? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -602,7 +158,6 @@ class Size extends OffsetBase { } } - /// Compares two Sizes for equality. // We don't compare the runtimeType because of _DebugSize in the framework. @override bool operator ==(Object other) { @@ -618,95 +173,51 @@ class Size extends OffsetBase { String toString() => 'Size(${width.toStringAsFixed(1)}, ${height.toStringAsFixed(1)})'; } -/// An immutable, 2D, axis-aligned, floating-point rectangle whose coordinates -/// are relative to a given origin. -/// -/// A Rect can be created with one its constructors or from an [Offset] and a -/// [Size] using the `&` operator: -/// -/// ```dart -/// Rect myRect = const Offset(1.0, 2.0) & const Size(3.0, 4.0); -/// ``` class Rect { - /// Construct a rectangle from its left, top, right, and bottom edges. const Rect.fromLTRB(this.left, this.top, this.right, this.bottom) : assert(left != null), // ignore: unnecessary_null_comparison assert(top != null), // ignore: unnecessary_null_comparison assert(right != null), // ignore: unnecessary_null_comparison assert(bottom != null); // ignore: unnecessary_null_comparison - /// Construct a rectangle from its left and top edges, its width, and its - /// height. - /// - /// To construct a [Rect] from an [Offset] and a [Size], you can use the - /// rectangle constructor operator `&`. See [Offset.&]. - const Rect.fromLTWH(double left, double top, double width, double height) : this.fromLTRB(left, top, left + width, top + height); - - /// Construct a rectangle that bounds the given circle. - /// - /// The `center` argument is assumed to be an offset from the origin. - Rect.fromCircle({ required Offset center, required double radius }) : this.fromCenter( - center: center, - width: radius * 2, - height: radius * 2, - ); - - /// Constructs a rectangle from its center point, width, and height. - /// - /// The `center` argument is assumed to be an offset from the origin. - Rect.fromCenter({ required Offset center, required double width, required double height }) : this.fromLTRB( - center.dx - width / 2, - center.dy - height / 2, - center.dx + width / 2, - center.dy + height / 2, - ); - - /// Construct the smallest rectangle that encloses the given offsets, treating - /// them as vectors from the origin. - Rect.fromPoints(Offset a, Offset b) : this.fromLTRB( - math.min(a.dx, b.dx), - math.min(a.dy, b.dy), - math.max(a.dx, b.dx), - math.max(a.dy, b.dy), - ); - - /// The offset of the left edge of this rectangle from the x axis. - final double left; + const Rect.fromLTWH(double left, double top, double width, double height) + : this.fromLTRB(left, top, left + width, top + height); - /// The offset of the top edge of this rectangle from the y axis. - final double top; + Rect.fromCircle({ required Offset center, required double radius }) + : this.fromCenter( + center: center, + width: radius * 2, + height: radius * 2, + ); - /// The offset of the right edge of this rectangle from the x axis. - final double right; + Rect.fromCenter({ required Offset center, required double width, required double height }) + : this.fromLTRB( + center.dx - width / 2, + center.dy - height / 2, + center.dx + width / 2, + center.dy + height / 2, + ); - /// The offset of the bottom edge of this rectangle from the y axis. - final double bottom; + Rect.fromPoints(Offset a, Offset b) + : this.fromLTRB( + math.min(a.dx, b.dx), + math.min(a.dy, b.dy), + math.max(a.dx, b.dx), + math.max(a.dy, b.dy), + ); - /// The distance between the left and right edges of this rectangle. + final double left; + final double top; + final double right; + final double bottom; double get width => right - left; - - /// The distance between the top and bottom edges of this rectangle. double get height => bottom - top; - - /// The distance between the upper-left corner and the lower-right corner of - /// this rectangle. Size get size => Size(width, height); - - /// Whether any of the dimensions are `NaN`. bool get hasNaN => left.isNaN || top.isNaN || right.isNaN || bottom.isNaN; - - /// A rectangle with left, top, right, and bottom edges all at zero. static const Rect zero = Rect.fromLTRB(0.0, 0.0, 0.0, 0.0); static const double _giantScalar = 1.0E+9; // matches kGiantRect from layer.h - - /// A rectangle that covers the entire coordinate space. - /// - /// This covers the space from -1e9,-1e9 to 1e9,1e9. - /// This is the space over which graphics operations are valid. static const Rect largest = Rect.fromLTRB(-_giantScalar, -_giantScalar, _giantScalar, _giantScalar); - - /// Whether any of the coordinates of this rectangle are equal to positive infinity. // included for consistency with Offset and Size bool get isInfinite { return left >= double.infinity @@ -715,63 +226,39 @@ class Rect { || bottom >= double.infinity; } - /// Whether all coordinates of this rectangle are finite. bool get isFinite => left.isFinite && top.isFinite && right.isFinite && bottom.isFinite; - - /// Whether this rectangle encloses a non-zero area. Negative areas are - /// considered empty. bool get isEmpty => left >= right || top >= bottom; - - /// Returns a new rectangle translated by the given offset. - /// - /// To translate a rectangle by separate x and y components rather than by an - /// [Offset], consider [translate]. Rect shift(Offset offset) { return Rect.fromLTRB(left + offset.dx, top + offset.dy, right + offset.dx, bottom + offset.dy); } - /// Returns a new rectangle with translateX added to the x components and - /// translateY added to the y components. - /// - /// To translate a rectangle by an [Offset] rather than by separate x and y - /// components, consider [shift]. Rect translate(double translateX, double translateY) { return Rect.fromLTRB(left + translateX, top + translateY, right + translateX, bottom + translateY); } - /// Returns a new rectangle with edges moved outwards by the given delta. Rect inflate(double delta) { return Rect.fromLTRB(left - delta, top - delta, right + delta, bottom + delta); } - /// Returns a new rectangle with edges moved inwards by the given delta. Rect deflate(double delta) => inflate(-delta); - - /// Returns a new rectangle that is the intersection of the given - /// rectangle and this rectangle. The two rectangles must overlap - /// for this to be meaningful. If the two rectangles do not overlap, - /// then the resulting Rect will have a negative width or height. Rect intersect(Rect other) { return Rect.fromLTRB( math.max(left, other.left), math.max(top, other.top), math.min(right, other.right), - math.min(bottom, other.bottom) + math.min(bottom, other.bottom), ); } - /// Returns a new rectangle which is the bounding box containing this - /// rectangle and the given rectangle. Rect expandToInclude(Rect other) { return Rect.fromLTRB( - math.min(left, other.left), - math.min(top, other.top), - math.max(right, other.right), - math.max(bottom, other.bottom), + math.min(left, other.left), + math.min(top, other.top), + math.max(right, other.right), + math.max(bottom, other.bottom), ); } - /// Whether `other` has a nonzero area of overlap with this rectangle. bool overlaps(Rect other) { if (right <= other.left || other.right <= left) return false; @@ -780,85 +267,21 @@ class Rect { return true; } - /// The lesser of the magnitudes of the [width] and the [height] of this - /// rectangle. double get shortestSide => math.min(width.abs(), height.abs()); - - /// The greater of the magnitudes of the [width] and the [height] of this - /// rectangle. double get longestSide => math.max(width.abs(), height.abs()); - - /// The offset to the intersection of the top and left edges of this rectangle. - /// - /// See also [Size.topLeft]. Offset get topLeft => Offset(left, top); - - /// The offset to the center of the top edge of this rectangle. - /// - /// See also [Size.topCenter]. Offset get topCenter => Offset(left + width / 2.0, top); - - /// The offset to the intersection of the top and right edges of this rectangle. - /// - /// See also [Size.topRight]. Offset get topRight => Offset(right, top); - - /// The offset to the center of the left edge of this rectangle. - /// - /// See also [Size.centerLeft]. Offset get centerLeft => Offset(left, top + height / 2.0); - - /// The offset to the point halfway between the left and right and the top and - /// bottom edges of this rectangle. - /// - /// See also [Size.center]. Offset get center => Offset(left + width / 2.0, top + height / 2.0); - - /// The offset to the center of the right edge of this rectangle. - /// - /// See also [Size.centerLeft]. Offset get centerRight => Offset(right, top + height / 2.0); - - /// The offset to the intersection of the bottom and left edges of this rectangle. - /// - /// See also [Size.bottomLeft]. Offset get bottomLeft => Offset(left, bottom); - - /// The offset to the center of the bottom edge of this rectangle. - /// - /// See also [Size.bottomLeft]. Offset get bottomCenter => Offset(left + width / 2.0, bottom); - - /// The offset to the intersection of the bottom and right edges of this rectangle. - /// - /// See also [Size.bottomRight]. Offset get bottomRight => Offset(right, bottom); - - /// Whether the point specified by the given offset (which is assumed to be - /// relative to the origin) lies between the left and right and the top and - /// bottom edges of this rectangle. - /// - /// Rectangles include their top and left edges but exclude their bottom and - /// right edges. bool contains(Offset offset) { return offset.dx >= left && offset.dx < right && offset.dy >= top && offset.dy < bottom; } - /// Linearly interpolate between two rectangles. - /// - /// If either rect is null, [Rect.zero] is used as a substitute. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Rect? lerp(Rect? a, Rect? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -902,92 +325,19 @@ class Rect { String toString() => 'Rect.fromLTRB(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)})'; } -/// A radius for either circular or elliptical shapes. class Radius { - /// Constructs a circular radius. [x] and [y] will have the same radius value. const Radius.circular(double radius) : this.elliptical(radius, radius); - - /// Constructs an elliptical radius with the given radii. const Radius.elliptical(this.x, this.y); - - /// The radius value on the horizontal axis. final double x; - - /// The radius value on the vertical axis. final double y; - - /// A radius with [x] and [y] values set to zero. - /// - /// You can use [Radius.zero] with [RRect] to have right-angle corners. static const Radius zero = Radius.circular(0.0); - - /// Unary negation operator. - /// - /// Returns a Radius with the distances negated. - /// - /// Radiuses with negative values aren't geometrically meaningful, but could - /// occur as part of expressions. For example, negating a radius of one pixel - /// and then adding the result to another radius is equivalent to subtracting - /// a radius of one pixel from the other. Radius operator -() => Radius.elliptical(-x, -y); - - /// Binary subtraction operator. - /// - /// Returns a radius whose [x] value is the left-hand-side operand's [x] - /// minus the right-hand-side operand's [x] and whose [y] value is the - /// left-hand-side operand's [y] minus the right-hand-side operand's [y]. Radius operator -(Radius other) => Radius.elliptical(x - other.x, y - other.y); - - /// Binary addition operator. - /// - /// Returns a radius whose [x] value is the sum of the [x] values of the - /// two operands, and whose [y] value is the sum of the [y] values of the - /// two operands. Radius operator +(Radius other) => Radius.elliptical(x + other.x, y + other.y); - - /// Multiplication operator. - /// - /// Returns a radius whose coordinates are the coordinates of the - /// left-hand-side operand (a radius) multiplied by the scalar - /// right-hand-side operand (a double). Radius operator *(double operand) => Radius.elliptical(x * operand, y * operand); - - /// Division operator. - /// - /// Returns a radius whose coordinates are the coordinates of the - /// left-hand-side operand (a radius) divided by the scalar right-hand-side - /// operand (a double). Radius operator /(double operand) => Radius.elliptical(x / operand, y / operand); - - /// Integer (truncating) division operator. - /// - /// Returns a radius whose coordinates are the coordinates of the - /// left-hand-side operand (a radius) divided by the scalar right-hand-side - /// operand (a double), rounded towards zero. Radius operator ~/(double operand) => Radius.elliptical((x ~/ operand).toDouble(), (y ~/ operand).toDouble()); - - /// Modulo (remainder) operator. - /// - /// Returns a radius whose coordinates are the remainder of dividing the - /// coordinates of the left-hand-side operand (a radius) by the scalar - /// right-hand-side operand (a double). Radius operator %(double operand) => Radius.elliptical(x % operand, y % operand); - - /// Linearly interpolate between two radii. - /// - /// If either is null, this function substitutes [Radius.zero] instead. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Radius? lerp(Radius? a, Radius? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -1032,32 +382,37 @@ class Radius { } } -/// An immutable rounded rectangle with the custom radii for all four corners. class RRect { - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and the same radii along its horizontal axis and its vertical axis. - const RRect.fromLTRBXY(double left, double top, double right, double bottom, - double radiusX, double radiusY) : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: radiusX, - tlRadiusY: radiusY, - trRadiusX: radiusX, - trRadiusY: radiusY, - blRadiusX: radiusX, - blRadiusY: radiusY, - brRadiusX: radiusX, - brRadiusY: radiusY, - uniformRadii: radiusX == radiusY, - ); - - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and the same radius in each corner. - RRect.fromLTRBR(double left, double top, double right, double bottom, - Radius radius) - : this._raw( + const RRect.fromLTRBXY( + double left, + double top, + double right, + double bottom, + double radiusX, + double radiusY, + ) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + uniformRadii: radiusX == radiusY, + ); + + RRect.fromLTRBR( + double left, + double top, + double right, + double bottom, + Radius radius, + ) : this._raw( top: top, left: left, right: right, @@ -1073,8 +428,6 @@ class RRect { uniformRadii: radius.x == radius.y, ); - /// Construct a rounded rectangle from its bounding box and the same radii - /// along its horizontal axis and its vertical axis. RRect.fromRectXY(Rect rect, double radiusX, double radiusY) : this._raw( top: rect.top, @@ -1092,8 +445,6 @@ class RRect { uniformRadii: radiusX == radiusY, ); - /// Construct a rounded rectangle from its bounding box and a radius that is - /// the same in each corner. RRect.fromRectAndRadius(Rect rect, Radius radius) : this._raw( top: rect.top, @@ -1111,10 +462,6 @@ class RRect { uniformRadii: radius.x == radius.y, ); - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and topLeft, topRight, bottomRight, and bottomLeft radii. - /// - /// The corner radii default to [Radius.zero], i.e. right-angled corners. RRect.fromLTRBAndCorners( double left, double top, @@ -1146,19 +493,13 @@ class RRect { topLeft.x == bottomRight.y, ); - /// Construct a rounded rectangle from its bounding box and and topLeft, - /// topRight, bottomRight, and bottomLeft radii. - /// - /// The corner radii default to [Radius.zero], i.e. right-angled corners RRect.fromRectAndCorners( - Rect rect, - { - Radius topLeft = Radius.zero, - Radius topRight = Radius.zero, - Radius bottomRight = Radius.zero, - Radius bottomLeft = Radius.zero - } - ) : this._raw( + Rect rect, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( top: rect.top, left: rect.left, right: rect.right, @@ -1208,62 +549,26 @@ class RRect { assert(blRadiusY != null), // ignore: unnecessary_null_comparison this.webOnlyUniformRadii = uniformRadii; - /// The offset of the left edge of this rectangle from the x axis. final double left; - - /// The offset of the top edge of this rectangle from the y axis. final double top; - - /// The offset of the right edge of this rectangle from the x axis. final double right; - - /// The offset of the bottom edge of this rectangle from the y axis. final double bottom; - - /// The top-left horizontal radius. final double tlRadiusX; - - /// The top-left vertical radius. final double tlRadiusY; - - /// The top-left [Radius]. Radius get tlRadius => Radius.elliptical(tlRadiusX, tlRadiusY); - - /// The top-right horizontal radius. final double trRadiusX; - - /// The top-right vertical radius. final double trRadiusY; - - /// The top-right [Radius]. Radius get trRadius => Radius.elliptical(trRadiusX, trRadiusY); - - /// The bottom-right horizontal radius. final double brRadiusX; - - /// The bottom-right vertical radius. final double brRadiusY; - - /// The bottom-right [Radius]. Radius get brRadius => Radius.elliptical(brRadiusX, brRadiusY); - - /// The bottom-left horizontal radius. final double blRadiusX; - - /// The bottom-left vertical radius. final double blRadiusY; - - /// If radii is equal for all corners. // webOnly final bool webOnlyUniformRadii; - - /// The bottom-left [Radius]. Radius get blRadius => Radius.elliptical(blRadiusX, blRadiusY); - - /// A rounded rectangle with all the values set to zero. static const RRect zero = RRect._raw(); - /// Returns a new [RRect] translated by the given offset. RRect shift(Offset offset) { return RRect._raw( left: left + offset.dx, @@ -1281,8 +586,6 @@ class RRect { ); } - /// Returns a new [RRect] with edges and radii moved outwards by the given - /// delta. RRect inflate(double delta) { return RRect._raw( left: left - delta, @@ -1300,22 +603,10 @@ class RRect { ); } - /// Returns a new [RRect] with edges and radii moved inwards by the given delta. RRect deflate(double delta) => inflate(-delta); - - /// The distance between the left and right edges of this rectangle. double get width => right - left; - - /// The distance between the top and bottom edges of this rectangle. double get height => bottom - top; - - /// The bounding box of this rounded rectangle (the rectangle with no rounded corners). Rect get outerRect => Rect.fromLTRB(left, top, right, bottom); - - /// The non-rounded rectangle that is constrained by the smaller of the two - /// diagonals, with each diagonal traveling through the middle of the curve - /// corners. The middle of a corner is the intersection of the curve with its - /// respective quadrant bisector. Rect get safeInnerRect { const double kInsetFactor = 0.29289321881; // 1-cos(pi/4) @@ -1332,12 +623,6 @@ class RRect { ); } - /// The rectangle that would be formed using the axis-aligned intersection of - /// the sides of the rectangle, i.e., the rectangle formed from the - /// inner-most centers of the ellipses that form the corners. This is the - /// intersection of the [wideMiddleRect] and the [tallMiddleRect]. If any of - /// the intersections are void, the resulting [Rect] will have negative width - /// or height. Rect get middleRect { final double leftRadius = math.max(blRadiusX, tlRadiusX); final double topRadius = math.max(tlRadiusY, trRadiusY); @@ -1351,10 +636,6 @@ class RRect { ); } - /// The biggest rectangle that is entirely inside the rounded rectangle and - /// has the full width of the rounded rectangle. If the rounded rectangle does - /// not have an axis-aligned intersection of its left and right side, the - /// resulting [Rect] will have negative width or height. Rect get wideMiddleRect { final double topRadius = math.max(tlRadiusY, trRadiusY); final double bottomRadius = math.max(brRadiusY, blRadiusY); @@ -1366,10 +647,6 @@ class RRect { ); } - /// The biggest rectangle that is entirely inside the rounded rectangle and - /// has the full height of the rounded rectangle. If the rounded rectangle - /// does not have an axis-aligned intersection of its top and bottom side, the - /// resulting [Rect] will have negative width or height. Rect get tallMiddleRect { final double leftRadius = math.max(blRadiusX, tlRadiusX); final double rightRadius = math.max(trRadiusX, brRadiusX); @@ -1381,23 +658,15 @@ class RRect { ); } - /// Whether this rounded rectangle encloses a non-zero area. - /// Negative areas are considered empty. bool get isEmpty => left >= right || top >= bottom; - - /// Whether all coordinates of this rounded rectangle are finite. bool get isFinite => left.isFinite && top.isFinite && right.isFinite && bottom.isFinite; - - /// Whether this rounded rectangle is a simple rectangle with zero - /// corner radii. bool get isRect { - return (tlRadiusX == 0.0 || tlRadiusY == 0.0) && - (trRadiusX == 0.0 || trRadiusY == 0.0) && - (blRadiusX == 0.0 || blRadiusY == 0.0) && - (brRadiusX == 0.0 || brRadiusY == 0.0); + return (tlRadiusX == 0.0 || tlRadiusY == 0.0) + && (trRadiusX == 0.0 || trRadiusY == 0.0) + && (blRadiusX == 0.0 || blRadiusY == 0.0) + && (brRadiusX == 0.0 || brRadiusY == 0.0); } - /// Whether this rounded rectangle has a side with no straight section. bool get isStadium { return tlRadius == trRadius && trRadius == brRadius @@ -1405,7 +674,6 @@ class RRect { && (width <= 2.0 * tlRadiusX || height <= 2.0 * tlRadiusY); } - /// Whether this rounded rectangle has no side with a straight section. bool get isEllipse { return tlRadius == trRadius && trRadius == brRadius @@ -1414,24 +682,12 @@ class RRect { && height <= 2.0 * tlRadiusY; } - /// Whether this rounded rectangle would draw as a circle. bool get isCircle => width == height && isEllipse; - - /// The lesser of the magnitudes of the [width] and the [height] of this - /// rounded rectangle. double get shortestSide => math.min(width.abs(), height.abs()); - - /// The greater of the magnitudes of the [width] and the [height] of this - /// rounded rectangle. double get longestSide => math.max(width.abs(), height.abs()); - - /// Whether any of the dimensions are `NaN`. bool get hasNaN => left.isNaN || top.isNaN || right.isNaN || bottom.isNaN || trRadiusX.isNaN || trRadiusY.isNaN || tlRadiusX.isNaN || tlRadiusY.isNaN || brRadiusX.isNaN || brRadiusY.isNaN || blRadiusX.isNaN || blRadiusY.isNaN; - - /// The offset to the point halfway between the left and right and the top and - /// bottom edges of this rectangle. Offset get center => Offset(left + width / 2.0, top + height / 2.0); // Returns the minimum between min and scale to which radius1 and radius2 @@ -1443,15 +699,6 @@ class RRect { return min; } - /// Scales all radii so that on each side their sum will not exceed the size - /// of the width/height. - /// - /// Skia already handles RRects with radii that are too large in this way. - /// Therefore, this method is only needed for RRect use cases that require - /// the appropriately scaled radii values. - /// - /// See the [Skia scaling implementation](https://github.com/google/skia/blob/master/src/core/SkRRect.cpp) - /// for more details. RRect scaleRadii() { double scale = 1.0; scale = _getMin(scale, blRadiusY, tlRadiusY, height); @@ -1492,13 +739,6 @@ class RRect { ); } - /// Whether the point specified by the given offset (which is assumed to be - /// relative to the origin) lies inside the rounded rectangle. - /// - /// This method may allocate (and cache) a copy of the object with normalized - /// radii the first time it is called on a particular [RRect] instance. When - /// using this method, prefer to reuse existing [RRect]s rather than - /// recreating the object each time. bool contains(Offset point) { if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) return false; // outside bounding box @@ -1547,21 +787,6 @@ class RRect { return true; } - /// Linearly interpolate between two rounded rectangles. - /// - /// If either is null, this function substitutes [RRect.zero] instead. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static RRect? lerp(RRect? a, RRect? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -1667,37 +892,9 @@ class RRect { ')'; } } - -/// A transform consisting of a translation, a rotation, and a uniform scale. -/// -/// Used by [Canvas.drawAtlas]. This is a more efficient way to represent these -/// simple transformations than a full matrix. // Modeled after Skia's SkRSXform. + class RSTransform { - /// Creates an RSTransform. - /// - /// An [RSTransform] expresses the combination of a translation, a rotation - /// around a particular point, and a scale factor. - /// - /// The first argument, `scos`, is the cosine of the rotation, multiplied by - /// the scale factor. - /// - /// The second argument, `ssin`, is the sine of the rotation, multiplied by - /// that same scale factor. - /// - /// The third argument is the x coordinate of the translation, minus the - /// `scos` argument multiplied by the x-coordinate of the rotation point, plus - /// the `ssin` argument multiplied by the y-coordinate of the rotation point. - /// - /// The fourth argument is the y coordinate of the translation, minus the `ssin` - /// argument multiplied by the x-coordinate of the rotation point, minus the - /// `scos` argument multiplied by the y-coordinate of the rotation point. - /// - /// The [RSTransform.fromComponents] method may be a simpler way to - /// construct these values. However, if there is a way to factor out the - /// computations of the sine and cosine of the rotation so that they can be - /// reused over multiple calls to this constructor, it may be more efficient - /// to directly use this constructor instead. RSTransform(double scos, double ssin, double tx, double ty) { _value ..[0] = scos @@ -1705,33 +902,13 @@ class RSTransform { ..[2] = tx ..[3] = ty; } - - /// Creates an RSTransform from its individual components. - /// - /// The `rotation` parameter gives the rotation in radians. - /// - /// The `scale` parameter describes the uniform scale factor. - /// - /// The `anchorX` and `anchorY` parameters give the coordinate of the point - /// around which to rotate. - /// - /// The `translateX` and `translateY` parameters give the coordinate of the - /// offset by which to translate. - /// - /// This constructor computes the arguments of the [new RSTransform] - /// constructor and then defers to that constructor to actually create the - /// object. If many [RSTransform] objects are being created and there is a way - /// to factor out the computations of the sine and cosine of the rotation - /// (which are computed each time this constructor is called) and reuse them - /// over multiple [RSTransform] objects, it may be more efficient to directly - /// use the more direct [new RSTransform] constructor instead. factory RSTransform.fromComponents({ required double rotation, required double scale, required double anchorX, required double anchorY, required double translateX, - required double translateY + required double translateY, }) { final double scos = math.cos(rotation) * scale; final double ssin = math.sin(rotation) * scale; @@ -1741,20 +918,8 @@ class RSTransform { } final Float32List _value = Float32List(4); - - /// The cosine of the rotation multiplied by the scale factor. double get scos => _value[0]; - - /// The sine of the rotation multiplied by that same scale factor. double get ssin => _value[1]; - - /// The x coordinate of the translation, minus [scos] multiplied by the - /// x-coordinate of the rotation point, plus [ssin] multiplied by the - /// y-coordinate of the rotation point. double get tx => _value[2]; - - /// The y coordinate of the translation, minus [ssin] multiplied by the - /// x-coordinate of the rotation point, minus [scos] multiplied by the - /// y-coordinate of the rotation point. double get ty => _value[3]; } diff --git a/lib/web_ui/lib/src/ui/hash_codes.dart b/lib/web_ui/lib/src/ui/hash_codes.dart index e91fb1cc5f46c..58efa17c69588 100644 --- a/lib/web_ui/lib/src/ui/hash_codes.dart +++ b/lib/web_ui/lib/src/ui/hash_codes.dart @@ -5,10 +5,13 @@ // @dart = 2.10 part of ui; -class _HashEnd { const _HashEnd(); } +class _HashEnd { + const _HashEnd(); +} + const _HashEnd _hashEnd = _HashEnd(); -/// Jenkins hash function, optimized for small integers. +// Jenkins hash function, optimized for small integers. // // Borrowed from the dart sdk: sdk/lib/math/jenkins_smi_hash.dart. class _Jenkins { @@ -28,28 +31,28 @@ class _Jenkins { } } -/// Combine up to twenty objects' hash codes into one value. -/// -/// If you only need to handle one object's hash code, then just refer to its -/// [Object.hashCode] getter directly. -/// -/// If you need to combine an arbitrary number of objects from a [List] or other -/// [Iterable], use [hashList]. The output of [hashList] can be used as one of -/// the arguments to this function. -/// -/// For example: -/// -/// ```dart -/// int hashCode => hashValues(foo, bar, hashList(quux), baz); -/// ``` int hashValues( - Object? arg01, Object? arg02, [ Object? arg03 = _hashEnd, - Object? arg04 = _hashEnd, Object? arg05 = _hashEnd, Object? arg06 = _hashEnd, - Object? arg07 = _hashEnd, Object? arg08 = _hashEnd, Object? arg09 = _hashEnd, - Object? arg10 = _hashEnd, Object? arg11 = _hashEnd, Object? arg12 = _hashEnd, - Object? arg13 = _hashEnd, Object? arg14 = _hashEnd, Object? arg15 = _hashEnd, - Object? arg16 = _hashEnd, Object? arg17 = _hashEnd, Object? arg18 = _hashEnd, - Object? arg19 = _hashEnd, Object? arg20 = _hashEnd ]) { + Object? arg01, + Object? arg02, [ + Object? arg03 = _hashEnd, + Object? arg04 = _hashEnd, + Object? arg05 = _hashEnd, + Object? arg06 = _hashEnd, + Object? arg07 = _hashEnd, + Object? arg08 = _hashEnd, + Object? arg09 = _hashEnd, + Object? arg10 = _hashEnd, + Object? arg11 = _hashEnd, + Object? arg12 = _hashEnd, + Object? arg13 = _hashEnd, + Object? arg14 = _hashEnd, + Object? arg15 = _hashEnd, + Object? arg16 = _hashEnd, + Object? arg17 = _hashEnd, + Object? arg18 = _hashEnd, + Object? arg19 = _hashEnd, + Object? arg20 = _hashEnd, +]) { int result = 0; result = _Jenkins.combine(result, arg01); result = _Jenkins.combine(result, arg02); @@ -111,9 +114,6 @@ int hashValues( return _Jenkins.finish(result); } -/// Combine the [Object.hashCode] values of an arbitrary number of objects from -/// an [Iterable] into one value. This function will return the same value if -/// given null as if given an empty list. int hashList(Iterable? arguments) { int result = 0; if (arguments != null) { diff --git a/lib/web_ui/lib/src/ui/initialization.dart b/lib/web_ui/lib/src/ui/initialization.dart index 6865da8121a2f..a7b06b3586def 100644 --- a/lib/web_ui/lib/src/ui/initialization.dart +++ b/lib/web_ui/lib/src/ui/initialization.dart @@ -5,7 +5,6 @@ // @dart = 2.10 part of ui; -/// Initializes the platform. Future webOnlyInitializePlatform({ engine.AssetManager? assetManager, }) { @@ -51,11 +50,6 @@ engine.FontCollection? _fontCollection; bool _webOnlyIsInitialized = false; bool get webOnlyIsInitialized => _webOnlyIsInitialized; - -/// Specifies that the platform should use the given [AssetManager] to load -/// assets. -/// -/// The given asset manager is used to initialize the font collection. Future webOnlySetAssetManager(engine.AssetManager assetManager) async { assert(assetManager != null, 'Cannot set assetManager to null'); // ignore: unnecessary_null_comparison if (assetManager == _assetManager) { @@ -71,7 +65,6 @@ Future webOnlySetAssetManager(engine.AssetManager assetManager) async { _fontCollection!.clear(); } - if (_assetManager != null) { if (engine.experimentalUseSkia) { await engine.skiaFontCollection.registerFonts(_assetManager!); @@ -85,29 +78,16 @@ Future webOnlySetAssetManager(engine.AssetManager assetManager) async { } } -/// Flag that shows whether the Flutter Testing Behavior is enabled. -/// -/// This flag can be used to decide if the code is running from a Flutter Test -/// such as a Widget test. -/// -/// For example in these tests we use a predictable-size font which makes widget -/// tests less flaky. -bool get debugEmulateFlutterTesterEnvironment => - _debugEmulateFlutterTesterEnvironment; +bool get debugEmulateFlutterTesterEnvironment => _debugEmulateFlutterTesterEnvironment; set debugEmulateFlutterTesterEnvironment(bool value) { _debugEmulateFlutterTesterEnvironment = value; if (_debugEmulateFlutterTesterEnvironment) { const Size logicalSize = Size(800.0, 600.0); - engine.window.webOnlyDebugPhysicalSizeOverride = - logicalSize * window.devicePixelRatio; + engine.window.webOnlyDebugPhysicalSizeOverride = logicalSize * window.devicePixelRatio; } } bool _debugEmulateFlutterTesterEnvironment = false; - -/// This class handles downloading assets over the network. engine.AssetManager get webOnlyAssetManager => _assetManager!; - -/// A collection of fonts that may be used by the platform. engine.FontCollection get webOnlyFontCollection => _fontCollection!; diff --git a/lib/web_ui/lib/src/ui/lerp.dart b/lib/web_ui/lib/src/ui/lerp.dart index 5cd4c8ac1a672..8287cbd4ee1cd 100644 --- a/lib/web_ui/lib/src/ui/lerp.dart +++ b/lib/web_ui/lib/src/ui/lerp.dart @@ -5,7 +5,6 @@ // @dart = 2.10 part of ui; -/// Linearly interpolate between two numbers. double? lerpDouble(num? a, num? b, double t) { if (a == null && b == null) { return null; @@ -23,7 +22,6 @@ double _lerpInt(int a, int b, double t) { return a + (b - a) * t; } -/// Same as [num.clamp] but specialized for [int]. int _clampInt(int value, int min, int max) { assert(min <= max); if (value < min) { diff --git a/lib/web_ui/lib/src/ui/natives.dart b/lib/web_ui/lib/src/ui/natives.dart index 4763db34e58a6..48bd8912ca80e 100644 --- a/lib/web_ui/lib/src/ui/natives.dart +++ b/lib/web_ui/lib/src/ui/natives.dart @@ -19,27 +19,12 @@ class _Logger { static void _printString(String? s) { print(s); } + static void _printDebugString(String? s) { html.window.console.error(s!); } } -/// Returns runtime Dart compilation trace as a UTF-8 encoded memory buffer. -/// -/// The buffer contains a list of symbols compiled by the Dart JIT at runtime up to the point -/// when this function was called. This list can be saved to a text file and passed to tools -/// such as `flutter build` or Dart `gen_snapshot` in order to precompile this code offline. -/// -/// The list has one symbol per line of the following format: `,,\n`. -/// Here are some examples: -/// -/// ``` -/// dart:core,Duration,get:inMilliseconds -/// package:flutter/src/widgets/binding.dart,::,runApp -/// file:///.../my_app.dart,::,main -/// ``` -/// -/// This function is only effective in debug and dynamic modes, and will throw in AOT mode. List saveCompilationTrace() { throw UnimplementedError(); } diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 6e8ee2975f963..06fc4f445a0fc 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -8,8 +8,7 @@ part of ui; // ignore: unused_element, Used in Shader assert. bool _offsetIsValid(Offset offset) { assert(offset != null, 'Offset argument was null.'); // ignore: unnecessary_null_comparison - assert(!offset.dx.isNaN && !offset.dy.isNaN, - 'Offset argument contained a NaN value.'); + assert(!offset.dx.isNaN && !offset.dy.isNaN, 'Offset argument contained a NaN value.'); return true; } @@ -23,12 +22,10 @@ bool _matrix4IsValid(Float32List matrix4) { void _validateColorStops(List colors, List? colorStops) { if (colorStops == null) { if (colors.length != 2) - throw ArgumentError( - '"colors" must have length 2 if "colorStops" is omitted.'); + throw ArgumentError('"colors" must have length 2 if "colorStops" is omitted.'); } else { if (colors.length != colorStops.length) - throw ArgumentError( - '"colors" and "colorStops" arguments must have equal length.'); + throw ArgumentError('"colors" and "colorStops" arguments must have equal length.'); } } @@ -36,92 +33,43 @@ Color _scaleAlpha(Color a, double factor) { return a.withAlpha(_clampInt((a.alpha * factor).round(), 0, 255)); } -/// An immutable 32 bit color value in ARGB class Color { - /// Construct a color from the lower 32 bits of an int. - /// - /// Bits 24-31 are the alpha value. - /// Bits 16-23 are the red value. - /// Bits 8-15 are the green value. - /// Bits 0-7 are the blue value. const Color(int value) : this.value = value & 0xFFFFFFFF; - - /// Construct a color from the lower 8 bits of four integers. const Color.fromARGB(int a, int r, int g, int b) : value = (((a & 0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | ((b & 0xff) << 0)) & 0xFFFFFFFF; - - /// Create a color from red, green, blue, and opacity, similar to `rgba()` in CSS. - /// - /// * `r` is [red], from 0 to 255. - /// * `g` is [green], from 0 to 255. - /// * `b` is [blue], from 0 to 255. - /// * `opacity` is alpha channel of this color as a double, with 0.0 being - /// transparent and 1.0 being fully opaque. - /// - /// Out of range values are brought into range using modulo 255. - /// - /// See also [fromARGB], which takes the opacity as an integer value. const Color.fromRGBO(int r, int g, int b, double opacity) : value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | ((b & 0xff) << 0)) & 0xFFFFFFFF; - - /// A 32 bit value representing this color. - /// - /// Bits 24-31 are the alpha value. - /// Bits 16-23 are the red value. - /// Bits 8-15 are the green value. - /// Bits 0-7 are the blue value. final int value; - - /// The alpha channel of this color in an 8 bit value. int get alpha => (0xff000000 & value) >> 24; - - /// The alpha channel of this color as a double. double get opacity => alpha / 0xFF; - - /// The red channel of this color in an 8 bit value. int get red => (0x00ff0000 & value) >> 16; - - /// The green channel of this color in an 8 bit value. int get green => (0x0000ff00 & value) >> 8; - - /// The blue channel of this color in an 8 bit value. int get blue => (0x000000ff & value) >> 0; - - /// Returns a new color that matches this color with the alpha channel - /// replaced with a (which ranges from 0 to 255). Color withAlpha(int a) { return Color.fromARGB(a, red, green, blue); } - /// Returns a new color that matches this color with the alpha channel - /// replaced with the given opacity (which ranges from 0.0 to 1.0). Color withOpacity(double opacity) { assert(opacity >= 0.0 && opacity <= 1.0); return withAlpha((255.0 * opacity).round()); } - /// Returns a new color that matches this color with the red channel replaced - /// with r. Color withRed(int r) { return Color.fromARGB(alpha, r, green, blue); } - /// Returns a new color that matches this color with the green channel - /// replaced with g. Color withGreen(int g) { return Color.fromARGB(alpha, red, g, blue); } - /// Returns a new color that matches this color with the blue channel replaced - /// with b. Color withBlue(int b) { return Color.fromARGB(alpha, red, green, b); } @@ -134,12 +82,6 @@ class Color { return math.pow((component + 0.055) / 1.055, 2.4) as double; } - /// Returns a brightness value between 0 for darkest and 1 for lightest. - /// - /// Represents the relative luminance of the color. This value is - /// computationally expensive to calculate. - /// - /// See . double computeLuminance() { // See final double R = _linearizeColorComponent(red / 0xFF); @@ -148,28 +90,6 @@ class Color { return 0.2126 * R + 0.7152 * G + 0.0722 * B; } - /// Linearly interpolate between two colors. - /// - /// This is intended to be fast but as a result may be ugly. Consider - /// [HSVColor] or writing custom logic for interpolating colors. - /// - /// If either color is null, this function linearly interpolates from a - /// transparent instance of the other color. This is usually preferable to - /// interpolating from [material.Colors.transparent] (`const - /// Color(0x00000000)`), which is specifically transparent _black_. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). Each channel - /// will be clamped to the range 0 to 255. - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static Color? lerp(Color? a, Color? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -192,14 +112,6 @@ class Color { } } - /// Combine the foreground color as a transparent color over top - /// of a background color, and return the resulting combined color. - /// - /// This uses standard alpha blending ("SRC over DST") rules to produce a - /// blended color from two colors. This can be used as a performance - /// enhancement when trying to avoid needless alpha blending compositing - /// operations for two things that are solid colors with the same shape, but - /// overlay each other: instead, just paint one with the combined color. static Color alphaBlend(Color foreground, Color background) { final int alpha = foreground.alpha; if (alpha == 0x00) { @@ -230,9 +142,6 @@ class Color { } } - /// Returns an alpha value representative of the provided [opacity] value. - /// - /// The [opacity] value may not be null. static int getAlphaFromOpacity(double opacity) { assert(opacity != null); // ignore: unnecessary_null_comparison return (opacity.clamp(0.0, 1.0) * 255).round(); @@ -246,8 +155,7 @@ class Color { if (other.runtimeType != runtimeType) { return false; } - return other is Color - && other.value == value; + return other is Color && other.value == value; } @override @@ -259,836 +167,110 @@ class Color { } } -/// Styles to use for line endings. -/// -/// See [Paint.strokeCap]. enum StrokeCap { - /// Begin and end contours with a flat edge and no extension. butt, - - /// Begin and end contours with a semi-circle extension. round, - - /// Begin and end contours with a half square extension. This is - /// similar to extending each contour by half the stroke width (as - /// given by [Paint.strokeWidth]). square, } -/// Styles to use for line segment joins. -/// -/// This only affects line joins for polygons drawn by [Canvas.drawPath] and -/// rectangles, not points drawn as lines with [Canvas.drawPoints]. -/// -/// See also: -/// -/// * [Paint.strokeJoin] and [Paint.strokeMiterLimit] for how this value is -/// used. -/// * [StrokeCap] for the different kinds of line endings. // These enum values must be kept in sync with SkPaint::Join. enum StrokeJoin { - /// Joins between line segments form sharp corners. - /// - /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4} - /// - /// The center of the line segment is colored in the diagram above to - /// highlight the join, but in normal usage the join is the same color as the - /// line. - /// - /// See also: - /// - /// * [Paint.strokeJoin], used to set the line segment join style to this - /// value. - /// * [Paint.strokeMiterLimit], used to define when a miter is drawn instead - /// of a bevel when the join is set to this value. miter, - - /// Joins between line segments are semi-circular. - /// - /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_join.mp4} - /// - /// The center of the line segment is colored in the diagram above to - /// highlight the join, but in normal usage the join is the same color as the - /// line. - /// - /// See also: - /// - /// * [Paint.strokeJoin], used to set the line segment join style to this - /// value. round, - - /// Joins between line segments connect the corners of the butt ends of the - /// line segments to give a beveled appearance. - /// - /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/bevel_join.mp4} - /// - /// The center of the line segment is colored in the diagram above to - /// highlight the join, but in normal usage the join is the same color as the - /// line. - /// - /// See also: - /// - /// * [Paint.strokeJoin], used to set the line segment join style to this - /// value. bevel, } -/// Strategies for painting shapes and paths on a canvas. -/// -/// See [Paint.style]. enum PaintingStyle { - /// Apply the [Paint] to the inside of the shape. For example, when - /// applied to the [Paint.drawCircle] call, this results in a disc - /// of the given size being painted. fill, - - /// Apply the [Paint] to the edge of the shape. For example, when - /// applied to the [Paint.drawCircle] call, this results is a hoop - /// of the given size being painted. The line drawn on the edge will - /// be the width given by the [Paint.strokeWidth] property. stroke, } -/// Algorithms to use when painting on the canvas. -/// -/// When drawing a shape or image onto a canvas, different algorithms can be -/// used to blend the pixels. The different values of [BlendMode] specify -/// different such algorithms. -/// -/// Each algorithm has two inputs, the _source_, which is the image being drawn, -/// and the _destination_, which is the image into which the source image is -/// being composited. The destination is often thought of as the _background_. -/// The source and destination both have four color channels, the red, green, -/// blue, and alpha channels. These are typically represented as numbers in the -/// range 0.0 to 1.0. The output of the algorithm also has these same four -/// channels, with values computed from the source and destination. -/// -/// The documentation of each value below describes how the algorithm works. In -/// each case, an image shows the output of blending a source image with a -/// destination image. In the images below, the destination is represented by an -/// image with horizontal lines and an opaque landscape photograph, and the -/// source is represented by an image with vertical lines (the same lines but -/// rotated) and a bird clip-art image. The [src] mode shows only the source -/// image, and the [dst] mode shows only the destination image. In the -/// documentation below, the transparency is illustrated by a checkerboard -/// pattern. The [clear] mode drops both the source and destination, resulting -/// in an output that is entirely transparent (illustrated by a solid -/// checkerboard pattern). -/// -/// The horizontal and vertical bars in these images show the red, green, and -/// blue channels with varying opacity levels, then all three color channels -/// together with those same varying opacity levels, then all three color -/// channels set to zero with those varying opacity levels, then two bars -/// showing a red/green/blue repeating gradient, the first with full opacity and -/// the second with partial opacity, and finally a bar with the three color -/// channels set to zero but the opacity varying in a repeating gradient. -/// -/// ## Application to the [Canvas] API -/// -/// When using [Canvas.saveLayer] and [Canvas.restore], the blend mode of the -/// [Paint] given to the [Canvas.saveLayer] will be applied when -/// [Canvas.restore] is called. Each call to [Canvas.saveLayer] introduces a new -/// layer onto which shapes and images are painted; when [Canvas.restore] is -/// called, that layer is then composited onto the parent layer, with the source -/// being the most-recently-drawn shapes and images, and the destination being -/// the parent layer. (For the first [Canvas.saveLayer] call, the parent layer -/// is the canvas itself.) -/// -/// See also: -/// -/// * [Paint.blendMode], which uses [BlendMode] to define the compositing -/// strategy. enum BlendMode { // This list comes from Skia's SkXfermode.h and the values (order) should be // kept in sync. // See: https://skia.org/user/api/skpaint#SkXfermode - - /// Drop both the source and destination images, leaving nothing. - /// - /// This corresponds to the "clear" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_clear.png) clear, - - /// Drop the destination image, only paint the source image. - /// - /// Conceptually, the destination is first cleared, then the source image is - /// painted. - /// - /// This corresponds to the "Copy" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_src.png) src, - - /// Drop the source image, only paint the destination image. - /// - /// Conceptually, the source image is discarded, leaving the destination - /// untouched. - /// - /// This corresponds to the "Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dst.png) dst, - - /// Composite the source image over the destination image. - /// - /// This is the default value. It represents the most intuitive case, where - /// shapes are painted on top of what is below, with transparent areas showing - /// the destination layer. - /// - /// This corresponds to the "Source over Destination" Porter-Duff operator, - /// also known as the Painter's Algorithm. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcOver.png) srcOver, - - /// Composite the source image under the destination image. - /// - /// This is the opposite of [srcOver]. - /// - /// This corresponds to the "Destination over Source" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstOver.png) - /// - /// This is useful when the source image should have been painted before the - /// destination image, but could not be. dstOver, - - /// Show the source image, but only where the two images overlap. The - /// destination image is not rendered, it is treated merely as a mask. The - /// color channels of the destination are ignored, only the opacity has an - /// effect. - /// - /// To show the destination image instead, consider [dstIn]. - /// - /// To reverse the semantic of the mask (only showing the source where the - /// destination is absent, rather than where it is present), consider - /// [srcOut]. - /// - /// This corresponds to the "Source in Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcIn.png) srcIn, - - /// Show the destination image, but only where the two images overlap. The - /// source image is not rendered, it is treated merely as a mask. The color - /// channels of the source are ignored, only the opacity has an effect. - /// - /// To show the source image instead, consider [srcIn]. - /// - /// To reverse the semantic of the mask (only showing the source where the - /// destination is present, rather than where it is absent), consider - /// [dstOut]. - /// - /// This corresponds to the "Destination in Source" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstIn.png) dstIn, - - /// Show the source image, but only where the two images do not overlap. The - /// destination image is not rendered, it is treated merely as a mask. The color - /// channels of the destination are ignored, only the opacity has an effect. - /// - /// To show the destination image instead, consider [dstOut]. - /// - /// To reverse the semantic of the mask (only showing the source where the - /// destination is present, rather than where it is absent), consider [srcIn]. - /// - /// This corresponds to the "Source out Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcOut.png) srcOut, - - /// Show the destination image, but only where the two images do not overlap. - /// The source image is not rendered, it is treated merely as a mask. The - /// color channels of the source are ignored, only the opacity has an effect. - /// - /// To show the source image instead, consider [srcOut]. - /// - /// To reverse the semantic of the mask (only showing the destination where - /// the source is present, rather than where it is absent), consider [dstIn]. - /// - /// This corresponds to the "Destination out Source" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstOut.png) dstOut, - - /// Composite the source image over the destination image, but only where it - /// overlaps the destination. - /// - /// This corresponds to the "Source atop Destination" Porter-Duff operator. - /// - /// This is essentially the [srcOver] operator, but with the output's opacity - /// channel being set to that of the destination image instead of being a - /// combination of both image's opacity channels. - /// - /// For a variant with the destination on top instead of the source, see - /// [dstATop]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcATop.png) srcATop, - - /// Composite the destination image over the source image, but only where it - /// overlaps the source. - /// - /// This corresponds to the "Destination atop Source" Porter-Duff operator. - /// - /// This is essentially the [dstOver] operator, but with the output's opacity - /// channel being set to that of the source image instead of being a - /// combination of both image's opacity channels. - /// - /// For a variant with the source on top instead of the destination, see - /// [srcATop]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstATop.png) dstATop, - - /// Apply a bitwise `xor` operator to the source and destination images. This - /// leaves transparency where they would overlap. - /// - /// This corresponds to the "Source xor Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_xor.png) xor, - - /// Sum the components of the source and destination images. - /// - /// Transparency in a pixel of one of the images reduces the contribution of - /// that image to the corresponding output pixel, as if the color of that - /// pixel in that image was darker. - /// - /// This corresponds to the "Source plus Destination" Porter-Duff operator. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_plus.png) plus, - - /// Multiply the color components of the source and destination images. - /// - /// This can only result in the same or darker colors (multiplying by white, - /// 1.0, results in no change; multiplying by black, 0.0, results in black). - /// - /// When compositing two opaque images, this has similar effect to overlapping - /// two transparencies on a projector. - /// - /// For a variant that also multiplies the alpha channel, consider [multiply]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_modulate.png) - /// - /// See also: - /// - /// * [screen], which does a similar computation but inverted. - /// * [overlay], which combines [modulate] and [screen] to favor the - /// destination image. - /// * [hardLight], which combines [modulate] and [screen] to favor the - /// source image. modulate, // Following blend modes are defined in the CSS Compositing standard. - - /// Multiply the inverse of the components of the source and destination - /// images, and inverse the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// This is essentially the same as [modulate] blend mode, but with the values - /// of the colors inverted before the multiplication and the result being - /// inverted back before rendering. - /// - /// This can only result in the same or lighter colors (multiplying by black, - /// 1.0, results in no change; multiplying by white, 0.0, results in white). - /// Similarly, in the alpha channel, it can only result in more opaque colors. - /// - /// This has similar effect to two projectors displaying their images on the - /// same screen simultaneously. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_screen.png) - /// - /// See also: - /// - /// * [modulate], which does a similar computation but without inverting the - /// values. - /// * [overlay], which combines [modulate] and [screen] to favor the - /// destination image. - /// * [hardLight], which combines [modulate] and [screen] to favor the - /// source image. screen, // The last coeff mode. - - /// Multiply the components of the source and destination images after - /// adjusting them to favor the destination. - /// - /// Specifically, if the destination value is smaller, this multiplies it with - /// the source value, whereas is the source value is smaller, it multiplies - /// the inverse of the source value with the inverse of the destination value, - /// then inverts the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_overlay.png) - /// - /// See also: - /// - /// * [modulate], which always multiplies the values. - /// * [screen], which always multiplies the inverses of the values. - /// * [hardLight], which is similar to [overlay] but favors the source image - /// instead of the destination image. overlay, - - /// Composite the source and destination image by choosing the lowest value - /// from each color channel. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_darken.png) darken, - - /// Composite the source and destination image by choosing the highest value - /// from each color channel. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_lighten.png) lighten, - - /// Divide the destination by the inverse of the source. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_colorDodge.png) colorDodge, - - /// Divide the inverse of the destination by the source, and inverse the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_colorBurn.png) colorBurn, - - /// Multiply the components of the source and destination images after - /// adjusting them to favor the source. - /// - /// Specifically, if the source value is smaller, this multiplies it with the - /// destination value, whereas is the destination value is smaller, it - /// multiplies the inverse of the destination value with the inverse of the - /// source value, then inverts the result. - /// - /// Inverting the components means that a fully saturated channel (opaque - /// white) is treated as the value 0.0, and values normally treated as 0.0 - /// (black, transparent) are treated as 1.0. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hardLight.png) - /// - /// See also: - /// - /// * [modulate], which always multiplies the values. - /// * [screen], which always multiplies the inverses of the values. - /// * [overlay], which is similar to [hardLight] but favors the destination - /// image instead of the source image. hardLight, - - /// Use [colorDodge] for source values below 0.5 and [colorBurn] for source - /// values above 0.5. - /// - /// This results in a similar but softer effect than [overlay]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_softLight.png) - /// - /// See also: - /// - /// * [color], which is a more subtle tinting effect. softLight, - - /// Subtract the smaller value from the bigger value for each channel. - /// - /// Compositing black has no effect; compositing white inverts the colors of - /// the other image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// The effect is similar to [exclusion] but harsher. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_difference.png) difference, - - /// Subtract double the product of the two images from the sum of the two - /// images. - /// - /// Compositing black has no effect; compositing white inverts the colors of - /// the other image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. - /// - /// The effect is similar to [difference] but softer. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_exclusion.png) exclusion, - - /// Multiply the components of the source and destination images, including - /// the alpha channel. - /// - /// This can only result in the same or darker colors (multiplying by white, - /// 1.0, results in no change; multiplying by black, 0.0, results in black). - /// - /// Since the alpha channel is also multiplied, a fully-transparent pixel - /// (opacity 0.0) in one image results in a fully transparent pixel in the - /// output. This is similar to [dstIn], but with the colors combined. - /// - /// For a variant that multiplies the colors but does not multiply the alpha - /// channel, consider [modulate]. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_multiply.png) multiply, // The last separable mode. - - /// Take the hue of the source image, and the saturation and luminosity of the - /// destination image. - /// - /// The effect is to tint the destination image with the source image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their hue from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hue.png) - /// - /// See also: - /// - /// * [color], which is a similar but stronger effect as it also applies the - /// saturation of the source image. - /// * [HSVColor], which allows colors to be expressed using Hue rather than - /// the red/green/blue channels of [Color]. hue, - - /// Take the saturation of the source image, and the hue and luminosity of the - /// destination image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their saturation from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hue.png) - /// - /// See also: - /// - /// * [color], which also applies the hue of the source image. - /// * [luminosity], which applies the luminosity of the source image to the - /// destination. saturation, - - /// Take the hue and saturation of the source image, and the luminosity of the - /// destination image. - /// - /// The effect is to tint the destination image with the source image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their hue and saturation from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_color.png) - /// - /// See also: - /// - /// * [hue], which is a similar but weaker effect. - /// * [softLight], which is a similar tinting effect but also tints white. - /// * [saturation], which only applies the saturation of the source image. color, - - /// Take the luminosity of the source image, and the hue and saturation of the - /// destination image. - /// - /// The opacity of the output image is computed in the same way as for - /// [srcOver]. Regions that are entirely transparent in the source image take - /// their luminosity from the destination. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_luminosity.png) - /// - /// See also: - /// - /// * [saturation], which applies the saturation of the source image to the - /// destination. - /// * [ImageFilter.blur], which can be used with [BackdropFilter] for a - /// related effect. luminosity, } -/// Different ways to clip a widget's content. enum Clip { - /// No clip at all. - /// - /// This is the default option for most widgets: if the content does not - /// overflow the widget boundary, don't pay any performance cost for clipping. - /// - /// If the content does overflow, please explicitly specify the following - /// [Clip] options: - /// * [hardEdge], which is the fastest clipping, but with lower fidelity. - /// * [antiAlias], which is a little slower than [hardEdge], but with smoothed edges. - /// * [antiAliasWithSaveLayer], which is much slower than [antiAlias], and should - /// rarely be used. none, - - /// Clip, but do not apply anti-aliasing. - /// - /// This mode enables clipping, but curves and non-axis-aligned straight lines will be - /// jagged as no effort is made to anti-alias. - /// - /// Faster than other clipping modes, but slower than [none]. - /// - /// This is a reasonable choice when clipping is needed, if the container is an axis- - /// aligned rectangle or an axis-aligned rounded rectangle with very small corner radii. - /// - /// See also: - /// - /// * [antiAlias], which is more reasonable when clipping is needed and the shape is not - /// an axis-aligned rectangle. hardEdge, - - /// Clip with anti-aliasing. - /// - /// This mode has anti-aliased clipping edges to achieve a smoother look. - /// - /// It' s much faster than [antiAliasWithSaveLayer], but slower than [hardEdge]. - /// - /// This will be the common case when dealing with circles and arcs. - /// - /// Different from [hardEdge] and [antiAliasWithSaveLayer], this clipping may have - /// bleeding edge artifacts. - /// (See https://fiddle.skia.org/c/21cb4c2b2515996b537f36e7819288ae for an example.) - /// - /// See also: - /// - /// * [hardEdge], which is a little faster, but with lower fidelity. - /// * [antiAliasWithSaveLayer], which is much slower, but can avoid the - /// bleeding edges if there's no other way. - /// * [Paint.isAntiAlias], which is the anti-aliasing switch for general draw operations. antiAlias, - - /// Clip with anti-aliasing and saveLayer immediately following the clip. - /// - /// This mode not only clips with anti-aliasing, but also allocates an offscreen - /// buffer. All subsequent paints are carried out on that buffer before finally - /// being clipped and composited back. - /// - /// This is very slow. It has no bleeding edge artifacts (that [antiAlias] has) - /// but it changes the semantics as an offscreen buffer is now introduced. - /// (See https://github.com/flutter/flutter/issues/18057#issuecomment-394197336 - /// for a difference between paint without saveLayer and paint with saveLayer.) - /// - /// This will be only rarely needed. One case where you might need this is if - /// you have an image overlaid on a very different background color. In these - /// cases, consider whether you can avoid overlaying multiple colors in one - /// spot (e.g. by having the background color only present where the image is - /// absent). If you can, [antiAlias] would be fine and much faster. - /// - /// See also: - /// - /// * [antiAlias], which is much faster, and has similar clipping results. antiAliasWithSaveLayer, } -/// A description of the style to use when drawing on a [Canvas]. -/// -/// Most APIs on [Canvas] take a [Paint] object to describe the style -/// to use for that operation. abstract class Paint { - /// Constructs an empty [Paint] object with all fields initialized to - /// their defaults. - factory Paint() => - engine.experimentalUseSkia ? engine.CkPaint() : engine.SurfacePaint(); - - /// Whether to dither the output when drawing images. - /// - /// If false, the default value, dithering will be enabled when the input - /// color depth is higher than the output color depth. For example, - /// drawing an RGB8 image onto an RGB565 canvas. - /// - /// This value also controls dithering of [shader]s, which can make - /// gradients appear smoother. - /// - /// Whether or not dithering affects the output is implementation defined. - /// Some implementations may choose to ignore this completely, if they're - /// unable to control dithering. - /// - /// To ensure that dithering is consistently enabled for your entire - /// application, set this to true before invoking any drawing related code. + factory Paint() => engine.experimentalUseSkia ? engine.CkPaint() : engine.SurfacePaint(); static bool enableDithering = false; - - /// A blend mode to apply when a shape is drawn or a layer is composited. - /// - /// The source colors are from the shape being drawn (e.g. from - /// [Canvas.drawPath]) or layer being composited (the graphics that were drawn - /// between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying - /// the [colorFilter], if any. - /// - /// The destination colors are from the background onto which the shape or - /// layer is being composited. - /// - /// Defaults to [BlendMode.srcOver]. - /// - /// See also: - /// - /// * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite - /// the layer when [restore] is called. - /// * [BlendMode], which discusses the user of [saveLayer] with [blendMode]. BlendMode get blendMode; set blendMode(BlendMode value); - - /// Whether to paint inside shapes, the edges of shapes, or both. - /// - /// If null, defaults to [PaintingStyle.fill]. PaintingStyle get style; set style(PaintingStyle value); - - /// How wide to make edges drawn when [style] is set to - /// [PaintingStyle.stroke] or [PaintingStyle.strokeAndFill]. The - /// width is given in logical pixels measured in the direction - /// orthogonal to the direction of the path. - /// - /// The values null and 0.0 correspond to a hairline width. double get strokeWidth; set strokeWidth(double value); - - /// The kind of finish to place on the end of lines drawn when - /// [style] is set to [PaintingStyle.stroke] or - /// [PaintingStyle.strokeAndFill]. - /// - /// If null, defaults to [StrokeCap.butt], i.e. no caps. StrokeCap get strokeCap; set strokeCap(StrokeCap value); - - /// The kind of finish to use for line segment joins. - /// [style] is set to [PaintingStyle.stroke] or - /// [PaintingStyle.strokeAndFill]. Only applies to drawPath not drawPoints. - /// - /// If null, defaults to [StrokeCap.butt], i.e. no caps. StrokeJoin get strokeJoin; set strokeJoin(StrokeJoin value); - - /// Whether to apply anti-aliasing to lines and images drawn on the - /// canvas. - /// - /// Defaults to true. The value null is treated as false. bool get isAntiAlias; set isAntiAlias(bool value); Color get color; set color(Color value); - - /// Whether the colors of the image are inverted when drawn. - /// - /// Inverting the colors of an image applies a new color filter that will - /// be composed with any user provided color filters. This is primarily - /// used for implementing smart invert on iOS. bool get invertColors; set invertColors(bool value); - - /// The shader to use when stroking or filling a shape. - /// - /// When this is null, the [color] is used instead. - /// - /// See also: - /// - /// * [Gradient], a shader that paints a color gradient. - /// * [ImageShader], a shader that tiles an [Image]. - /// * [colorFilter], which overrides [shader]. - /// * [color], which is used if [shader] and [colorFilter] are null. Shader? get shader; set shader(Shader? value); - - /// A mask filter (for example, a blur) to apply to a shape after it has been - /// drawn but before it has been composited into the image. - /// - /// See [MaskFilter] for details. MaskFilter? get maskFilter; set maskFilter(MaskFilter? value); - - /// Controls the performance vs quality trade-off to use when applying - /// filters, such as [maskFilter], or when drawing images, as with - /// [Canvas.drawImageRect] or [Canvas.drawImageNine]. - /// - /// Defaults to [FilterQuality.none]. // TODO(ianh): verify that the image drawing methods actually respect this FilterQuality get filterQuality; set filterQuality(FilterQuality value); - - /// A color filter to apply when a shape is drawn or when a layer is - /// composited. - /// - /// See [ColorFilter] for details. - /// - /// When a shape is being drawn, [colorFilter] overrides [color] and [shader]. ColorFilter? get colorFilter; set colorFilter(ColorFilter? value); double get strokeMiterLimit; set strokeMiterLimit(double value); - - /// The [ImageFilter] to use when drawing raster images. - /// - /// For example, to blur an image using [Canvas.drawImage], apply an - /// [ImageFilter.blur]: - /// - /// ```dart - /// import 'dart:ui' as ui; - /// - /// ui.Image image; - /// - /// void paint(Canvas canvas, Size size) { - /// canvas.drawImage( - /// image, - /// Offset.zero, - /// Paint()..imageFilter = ui.ImageFilter.blur(sigmaX: .5, sigmaY: .5), - /// ); - /// } - /// ``` - /// - /// See also: - /// - /// * [MaskFilter], which is used for drawing geometry. ImageFilter? get imageFilter; set imageFilter(ImageFilter? value); } -/// Base class for objects such as [Gradient] and [ImageShader] which -/// correspond to shaders as used by [Paint.shader]. abstract class Shader { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. Shader._(); } -/// A shader (as used by [Paint.shader]) that renders a color gradient. -/// -/// There are several types of gradients, represented by the various -/// constructors on this class. abstract class Gradient extends Shader { - /// Creates a linear gradient from `from` to `to`. - /// - /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 - /// that specifies where `color[i]` begins in the gradient. If `colorStops` is - /// not provided, then only two stops, at 0.0 and 1.0, are implied (and - /// `color` must therefore only have two entries). - /// - /// The behavior before `from` and after `to` is described by the `tileMode` - /// argument. For details, see the [TileMode] enum. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png) - /// - /// If `from`, `to`, `colors`, or `tileMode` are null, or if `colors` or - /// `colorStops` contain null values, this constructor will throw a - /// [NoSuchMethodError]. factory Gradient.linear( Offset from, Offset to, @@ -1096,38 +278,9 @@ abstract class Gradient extends Shader { List? colorStops, TileMode tileMode = TileMode.clamp, Float64List? matrix4, - ]) => - engine.GradientLinear(from, to, colors, colorStops, tileMode, matrix4); - - /// Creates a radial gradient centered at `center` that ends at `radius` - /// distance from the center. - /// - /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 - /// that specifies where `color[i]` begins in the gradient. If `colorStops` is - /// not provided, then only two stops, at 0.0 and 1.0, are implied (and - /// `color` must therefore only have two entries). - /// - /// The behavior before and after the radius is described by the `tileMode` - /// argument. For details, see the [TileMode] enum. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png) - /// - /// If `center`, `radius`, `colors`, or `tileMode` are null, or if `colors` or - /// `colorStops` contain null values, this constructor will throw a - /// [NoSuchMethodError]. - /// - /// If `matrix4` is provided, the gradient fill will be transformed by the - /// specified 4x4 matrix relative to the local coordinate system. `matrix4` - /// must be a column-major matrix packed into a list of 16 values. - /// - /// If `focal` is provided and not equal to `center` and `focalRadius` is - /// provided and not equal to 0.0, the generated shader will be a two point - /// conical radial gradient, with `focal` being the center of the focal - /// circle and `focalRadius` being the radius of that circle. If `focal` is - /// provided and not equal to `center`, at least one of the two offsets must - /// not be equal to [Offset.zero]. + ]) => engine.experimentalUseSkia + ? engine.CkGradientLinear(from, to, colors, colorStops, tileMode, matrix4) + : engine.GradientLinear(from, to, colors, colorStops, tileMode, matrix4); factory Gradient.radial( Offset center, double radius, @@ -1143,229 +296,61 @@ abstract class Gradient extends Shader { // If focal == center and the focal radius is 0.0, it's still a regular radial gradient final Float32List? matrix32 = matrix4 != null ? engine.toMatrix32(matrix4) : null; if (focal == null || (focal == center && focalRadius == 0.0)) { - return engine.GradientRadial( - center, radius, colors, colorStops, tileMode, matrix32); + return engine.experimentalUseSkia + ? engine.CkGradientRadial(center, radius, colors, colorStops, tileMode, matrix32) + : engine.GradientRadial(center, radius, colors, colorStops, tileMode, matrix32); } else { assert(center != Offset.zero || focal != Offset.zero); // will result in exception(s) in Skia side - return engine.GradientConical(focal, focalRadius, center, radius, colors, - colorStops, tileMode, matrix32); + return engine.experimentalUseSkia + ? engine.CkGradientConical( + focal, focalRadius, center, radius, colors, colorStops, tileMode, matrix32) + : engine.GradientConical( + focal, focalRadius, center, radius, colors, colorStops, tileMode, matrix32); } } - - /// Creates a sweep gradient centered at `center` that starts at `startAngle` - /// and ends at `endAngle`. - /// - /// `startAngle` and `endAngle` should be provided in radians, with zero - /// radians being the horizontal line to the right of the `center` and with - /// positive angles going clockwise around the `center`. - /// - /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 - /// that specifies where `color[i]` begins in the gradient. If `colorStops` is - /// not provided, then only two stops, at 0.0 and 1.0, are implied (and - /// `color` must therefore only have two entries). - /// - /// The behavior before `startAngle` and after `endAngle` is described by the - /// `tileMode` argument. For details, see the [TileMode] enum. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_sweep.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_sweep.png) - /// - /// If `center`, `colors`, `tileMode`, `startAngle`, or `endAngle` are null, - /// or if `colors` or `colorStops` contain null values, this constructor will - /// throw a [NoSuchMethodError]. - /// - /// If `matrix4` is provided, the gradient fill will be transformed by the - /// specified 4x4 matrix relative to the local coordinate system. `matrix4` - /// must be a column-major matrix packed into a list of 16 values. factory Gradient.sweep( Offset center, List colors, [ List? colorStops, TileMode tileMode = TileMode.clamp, - double startAngle/*?*/ = 0.0, - double endAngle/*!*/ = math.pi * 2, + double startAngle = 0.0, + double endAngle = math.pi * 2, Float64List? matrix4, - ]) => - engine.GradientSweep( - center, colors, colorStops, tileMode, startAngle, endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null); + ]) => engine.experimentalUseSkia + ? engine.CkGradientSweep(center, colors, colorStops, tileMode, startAngle, + endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null) + : engine.GradientSweep(center, colors, colorStops, tileMode, startAngle, + endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null); } -/// Opaque handle to raw decoded image data (pixels). -/// -/// To obtain an [Image] object, use [instantiateImageCodec]. -/// -/// To draw an [Image], use one of the methods on the [Canvas] class, such as -/// [Canvas.drawImage]. abstract class Image { - /// The number of image pixels along the image's horizontal axis. int get width; - - /// The number of image pixels along the image's vertical axis. int get height; - - /// Converts the [Image] object into a byte array. - /// - /// The [format] argument specifies the format in which the bytes will be - /// returned. - /// - /// Returns a future that completes with the binary image data or an error - /// if encoding fails. - Future toByteData( - {ImageByteFormat format = ImageByteFormat.rawRgba}); - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. + Future toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}); void dispose(); @override String toString() => '[$width\u00D7$height]'; } -/// A description of a color filter to apply when drawing a shape or compositing -/// a layer with a particular [Paint]. A color filter is a function that takes -/// two colors, and outputs one color. When applied during compositing, it is -/// independently applied to each pixel of the layer being drawn before the -/// entire layer is merged with the destination. -/// -/// Instances of this class are used with [Paint.colorFilter] on [Paint] -/// objects. abstract class ColorFilter { - /// Creates a color filter that applies the blend mode given as the second - /// argument. The source color is the one given as the first argument, and the - /// destination color is the one from the layer being composited. - /// - /// The output of this filter is then composited into the background according - /// to the [Paint.blendMode], using the output of this filter as the source - /// and the background as the destination. - const factory ColorFilter.mode(Color color, BlendMode blendMode) = - engine.EngineColorFilter.mode; - - /// Construct a color filter that transforms a color by a 4x5 matrix. - /// - /// Every pixel's color value, repsented as an `[R, G, B, A]`, is matrix - /// multiplied to create a new color: - /// - /// ``` - /// | R' | | a00 a01 a02 a03 a04 | | R | - /// | G' | = | a10 a11 a22 a33 a44 | * | G | - /// | B' | | a20 a21 a22 a33 a44 | | B | - /// | A' | | a30 a31 a22 a33 a44 | | A | - /// ``` - /// - /// The matrix is in row-major order and the translation column is specified - /// in unnormalized, 0...255, space. For example, the identity matrix is: - /// - /// ``` - /// const ColorMatrix identity = ColorFilter.matrix([ - /// 1, 0, 0, 0, 0, - /// 0, 1, 0, 0, 0, - /// 0, 0, 1, 0, 0, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - /// - /// ## Examples - /// - /// An inversion color matrix: - /// - /// ``` - /// const ColorFilter invert = ColorFilter.matrix([ - /// -1, 0, 0, 0, 255, - /// 0, -1, 0, 0, 255, - /// 0, 0, -1, 0, 255, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - /// - /// A sepia-toned color matrix (values based on the [Filter Effects Spec](https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent)): - /// - /// ``` - /// const ColorFilter sepia = ColorFilter.matrix([ - /// 0.393, 0.769, 0.189, 0, 0, - /// 0.349, 0.686, 0.168, 0, 0, - /// 0.272, 0.534, 0.131, 0, 0, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - /// - /// A greyscale color filter (values based on the [Filter Effects Spec](https://www.w3.org/TR/filter-effects-1/#grayscaleEquivalent)): - /// - /// ``` - /// const ColorFilter greyscale = ColorFilter.matrix([ - /// 0.2126, 0.7152, 0.0722, 0, 0, - /// 0.2126, 0.7152, 0.0722, 0, 0, - /// 0.2126, 0.7152, 0.0722, 0, 0, - /// 0, 0, 0, 1, 0, - /// ]); - /// ``` - const factory ColorFilter.matrix(List matrix) = - engine.EngineColorFilter.matrix; - - /// Construct a color filter that applies the sRGB gamma curve to the RGB - /// channels. - const factory ColorFilter.linearToSrgbGamma() = - engine.EngineColorFilter.linearToSrgbGamma; - - /// Creates a color filter that applies the inverse of the sRGB gamma curve - /// to the RGB channels. - const factory ColorFilter.srgbToLinearGamma() = - engine.EngineColorFilter.srgbToLinearGamma; - - List webOnlySerializeToCssPaint() { - throw UnsupportedError('ColorFilter for CSS paint not yet supported'); - } + const factory ColorFilter.mode(Color color, BlendMode blendMode) = engine.EngineColorFilter.mode; + const factory ColorFilter.matrix(List matrix) = engine.EngineColorFilter.matrix; + const factory ColorFilter.linearToSrgbGamma() = engine.EngineColorFilter.linearToSrgbGamma; + const factory ColorFilter.srgbToLinearGamma() = engine.EngineColorFilter.srgbToLinearGamma; } -/// Styles to use for blurs in [MaskFilter] objects. // These enum values must be kept in sync with SkBlurStyle. enum BlurStyle { // These mirror SkBlurStyle and must be kept in sync. - - /// Fuzzy inside and outside. This is useful for painting shadows that are - /// offset from the shape that ostensibly is casting the shadow. normal, - - /// Solid inside, fuzzy outside. This corresponds to drawing the shape, and - /// additionally drawing the blur. This can make objects appear brighter, - /// maybe even as if they were fluorescent. solid, - - /// Nothing inside, fuzzy outside. This is useful for painting shadows for - /// partially transparent shapes, when they are painted separately but without - /// an offset, so that the shadow doesn't paint below the shape. outer, - - /// Fuzzy inside, nothing outside. This can make shapes appear to be lit from - /// within. inner, } -/// A mask filter to apply to shapes as they are painted. A mask filter is a -/// function that takes a bitmap of color pixels, and returns another bitmap of -/// color pixels. -/// -/// Instances of this class are used with [Paint.maskFilter] on [Paint] objects. class MaskFilter { - /// Creates a mask filter that takes the shape being drawn and blurs it. - /// - /// This is commonly used to approximate shadows. - /// - /// The `style` argument controls the kind of effect to draw; see [BlurStyle]. - /// - /// The `sigma` argument controls the size of the effect. It is the standard - /// deviation of the Gaussian blur to apply. The value must be greater than - /// zero. The sigma corresponds to very roughly half the radius of the effect - /// in pixels. - /// - /// A blur is an expensive operation and should therefore be used sparingly. - /// - /// The arguments must not be null. - /// - /// See also: - /// - /// * [Canvas.drawShadow], which is a more efficient way to draw shadows. const MaskFilter.blur( this._style, this._sigma, @@ -1374,11 +359,7 @@ class MaskFilter { final BlurStyle _style; final double _sigma; - - /// On the web returns the value of sigma passed to [MaskFilter.blur]. double get webOnlySigma => _sigma; - - /// On the web returns the value of `style` passed to [MaskFilter.blur]. BlurStyle get webOnlyBlurStyle => _style; @override @@ -1391,52 +372,20 @@ class MaskFilter { @override int get hashCode => hashValues(_style, _sigma); - List webOnlySerializeToCssPaint() { - return [_style.index, _sigma]; - } - @override String toString() => 'MaskFilter.blur($_style, ${_sigma.toStringAsFixed(1)})'; } -/// Quality levels for image filters. -/// -/// See [Paint.filterQuality]. enum FilterQuality { // This list comes from Skia's SkFilterQuality.h and the values (order) should // be kept in sync. - - /// Fastest possible filtering, albeit also the lowest quality. - /// - /// Typically this implies nearest-neighbour filtering. none, - - /// Better quality than [none], faster than [medium]. - /// - /// Typically this implies bilinear interpolation. low, - - /// Better quality than [low], faster than [high]. - /// - /// Typically this implies a combination of bilinear interpolation and - /// pyramidal parametric prefiltering (mipmaps). medium, - - /// Best possible quality filtering, albeit also the slowest. - /// - /// Typically this implies bicubic interpolation or better. high, } -/// A filter operation to apply to a raster image. -/// -/// See also: -/// -/// * [BackdropFilter], a widget that applies [ImageFilter] to its rendering. -/// * [SceneBuilder.pushBackdropFilter], which is the low-level API for using -/// this class. class ImageFilter { - /// Creates an image filter that applies a Gaussian blur. factory ImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) { if (engine.experimentalUseSkia) { return engine.CkImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY); @@ -1444,128 +393,46 @@ class ImageFilter { return engine.EngineImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY); } - ImageFilter.matrix(Float64List matrix4, - {FilterQuality filterQuality = FilterQuality.low}) { + ImageFilter.matrix(Float64List matrix4, {FilterQuality filterQuality = FilterQuality.low}) { // TODO(flutter_web): add implementation. - throw UnimplementedError( - 'ImageFilter.matrix not implemented for web platform.'); + throw UnimplementedError('ImageFilter.matrix not implemented for web platform.'); // if (matrix4.length != 16) // throw ArgumentError('"matrix4" must have 16 entries.'); } } -/// The format in which image bytes should be returned when using -/// [Image.toByteData]. enum ImageByteFormat { - /// Raw RGBA format. - /// - /// Unencoded bytes, in RGBA row-primary form, 8 bits per channel. rawRgba, - - /// Raw unmodified format. - /// - /// Unencoded bytes, in the image's existing format. For example, a grayscale - /// image may use a single 8-bit channel for each pixel. rawUnmodified, - - /// PNG format. - /// - /// A loss-less compression format for images. This format is well suited for - /// images with hard edges, such as screenshots or sprites, and images with - /// text. Transparency is supported. The PNG format supports images up to - /// 2,147,483,647 pixels in either dimension, though in practice available - /// memory provides a more immediate limitation on maximum image size. - /// - /// PNG images normally use the `.png` file extension and the `image/png` MIME - /// type. - /// - /// See also: - /// - /// * , the Wikipedia page on PNG. - /// * , the PNG standard. png, } -/// The format of pixel data given to [decodeImageFromPixels]. enum PixelFormat { - /// Each pixel is 32 bits, with the highest 8 bits encoding red, the next 8 - /// bits encoding green, the next 8 bits encoding blue, and the lowest 8 bits - /// encoding alpha. rgba8888, - - /// Each pixel is 32 bits, with the highest 8 bits encoding blue, the next 8 - /// bits encoding green, the next 8 bits encoding red, and the lowest 8 bits - /// encoding alpha. bgra8888, } -/// Callback signature for [decodeImageFromList]. typedef ImageDecoderCallback = void Function(Image result); -/// Information for a single frame of an animation. -/// -/// To obtain an instance of the [FrameInfo] interface, see -/// [Codec.getNextFrame]. abstract class FrameInfo { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. - /// - /// To obtain an instance of the [FrameInfo] interface, see - /// [Codec.getNextFrame]. FrameInfo._(); - - /// The duration this frame should be shown. Duration get duration => Duration(milliseconds: _durationMillis); int get _durationMillis => 0; - - /// The [Image] object for this frame. Image get image; } -/// A handle to an image codec. class Codec { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. - /// - /// To obtain an instance of the [Codec] interface, see - /// [instantiateImageCodec]. Codec._(); - - /// Number of frames in this image. int get frameCount => 0; - - /// Number of times to repeat the animation. - /// - /// * 0 when the animation should be played once. - /// * -1 for infinity repetitions. int get repetitionCount => 0; - - /// Fetches the next animation frame. - /// - /// Wraps back to the first frame after returning the last frame. - /// - /// The returned future can complete with an error if the decoding has failed. Future getNextFrame() { return engine.futurize(_getNextFrame); } - /// Returns an error message on failure, null on success. String? _getNextFrame(engine.Callback callback) => null; - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. void dispose() {} } -/// Instantiates an image codec [Codec] object. -/// -/// [list] is the binary image data (e.g a PNG or GIF binary data). -/// The data can be for either static or animated images. -/// -/// The following image formats are supported: {@macro flutter.dart:ui.imageFormats} -/// -/// The returned future can complete with an error if the image decoding has -/// failed. Future instantiateImageCodec( Uint8List list, { int? targetWidth, @@ -1577,9 +444,6 @@ Future instantiateImageCodec( _instantiateImageCodec(list, callback)); } -/// Instantiates a [Codec] object for an image binary data. -/// -/// Returns an error message if the instantiation has failed, null otherwise. String? _instantiateImageCodec( Uint8List list, engine.Callback callback, { @@ -1610,35 +474,29 @@ Future webOnlyInstantiateImageCodecFromUrl(Uri uri, } String? _instantiateImageCodecFromUrl( - Uri uri, - engine.WebOnlyImageCodecChunkCallback? chunkCallback, - engine.Callback callback) { - callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); - return null; + Uri uri, + engine.WebOnlyImageCodecChunkCallback? chunkCallback, + engine.Callback callback, +) { + if (engine.experimentalUseSkia) { + engine.skiaInstantiateWebImageCodec(uri.toString(), callback, chunkCallback); + return null; + } else { + callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); + return null; + } } -/// Loads a single image frame from a byte array into an [Image] object. -/// -/// This is a convenience wrapper around [instantiateImageCodec]. -/// Prefer using [instantiateImageCodec] which also supports multi frame images. void decodeImageFromList(Uint8List list, ImageDecoderCallback callback) { _decodeImageFromListAsync(list, callback); } -Future _decodeImageFromListAsync( - Uint8List list, ImageDecoderCallback callback) async { +Future _decodeImageFromListAsync(Uint8List list, ImageDecoderCallback callback) async { final Codec codec = await instantiateImageCodec(list); final FrameInfo frameInfo = await codec.getNextFrame(); callback(frameInfo.image); } -/// Convert an array of pixel values into an [Image] object. -/// -/// [pixels] is the pixel data in the encoding described by [format]. -/// -/// [rowBytes] is the number of bytes consumed by each row of pixels in the -/// data buffer. If unspecified, it defaults to [width] multipled by the -/// number of bytes per pixel in the provided [format]. void decodeImageFromPixels( Uint8List pixels, int width, @@ -1650,95 +508,50 @@ void decodeImageFromPixels( int? targetHeight, bool allowUpscaling = true, }) { - final Future codecFuture = _futurize( - (engine.Callback callback) { - return _instantiateImageCodec( - pixels, - callback, - width: width, - height: height, - format: format, - rowBytes: rowBytes, - ); + final Future codecFuture = _futurize((engine.Callback callback) { + return _instantiateImageCodec( + pixels, + callback, + width: width, + height: height, + format: format, + rowBytes: rowBytes, + ); }); codecFuture .then((Codec codec) => codec.getNextFrame()) .then((FrameInfo frameInfo) => callback(frameInfo.image)); } -/// A single shadow. -/// -/// Multiple shadows are stacked together in a [TextStyle]. class Shadow { - /// Construct a shadow. - /// - /// The default shadow is a black shadow with zero offset and zero blur. - /// Default shadows should be completely covered by the casting element, - /// and not be visble. - /// - /// Transparency should be adjusted through the [color] alpha. - /// - /// Shadow order matters due to compositing multiple translucent objects not - /// being commutative. const Shadow({ this.color = const Color(_kColorDefault), this.offset = Offset.zero, this.blurRadius = 0.0, - }) : assert(color != null, 'Text shadow color was null.'), // ignore: unnecessary_null_comparison - assert(offset != null, 'Text shadow offset was null.'), // ignore: unnecessary_null_comparison + }) : assert(color != null, + 'Text shadow color was null.'), // ignore: unnecessary_null_comparison + assert(offset != null, + 'Text shadow offset was null.'), // ignore: unnecessary_null_comparison assert(blurRadius >= 0.0, 'Text shadow blur radius should be non-negative.'); static const int _kColorDefault = 0xFF000000; - - /// Color that the shadow will be drawn with. - /// - /// The shadows are shapes composited directly over the base canvas, and do not - /// represent optical occlusion. final Color color; - - /// The displacement of the shadow from the casting element. - /// - /// Positive x/y offsets will shift the shadow to the right and down, while - /// negative offsets shift the shadow to the left and up. The offsets are - /// relative to the position of the element that is casting it. final Offset offset; - - /// The standard deviation of the Gaussian to convolve with the shadow's shape. final double blurRadius; - - /// Converts a blur radius in pixels to sigmas. - /// - /// See the sigma argument to [MaskFilter.blur]. - /// // See SkBlurMask::ConvertRadiusToSigma(). // static double convertRadiusToSigma(double radius) { return radius * 0.57735 + 0.5; } - /// The [blurRadius] in sigmas instead of logical pixels. - /// - /// See the sigma argument to [MaskFilter.blur]. double get blurSigma => convertRadiusToSigma(blurRadius); - - /// Create the [Paint] object that corresponds to this shadow description. - /// - /// The [offset] is not represented in the [Paint] object. - /// To honor this as well, the shape should be translated by [offset] before - /// being filled using this [Paint]. - /// - /// This class does not provide a way to disable shadows to avoid inconsistencies - /// in shadow blur rendering, primarily as a method of reducing test flakiness. - /// [toPaint] should be overriden in subclasses to provide this functionality. Paint toPaint() { return Paint() ..color = color ..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma); } - /// Returns a new shadow with its [offset] and [blurRadius] scaled by the given - /// factor. Shadow scale(double factor) { return Shadow( color: color, @@ -1747,25 +560,6 @@ class Shadow { ); } - /// Linearly interpolate between two shadows. - /// - /// If either shadow is null, this function linearly interpolates from a - /// a shadow that matches the other shadow in color but has a zero - /// offset and a zero blurRadius. - /// - /// {@template dart.ui.shadow.lerp} - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. - /// {@endtemplate} static Shadow? lerp(Shadow? a, Shadow? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (b == null) { @@ -1787,11 +581,6 @@ class Shadow { } } - /// Linearly interpolate between two lists of shadows. - /// - /// If the lists differ in length, excess items are lerped with null. - /// - /// {@macro dart.ui.shadow.lerp} static List? lerpList(List? a, List? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (a == null && b == null) { @@ -1816,10 +605,10 @@ class Shadow { if (identical(this, other)) { return true; } - return other is Shadow - && other.color == color - && other.offset == offset - && other.blurRadius == blurRadius; + return other is Shadow && + other.color == color && + other.offset == offset && + other.blurRadius == blurRadius; } @override @@ -1829,34 +618,17 @@ class Shadow { String toString() => 'TextShadow($color, $offset, $blurRadius)'; } - -/// A shader (as used by [Paint.shader]) that tiles an image. class ImageShader extends Shader { - /// Creates an image-tiling shader. The first argument specifies the image to - /// tile. The second and third arguments specify the [TileMode] for the x - /// direction and y direction respectively. The fourth argument gives the - /// matrix to apply to the effect. All the arguments are required and must not - /// be null. - factory ImageShader( - Image image, - TileMode tmx, - TileMode tmy, - Float64List matrix4) { + factory ImageShader(Image image, TileMode tmx, TileMode tmy, Float64List matrix4) { if (engine.experimentalUseSkia) { - return engine.EngineImageShader(image, tmx, tmy, matrix4); + return engine.CkImageShader(image, tmx, tmy, matrix4); } - throw UnsupportedError( - 'ImageShader not implemented for web platform.'); + throw UnsupportedError('ImageShader not implemented for web platform.'); } } - -/// A handle to a read-only byte buffer that is managed by the engine. class ImmutableBuffer { ImmutableBuffer._(this.length); - - /// Creates a copy of the data from a [Uint8List] suitable for internal use - /// in the engine. static Future fromUint8List(Uint8List list) async { final ImmutableBuffer instance = ImmutableBuffer._(list.length); instance._list = list; @@ -1864,37 +636,22 @@ class ImmutableBuffer { } Uint8List? _list; - - /// The length, in bytes, of the underlying data. final int length; - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. void dispose() => _list = null; } -/// A descriptor of data that can be turned into an [Image] via a [Codec]. -/// -/// Use this class to determine the height, width, and byte size of image data -/// before decoding it. class ImageDescriptor { - ImageDescriptor._() : _width = null, _height = null, _rowBytes = null, _format = null; - - /// Creates an image descriptor from encoded data in a supported format. + ImageDescriptor._() + : _width = null, + _height = null, + _rowBytes = null, + _format = null; static Future encoded(ImmutableBuffer buffer) async { final ImageDescriptor descriptor = ImageDescriptor._(); descriptor._data = buffer._list; return descriptor; } - /// Creates an image descriptor from raw image pixels. - /// - /// The `pixels` parameter is the pixel data in the encoding described by - /// `format`. - /// - /// The `rowBytes` parameter is the number of bytes consumed by each row of - /// pixels in the data buffer. If unspecified, it defaults to `width` multiplied - /// by the number of bytes per pixel in the provided `format`. // Not async because there's no expensive work to do here. ImageDescriptor.raw( ImmutableBuffer buffer, { @@ -1902,7 +659,10 @@ class ImageDescriptor { required int height, int? rowBytes, required PixelFormat pixelFormat, - }) : _width = width, _height = height, _rowBytes = rowBytes, _format = pixelFormat { + }) : _width = width, + _height = height, + _rowBytes = rowBytes, + _format = pixelFormat { _data = buffer._list; } @@ -1913,24 +673,14 @@ class ImageDescriptor { final PixelFormat? _format; Never _throw(String parameter) { - throw UnsupportedError('ImageDescriptor.$parameter is not supported on web.'); + throw UnsupportedError('ImageDescriptor.$parameter is not supported on web.'); } - /// The width, in pixels, of the image. int get width => _width ?? _throw('width'); - - /// The height, in pixels, of the image. int get height => _height ?? _throw('height'); - - /// The number of bytes per pixel in the image. - int get bytesPerPixel => throw UnsupportedError('ImageDescriptor.bytesPerPixel is not supported on web.'); - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. + int get bytesPerPixel => + throw UnsupportedError('ImageDescriptor.bytesPerPixel is not supported on web.'); void dispose() => _data = null; - - /// Creates a [Codec] object which is suitable for decoding the data in the - /// buffer to an [Image]. Future instantiateCodec({int? targetWidth, int? targetHeight}) { if (_data == null) { throw StateError('Object is disposed'); @@ -1943,16 +693,15 @@ class ImageDescriptor { allowUpscaling: false, ); } - return _futurize( - (engine.Callback callback) { - return _instantiateImageCodec( - _data!, - callback, - width: _width, - height: _height, - format: _format, - rowBytes: _rowBytes, - ); - }); + return _futurize((engine.Callback callback) { + return _instantiateImageCodec( + _data!, + callback, + width: _width, + height: _height, + format: _format, + rowBytes: _rowBytes, + ); + }); } } diff --git a/lib/web_ui/lib/src/ui/path.dart b/lib/web_ui/lib/src/ui/path.dart index 1514d2432f409..02d4e40d73484 100644 --- a/lib/web_ui/lib/src/ui/path.dart +++ b/lib/web_ui/lib/src/ui/path.dart @@ -5,25 +5,7 @@ // @dart = 2.10 part of ui; -/// A complex, one-dimensional subset of a plane. -/// -/// A path consists of a number of subpaths, and a _current point_. -/// -/// Subpaths consist of segments of various types, such as lines, -/// arcs, or beziers. Subpaths can be open or closed, and can -/// self-intersect. -/// -/// Closed subpaths enclose a (possibly discontiguous) region of the -/// plane based on the current [fillType]. -/// -/// The _current point_ is initially at the origin. After each -/// operation adding a segment to a subpath, the current point is -/// updated to the end of that segment. -/// -/// Paths can be drawn on canvases using [Canvas.drawPath], and can -/// used to create clip regions using [Canvas.clipPath]. abstract class Path { - /// Create a new empty [Path] object. factory Path() { if (engine.experimentalUseSkia) { return engine.CkPath(); @@ -31,11 +13,6 @@ abstract class Path { return engine.SurfacePath(); } } - - /// Creates a copy of another [Path]. - /// - /// This copy is fast and does not require additional memory unless either - /// the `source` path or the path returned by this constructor are modified. factory Path.from(Path source) { if (engine.experimentalUseSkia) { return engine.CkPath.from(source as engine.CkPath); @@ -43,231 +20,47 @@ abstract class Path { return engine.SurfacePath.from(source as engine.SurfacePath); } } - - /// Determines how the interior of this path is calculated. - /// - /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. PathFillType get fillType; set fillType(PathFillType value); - - /// Starts a new subpath at the given coordinate. void moveTo(double x, double y); - - /// Starts a new subpath at the given offset from the current point. void relativeMoveTo(double dx, double dy); - - /// Adds a straight line segment from the current point to the given - /// point. void lineTo(double x, double y); - - /// Adds a straight line segment from the current point to the point - /// at the given offset from the current point. void relativeLineTo(double dx, double dy); - - /// Adds a quadratic bezier segment that curves from the current - /// point to the given point (x2,y2), using the control point - /// (x1,y1). void quadraticBezierTo(double x1, double y1, double x2, double y2); - - /// Adds a quadratic bezier segment that curves from the current - /// point to the point at the offset (x2,y2) from the current point, - /// using the control point at the offset (x1,y1) from the current - /// point. void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2); - - /// Adds a cubic bezier segment that curves from the current point - /// to the given point (x3,y3), using the control points (x1,y1) and - /// (x2,y2). - void cubicTo( - double x1, double y1, double x2, double y2, double x3, double y3); - - /// Adds a cubic bezier segment that curves from the current point - /// to the point at the offset (x3,y3) from the current point, using - /// the control points at the offsets (x1,y1) and (x2,y2) from the - /// current point. - void relativeCubicTo( - double x1, double y1, double x2, double y2, double x3, double y3); - - /// Adds a bezier segment that curves from the current point to the - /// given point (x2,y2), using the control points (x1,y1) and the - /// weight w. If the weight is greater than 1, then the curve is a - /// hyperbola; if the weight equals 1, it's a parabola; and if it is - /// less than 1, it is an ellipse. + void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3); + void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3); void conicTo(double x1, double y1, double x2, double y2, double w); - - /// Adds a bezier segment that curves from the current point to the - /// point at the offset (x2,y2) from the current point, using the - /// control point at the offset (x1,y1) from the current point and - /// the weight w. If the weight is greater than 1, then the curve is - /// a hyperbola; if the weight equals 1, it's a parabola; and if it - /// is less than 1, it is an ellipse. void relativeConicTo(double x1, double y1, double x2, double y2, double w); - - /// If the `forceMoveTo` argument is false, adds a straight line - /// segment and an arc segment. - /// - /// If the `forceMoveTo` argument is true, starts a new subpath - /// consisting of an arc segment. - /// - /// In either case, the arc segment consists of the arc that follows - /// the edge of the oval bounded by the given rectangle, from - /// startAngle radians around the oval up to startAngle + sweepAngle - /// radians around the oval, with zero radians being the point on - /// the right hand side of the oval that crosses the horizontal line - /// that intersects the center of the rectangle and with positive - /// angles going clockwise around the oval. - /// - /// The line segment added if `forceMoveTo` is false starts at the - /// current point and ends at the start of the arc. - void arcTo( - Rect rect, double startAngle, double sweepAngle, bool forceMoveTo); - - /// Appends up to four conic curves weighted to describe an oval of `radius` - /// and rotated by `rotation`. - /// - /// The first curve begins from the last point in the path and the last ends - /// at `arcEnd`. The curves follow a path in a direction determined by - /// `clockwise` and `largeArc` in such a way that the sweep angle - /// is always less than 360 degrees. - /// - /// A simple line is appended if either either radii are zero or the last - /// point in the path is `arcEnd`. The radii are scaled to fit the last path - /// point if both are greater than zero but too small to describe an arc. - /// - /// See Conversion from endpoint to center parametrization described in - /// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter - /// as reference for implementation. + void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo); void arcToPoint( - Offset arcEnd, { - Radius radius = Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }); - - /// Appends up to four conic curves weighted to describe an oval of `radius` - /// and rotated by `rotation`. - /// - /// The last path point is described by (px, py). - /// - /// The first curve begins from the last point in the path and the last ends - /// at `arcEndDelta.dx + px` and `arcEndDelta.dy + py`. The curves follow a - /// path in a direction determined by `clockwise` and `largeArc` - /// in such a way that the sweep angle is always less than 360 degrees. - /// - /// A simple line is appended if either either radii are zero, or, both - /// `arcEndDelta.dx` and `arcEndDelta.dy` are zero. The radii are scaled to - /// fit the last path point if both are greater than zero but too small to - /// describe an arc. + Offset arcEnd, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }); void relativeArcToPoint( - Offset arcEndDelta, { - Radius radius = Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }); - - /// Adds a new subpath that consists of four lines that outline the - /// given rectangle. + Offset arcEndDelta, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }); void addRect(Rect rect); - - /// Adds a new subpath that consists of a curve that forms the - /// ellipse that fills the given rectangle. - /// - /// To add a circle, pass an appropriate rectangle as `oval`. - /// [Rect.fromCircle] can be used to easily describe the circle's center - /// [Offset] and radius. void addOval(Rect oval); - - /// Adds a new subpath with one arc segment that consists of the arc - /// that follows the edge of the oval bounded by the given - /// rectangle, from startAngle radians around the oval up to - /// startAngle + sweepAngle radians around the oval, with zero - /// radians being the point on the right hand side of the oval that - /// crosses the horizontal line that intersects the center of the - /// rectangle and with positive angles going clockwise around the - /// oval. void addArc(Rect oval, double startAngle, double sweepAngle); - - /// Adds a new subpath with a sequence of line segments that connect the given - /// points. - /// - /// If `close` is true, a final line segment will be added that connects the - /// last point to the first point. - /// - /// The `points` argument is interpreted as offsets from the origin. void addPolygon(List points, bool close); - - /// Adds a new subpath that consists of the straight lines and - /// curves needed to form the rounded rectangle described by the - /// argument. void addRRect(RRect rrect); - - /// Adds a new subpath that consists of the given `path` offset by the given - /// `offset`. - /// - /// If `matrix4` is specified, the path will be transformed by this matrix - /// after the matrix is translated by the given offset. The matrix is a 4x4 - /// matrix stored in column major order. void addPath(Path path, Offset offset, {Float64List? matrix4}); - - /// Adds the given path to this path by extending the current segment of this - /// path with the first segment of the given path. - /// - /// If `matrix4` is specified, the path will be transformed by this matrix - /// after the matrix is translated by the given `offset`. The matrix is a 4x4 - /// matrix stored in column major order. void extendWithPath(Path path, Offset offset, {Float64List? matrix4}); - - /// Closes the last subpath, as if a straight line had been drawn - /// from the current point to the first point of the subpath. void close(); - - /// Clears the [Path] object of all subpaths, returning it to the - /// same state it had when it was created. The _current point_ is - /// reset to the origin. void reset(); - - /// Tests to see if the given point is within the path. (That is, whether the - /// point would be in the visible portion of the path if the path was used - /// with [Canvas.clipPath].) - /// - /// The `point` argument is interpreted as an offset from the origin. - /// - /// Returns true if the point is in the path, and false otherwise. bool contains(Offset point); - - /// Returns a copy of the path with all the segments of every - /// subpath translated by the given offset. Path shift(Offset offset); - - /// Returns a copy of the path with all the segments of every - /// sub path transformed by the given matrix. Path transform(Float64List matrix4); - - /// Computes the bounding rectangle for this path. - /// - /// A path containing only axis-aligned points on the same straight line will - /// have no area, and therefore `Rect.isEmpty` will return true for such a - /// path. Consider checking `rect.width + rect.height > 0.0` instead, or - /// using the [computeMetrics] API to check the path length. - /// - /// For many more elaborate paths, the bounds may be inaccurate. For example, - /// when a path contains a circle, the points used to compute the bounds are - /// the circle's implied control points, which form a square around the - /// circle; if the circle has a transformation applied using [transform] then - /// that square is rotated, and the (axis-aligned, non-rotated) bounding box - /// therefore ends up grossly overestimating the actual area covered by the - /// circle. // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds Rect getBounds(); - - /// Combines the two paths according to the manner specified by the given - /// `operation`. - /// - /// The resulting path will be constructed from non-overlapping contours. The - /// curve order is reduced where possible so that cubics may be turned into - /// quadratics, and quadratics maybe turned into lines. static Path combine(PathOperation operation, Path path1, Path path2) { assert(path1 != null); // ignore: unnecessary_null_comparison assert(path2 != null); // ignore: unnecessary_null_comparison @@ -277,9 +70,5 @@ abstract class Path { throw UnimplementedError(); } - /// Creates a [PathMetrics] object for this path. - /// - /// If `forceClosed` is set to true, the contours of the path will be measured - /// as if they had been closed, even if they were not explicitly closed. PathMetrics computeMetrics({bool forceClosed = false}); } diff --git a/lib/web_ui/lib/src/ui/path_metrics.dart b/lib/web_ui/lib/src/ui/path_metrics.dart index b65e2b9288fa2..65f07f881f1a2 100644 --- a/lib/web_ui/lib/src/ui/path_metrics.dart +++ b/lib/web_ui/lib/src/ui/path_metrics.dart @@ -5,28 +5,11 @@ // @dart = 2.10 part of ui; -/// An iterable collection of [PathMetric] objects describing a [Path]. -/// -/// A [PathMetrics] object is created by using the [Path.computeMetrics] method, -/// and represents the path as it stood at the time of the call. Subsequent -/// modifications of the path do not affect the [PathMetrics] object. -/// -/// Each path metric corresponds to a segment, or contour, of a path. -/// -/// For example, a path consisting of a [Path.lineTo], a [Path.moveTo], and -/// another [Path.lineTo] will contain two contours and thus be represented by -/// two [PathMetric] objects. -/// -/// This iterable does not memoize. Callers who need to traverse the list -/// multiple times, or who need to randomly access elements of the list, should -/// use [toList] on this object. abstract class PathMetrics extends collection.IterableBase { @override Iterator get iterator; } -/// Used by [PathMetrics] to track iteration from one segment of a path to the -/// next for measurement. abstract class PathMetricIterator implements Iterator { @override PathMetric get current; @@ -35,114 +18,23 @@ abstract class PathMetricIterator implements Iterator { bool moveNext(); } -/// Utilities for measuring a [Path] and extracting sub-paths. -/// -/// Iterate over the object returned by [Path.computeMetrics] to obtain -/// [PathMetric] objects. Callers that want to randomly access elements or -/// iterate multiple times should use `path.computeMetrics().toList()`, since -/// [PathMetrics] does not memoize. -/// -/// Once created, the metrics are only valid for the path as it was specified -/// when [Path.computeMetrics] was called. If additional contours are added or -/// any contours are updated, the metrics need to be recomputed. Previously -/// created metrics will still refer to a snapshot of the path at the time they -/// were computed, rather than to the actual metrics for the new mutations to -/// the path. -/// -/// Implementation is based on -/// https://github.com/google/skia/blob/master/src/core/SkContourMeasure.cpp -/// to maintain consistency with native platforms. abstract class PathMetric { - /// Return the total length of the current contour. double get length; - - /// The zero-based index of the contour. - /// - /// [Path] objects are made up of zero or more contours. The first contour is - /// created once a drawing command (e.g. [Path.lineTo]) is issued. A - /// [Path.moveTo] command after a drawing command may create a new contour, - /// although it may not if optimizations are applied that determine the move - /// command did not actually result in moving the pen. - /// - /// This property is only valid with reference to its original iterator and - /// the contours of the path at the time the path's metrics were computed. If - /// additional contours were added or existing contours updated, this metric - /// will be invalid for the current state of the path. int get contourIndex; - - /// Computes the position of hte current contour at the given offset, and the - /// angle of the path at that point. - /// - /// For example, calling this method with a distance of 1.41 for a line from - /// 0.0,0.0 to 2.0,2.0 would give a point 1.0,1.0 and the angle 45 degrees - /// (but in radians). - /// - /// Returns null if the contour has zero [length]. - /// - /// The distance is clamped to the [length] of the current contour. Tangent? getTangentForOffset(double distance); - - /// Given a start and stop distance, return the intervening segment(s). - /// - /// `start` and `end` are pinned to legal values (0..[length]) - /// Returns null if the segment is 0 length or `start` > `stop`. - /// Begin the segment with a moveTo if `startWithMoveTo` is true. Path? extractPath(double start, double end, {bool startWithMoveTo = true}); - - /// Whether the contour is closed. - /// - /// Returns true if the contour ends with a call to [Path.close] (which may - /// have been implied when using [Path.addRect]) or if `forceClosed` was - /// specified as true in the call to [Path.computeMetrics]. Returns false - /// otherwise. bool get isClosed; } -/// The geometric description of a tangent: the angle at a point. -/// -/// See also: -/// * [PathMetric.getTangentForOffset], which returns the tangent of an offset -/// along a path. class Tangent { - /// Creates a [Tangent] with the given values. - /// - /// The arguments must not be null. const Tangent(this.position, this.vector) : assert(position != null), // ignore: unnecessary_null_comparison assert(vector != null); // ignore: unnecessary_null_comparison - - /// Creates a [Tangent] based on the angle rather than the vector. - /// - /// The [vector] is computed to be the unit vector at the given angle, - /// interpreted as clockwise radians from the x axis. factory Tangent.fromAngle(Offset position, double angle) { return Tangent(position, Offset(math.cos(angle), math.sin(angle))); } - - /// Position of the tangent. - /// - /// When used with [PathMetric.getTangentForOffset], this represents the - /// precise position that the given offset along the path corresponds to. final Offset position; - - /// The vector of the curve at [position]. - /// - /// When used with [PathMetric.getTangentForOffset], this is the vector of the - /// curve that is at the given offset along the path (i.e. the direction of - /// the curve at [position]). final Offset vector; - - /// The direction of the curve at [position]. - /// - /// When used with [PathMetric.getTangentForOffset], this is the angle of the - /// curve that is the given offset along the path (i.e. the direction of the - /// curve at [position]). - /// - /// This value is in radians, with 0.0 meaning pointing along the x axis in - /// the positive x-axis direction, positive numbers pointing downward toward - /// the negative y-axis, i.e. in a clockwise direction, and negative numbers - /// pointing upward toward the positive y-axis, i.e. in a counter-clockwise - /// direction. // flip the sign to be consistent with [Path.arcTo]'s `sweepAngle` double get angle => -math.atan2(vector.dy, vector.dx); } diff --git a/lib/web_ui/lib/src/ui/pointer.dart b/lib/web_ui/lib/src/ui/pointer.dart index e2f351cafedb5..0ad384b4d1209 100644 --- a/lib/web_ui/lib/src/ui/pointer.dart +++ b/lib/web_ui/lib/src/ui/pointer.dart @@ -5,71 +5,31 @@ // @dart = 2.10 part of ui; -/// How the pointer has changed since the last report. enum PointerChange { - /// The input from the pointer is no longer directed towards this receiver. cancel, - - /// The device has started tracking the pointer. - /// - /// For example, the pointer might be hovering above the device, having not yet - /// made contact with the surface of the device. add, - - /// The device is no longer tracking the pointer. - /// - /// For example, the pointer might have drifted out of the device's hover - /// detection range or might have been disconnected from the system entirely. remove, - - /// The pointer has moved with respect to the device while not in contact with - /// the device. hover, - - /// The pointer has made contact with the device. down, - - /// The pointer has moved with respect to the device while in contact with the - /// device. move, - - /// The pointer has stopped making contact with the device. up, } -/// The kind of pointer device. enum PointerDeviceKind { - /// A touch-based pointer device. touch, - - /// A mouse-based pointer device. mouse, - - /// A pointer device with a stylus. stylus, - - /// A pointer device with a stylus that has been inverted. invertedStylus, - - /// An unknown pointer device. unknown } -/// The kind of [PointerDeviceKind.signal]. enum PointerSignalKind { - /// The event is not associated with a pointer signal. none, - - /// A pointer-generated scroll (e.g., mouse wheel or trackpad scroll). scroll, - - /// An unknown pointer signal kind. unknown } -/// Information about the state of a pointer. class PointerData { - /// Creates an object that represents the state of a pointer. const PointerData({ this.embedderId = 0, this.timeStamp = Duration.zero, @@ -101,214 +61,74 @@ class PointerData { this.scrollDeltaX = 0.0, this.scrollDeltaY = 0.0, }); - - /// Unique identifier that ties the [PointerEvent] to embedder event created it. - /// - /// No two pointer events can have the same [embedderId]. This is different from - /// [pointerIdentifier] - used for hit-testing, whereas [embedderId] is used to - /// identify the platform event. final int embedderId; - - /// Time of event dispatch, relative to an arbitrary timeline. final Duration timeStamp; - - /// How the pointer has changed since the last report. final PointerChange change; - - /// The kind of input device for which the event was generated. final PointerDeviceKind kind; - - /// The kind of signal for a pointer signal event. final PointerSignalKind? signalKind; - - /// Unique identifier for the pointing device, reused across interactions. final int device; - - /// Unique identifier for the pointer. - /// - /// This field changes for each new pointer down event. Framework uses this - /// identifier to determine hit test result. final int pointerIdentifier; - - /// X coordinate of the position of the pointer, in physical pixels in the - /// global coordinate space. final double physicalX; - - /// Y coordinate of the position of the pointer, in physical pixels in the - /// global coordinate space. final double physicalY; - - /// The distance of pointer movement on X coordinate in physical pixels. final double physicalDeltaX; - - /// The distance of pointer movement on Y coordinate in physical pixels. final double physicalDeltaY; - - /// Bit field using the *Button constants (primaryMouseButton, - /// secondaryStylusButton, etc). For example, if this has the value 6 and the - /// [kind] is [PointerDeviceKind.invertedStylus], then this indicates an - /// upside-down stylus with both its primary and secondary buttons pressed. final int buttons; - - /// Set if an application from a different security domain is in any way - /// obscuring this application's window. (Aspirational; not currently - /// implemented.) final bool obscured; - - /// Set if this pointer data was synthesized by pointer data packet converter. - /// pointer data packet converter will synthesize additional pointer datas if - /// the input sequence of pointer data is illegal. - /// - /// For example, a down pointer data will be synthesized if the converter receives - /// a move pointer data while the pointer is not previously down. final bool synthesized; - - /// The pressure of the touch as a number ranging from 0.0, indicating a touch - /// with no discernible pressure, to 1.0, indicating a touch with "normal" - /// pressure, and possibly beyond, indicating a stronger touch. For devices - /// that do not detect pressure (e.g. mice), returns 1.0. final double pressure; - - /// The minimum value that [pressure] can return for this pointer. For devices - /// that do not detect pressure (e.g. mice), returns 1.0. This will always be - /// a number less than or equal to 1.0. final double pressureMin; - - /// The maximum value that [pressure] can return for this pointer. For devices - /// that do not detect pressure (e.g. mice), returns 1.0. This will always be - /// a greater than or equal to 1.0. final double pressureMax; - - /// The distance of the detected object from the input surface (e.g. the - /// distance of a stylus or finger from a touch screen), in arbitrary units on - /// an arbitrary (not necessarily linear) scale. If the pointer is down, this - /// is 0.0 by definition. final double distance; - - /// The maximum value that a distance can return for this pointer. If this - /// input device cannot detect "hover touch" input events, then this will be - /// 0.0. final double distanceMax; - - /// The area of the screen being pressed, scaled to a value between 0 and 1. - /// The value of size can be used to determine fat touch events. This value - /// is only set on Android, and is a device specific approximation within - /// the range of detectable values. So, for example, the value of 0.1 could - /// mean a touch with the tip of the finger, 0.2 a touch with full finger, - /// and 0.3 the full palm. final double size; - - /// The radius of the contact ellipse along the major axis, in logical pixels. final double radiusMajor; - - /// The radius of the contact ellipse along the minor axis, in logical pixels. final double radiusMinor; - - /// The minimum value that could be reported for radiusMajor and radiusMinor - /// for this pointer, in logical pixels. final double radiusMin; - - /// The minimum value that could be reported for radiusMajor and radiusMinor - /// for this pointer, in logical pixels. final double radiusMax; - - /// For PointerDeviceKind.touch events: - /// - /// The angle of the contact ellipse, in radius in the range: - /// - /// -pi/2 < orientation <= pi/2 - /// - /// ...giving the angle of the major axis of the ellipse with the y-axis - /// (negative angles indicating an orientation along the top-left / - /// bottom-right diagonal, positive angles indicating an orientation along the - /// top-right / bottom-left diagonal, and zero indicating an orientation - /// parallel with the y-axis). - /// - /// For PointerDeviceKind.stylus and PointerDeviceKind.invertedStylus events: - /// - /// The angle of the stylus, in radians in the range: - /// - /// -pi < orientation <= pi - /// - /// ...giving the angle of the axis of the stylus projected onto the input - /// surface, relative to the positive y-axis of that surface (thus 0.0 - /// indicates the stylus, if projected onto that surface, would go from the - /// contact point vertically up in the positive y-axis direction, pi would - /// indicate that the stylus would go down in the negative y-axis direction; - /// pi/4 would indicate that the stylus goes up and to the right, -pi/2 would - /// indicate that the stylus goes to the left, etc). final double orientation; - - /// For PointerDeviceKind.stylus and PointerDeviceKind.invertedStylus events: - /// - /// The angle of the stylus, in radians in the range: - /// - /// 0 <= tilt <= pi/2 - /// - /// ...giving the angle of the axis of the stylus, relative to the axis - /// perpendicular to the input surface (thus 0.0 indicates the stylus is - /// orthogonal to the plane of the input surface, while pi/2 indicates that - /// the stylus is flat on that surface). final double tilt; - - /// Opaque platform-specific data associated with the event. final int platformData; - - /// For events with signalKind of PointerSignalKind.scroll: - /// - /// The amount to scroll in the x direction, in physical pixels. final double scrollDeltaX; - - /// For events with signalKind of PointerSignalKind.scroll: - /// - /// The amount to scroll in the y direction, in physical pixels. final double scrollDeltaY; @override String toString() => 'PointerData(x: $physicalX, y: $physicalY)'; - - /// Returns a complete textual description of the information in this object. String toStringFull() { return '$runtimeType(' - 'embedderId: $embedderId, ' - 'timeStamp: $timeStamp, ' - 'change: $change, ' - 'kind: $kind, ' - 'signalKind: $signalKind, ' - 'device: $device, ' - 'pointerIdentifier: $pointerIdentifier, ' - 'physicalX: $physicalX, ' - 'physicalY: $physicalY, ' - 'physicalDeltaX: $physicalDeltaX, ' - 'physicalDeltaY: $physicalDeltaY, ' - 'buttons: $buttons, ' - 'synthesized: $synthesized, ' - 'pressure: $pressure, ' - 'pressureMin: $pressureMin, ' - 'pressureMax: $pressureMax, ' - 'distance: $distance, ' - 'distanceMax: $distanceMax, ' - 'size: $size, ' - 'radiusMajor: $radiusMajor, ' - 'radiusMinor: $radiusMinor, ' - 'radiusMin: $radiusMin, ' - 'radiusMax: $radiusMax, ' - 'orientation: $orientation, ' - 'tilt: $tilt, ' - 'platformData: $platformData, ' - 'scrollDeltaX: $scrollDeltaX, ' - 'scrollDeltaY: $scrollDeltaY' + 'embedderId: $embedderId, ' + 'timeStamp: $timeStamp, ' + 'change: $change, ' + 'kind: $kind, ' + 'signalKind: $signalKind, ' + 'device: $device, ' + 'pointerIdentifier: $pointerIdentifier, ' + 'physicalX: $physicalX, ' + 'physicalY: $physicalY, ' + 'physicalDeltaX: $physicalDeltaX, ' + 'physicalDeltaY: $physicalDeltaY, ' + 'buttons: $buttons, ' + 'synthesized: $synthesized, ' + 'pressure: $pressure, ' + 'pressureMin: $pressureMin, ' + 'pressureMax: $pressureMax, ' + 'distance: $distance, ' + 'distanceMax: $distanceMax, ' + 'size: $size, ' + 'radiusMajor: $radiusMajor, ' + 'radiusMinor: $radiusMinor, ' + 'radiusMin: $radiusMin, ' + 'radiusMax: $radiusMax, ' + 'orientation: $orientation, ' + 'tilt: $tilt, ' + 'platformData: $platformData, ' + 'scrollDeltaX: $scrollDeltaX, ' + 'scrollDeltaY: $scrollDeltaY' ')'; } } -/// A sequence of reports about the state of pointers. class PointerDataPacket { - /// Creates a packet of pointer data reports. - const PointerDataPacket({ this.data = const [] }) : assert(data != null); // ignore: unnecessary_null_comparison - - /// Data about the individual pointers in this packet. - /// - /// This list might contain multiple pieces of data about the same pointer. + const PointerDataPacket({this.data = const []}) + : assert(data != null); // ignore: unnecessary_null_comparison final List data; } diff --git a/lib/web_ui/lib/src/ui/semantics.dart b/lib/web_ui/lib/src/ui/semantics.dart index a0fde9a5b7916..24b0acc4f318c 100644 --- a/lib/web_ui/lib/src/ui/semantics.dart +++ b/lib/web_ui/lib/src/ui/semantics.dart @@ -5,8 +5,6 @@ // @dart = 2.10 part of ui; -/// The possible actions that can be conveyed from the operating system -/// accessibility APIs to a semantics node. class SemanticsAction { const SemanticsAction._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison @@ -31,172 +29,34 @@ class SemanticsAction { static const int _kDismissIndex = 1 << 18; static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; - - /// The numerical value for this action. - /// - /// Each action has one bit set in this bit field. final int index; - - /// The equivalent of a user briefly tapping the screen with the finger - /// without moving it. static const SemanticsAction tap = SemanticsAction._(_kTapIndex); - - /// The equivalent of a user pressing and holding the screen with the finger - /// for a few seconds without moving it. static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex); - - /// The equivalent of a user moving their finger across the screen from right - /// to left. - /// - /// This action should be recognized by controls that are horizontally - /// scrollable. - static const SemanticsAction scrollLeft = - SemanticsAction._(_kScrollLeftIndex); - - /// The equivalent of a user moving their finger across the screen from left - /// to right. - /// - /// This action should be recognized by controls that are horizontally - /// scrollable. - static const SemanticsAction scrollRight = - SemanticsAction._(_kScrollRightIndex); - - /// The equivalent of a user moving their finger across the screen from - /// bottom to top. - /// - /// This action should be recognized by controls that are vertically - /// scrollable. + static const SemanticsAction scrollLeft = SemanticsAction._(_kScrollLeftIndex); + static const SemanticsAction scrollRight = SemanticsAction._(_kScrollRightIndex); static const SemanticsAction scrollUp = SemanticsAction._(_kScrollUpIndex); - - /// The equivalent of a user moving their finger across the screen from top - /// to bottom. - /// - /// This action should be recognized by controls that are vertically - /// scrollable. - static const SemanticsAction scrollDown = - SemanticsAction._(_kScrollDownIndex); - - /// A request to increase the value represented by the semantics node. - /// - /// For example, this action might be recognized by a slider control. + static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex); static const SemanticsAction increase = SemanticsAction._(_kIncreaseIndex); - - /// A request to decrease the value represented by the semantics node. - /// - /// For example, this action might be recognized by a slider control. static const SemanticsAction decrease = SemanticsAction._(_kDecreaseIndex); - - /// A request to fully show the semantics node on screen. - /// - /// For example, this action might be send to a node in a scrollable list that - /// is partially off screen to bring it on screen. - static const SemanticsAction showOnScreen = - SemanticsAction._(_kShowOnScreenIndex); - - /// Move the cursor forward by one character. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. + static const SemanticsAction showOnScreen = SemanticsAction._(_kShowOnScreenIndex); static const SemanticsAction moveCursorForwardByCharacter = SemanticsAction._(_kMoveCursorForwardByCharacterIndex); - - /// Move the cursor backward by one character. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. static const SemanticsAction moveCursorBackwardByCharacter = SemanticsAction._(_kMoveCursorBackwardByCharacterIndex); - - /// Set the text selection to the given range. - /// - /// The provided argument is a Map which includes the keys `base` - /// and `extent` indicating where the selection within the `value` of the - /// semantics node should start and where it should end. Values for both - /// keys can range from 0 to length of `value` (inclusive). - /// - /// Setting `base` and `extent` to the same value will move the cursor to - /// that position (without selecting anything). - static const SemanticsAction setSelection = - SemanticsAction._(_kSetSelectionIndex); - - /// Copy the current selection to the clipboard. + static const SemanticsAction setSelection = SemanticsAction._(_kSetSelectionIndex); static const SemanticsAction copy = SemanticsAction._(_kCopyIndex); - - /// Cut the current selection and place it in the clipboard. static const SemanticsAction cut = SemanticsAction._(_kCutIndex); - - /// Paste the current content of the clipboard. static const SemanticsAction paste = SemanticsAction._(_kPasteIndex); - - /// Indicates that the nodes has gained accessibility focus. - /// - /// This handler is invoked when the node annotated with this handler gains - /// the accessibility focus. The accessibility focus is the - /// green (on Android with TalkBack) or black (on iOS with VoiceOver) - /// rectangle shown on screen to indicate what element an accessibility - /// user is currently interacting with. - /// - /// The accessibility focus is different from the input focus. The input focus - /// is usually held by the element that currently responds to keyboard inputs. - /// Accessibility focus and input focus can be held by two different nodes! static const SemanticsAction didGainAccessibilityFocus = SemanticsAction._(_kDidGainAccessibilityFocusIndex); - - /// Indicates that the nodes has lost accessibility focus. - /// - /// This handler is invoked when the node annotated with this handler - /// loses the accessibility focus. The accessibility focus is - /// the green (on Android with TalkBack) or black (on iOS with VoiceOver) - /// rectangle shown on screen to indicate what element an accessibility - /// user is currently interacting with. - /// - /// The accessibility focus is different from the input focus. The input focus - /// is usually held by the element that currently responds to keyboard inputs. - /// Accessibility focus and input focus can be held by two different nodes! static const SemanticsAction didLoseAccessibilityFocus = SemanticsAction._(_kDidLoseAccessibilityFocusIndex); - - /// Indicates that the user has invoked a custom accessibility action. - /// - /// This handler is added automatically whenever a custom accessibility - /// action is added to a semantics node. static const SemanticsAction customAction = SemanticsAction._(_kCustomAction); - - /// A request that the node should be dismissed. - /// - /// A [Snackbar], for example, may have a dismiss action to indicate to the - /// user that it can be removed after it is no longer relevant. On Android, - /// (with TalkBack) special hint text is spoken when focusing the node and - /// a custom action is availible in the local context menu. On iOS, - /// (with VoiceOver) users can perform a standard gesture to dismiss it. static const SemanticsAction dismiss = SemanticsAction._(_kDismissIndex); - - /// Move the cursor forward by one word. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. static const SemanticsAction moveCursorForwardByWord = SemanticsAction._(_kMoveCursorForwardByWordIndex); - - /// Move the cursor backward by one word. - /// - /// This is for example used by the cursor control in text fields. - /// - /// The action includes a boolean argument, which indicates whether the cursor - /// movement should extend (or start) a selection. static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex); - - /// The possible semantics actions. - /// - /// The map's key is the [index] of the action and the value is the action - /// itself. static const Map values = { _kTapIndex: tap, _kLongPressIndex: longPress, @@ -272,7 +132,6 @@ class SemanticsAction { } } -/// A Boolean value that can be associated with a semantics node. class SemanticsFlag { static const int _kHasCheckedStateIndex = 1 << 0; static const int _kIsCheckedIndex = 1 << 1; @@ -299,233 +158,30 @@ class SemanticsFlag { static const int _kIsLinkIndex = 1 << 22; const SemanticsFlag._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison - - /// The numerical value for this flag. - /// - /// Each flag has one bit set in this bit field. final int index; - - /// The semantics node has the quality of either being "checked" or "unchecked". - /// - /// This flag is mutually exclusive with [hasToggledState]. - /// - /// For example, a checkbox or a radio button widget has checked state. - /// - /// See also: - /// - /// * [SemanticsFlag.isChecked], which controls whether the node is "checked" or "unchecked". - static const SemanticsFlag hasCheckedState = - SemanticsFlag._(_kHasCheckedStateIndex); - - /// Whether a semantics node that [hasCheckedState] is checked. - /// - /// If true, the semantics node is "checked". If false, the semantics node is - /// "unchecked". - /// - /// For example, if a checkbox has a visible checkmark, [isChecked] is true. - /// - /// See also: - /// - /// * [SemanticsFlag.hasCheckedState], which enables a checked state. + static const SemanticsFlag hasCheckedState = SemanticsFlag._(_kHasCheckedStateIndex); static const SemanticsFlag isChecked = SemanticsFlag._(_kIsCheckedIndex); - - /// Whether a semantics node is selected. - /// - /// If true, the semantics node is "selected". If false, the semantics node is - /// "unselected". - /// - /// For example, the active tab in a tab bar has [isSelected] set to true. static const SemanticsFlag isSelected = SemanticsFlag._(_kIsSelectedIndex); - - /// Whether the semantic node represents a button. - /// - /// Platforms has special handling for buttons, for example Android's TalkBack - /// and iOS's VoiceOver provides an additional hint when the focused object is - /// a button. static const SemanticsFlag isButton = SemanticsFlag._(_kIsButtonIndex); - - /// Whether the semantic node represents a link. - /// - /// Platforms have special handling for links, for example, iOS's VoiceOver - /// provides an additional hint when the focused object is a link. static const SemanticsFlag isLink = SemanticsFlag._(_kIsLinkIndex); - - /// Whether the semantic node represents a text field. - /// - /// Text fields are announced as such and allow text input via accessibility - /// affordances. static const SemanticsFlag isTextField = SemanticsFlag._(_kIsTextFieldIndex); - - /// Whether the semantic node is read only. - /// - /// Only applicable when [isTextField] is true. static const SemanticsFlag isReadOnly = SemanticsFlag._(_kIsReadOnlyIndex); - - /// Whether the semantic node is able to hold the user's focus. - /// - /// The focused element is usually the current receiver of keyboard inputs. static const SemanticsFlag isFocusable = SemanticsFlag._(_kIsFocusableIndex); - - /// Whether the semantic node currently holds the user's focus. - /// - /// The focused element is usually the current receiver of keyboard inputs. static const SemanticsFlag isFocused = SemanticsFlag._(_kIsFocusedIndex); - - /// The semantics node has the quality of either being "enabled" or - /// "disabled". - /// - /// For example, a button can be enabled or disabled and therefore has an - /// "enabled" state. Static text is usually neither enabled nor disabled and - /// therefore does not have an "enabled" state. - static const SemanticsFlag hasEnabledState = - SemanticsFlag._(_kHasEnabledStateIndex); - - /// Whether a semantic node that [hasEnabledState] is currently enabled. - /// - /// A disabled element does not respond to user interaction. For example, a - /// button that currently does not respond to user interaction should be - /// marked as disabled. + static const SemanticsFlag hasEnabledState = SemanticsFlag._(_kHasEnabledStateIndex); static const SemanticsFlag isEnabled = SemanticsFlag._(_kIsEnabledIndex); - - /// Whether a semantic node is in a mutually exclusive group. - /// - /// For example, a radio button is in a mutually exclusive group because - /// only one radio button in that group can be marked as [isChecked]. - static const SemanticsFlag isInMutuallyExclusiveGroup = - SemanticsFlag._(_kIsInMutuallyExclusiveGroupIndex); - - /// Whether a semantic node is a header that divides content into sections. - /// - /// For example, headers can be used to divide a list of alphabetically - /// sorted words into the sections A, B, C, etc. as can be found in many - /// address book applications. + static const SemanticsFlag isInMutuallyExclusiveGroup = SemanticsFlag._(_kIsInMutuallyExclusiveGroupIndex); static const SemanticsFlag isHeader = SemanticsFlag._(_kIsHeaderIndex); - - /// Whether the value of the semantics node is obscured. - /// - /// This is usually used for text fields to indicate that its content - /// is a password or contains other sensitive information. static const SemanticsFlag isObscured = SemanticsFlag._(_kIsObscuredIndex); - - /// Whether the semantics node is the root of a subtree for which a route name - /// should be announced. - /// - /// When a node with this flag is removed from the semantics tree, the - /// framework will select the last in depth-first, paint order node with this - /// flag. When a node with this flag is added to the semantics tree, it is - /// selected automatically, unless there were multiple nodes with this flag - /// added. In this case, the last added node in depth-first, paint order - /// will be selected. - /// - /// From this selected node, the framework will search in depth-first, paint - /// order for the first node with a [namesRoute] flag and a non-null, - /// non-empty label. The [namesRoute] and [scopesRoute] flags may be on the - /// same node. The label of the found node will be announced as an edge - /// transition. If no non-empty, non-null label is found then: - /// - /// * VoiceOver will make a chime announcement. - /// * TalkBack will make no announcement - /// - /// Semantic nodes annotated with this flag are generally not a11y focusable. - /// - /// This is used in widgets such as Routes, Drawers, and Dialogs to - /// communicate significant changes in the visible screen. static const SemanticsFlag scopesRoute = SemanticsFlag._(_kScopesRouteIndex); - - /// Whether the semantics node label is the name of a visually distinct - /// route. - /// - /// This is used by certain widgets like Drawers and Dialogs, to indicate - /// that the node's semantic label can be used to announce an edge triggered - /// semantics update. - /// - /// Semantic nodes annotated with this flag will still recieve a11y focus. - /// - /// Updating this label within the same active route subtree will not cause - /// additional announcements. static const SemanticsFlag namesRoute = SemanticsFlag._(_kNamesRouteIndex); - - /// Whether the semantics node is considered hidden. - /// - /// Hidden elements are currently not visible on screen. They may be covered - /// by other elements or positioned outside of the visible area of a viewport. - /// - /// Hidden elements cannot gain accessibility focus though regular touch. The - /// only way they can be focused is by moving the focus to them via linear - /// navigation. - /// - /// Platforms are free to completely ignore hidden elements and new platforms - /// are encouraged to do so. - /// - /// Instead of marking an element as hidden it should usually be excluded from - /// the semantics tree altogether. Hidden elements are only included in the - /// semantics tree to work around platform limitations and they are mainly - /// used to implement accessibility scrolling on iOS. static const SemanticsFlag isHidden = SemanticsFlag._(_kIsHiddenIndex); - - /// Whether the semantics node represents an image. - /// - /// Both TalkBack and VoiceOver will inform the user the semantics node - /// represents an image. static const SemanticsFlag isImage = SemanticsFlag._(_kIsImageIndex); - - /// Whether the semantics node is a live region. - /// - /// A live region indicates that updates to semantics node are important. - /// Platforms may use this information to make polite announcements to the - /// user to inform them of updates to this node. - /// - /// An example of a live region is a [SnackBar] widget. On Android, A live - /// region causes a polite announcement to be generated automatically, even - /// if the user does not have focus of the widget. - static const SemanticsFlag isLiveRegion = - SemanticsFlag._(_kIsLiveRegionIndex); - - /// The semantics node has the quality of either being "on" or "off". - /// - /// This flag is mutually exclusive with [hasCheckedState]. - /// - /// For example, a switch has toggled state. - /// - /// See also: - /// - /// * [SemanticsFlag.isToggled], which controls whether the node is "on" or "off". - static const SemanticsFlag hasToggledState = - SemanticsFlag._(_kHasToggledStateIndex); - - /// If true, the semantics node is "on". If false, the semantics node is - /// "off". - /// - /// For example, if a switch is in the on position, [isToggled] is true. - /// - /// See also: - /// - /// * [SemanticsFlag.hasToggledState], which enables a toggled state. + static const SemanticsFlag isLiveRegion = SemanticsFlag._(_kIsLiveRegionIndex); + static const SemanticsFlag hasToggledState = SemanticsFlag._(_kHasToggledStateIndex); static const SemanticsFlag isToggled = SemanticsFlag._(_kIsToggledIndex); - - /// Whether the platform can scroll the semantics node when the user attempts - /// to move focus to an offscreen child. - /// - /// For example, a [ListView] widget has implicit scrolling so that users can - /// easily move to the next visible set of children. A [TabBar] widget does - /// not have implicit scrolling, so that users can navigate into the tab - /// body when reaching the end of the tab bar. - static const SemanticsFlag hasImplicitScrolling = - SemanticsFlag._(_kHasImplicitScrollingIndex); - - /// Whether the value of the semantics node is coming from a multi-line text - /// field. - /// - /// This isused for text fields to distinguish single-line text field from - /// multi-line ones. + static const SemanticsFlag hasImplicitScrolling = SemanticsFlag._(_kHasImplicitScrollingIndex); static const SemanticsFlag isMultiline = SemanticsFlag._(_kIsMultilineIndex); - - /// The possible semantics flags. - /// - /// The map's key is the [index] of the flag and the value is the flag itself. - /// The possible semantics flags. - /// - /// The map's key is the [index] of the flag and the value is the flag itself. static const Map values = { _kHasCheckedStateIndex: hasCheckedState, _kIsCheckedIndex: isChecked, @@ -607,65 +263,10 @@ class SemanticsFlag { } } -/// An object that creates [SemanticsUpdate] objects. -/// -/// Once created, the [SemanticsUpdate] objects can be passed to -/// [Window.updateSemantics] to update the semantics conveyed to the user. class SemanticsUpdateBuilder { - /// Creates an empty [SemanticsUpdateBuilder] object. SemanticsUpdateBuilder(); - final List _nodeUpdates = - []; - - /// Update the information associated with the node with the given `id`. - /// - /// The semantics nodes form a tree, with the root of the tree always having - /// an id of zero. The `childrenInTraversalOrder` and `childrenInHitTestOrder` - /// are the ids of the nodes that are immediate children of this node. The - /// former enumerates children in traversal order, and the latter enumerates - /// the same children in the hit test order. The two lists must have the same - /// length and contain the same ids. They may only differ in the order the - /// ids are listed in. For more information about different child orders, see - /// [DebugSemanticsDumpOrder]. - /// - /// The system retains the nodes that are currently reachable from the root. - /// A given update need not contain information for nodes that do not change - /// in the update. If a node is not reachable from the root after an update, - /// the node will be discarded from the tree. - /// - /// The `flags` are a bit field of [SemanticsFlag]s that apply to this node. - /// - /// The `actions` are a bit field of [SemanticsAction]s that can be undertaken - /// by this node. If the user wishes to undertake one of these actions on this - /// node, the [Window.onSemanticsAction] will be called with `id` and one of - /// the possible [SemanticsAction]s. Because the semantics tree is maintained - /// asynchronously, the [Window.onSemanticsAction] callback might be called - /// with an action that is no longer possible. - /// - /// The `label` is a string that describes this node. The `value` property - /// describes the current value of the node as a string. The `increasedValue` - /// string will become the `value` string after a [SemanticsAction.increase] - /// action is performed. The `decreasedValue` string will become the `value` - /// string after a [SemanticsAction.decrease] action is performed. The `hint` - /// string describes what result an action performed on this node has. The - /// reading direction of all these strings is given by `textDirection`. - /// - /// The fields 'textSelectionBase' and 'textSelectionExtent' describe the - /// currently selected text within `value`. - /// - /// For scrollable nodes `scrollPosition` describes the current scroll - /// position in logical pixel. `scrollExtentMax` and `scrollExtentMin` - /// describe the maximum and minimum in-rage values that `scrollPosition` can - /// be. Both or either may be infinity to indicate unbound scrolling. The - /// value for `scrollPosition` can (temporarily) be outside this range, for - /// example during an overscroll. - /// - /// The `rect` is the region occupied by this node in its own coordinate - /// system. - /// - /// The `transform` is a matrix that maps this node's coordinate system into - /// its parent's coordinate system. + final List _nodeUpdates = []; void updateNode({ required int id, required int flags, @@ -726,16 +327,14 @@ class SemanticsUpdateBuilder { )); } - void updateCustomAction( - {required int id, String? label, String? hint, int overrideId = -1}) { + void updateCustomAction({ + required int id, + String? label, + String? hint, + int overrideId = -1, + }) { // TODO(yjbanov): implement. } - - /// Creates a [SemanticsUpdate] object that encapsulates the updates recorded - /// by this object. - /// - /// The returned object can be passed to [Window.updateSemantics] to actually - /// update the semantics retained by the system. SemanticsUpdate build() { return SemanticsUpdate._( nodeUpdates: _nodeUpdates, @@ -743,23 +342,8 @@ class SemanticsUpdateBuilder { } } -/// An opaque object representing a batch of semantics updates. -/// -/// To create a SemanticsUpdate object, use a [SemanticsUpdateBuilder]. -/// -/// Semantics updates can be applied to the system's retained semantics tree -/// using the [Window.updateSemantics] method. abstract class SemanticsUpdate { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. - /// - /// To create a SemanticsUpdate object, use a [SemanticsUpdateBuilder]. factory SemanticsUpdate._({List? nodeUpdates}) = engine.SemanticsUpdate; - - /// Releases the resources used by this semantics update. - /// - /// After calling this function, the semantics update is cannot be used - /// further. void dispose(); } diff --git a/lib/web_ui/lib/src/ui/test_embedding.dart b/lib/web_ui/lib/src/ui/test_embedding.dart index 955dbfe53c0db..7007ebf4420c2 100644 --- a/lib/web_ui/lib/src/ui/test_embedding.dart +++ b/lib/web_ui/lib/src/ui/test_embedding.dart @@ -7,30 +7,22 @@ // @dart = 2.10 part of ui; -/// Used to track when the platform is initialized. This ensures the test fonts -/// are available. Future? _testPlatformInitializedFuture; -/// If the platform is already initialized (by a previous test), then run the test -/// body immediately. Otherwise, initialize the platform then run the test. -Future ensureTestPlatformInitializedThenRunTest( - dynamic Function() body) { +Future ensureTestPlatformInitializedThenRunTest(dynamic Function() body) { if (_testPlatformInitializedFuture == null) { debugEmulateFlutterTesterEnvironment = true; // Initializing the platform will ensure that the test font is loaded. - _testPlatformInitializedFuture = webOnlyInitializePlatform( - assetManager: engine.WebOnlyMockAssetManager()); + _testPlatformInitializedFuture = + webOnlyInitializePlatform(assetManager: engine.WebOnlyMockAssetManager()); } return _testPlatformInitializedFuture!.then((_) => body()); } -/// Used to track when the platform is initialized. This ensures the test fonts -/// are available. // TODO(yjbanov): can we make this late non-null? See https://github.com/dart-lang/sdk/issues/42214 Future? _platformInitializedFuture; -/// Initializes domRenderer with specific devicePixelRatio and physicalSize. Future webOnlyInitializeTestDomRenderer({double devicePixelRatio = 3.0}) { // Force-initialize DomRenderer so it doesn't overwrite test pixel ratio. engine.domRenderer; diff --git a/lib/web_ui/lib/src/ui/text.dart b/lib/web_ui/lib/src/ui/text.dart index 58698d862a6d4..054ac36a84ba4 100644 --- a/lib/web_ui/lib/src/ui/text.dart +++ b/lib/web_ui/lib/src/ui/text.dart @@ -6,99 +6,34 @@ // @dart = 2.10 part of ui; -/// Whether to slant the glyphs in the font enum FontStyle { - /// Use the upright glyphs normal, - - /// Use glyphs designed for slanting italic, } -/// Where to vertically align the placeholder relative to the surrounding text. -/// -/// Used by [ParagraphBuilder.addPlaceholder]. enum PlaceholderAlignment { - /// Match the baseline of the placeholder with the baseline. - /// - /// The [TextBaseline] to use must be specified and non-null when using this - /// alignment mode. baseline, - - /// Align the bottom edge of the placeholder with the baseline such that the - /// placeholder sits on top of the baseline. - /// - /// The [TextBaseline] to use must be specified and non-null when using this - /// alignment mode. aboveBaseline, - - /// Align the top edge of the placeholder with the baseline specified - /// such that the placeholder hangs below the baseline. - /// - /// The [TextBaseline] to use must be specified and non-null when using this - /// alignment mode. belowBaseline, - - /// Align the top edge of the placeholder with the top edge of the font. - /// - /// When the placeholder is very tall, the extra space will hang from - /// the top and extend through the bottom of the line. top, - - /// Align the bottom edge of the placeholder with the top edge of the font. - /// - /// When the placeholder is very tall, the extra space will rise from the - /// bottom and extend through the top of the line. bottom, - - /// Align the middle of the placeholder with the middle of the text. - /// - /// When the placeholder is very tall, the extra space will grow equally - /// from the top and bottom of the line. middle, } -/// The thickness of the glyphs used to draw the text class FontWeight { const FontWeight._(this.index); - - /// The encoded integer value of this font weight. final int index; - - /// Thin, the least thick static const FontWeight w100 = FontWeight._(0); - - /// Extra-light static const FontWeight w200 = FontWeight._(1); - - /// Light static const FontWeight w300 = FontWeight._(2); - - /// Normal / regular / plain static const FontWeight w400 = FontWeight._(3); - - /// Medium static const FontWeight w500 = FontWeight._(4); - - /// Semi-bold static const FontWeight w600 = FontWeight._(5); - - /// Bold static const FontWeight w700 = FontWeight._(6); - - /// Extra-bold static const FontWeight w800 = FontWeight._(7); - - /// Black, the most thick static const FontWeight w900 = FontWeight._(8); - - /// The default font weight. static const FontWeight normal = w400; - - /// A commonly used font weight that is heavier than normal. static const FontWeight bold = w700; - - /// A list of all the font weights. static const List values = [ w100, w200, @@ -110,27 +45,6 @@ class FontWeight { w800, w900 ]; - - /// Linearly interpolates between two font weights. - /// - /// Rather than using fractional weights, the interpolation rounds to the - /// nearest weight. - /// - /// Any null values for `a` or `b` are interpreted as equivalent to [normal] - /// (also known as [w400]). - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). The result - /// is clamped to the range [w100]–[w900]. - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. static FontWeight? lerp(FontWeight? a, FontWeight? b, double t) { assert(t != null); // ignore: unnecessary_null_comparison if (a == null && b == null) @@ -154,129 +68,35 @@ class FontWeight { } } -/// A feature tag and value that affect the selection of glyphs in a font. class FontFeature { - /// Creates a [FontFeature] object, which can be added to a [TextStyle] to - /// change how the engine selects glyphs when rendering text. - /// - /// `feature` is the four-character tag that identifies the feature. - /// These tags are specified by font formats such as OpenType. - /// - /// `value` is the value that the feature will be set to. The behavior - /// of the value depends on the specific feature. Many features are - /// flags whose value can be 1 (when enabled) or 0 (when disabled). - /// - /// See const FontFeature(this.feature, [this.value = 1]) : assert(feature != null), // ignore: unnecessary_null_comparison assert(feature.length == 4), assert(value != null), // ignore: unnecessary_null_comparison assert(value >= 0); - - /// Create a [FontFeature] object that enables the feature with the given tag. const FontFeature.enable(String feature) : this(feature, 1); - - /// Create a [FontFeature] object that disables the feature with the given tag. const FontFeature.disable(String feature) : this(feature, 0); - - /// Randomize the alternate forms used in text. - /// - /// For example, this can be used with suitably-prepared handwriting fonts to - /// vary the forms used for each character, so that, for instance, the word - /// "cross-section" would be rendered with two different "c"s, two different "o"s, - /// and three different "s"s. - /// - /// See also: - /// - /// * const FontFeature.randomize() : feature = 'rand', value = 1; - - /// Select a stylistic set. - /// - /// Fonts may have up to 20 stylistic sets, numbered 1 through 20. - /// - /// See also: - /// - /// * factory FontFeature.stylisticSet(int value) { assert(value >= 1); assert(value <= 20); return FontFeature('ss${value.toString().padLeft(2, "0")}'); } - - /// Use the slashed zero. - /// - /// Some fonts contain both a circular zero and a zero with a slash. This - /// enables the use of the latter form. - /// - /// This is overridden by [FontFeature.oldstyleFigures]. - /// - /// See also: - /// - /// * const FontFeature.slashedZero() : feature = 'zero', value = 1; - - /// Use oldstyle figures. - /// - /// Some fonts have variants of the figures (e.g. the digit 9) that, when - /// this feature is enabled, render with descenders under the baseline instead - /// of being entirely above the baseline. - /// - /// This overrides [FontFeature.slashedZero]. - /// - /// See also: - /// - /// * const FontFeature.oldstyleFigures() : feature = 'onum', value = 1; - - /// Use proportional (varying width) figures. - /// - /// For fonts that have both proportional and tabular (monospace) figures, - /// this enables the proportional figures. - /// - /// This is mutually exclusive with [FontFeature.tabularFigures]. - /// - /// The default behavior varies from font to font. - /// - /// See also: - /// - /// * const FontFeature.proportionalFigures() : feature = 'pnum', value = 1; - - /// Use tabular (monospace) figures. - /// - /// For fonts that have both proportional (varying width) and tabular figures, - /// this enables the tabular figures. - /// - /// This is mutually exclusive with [FontFeature.proportionalFigures]. - /// - /// The default behavior varies from font to font. - /// - /// See also: - /// - /// * const FontFeature.tabularFigures() : feature = 'tnum', value = 1; - - /// The tag that identifies the effect of this feature. Must consist of 4 - /// ASCII characters (typically lowercase letters). - /// - /// See final String feature; - - /// The value assigned to this feature. - /// - /// Must be a positive integer. Many features are Boolean values that accept - /// values of either 0 (feature is disabled) or 1 (feature is enabled). final int value; @override @@ -299,53 +119,23 @@ class FontFeature { String toString() => 'FontFeature($feature, $value)'; } -/// Whether and how to align text horizontally. // The order of this enum must match the order of the values in RenderStyleConstants.h's ETextAlign. enum TextAlign { - /// Align the text on the left edge of the container. left, - - /// Align the text on the right edge of the container. right, - - /// Align the text in the center of the container. center, - - /// Stretch lines of text that end with a soft line break to fill the width of - /// the container. - /// - /// Lines that end with hard line breaks are aligned towards the [start] edge. justify, - - /// Align the text on the leading edge of the container. - /// - /// For left-to-right text ([TextDirection.ltr]), this is the left edge. - /// - /// For right-to-left text ([TextDirection.rtl]), this is the right edge. start, - - /// Align the text on the trailing edge of the container. - /// - /// For left-to-right text ([TextDirection.ltr]), this is the right edge. - /// - /// For right-to-left text ([TextDirection.rtl]), this is the left edge. end, } -/// A horizontal line used for aligning text. enum TextBaseline { - /// The horizontal line used to align the bottom of glyphs for alphabetic characters. alphabetic, - - /// The horizontal line used to align ideographic characters. ideographic, } -/// A linear decoration to draw near the text. class TextDecoration { const TextDecoration._(this._mask); - - /// Creates a decoration that paints the union of all the given decorations. factory TextDecoration.combine(List decorations) { int mask = 0; for (TextDecoration decoration in decorations) { @@ -355,22 +145,13 @@ class TextDecoration { } final int _mask; - - /// Whether this decoration will paint at least as much decoration as the given decoration. bool contains(TextDecoration other) { return (_mask | other._mask) == _mask; } - /// Do not draw a decoration static const TextDecoration none = TextDecoration._(0x0); - - /// Draw a line underneath each line of text static const TextDecoration underline = TextDecoration._(0x1); - - /// Draw a line above each line of text static const TextDecoration overline = TextDecoration._(0x2); - - /// Draw a line through each line of text static const TextDecoration lineThrough = TextDecoration._(0x4); @override @@ -404,81 +185,24 @@ class TextDecoration { } } -/// The style in which to draw a text decoration enum TextDecorationStyle { - /// Draw a solid line solid, - - /// Draw two lines double, - - /// Draw a dotted line dotted, - - /// Draw a dashed line dashed, - - /// Draw a sinusoidal line wavy } -/// Defines how the paragraph will apply [TextStyle.height] the ascent of the -/// first line and descent of the last line. -/// -/// The boolean value represents whether the [TextStyle.height] modifier will -/// be applied to the corresponding metric. By default, all properties are true, -/// and [TextStyle.height] is applied as normal. When set to false, the font's -/// default ascent will be used. class TextHeightBehavior { - - /// Creates a new TextHeightBehavior object. - /// - /// * applyHeightToFirstAscent: When true, the [TextStyle.height] modifier - /// will be applied to the ascent of the first line. When false, the font's - /// default ascent will be used. - /// * applyHeightToLastDescent: When true, the [TextStyle.height] modifier - /// will be applied to the descent of the last line. When false, the font's - /// default descent will be used. - /// - /// All properties default to true (height modifications applied as normal). const TextHeightBehavior({ this.applyHeightToFirstAscent = true, this.applyHeightToLastDescent = true, }); - - /// Creates a new TextHeightBehavior object from an encoded form. - /// - /// See [encode] for the creation of the encoded form. const TextHeightBehavior.fromEncoded(int encoded) : applyHeightToFirstAscent = (encoded & 0x1) == 0, applyHeightToLastDescent = (encoded & 0x2) == 0; - - - /// Whether to apply the [TextStyle.height] modifier to the ascent of the first - /// line in the paragraph. - /// - /// When true, the [TextStyle.height] modifier will be applied to to the ascent - /// of the first line. When false, the font's default ascent will be used and - /// the [TextStyle.height] will have no effect on the ascent of the first line. - /// - /// This property only has effect if a non-null [TextStyle.height] is specified. - /// - /// Defaults to true (height modifications applied as normal). final bool applyHeightToFirstAscent; - - /// Whether to apply the [TextStyle.height] modifier to the descent of the last - /// line in the paragraph. - /// - /// When true, the [TextStyle.height] modifier will be applied to to the descent - /// of the last line. When false, the font's default descent will be used and - /// the [TextStyle.height] will have no effect on the descent of the last line. - /// - /// This property only has effect if a non-null [TextStyle.height] is specified. - /// - /// Defaults to true (height modifications applied as normal). final bool applyHeightToLastDescent; - - /// Returns an encoded int representation of this object. int encode() { return (applyHeightToFirstAscent ? 0 : 1 << 0) | (applyHeightToLastDescent ? 0 : 1 << 1); } @@ -486,7 +210,7 @@ class TextHeightBehavior { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) - return false; + return false; return other is TextHeightBehavior && other.applyHeightToFirstAscent == applyHeightToFirstAscent && other.applyHeightToLastDescent == applyHeightToLastDescent; @@ -509,33 +233,7 @@ class TextHeightBehavior { } } -/// An opaque object that determines the size, position, and rendering of text. abstract class TextStyle { - /// Creates a new TextStyle object. - /// - /// * `color`: The color to use when painting the text. If this is specified, `foreground` must be null. - /// * `decoration`: The decorations to paint near the text (e.g., an underline). - /// * `decorationColor`: The color in which to paint the text decorations. - /// * `decorationStyle`: The style in which to paint the text decorations (e.g., dashed). - /// * `fontWeight`: The typeface thickness to use when painting the text (e.g., bold). - /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., italics). - /// * `fontFamily`: The name of the font to use when painting the text (e.g., Roboto). If a `fontFamilyFallback` is - /// provided and `fontFamily` is not, then the first font family in `fontFamilyFallback` will take the postion of - /// the preferred font family. When a higher priority font cannot be found or does not contain a glyph, a lower - /// priority font will be used. - /// * `fontFamilyFallback`: An ordered list of the names of the fonts to fallback on when a glyph cannot - /// be found in a higher priority font. When the `fontFamily` is null, the first font family in this list - /// is used as the preferred font. Internally, the 'fontFamily` is concatenated to the front of this list. - /// When no font family is provided through 'fontFamilyFallback' (null or empty) or `fontFamily`, then the - /// platform default font will be used. - /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting the text. - /// * `letterSpacing`: The amount of space (in logical pixels) to add between each letter. - /// * `wordSpacing`: The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). - /// * `textBaseline`: The common baseline that should be aligned between this text span and its parent text span, or, for the root text spans, with the line box. - /// * `height`: The height of this text span, as a multiple of the font size. - /// * `locale`: The locale used to select region-specific glyphs. - /// * `background`: The paint drawn as a background for the text. - /// * `foreground`: The paint used to draw the text. If this is specified, `color` must be null. factory TextStyle({ Color? color, TextDecoration? decoration, @@ -559,109 +257,54 @@ abstract class TextStyle { }) { if (engine.experimentalUseSkia) { return engine.CkTextStyle( - color: color, - decoration: decoration, - decorationColor: decorationColor, - decorationStyle: decorationStyle, - decorationThickness: decorationThickness, - fontWeight: fontWeight, - fontStyle: fontStyle, - textBaseline: textBaseline, - fontFamily: fontFamily, - fontFamilyFallback: fontFamilyFallback, - fontSize: fontSize, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - height: height, - locale: locale, - background: background as engine.CkPaint?, - foreground: foreground as engine.CkPaint?, - shadows: shadows, - fontFeatures: fontFeatures, + color: color, + decoration: decoration, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + fontWeight: fontWeight, + fontStyle: fontStyle, + textBaseline: textBaseline, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + fontSize: fontSize, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + height: height, + locale: locale, + background: background as engine.CkPaint?, + foreground: foreground as engine.CkPaint?, + shadows: shadows, + fontFeatures: fontFeatures, ); } else { return engine.EngineTextStyle( - color: color, - decoration: decoration, - decorationColor: decorationColor, - decorationStyle: decorationStyle, - decorationThickness: decorationThickness, - fontWeight: fontWeight, - fontStyle: fontStyle, - textBaseline: textBaseline, - fontFamily: fontFamily, - fontFamilyFallback: fontFamilyFallback, - fontSize: fontSize, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - height: height, - locale: locale, - background: background, - foreground: foreground, - shadows: shadows, - fontFeatures: fontFeatures, + color: color, + decoration: decoration, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + fontWeight: fontWeight, + fontStyle: fontStyle, + textBaseline: textBaseline, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + fontSize: fontSize, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + height: height, + locale: locale, + background: background, + foreground: foreground, + shadows: shadows, + fontFeatures: fontFeatures, ); } } } -/// An opaque object that determines the configuration used by -/// [ParagraphBuilder] to position lines within a [Paragraph] of text. abstract class ParagraphStyle { - /// Creates a new ParagraphStyle object. - /// - /// * `textAlign`: The alignment of the text within the lines of the - /// paragraph. If the last line is ellipsized (see `ellipsis` below), the - /// alignment is applied to that line after it has been truncated but before - /// the ellipsis has been added. // See: https://github.com/flutter/flutter/issues/9819 - /// - /// * `textDirection`: The directionality of the text, left-to-right (e.g. - /// Norwegian) or right-to-left (e.g. Hebrew). This controls the overall - /// directionality of the paragraph, as well as the meaning of - /// [TextAlign.start] and [TextAlign.end] in the `textAlign` field. - /// - /// * `maxLines`: The maximum number of lines painted. Lines beyond this - /// number are silently dropped. For example, if `maxLines` is 1, then only - /// one line is rendered. If `maxLines` is null, but `ellipsis` is not null, - /// then lines after the first one that overflows the width constraints are - /// dropped. The width constraints are those set in the - /// [ParagraphConstraints] object passed to the [Paragraph.layout] method. - /// - /// * `fontFamily`: The name of the font to use when painting the text (e.g., - /// Roboto). - /// - /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting - /// the text. - /// - /// * `height`: The minimum height of the line boxes, as a multiple of the - /// font size. The lines of the paragraph will be at least - /// `(height + leading) * fontSize` tall when fontSize - /// is not null. When fontSize is null, there is no minimum line height. Tall - /// glyphs due to baseline alignment or large [TextStyle.fontSize] may cause - /// the actual line height after layout to be taller than specified here. - /// [fontSize] must be provided for this property to take effect. - /// - /// * `fontWeight`: The typeface thickness to use when painting the text - /// (e.g., bold). - /// - /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., - /// italics). - /// - /// * `strutStyle`: The properties of the strut. Strut defines a set of minimum - /// vertical line height related metrics and can be used to obtain more - /// advanced line spacing behavior. - /// - /// * `ellipsis`: String used to ellipsize overflowing text. If `maxLines` is - /// not null, then the `ellipsis`, if any, is applied to the last rendered - /// line, if that line overflows the width constraints. If `maxLines` is - /// null, then the `ellipsis` is applied to the first line that overflows - /// the width constraints, and subsequent lines are dropped. The width - /// constraints are those set in the [ParagraphConstraints] object passed to - /// the [Paragraph.layout] method. The empty string and the null value are - /// considered equivalent and turn off this behavior. - /// - /// * `locale`: The locale used to select region-specific glyphs. factory ParagraphStyle({ TextAlign? textAlign, TextDirection? textDirection, @@ -711,41 +354,6 @@ abstract class ParagraphStyle { } abstract class StrutStyle { - /// Creates a new StrutStyle object. - /// - /// * `fontFamily`: The name of the font to use when painting the text (e.g., - /// Roboto). - /// - /// * `fontFamilyFallback`: An ordered list of font family names that will be searched for when - /// the font in `fontFamily` cannot be found. - /// - /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting - /// the text. - /// - /// * `lineHeight`: The minimum height of the line boxes, as a multiple of the - /// font size. The lines of the paragraph will be at least - /// `(lineHeight + leading) * fontSize` tall when fontSize - /// is not null. When fontSize is null, there is no minimum line height. Tall - /// glyphs due to baseline alignment or large [TextStyle.fontSize] may cause - /// the actual line height after layout to be taller than specified here. - /// [fontSize] must be provided for this property to take effect. - /// - /// * `leading`: The minimum amount of leading between lines as a multiple of - /// the font size. [fontSize] must be provided for this property to take effect. - /// - /// * `fontWeight`: The typeface thickness to use when painting the text - /// (e.g., bold). - /// - /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., - /// italics). - /// - /// * `forceStrutHeight`: When true, the paragraph will force all lines to be exactly - /// `(lineHeight + leading) * fontSize` tall from baseline to baseline. - /// [TextStyle] is no longer able to influence the line height, and any tall - /// glyphs may overlap with lines above. If a [fontFamily] is specified, the - /// total ascent of the first line will be the min of the `Ascent + half-leading` - /// of the [fontFamily] and `(lineHeight + leading) * fontSize`. Otherwise, it - /// will be determined by the Ascent + half-leading of the first text. factory StrutStyle({ String? fontFamily, List? fontFamilyFallback, @@ -758,104 +366,13 @@ abstract class StrutStyle { }) = engine.EngineStrutStyle; } -/// A direction in which text flows. -/// -/// Some languages are written from the left to the right (for example, English, -/// Tamil, or Chinese), while others are written from the right to the left (for -/// example Aramaic, Hebrew, or Urdu). Some are also written in a mixture, for -/// example Arabic is mostly written right-to-left, with numerals written -/// left-to-right. -/// -/// The text direction must be provided to APIs that render text or lay out -/// boxes horizontally, so that they can determine which direction to start in: -/// either right-to-left, [TextDirection.rtl]; or left-to-right, -/// [TextDirection.ltr]. -/// -/// ## Design discussion -/// -/// Flutter is designed to address the needs of applications written in any of -/// the world's currently-used languages, whether they use a right-to-left or -/// left-to-right writing direction. Flutter does not support other writing -/// modes, such as vertical text or boustrophedon text, as these are rarely used -/// in computer programs. -/// -/// It is common when developing user interface frameworks to pick a default -/// text direction — typically left-to-right, the direction most familiar to the -/// engineers working on the framework — because this simplifies the development -/// of applications on the platform. Unfortunately, this frequently results in -/// the platform having unexpected left-to-right biases or assumptions, as -/// engineers will typically miss places where they need to support -/// right-to-left text. This then results in bugs that only manifest in -/// right-to-left environments. -/// -/// In an effort to minimize the extent to which Flutter experiences this -/// category of issues, the lowest levels of the Flutter framework do not have a -/// default text reading direction. Any time a reading direction is necessary, -/// for example when text is to be displayed, or when a -/// writing-direction-dependent value is to be interpreted, the reading -/// direction must be explicitly specified. Where possible, such as in `switch` -/// statements, the right-to-left case is listed first, to avoid the impression -/// that it is an afterthought. -/// -/// At the higher levels (specifically starting at the widgets library), an -/// ambient [Directionality] is introduced, which provides a default. Thus, for -/// instance, a [Text] widget in the scope of a [MaterialApp] widget does not -/// need to be given an explicit writing direction. The [Directionality.of] -/// static method can be used to obtain the ambient text direction for a -/// particular [BuildContext]. -/// -/// ### Known left-to-right biases in Flutter -/// -/// Despite the design intent described above, certain left-to-right biases have -/// nonetheless crept into Flutter's design. These include: -/// -/// * The [Canvas] origin is at the top left, and the x-axis increases in a -/// left-to-right direction. -/// -/// * The default localization in the widgets and material libraries is -/// American English, which is left-to-right. -/// -/// ### Visual properties vs directional properties -/// -/// Many classes in the Flutter framework are offered in two versions, a -/// visually-oriented variant, and a text-direction-dependent variant. For -/// example, [EdgeInsets] is described in terms of top, left, right, and bottom, -/// while [EdgeInsetsDirectional] is described in terms of top, start, end, and -/// bottom, where start and end correspond to right and left in right-to-left -/// text and left and right in left-to-right text. -/// -/// There are distinct use cases for each of these variants. -/// -/// Text-direction-dependent variants are useful when developing user interfaces -/// that should "flip" with the text direction. For example, a paragraph of text -/// in English will typically be left-aligned and a quote will be indented from -/// the left, while in Arabic it will be right-aligned and indented from the -/// right. Both of these cases are described by the direction-dependent -/// [TextAlign.start] and [EdgeInsetsDirectional.start]. -/// -/// In contrast, the visual variants are useful when the text direction is known -/// and not affected by the reading direction. For example, an application -/// giving driving directions might show a "turn left" arrow on the left and a -/// "turn right" arrow on the right — and would do so whether the application -/// was localized to French (left-to-right) or Hebrew (right-to-left). -/// -/// In practice, it is also expected that many developers will only be -/// targeting one language, and in that case it may be simpler to think in -/// visual terms. // The order of this enum must match the order of the values in TextDirection.h's TextDirection. enum TextDirection { - /// The text flows from right to left (e.g. Arabic, Hebrew). rtl, - - /// The text flows from left to right (e.g., English, French). ltr, } -/// A rectangle enclosing a run of text. -/// -/// This is similar to [Rect] but includes an inherent [TextDirection]. class TextBox { - /// Creates an object that describes a box containing text. const TextBox.fromLTRBD( this.left, this.top, @@ -863,43 +380,16 @@ class TextBox { this.bottom, this.direction, ); - - /// The left edge of the text box, irrespective of direction. - /// - /// To get the leading edge (which may depend on the [direction]), consider [start]. final double left; - - /// The top edge of the text box. final double top; - - /// The right edge of the text box, irrespective of direction. - /// - /// To get the trailing edge (which may depend on the [direction]), consider [end]. final double right; - - /// The bottom edge of the text box. final double bottom; - - /// The direction in which text inside this box flows. final TextDirection direction; - - /// Returns a rect of the same size as this box. Rect toRect() => Rect.fromLTRB(left, top, right, bottom); - - /// The [left] edge of the box for left-to-right text; the [right] edge of the box for right-to-left text. - /// - /// See also: - /// - /// * [direction], which specifies the text direction. double get start { return (direction == TextDirection.ltr) ? left : right; } - /// The [right] edge of the box for left-to-right text; the [left] edge of the box for right-to-left text. - /// - /// See also: - /// - /// * [direction], which specifies the text direction. double get end { return (direction == TextDirection.ltr) ? right : left; } @@ -929,95 +419,18 @@ class TextBox { } } -/// A way to disambiguate a [TextPosition] when its offset could match two -/// different locations in the rendered string. -/// -/// For example, at an offset where the rendered text wraps, there are two -/// visual positions that the offset could represent: one prior to the line -/// break (at the end of the first line) and one after the line break (at the -/// start of the second line). A text affinity disambiguates between these two -/// cases. -/// -/// This affects only line breaks caused by wrapping, not explicit newline -/// characters. For newline characters, the position is fully specified by the -/// offset alone, and there is no ambiguity. -/// -/// [TextAffinity] also affects bidirectional text at the interface between LTR -/// and RTL text. Consider the following string, where the lowercase letters -/// will be displayed as LTR and the uppercase letters RTL: "helloHELLO". When -/// rendered, the string would appear visually as "helloOLLEH". An offset of 5 -/// would be ambiguous without a corresponding [TextAffinity]. Looking at the -/// string in code, the offset represents the position just after the "o" and -/// just before the "H". When rendered, this offset could be either in the -/// middle of the string to the right of the "o" or at the end of the string to -/// the right of the "H". enum TextAffinity { - /// The position has affinity for the upstream side of the text position, i.e. - /// in the direction of the beginning of the string. - /// - /// In the example of an offset at the place where text is wrapping, upstream - /// indicates the end of the first line. - /// - /// In the bidirectional text example "helloHELLO", an offset of 5 with - /// [TextAffinity] upstream would appear in the middle of the rendered text, - /// just to the right of the "o". See the definition of [TextAffinity] for the - /// full example. upstream, - - /// The position has affinity for the downstream side of the text position, - /// i.e. in the direction of the end of the string. - /// - /// In the example of an offset at the place where text is wrapping, - /// downstream indicates the beginning of the second line. - /// - /// In the bidirectional text example "helloHELLO", an offset of 5 with - /// [TextAffinity] downstream would appear at the end of the rendered text, - /// just to the right of the "H". See the definition of [TextAffinity] for the - /// full example. downstream, } -/// A position in a string of text. -/// -/// A TextPosition can be used to locate a position in a string in code (using -/// the [offset] property), and it can also be used to locate the same position -/// visually in a rendered string of text (using [offset] and, when needed to -/// resolve ambiguity, [affinity]). -/// -/// The location of an offset in a rendered string is ambiguous in two cases. -/// One happens when rendered text is forced to wrap. In this case, the offset -/// where the wrap occurs could visually appear either at the end of the first -/// line or the beginning of the second line. The second way is with -/// bidirectional text. An offset at the interface between two different text -/// directions could have one of two locations in the rendered text. -/// -/// See the documentation for [TextAffinity] for more information on how -/// TextAffinity disambiguates situations like these. class TextPosition { - /// Creates an object representing a particular position in a string. - /// - /// The arguments must not be null (so the [offset] argument is required). const TextPosition({ required this.offset, this.affinity = TextAffinity.downstream, }) : assert(offset != null), // ignore: unnecessary_null_comparison assert(affinity != null); // ignore: unnecessary_null_comparison - - /// The index of the character that immediately follows the position in the - /// string representation of the text. - /// - /// For example, given the string `'Hello'`, offset 0 represents the cursor - /// being before the `H`, while offset 5 represents the cursor being just - /// after the `o`. final int offset; - - /// Disambiguates cases where the position in the string given by [offset] - /// could represent two different visual positions in the rendered text. For - /// example, this can happen when text is forced to wrap, or when one string - /// of text is rendered with multiple text directions. - /// - /// See the documentation for [TextAffinity] for more information on how - /// TextAffinity disambiguates situations like these. final TextAffinity affinity; @override @@ -1039,64 +452,32 @@ class TextPosition { } } -/// A range of characters in a string of text. class TextRange { - /// Creates a text range. - /// - /// The [start] and [end] arguments must not be null. Both the [start] and - /// [end] must either be greater than or equal to zero or both exactly -1. - /// - /// Instead of creating an empty text range, consider using the [empty] - /// constant. const TextRange({ required this.start, required this.end, }) : assert(start != null && start >= -1), // ignore: unnecessary_null_comparison assert(end != null && end >= -1); // ignore: unnecessary_null_comparison - - /// A text range that starts and ends at offset. - /// - /// The [offset] argument must be non-null and greater than or equal to -1. const TextRange.collapsed(int offset) : assert(offset != null && offset >= -1), // ignore: unnecessary_null_comparison start = offset, end = offset; - - /// A text range that contains nothing and is not in the text. static const TextRange empty = TextRange(start: -1, end: -1); - - /// The index of the first character in the range. - /// - /// If [start] and [end] are both -1, the text range is empty. final int start; - - /// The next index after the characters in this range. - /// - /// If [start] and [end] are both -1, the text range is empty. final int end; - - /// Whether this range represents a valid position in the text. bool get isValid => start >= 0 && end >= 0; - - /// Whether this range is empty (but still potentially placed inside the text). bool get isCollapsed => start == end; - - /// Whether the start of this range precedes the end. bool get isNormalized => end >= start; - - /// The text before this range. String textBefore(String text) { assert(isNormalized); return text.substring(0, start); } - /// The text after this range. String textAfter(String text) { assert(isNormalized); return text.substring(end); } - /// The text inside this range. String textInside(String text) { assert(isNormalized); return text.substring(start, end); @@ -1122,37 +503,10 @@ class TextRange { String toString() => 'TextRange(start: $start, end: $end)'; } -/// Layout constraints for [Paragraph] objects. -/// -/// Instances of this class are typically used with [Paragraph.layout]. -/// -/// The only constraint that can be specified is the [width]. See the discussion -/// at [width] for more details. class ParagraphConstraints { - /// Creates constraints for laying out a pargraph. - /// - /// The [width] argument must not be null. const ParagraphConstraints({ required this.width, }) : assert(width != null); // ignore: unnecessary_null_comparison - - /// The width the paragraph should use whey computing the positions of glyphs. - /// - /// If possible, the paragraph will select a soft line break prior to reaching - /// this width. If no soft line break is available, the paragraph will select - /// a hard line break prior to reaching this width. If that would force a line - /// break without any characters having been placed (i.e. if the next - /// character to be laid out does not fit within the given width constraint) - /// then the next character is allowed to overflow the width constraint and a - /// forced line break is placed after it (even if an explicit line break - /// follows). - /// - /// The width influences how ellipses are applied. See the discussion at [new - /// ParagraphStyle] for more details. - /// - /// This width is also used to position glyphs according to the [TextAlign] - /// alignment described in the [ParagraphStyle] used when building the - /// [Paragraph] with a [ParagraphBuilder]. final double width; @override @@ -1171,72 +525,19 @@ class ParagraphConstraints { String toString() => '$runtimeType(width: $width)'; } -/// Defines various ways to vertically bound the boxes returned by -/// [Paragraph.getBoxesForRange]. enum BoxHeightStyle { - /// Provide tight bounding boxes that fit heights per run. This style may result - /// in uneven bounding boxes that do not nicely connect with adjacent boxes. tight, - - /// The height of the boxes will be the maximum height of all runs in the - /// line. All boxes in the same line will be the same height. This does not - /// guarantee that the boxes will cover the entire vertical height of the line - /// when there is additional line spacing. - /// - /// See [BoxHeightStyle.includeLineSpacingTop], [BoxHeightStyle.includeLineSpacingMiddle], - /// and [BoxHeightStyle.includeLineSpacingBottom] for styles that will cover - /// the entire line. max, - - /// Extends the top and bottom edge of the bounds to fully cover any line - /// spacing. - /// - /// The top and bottom of each box will cover half of the - /// space above and half of the space below the line. - /// - /// {@template flutter.dart:ui.boxHeightStyle.includeLineSpacing} - /// The top edge of each line should be the same as the bottom edge - /// of the line above. There should be no gaps in vertical coverage given any - /// amount of line spacing. Line spacing is not included above the first line - /// and below the last line due to no additional space present there. - /// {@endtemplate} includeLineSpacingMiddle, - - /// Extends the top edge of the bounds to fully cover any line spacing. - /// - /// The line spacing will be added to the top of the box. - /// - /// {@macro flutter.dart:ui.boxHeightStyle.includeLineSpacing} includeLineSpacingTop, - - /// Extends the bottom edge of the bounds to fully cover any line spacing. - /// - /// The line spacing will be added to the bottom of the box. - /// - /// {@macro flutter.dart:ui.boxHeightStyle.includeLineSpacing} includeLineSpacingBottom, - - /// Calculate box heights based on the metrics of this paragraph's [StrutStyle]. - /// - /// Boxes based on the strut will have consistent heights throughout the - /// entire paragraph. The top edge of each line will align with the bottom - /// edge of the previous line. It is possible for glyphs to extend outside - /// these boxes. strut, } -/// Defines various ways to horizontally bound the boxes returned by -/// [Paragraph.getBoxesForRange]. enum BoxWidthStyle { // Provide tight bounding boxes that fit widths to the runs of each line // independently. tight, - - /// Adds up to two additional boxes as needed at the beginning and/or end - /// of each line so that the widths of the boxes in line are the same width - /// as the widest line in the paragraph. The additional boxes on each line - /// are only added when the relevant box at the relevant edge of that line - /// does not span the maximum width of the paragraph. max, } @@ -1252,237 +553,38 @@ abstract class LineMetrics { required double baseline, required int lineNumber, }) = engine.EngineLineMetrics; - - /// {@template dart.ui.LineMetrics.hardBreak} - /// True if this line ends with an explicit line break (e.g. '\n') or is the end - /// of the paragraph. False otherwise. - /// {@endtemplate} bool get hardBreak; - - /// {@template dart.ui.LineMetrics.ascent} - /// The rise from the [baseline] as calculated from the font and style for this line. - /// - /// This is the final computed ascent and can be impacted by the strut, height, scaling, - /// as well as outlying runs that are very tall. - /// - /// The [ascent] is provided as a positive value, even though it is typically defined - /// in fonts as negative. This is to ensure the signage of operations with these - /// metrics directly reflects the intended signage of the value. For example, - /// the y coordinate of the top edge of the line is `baseline - ascent`. - /// {@endtemplate} double get ascent; - - /// {@template dart.ui.LineMetrics.descent} - /// The drop from the [baseline] as calculated from the font and style for this line. - /// - /// This is the final computed ascent and can be impacted by the strut, height, scaling, - /// as well as outlying runs that are very tall. - /// - /// The y coordinate of the bottom edge of the line is `baseline + descent`. - /// {@endtemplate} double get descent; - - /// {@template dart.ui.LineMetrics.unscaledAscent} - /// The rise from the [baseline] as calculated from the font and style for this line - /// ignoring the [TextStyle.height]. - /// - /// The [unscaledAscent] is provided as a positive value, even though it is typically - /// defined in fonts as negative. This is to ensure the signage of operations with - /// these metrics directly reflects the intended signage of the value. - /// {@endtemplate} double get unscaledAscent; - - /// {@template dart.ui.LineMetrics.height} - /// Total height of the line from the top edge to the bottom edge. - /// - /// This is equivalent to `round(ascent + descent)`. This value is provided - /// separately due to rounding causing sub-pixel differences from the unrounded - /// values. - /// {@endtemplate} double get height; - - /// {@template dart.ui.LineMetrics.width} - /// Width of the line from the left edge of the leftmost glyph to the right - /// edge of the rightmost glyph. - /// - /// This is not the same as the width of the pargraph. - /// - /// See also: - /// - /// * [Paragraph.width], the max width passed in during layout. - /// * [Paragraph.longestLine], the width of the longest line in the paragraph. - /// {@endtemplate} double get width; - - /// {@template dart.ui.LineMetrics.left} - /// The x coordinate of left edge of the line. - /// - /// The right edge can be obtained with `left + width`. - /// {@endtemplate} double get left; - - /// {@template dart.ui.LineMetrics.baseline} - /// The y coordinate of the baseline for this line from the top of the paragraph. - /// - /// The bottom edge of the paragraph up to and including this line may be obtained - /// through `baseline + descent`. - /// {@endtemplate} double get baseline; - - /// {@template dart.ui.LineMetrics.lineNumber} - /// The number of this line in the overall paragraph, with the first line being - /// index zero. - /// - /// For example, the first line is line 0, second line is line 1. - /// {@endtemplate} int get lineNumber; } -/// A paragraph of text. -/// -/// A paragraph retains the size and position of each glyph in the text and can -/// be efficiently resized and painted. -/// -/// To create a [Paragraph] object, use a [ParagraphBuilder]. -/// -/// Paragraphs can be displayed on a [Canvas] using the [Canvas.drawParagraph] -/// method. abstract class Paragraph { - /// The amount of horizontal space this paragraph occupies. - /// - /// Valid only after [layout] has been called. double get width; - - /// The amount of vertical space this paragraph occupies. - /// - /// Valid only after [layout] has been called. double get height; - - /// The distance from the left edge of the leftmost glyph to the right edge of - /// the rightmost glyph in the paragraph. - /// - /// Valid only after [layout] has been called. double get longestLine; - - /// {@template dart.ui.paragraph.minIntrinsicWidth} - /// The minimum width that this paragraph could be without failing to paint - /// its contents within itself. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get minIntrinsicWidth; - - /// {@template dart.ui.paragraph.maxIntrinsicWidth} - /// Returns the smallest width beyond which increasing the width never - /// decreases the height. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get maxIntrinsicWidth; - - /// {@template dart.ui.paragraph.alphabeticBaseline} - /// The distance from the top of the paragraph to the alphabetic - /// baseline of the first line, in logical pixels. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get alphabeticBaseline; - - /// {@template dart.ui.paragraph.ideographicBaseline} - /// The distance from the top of the paragraph to the ideographic - /// baseline of the first line, in logical pixels. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. double get ideographicBaseline; - - /// True if there is more vertical content, but the text was truncated, either - /// because we reached `maxLines` lines of text or because the `maxLines` was - /// null, `ellipsis` was not null, and one of the lines exceeded the width - /// constraint. - /// - /// See the discussion of the `maxLines` and `ellipsis` arguments at [new - /// ParagraphStyle]. bool get didExceedMaxLines; - - /// Computes the size and position of each glyph in the paragraph. - /// - /// The [ParagraphConstraints] control how wide the text is allowed to be. void layout(ParagraphConstraints constraints); - - /// Returns a list of text boxes that enclose the given text range. - /// - /// The [boxHeightStyle] and [boxWidthStyle] parameters allow customization - /// of how the boxes are bound vertically and horizontally. Both style - /// parameters default to the tight option, which will provide close-fitting - /// boxes and will not account for any line spacing. - /// - /// The [boxHeightStyle] and [boxWidthStyle] parameters must not be null. - /// - /// See [BoxHeightStyle] and [BoxWidthStyle] for full descriptions of each option. List getBoxesForRange(int start, int end, {BoxHeightStyle boxHeightStyle = BoxHeightStyle.tight, BoxWidthStyle boxWidthStyle = BoxWidthStyle.tight}); - - /// Returns the text position closest to the given offset. - /// - /// It does so by performing a binary search to find where the tap occurred - /// within the text. TextPosition getPositionForOffset(Offset offset); - - /// Returns the [TextRange] of the word at the given [TextPosition]. - /// - /// Characters not part of a word, such as spaces, symbols, and punctuation, - /// have word breaks on both sides. In such cases, this method will return - /// [offset, offset+1]. Word boundaries are defined more precisely in Unicode - /// Standard Annex #29 http://www.unicode.org/reports/tr29/#Word_Boundaries TextRange getWordBoundary(TextPosition position); - - /// Returns the [TextRange] of the line at the given [TextPosition]. - /// - /// The newline (if any) is returned as part of the range. - /// - /// Not valid until after layout. - /// - /// This can potentially be expensive, since it needs to compute the line - /// metrics, so use it sparingly. TextRange getLineBoundary(TextPosition position); - - /// Returns a list of text boxes that enclose all placeholders in the paragraph. - /// - /// The order of the boxes are in the same order as passed in through [addPlaceholder]. - /// - /// Coordinates of the [TextBox] are relative to the upper-left corner of the paragraph, - /// where positive y values indicate down. List getBoxesForPlaceholders(); - - /// Returns the full list of [LineMetrics] that describe in detail the various - /// metrics of each laid out line. - /// - /// Not valid until after layout. - /// - /// This can potentially return a large amount of data, so it is not recommended - /// to repeatedly call this. Instead, cache the results. List computeLineMetrics(); } -/// Builds a [Paragraph] containing text with the given styling information. -/// -/// To set the paragraph's alignment, truncation, and ellipsising behavior, pass -/// an appropriately-configured [ParagraphStyle] object to the [new -/// ParagraphBuilder] constructor. -/// -/// Then, call combinations of [pushStyle], [addText], and [pop] to add styled -/// text to the object. -/// -/// Finally, call [build] to obtain the constructed [Paragraph] object. After -/// this point, the builder is no longer usable. -/// -/// After constructing a [Paragraph], call [Paragraph.layout] on it and then -/// paint it with [Canvas.drawParagraph]. abstract class ParagraphBuilder { - /// Creates a [ParagraphBuilder] object, which is used to create a - /// [Paragraph]. factory ParagraphBuilder(ParagraphStyle style) { if (engine.experimentalUseSkia) { return engine.CkParagraphBuilder(style); @@ -1490,81 +592,12 @@ abstract class ParagraphBuilder { return engine.EngineParagraphBuilder(style as engine.EngineParagraphStyle); } } - - /// Applies the given style to the added text until [pop] is called. - /// - /// See [pop] for details. void pushStyle(TextStyle style); - - /// Ends the effect of the most recent call to [pushStyle]. - /// - /// Internally, the paragraph builder maintains a stack of text styles. Text - /// added to the paragraph is affected by all the styles in the stack. Calling - /// [pop] removes the topmost style in the stack, leaving the remaining styles - /// in effect. void pop(); - - /// Adds the given text to the paragraph. - /// - /// The text will be styled according to the current stack of text styles. void addText(String text); - - /// Applies the given paragraph style and returns a [Paragraph] containing the - /// added text and associated styling. - /// - /// After calling this function, the paragraph builder object is invalid and - /// cannot be used further. Paragraph build(); - - /// The number of placeholders currently in the paragraph. int get placeholderCount; - - /// The scales of the placeholders in the paragraph. List get placeholderScales; - - /// Adds an inline placeholder space to the paragraph. - /// - /// The paragraph will contain a rectangular space with no text of the dimensions - /// specified. - /// - /// The `width` and `height` parameters specify the size of the placeholder rectangle. - /// - /// The `alignment` parameter specifies how the placeholder rectangle will be vertically - /// aligned with the surrounding text. When [PlaceholderAlignment.baseline], - /// [PlaceholderAlignment.aboveBaseline], and [PlaceholderAlignment.belowBaseline] - /// alignment modes are used, the baseline needs to be set with the `baseline`. - /// When using [PlaceholderAlignment.baseline], `baselineOffset` indicates the distance - /// of the baseline down from the top of of the rectangle. The default `baselineOffset` - /// is the `height`. - /// - /// Examples: - /// - /// * For a 30x50 placeholder with the bottom edge aligned with the bottom of the text, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.bottom);` - /// * For a 30x50 placeholder that is vertically centered around the text, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.middle);`. - /// * For a 30x50 placeholder that sits completely on top of the alphabetic baseline, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.aboveBaseline, baseline: TextBaseline.alphabetic)`. - /// * For a 30x50 placeholder with 40 pixels above and 10 pixels below the alphabetic baseline, use: - /// `addPlaceholder(30, 50, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic, baselineOffset: 40)`. - /// - /// Lines are permitted to break around each placeholder. - /// - /// Decorations will be drawn based on the font defined in the most recently - /// pushed [TextStyle]. The decorations are drawn as if unicode text were present - /// in the placeholder space, and will draw the same regardless of the height and - /// alignment of the placeholder. To hide or manually adjust decorations to fit, - /// a text style with the desired decoration behavior should be pushed before - /// adding a placeholder. - /// - /// Any decorations drawn through a placeholder will exist on the same canvas/layer - /// as the text. This means any content drawn on top of the space reserved by - /// the placeholder will be drawn over the decoration, possibly obscuring the - /// decoration. - /// - /// Placeholders are represented by a unicode 0xFFFC "object replacement character" - /// in the text buffer. For each placeholder, one object replacement character is - /// added on to the text buffer. void addPlaceholder( double width, double height, @@ -1575,15 +608,10 @@ abstract class ParagraphBuilder { }); } -/// Loads a font from a buffer and makes it available for rendering text. -/// -/// * `list`: A list of bytes containing the font file. -/// * `fontFamily`: The family name used to identify the font in text styles. -/// If this is not provided, then the family name will be extracted from the font file. Future loadFontFromList(Uint8List list, {String? fontFamily}) { if (engine.experimentalUseSkia) { return engine.skiaFontCollection.loadFontFromList(list, fontFamily: fontFamily).then( - (_) => engine.sendFontChangeMessage() + (_) => engine.sendFontChangeMessage() ); } else { return _fontCollection!.loadFontFromList(list, fontFamily: fontFamily!).then( diff --git a/lib/web_ui/lib/src/ui/tile_mode.dart b/lib/web_ui/lib/src/ui/tile_mode.dart index 9ce9ddf3c21fc..567b9a551573b 100644 --- a/lib/web_ui/lib/src/ui/tile_mode.dart +++ b/lib/web_ui/lib/src/ui/tile_mode.dart @@ -5,53 +5,9 @@ // @dart = 2.10 part of ui; -/// Defines what happens at the edge of the gradient. -/// -/// A gradient is defined along a finite inner area. In the case of a linear -/// gradient, it's between the parallel lines that are orthogonal to the line -/// drawn between two points. In the case of radial gradients, it's the disc -/// that covers the circle centered on a particular point up to a given radius. -/// -/// This enum is used to define how the gradient should paint the regions -/// outside that defined inner area. -/// -/// See also: -/// -/// * [painting.Gradient], the superclass for [LinearGradient] and -/// [RadialGradient], as used by [BoxDecoration] et al, which works in -/// relative coordinates and can create a [Shader] representing the gradient -/// for a particular [Rect] on demand. -/// * [dart:ui.Gradient], the low-level class used when dealing with the -/// [Paint.shader] property directly, with its [new Gradient.linear] and [new -/// Gradient.radial] constructors. // These enum values must be kept in sync with SkShader::TileMode. enum TileMode { - /// Edge is clamped to the final color. - /// - /// The gradient will paint the all the regions outside the inner area with - /// the color of the point closest to that region. - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png) clamp, - - /// Edge is repeated from first color to last. - /// - /// This is as if the stop points from 0.0 to 1.0 were then repeated from 1.0 - /// to 2.0, 2.0 to 3.0, and so forth (and for linear gradients, similarly from - /// -1.0 to 0.0, -2.0 to -1.0, etc). - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png) repeated, - - /// Edge is mirrored from last color to first. - /// - /// This is as if the stop points from 0.0 to 1.0 were then repeated backwards - /// from 2.0 to 1.0, then forwards from 2.0 to 3.0, then backwards again from - /// 4.0 to 3.0, and so forth (and for linear gradients, similarly from in the - /// negative direction). - /// - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png) - /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png) mirror, } diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index fd9e667401ae2..fa58ff01e5537 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -6,127 +6,35 @@ // @dart = 2.10 part of ui; -/// Signature of callbacks that have no arguments and return no data. typedef VoidCallback = void Function(); - -/// Signature for frame-related callbacks from the scheduler. -/// -/// The `timeStamp` is the number of milliseconds since the beginning of the -/// scheduler's epoch. Use timeStamp to determine how far to advance animation -/// timelines so that all the animations in the system are synchronized to a -/// common time base. typedef FrameCallback = void Function(Duration duration); - -/// Signature for [Window.onReportTimings]. typedef TimingsCallback = void Function(List timings); - -/// Signature for [Window.onPointerDataPacket]. typedef PointerDataPacketCallback = void Function(PointerDataPacket packet); - -/// Signature for [Window.onSemanticsAction]. -typedef SemanticsActionCallback = void Function( - int id, SemanticsAction action, ByteData? args); - -/// Signature for responses to platform messages. -/// -/// Used as a parameter to [Window.sendPlatformMessage] and -/// [Window.onPlatformMessage]. +typedef SemanticsActionCallback = void Function(int id, SemanticsAction action, ByteData? args); typedef PlatformMessageResponseCallback = void Function(ByteData? data); - -/// Signature for [Window.onPlatformMessage]. typedef PlatformMessageCallback = void Function( String name, ByteData? data, PlatformMessageResponseCallback? callback); -/// States that an application can be in. -/// -/// The values below describe notifications from the operating system. -/// Applications should not expect to always receive all possible -/// notifications. For example, if the users pulls out the battery from the -/// device, no notification will be sent before the application is suddenly -/// terminated, along with the rest of the operating system. -/// -/// See also: -/// -/// * [WidgetsBindingObserver], for a mechanism to observe the lifecycle state -/// from the widgets layer. enum AppLifecycleState { - /// The application is visible and responding to user input. resumed, - - /// The application is in an inactive state and is not receiving user input. - /// - /// On iOS, this state corresponds to an app or the Flutter host view running - /// in the foreground inactive state. Apps transition to this state when in - /// a phone call, responding to a TouchID request, when entering the app - /// switcher or the control center, or when the UIViewController hosting the - /// Flutter app is transitioning. - /// - /// On Android, this corresponds to an app or the Flutter host view running - /// in the foreground inactive state. Apps transition to this state when - /// another activity is focused, such as a split-screen app, a phone call, - /// a picture-in-picture app, a system dialog, or another window. - /// - /// Apps in this state should assume that they may be [paused] at any time. inactive, - - /// The application is not currently visible to the user, not responding to - /// user input, and running in the background. - /// - /// When the application is in this state, the engine will not call the - /// [Window.onBeginFrame] and [Window.onDrawFrame] callbacks. paused, - - /// The application is detached from view. - /// - /// When the application is in this state, the engine is running without - /// a platform UI. detached, } -/// A representation of distances for each of the four edges of a rectangle, -/// used to encode the view insets and padding that applications should place -/// around their user interface, as exposed by [Window.viewInsets] and -/// [Window.padding]. View insets and padding are preferably read via -/// [MediaQuery.of]. -/// -/// For the engine implementation of this class see the [engine.WindowPadding]. -/// -/// For a generic class that represents distances around a rectangle, see the -/// [EdgeInsets] class. -/// -/// See also: -/// -/// * [WidgetsBindingObserver], for a widgets layer mechanism to receive -/// notifications when the padding changes. -/// * [MediaQuery.of], for the preferred mechanism for accessing these values. -/// * [Scaffold], which automatically applies the padding in material design -/// applications. abstract class WindowPadding { - const factory WindowPadding._( - {required double left, - required double top, - required double right, - required double bottom}) = engine.WindowPadding; + const factory WindowPadding._({ + required double left, + required double top, + required double right, + required double bottom, + }) = engine.WindowPadding; - /// The distance from the left edge to the first unpadded pixel, in physical - /// pixels. double get left; - - /// The distance from the top edge to the first unpadded pixel, in physical - /// pixels. double get top; - - /// The distance from the right edge to the first unpadded pixel, in physical - /// pixels. double get right; - - /// The distance from the bottom edge to the first unpadded pixel, in physical - /// pixels. double get bottom; - - /// A window padding that has zeros for each edge. - static const WindowPadding zero = - WindowPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0); + static const WindowPadding zero = WindowPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0); @override String toString() { @@ -134,79 +42,13 @@ abstract class WindowPadding { } } -/// An identifier used to select a user's language and formatting preferences. -/// -/// This represents a [Unicode Language -/// Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier) -/// (i.e. without Locale extensions), except variants are not supported. -/// -/// Locales are canonicalized according to the "preferred value" entries in the -/// [IANA Language Subtag -/// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry). -/// For example, `const Locale('he')` and `const Locale('iw')` are equal and -/// both have the [languageCode] `he`, because `iw` is a deprecated language -/// subtag that was replaced by the subtag `he`. -/// -/// See also: -/// -/// * [Window.locale], which specifies the system's currently selected -/// [Locale]. class Locale { - /// Creates a new Locale object. The first argument is the - /// primary language subtag, the second is the region (also - /// referred to as 'country') subtag. - /// - /// For example: - /// - /// ```dart - /// const Locale swissFrench = const Locale('fr', 'CH'); - /// const Locale canadianFrench = const Locale('fr', 'CA'); - /// ``` - /// - /// The primary language subtag must not be null. The region subtag is - /// optional. When there is no region/country subtag, the parameter should - /// be omitted or passed `null` instead of an empty-string. - /// - /// The subtag values are _case sensitive_ and must be one of the valid - /// subtags according to CLDR supplemental data: - /// [language](http://unicode.org/cldr/latest/common/validity/language.xml), - /// [region](http://unicode.org/cldr/latest/common/validity/region.xml). The - /// primary language subtag must be at least two and at most eight lowercase - /// letters, but not four letters. The region region subtag must be two - /// uppercase letters or three digits. See the [Unicode Language - /// Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier) - /// specification. - /// - /// Validity is not checked by default, but some methods may throw away - /// invalid data. - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which also allows a [scriptCode] to be - /// specified. const Locale( this._languageCode, [ this._countryCode, ]) : assert(_languageCode != null), // ignore: unnecessary_null_comparison assert(_languageCode != ''), scriptCode = null; - - /// Creates a new Locale object. - /// - /// The keyword arguments specify the subtags of the Locale. - /// - /// The subtag values are _case sensitive_ and must be valid subtags according - /// to CLDR supplemental data: - /// [language](http://unicode.org/cldr/latest/common/validity/language.xml), - /// [script](http://unicode.org/cldr/latest/common/validity/script.xml) and - /// [region](http://unicode.org/cldr/latest/common/validity/region.xml) for - /// each of languageCode, scriptCode and countryCode respectively. - /// - /// The [countryCode] subtag is optional. When there is no country subtag, - /// the parameter should be omitted or passed `null` instead of an empty-string. - /// - /// Validity is not checked by default, but some methods may throw away - /// invalid data. const Locale.fromSubtags({ String languageCode = 'und', this.scriptCode, @@ -217,30 +59,6 @@ class Locale { assert(scriptCode != ''), assert(countryCode != ''), _countryCode = countryCode; - - /// The primary language subtag for the locale. - /// - /// This must not be null. It may be 'und', representing 'undefined'. - /// - /// This is expected to be string registered in the [IANA Language Subtag - /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) - /// with the type "language". The string specified must match the case of the - /// string in the registry. - /// - /// Language subtags that are deprecated in the registry and have a preferred - /// code are changed to their preferred code. For example, `const - /// Locale('he')` and `const Locale('iw')` are equal, and both have the - /// [languageCode] `he`, because `iw` is a deprecated language subtag that was - /// replaced by the subtag `he`. - /// - /// This must be a valid Unicode Language subtag as listed in [Unicode CLDR - /// supplemental - /// data](http://unicode.org/cldr/latest/common/validity/language.xml). - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which describes the conventions for creating - /// [Locale] objects. String get languageCode => _deprecatedLanguageSubtagMap[_languageCode] ?? _languageCode; final String _languageCode; @@ -326,40 +144,7 @@ class Locale { 'yos': 'zom', // Yos; deprecated 2013-09-10 'yuu': 'yug', // Yugh; deprecated 2014-02-28 }; - - /// The script subtag for the locale. - /// - /// This may be null, indicating that there is no specified script subtag. - /// - /// This must be a valid Unicode Language Identifier script subtag as listed - /// in [Unicode CLDR supplemental - /// data](http://unicode.org/cldr/latest/common/validity/script.xml). - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which describes the conventions for creating - /// [Locale] objects. final String? scriptCode; - - /// The region subtag for the locale. - /// - /// This may be null, indicating that there is no specified region subtag. - /// - /// This is expected to be string registered in the [IANA Language Subtag - /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) - /// with the type "region". The string specified must match the case of the - /// string in the registry. - /// - /// Region subtags that are deprecated in the registry and have a preferred - /// code are changed to their preferred code. For example, `const Locale('de', - /// 'DE')` and `const Locale('de', 'DD')` are equal, and both have the - /// [countryCode] `DE`, because `DD` is a deprecated language subtag that was - /// replaced by the subtag `DE`. - /// - /// See also: - /// - /// * [new Locale.fromSubtags], which describes the conventions for creating - /// [Locale] objects. String? get countryCode => _deprecatedRegionSubtagMap[_countryCode] ?? _countryCode; final String? _countryCode; @@ -406,443 +191,65 @@ class Locale { } } -/// The most basic interface to the host operating system's user interface. -/// -/// There is a single Window instance in the system, which you can -/// obtain from the [window] property. abstract class Window { - /// The number of device pixels for each logical pixel. This number might not - /// be a power of two. Indeed, it might not even be an integer. For example, - /// the Nexus 6 has a device pixel ratio of 3.5. - /// - /// Device pixels are also referred to as physical pixels. Logical pixels are - /// also referred to as device-independent or resolution-independent pixels. - /// - /// By definition, there are roughly 38 logical pixels per centimeter, or - /// about 96 logical pixels per inch, of the physical display. The value - /// returned by [devicePixelRatio] is ultimately obtained either from the - /// hardware itself, the device drivers, or a hard-coded value stored in the - /// operating system or firmware, and may be inaccurate, sometimes by a - /// significant margin. - /// - /// The Flutter framework operates in logical pixels, so it is rarely - /// necessary to directly deal with this property. - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. double get devicePixelRatio; - - /// The dimensions of the rectangle into which the application will be drawn, - /// in physical pixels. - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// At startup, the size of the application window may not be known before - /// Dart code runs. If this value is observed early in the application - /// lifecycle, it may report [Size.zero]. - /// - /// This value does not take into account any on-screen keyboards or other - /// system UI. The [padding] and [viewInsets] properties provide a view into - /// how much of each side of the application may be obscured by system UI. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. Size get physicalSize; - - /// The physical depth is the maximum elevation that the Window allows. - /// - /// Physical layers drawn at or above this elevation will have their elevation - /// clamped to this value. This can happen if the physical layer itself has - /// an elevation larger than available depth, or if some ancestor of the layer - /// causes it to have a cumulative elevation that is larger than the available - /// depth. - /// - /// The default value is [double.maxFinite], which is used for platforms that - /// do not specify a maximum elevation. This property is currently on expected - /// to be set to a non-default value on Fuchsia. - double get physicalDepth; - - /// The number of physical pixels on each side of the display rectangle into - /// which the application can render, but over which the operating system - /// will likely place system UI, such as the keyboard, that fully obscures - /// any content. - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. - /// * [MediaQuery.of], a simpler mechanism for the same. - /// * [Scaffold], which automatically applies the view insets in material - /// design applications. WindowPadding get viewInsets => WindowPadding.zero; WindowPadding get viewPadding => WindowPadding.zero; WindowPadding get systemGestureInsets => WindowPadding.zero; - - /// The number of physical pixels on each side of the display rectangle into - /// which the application can render, but which may be partially obscured by - /// system UI (such as the system notification area), or or physical - /// intrusions in the display (e.g. overscan regions on television screens or - /// phone sensor housings). - /// - /// When this changes, [onMetricsChanged] is called. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. - /// * [MediaQuery.of], a simpler mechanism for the same. - /// * [Scaffold], which automatically applies the padding in material design - /// applications. WindowPadding get padding => WindowPadding.zero; - - /// The system-reported text scale. - /// - /// This establishes the text scaling factor to use when rendering text, - /// according to the user's platform preferences. - /// - /// The [onTextScaleFactorChanged] callback is called whenever this value - /// changes. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. double get textScaleFactor => _textScaleFactor; double _textScaleFactor = 1.0; - - /// The setting indicating whether time should always be shown in the 24-hour - /// format. - /// - /// This option is used by [showTimePicker]. bool get alwaysUse24HourFormat => _alwaysUse24HourFormat; bool _alwaysUse24HourFormat = false; - - /// A callback that is invoked whenever [textScaleFactor] changes value. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this callback is invoked. VoidCallback? get onTextScaleFactorChanged; set onTextScaleFactorChanged(VoidCallback? callback); - - /// The setting indicating the current brightness mode of the host platform. Brightness get platformBrightness; - - /// A callback that is invoked whenever [platformBrightness] changes value. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this callback is invoked. VoidCallback? get onPlatformBrightnessChanged; set onPlatformBrightnessChanged(VoidCallback? callback); - - /// A callback that is invoked whenever the [devicePixelRatio], - /// [physicalSize], [padding], or [viewInsets] values change, for example - /// when the device is rotated or when the application is resized (e.g. when - /// showing applications side-by-side on Android). - /// - /// The engine invokes this callback in the same zone in which the callback - /// was set. - /// - /// The framework registers with this callback and updates the layout - /// appropriately. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// register for notifications when this is called. - /// * [MediaQuery.of], a simpler mechanism for the same. VoidCallback? get onMetricsChanged; set onMetricsChanged(VoidCallback? callback); - - /// The system-reported default locale of the device. - /// - /// This establishes the language and formatting conventions that application - /// should, if possible, use to render their user interface. - /// - /// This is the first locale selected by the user and is the user's - /// primary locale (the locale the device UI is displayed in) - /// - /// This is equivalent to `locales.first` and will provide an empty non-null locale - /// if the [locales] list has not been set or is empty. Locale? get locale; - - /// The full system-reported supported locales of the device. - /// - /// This establishes the language and formatting conventions that application - /// should, if possible, use to render their user interface. - /// - /// The list is ordered in order of priority, with lower-indexed locales being - /// preferred over higher-indexed ones. The first element is the primary [locale]. - /// - /// The [onLocaleChanged] callback is called whenever this value changes. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this value changes. List? get locales; - - /// Performs the platform-native locale resolution. - /// - /// Each platform may return different results. - /// - /// If the platform fails to resolve a locale, then this will return null. - /// - /// This method returns synchronously and is a direct call to - /// platform specific APIs without invoking method channels. Locale? computePlatformResolvedLocale(List supportedLocales) { // TODO(garyq): Implement on web. return null; } - /// A callback that is invoked whenever [locale] changes value. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to - /// observe when this callback is invoked. VoidCallback? get onLocaleChanged; set onLocaleChanged(VoidCallback? callback); - - /// Requests that, at the next appropriate opportunity, the [onBeginFrame] - /// and [onDrawFrame] callbacks be invoked. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. void scheduleFrame(); - - /// A callback that is invoked to notify the application that it is an - /// appropriate time to provide a scene using the [SceneBuilder] API and the - /// [render] method. When possible, this is driven by the hardware VSync - /// signal. This is only called if [scheduleFrame] has been called since the - /// last time this callback was invoked. - /// - /// The [onDrawFrame] callback is invoked immediately after [onBeginFrame], - /// after draining any microtasks (e.g. completions of any [Future]s) queued - /// by the [onBeginFrame] handler. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. - /// * [RendererBinding], the Flutter framework class which manages layout and - /// painting. FrameCallback? get onBeginFrame; set onBeginFrame(FrameCallback? callback); - - /// A callback that is invoked to report the [FrameTiming] of recently - /// rasterized frames. - /// - /// This can be used to see if the application has missed frames (through - /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high - /// latencies (through [FrameTiming.totalSpan]). - /// - /// Unlike [Timeline], the timing information here is available in the release - /// mode (additional to the profile and the debug mode). Hence this can be - /// used to monitor the application's performance in the wild. - /// - /// The callback may not be immediately triggered after each frame. Instead, - /// it tries to batch frames together and send all their timings at once to - /// decrease the overhead (as this is available in the release mode). The - /// timing of any frame will be sent within about 1 second even if there are - /// no later frames to batch. TimingsCallback? get onReportTimings; set onReportTimings(TimingsCallback? callback); - - /// A callback that is invoked for each frame after [onBeginFrame] has - /// completed and after the microtask queue has been drained. This can be - /// used to implement a second phase of frame rendering that happens - /// after any deferred work queued by the [onBeginFrame] phase. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. - /// * [RendererBinding], the Flutter framework class which manages layout and - /// painting. VoidCallback? get onDrawFrame; set onDrawFrame(VoidCallback? callback); - - /// A callback that is invoked when pointer data is available. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. - /// - /// See also: - /// - /// * [GestureBinding], the Flutter framework class which manages pointer - /// events. PointerDataPacketCallback? get onPointerDataPacket; set onPointerDataPacket(PointerDataPacketCallback? callback); - - /// The route or path that the embedder requested when the application was - /// launched. - /// - /// This will be the string "`/`" if no particular route was requested. - /// - /// ## Android - /// - /// On Android, calling - /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `createFlutterView` method in your `FlutterActivity` - /// subclass is a suitable time to set the value. The application's - /// `AndroidManifest.xml` file must also be updated to have a suitable - /// [``](https://developer.android.com/guide/topics/manifest/intent-filter-element.html). - /// - /// ## iOS - /// - /// On iOS, calling - /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `application:didFinishLaunchingWithOptions:` method is a - /// suitable time to set this value. - /// - /// See also: - /// - /// * [Navigator], a widget that handles routing. - /// * [SystemChannels.navigation], which handles subsequent navigation - /// requests from the embedder. String get defaultRouteName; - - /// Whether the user has requested that [updateSemantics] be called when - /// the semantic contents of window changes. - /// - /// The [onSemanticsEnabledChanged] callback is called whenever this value - /// changes. - /// - /// This defaults to `true` on the Web because we may never receive a signal - /// that an assistive technology is turned on. - bool get semanticsEnabled => - engine.EngineSemanticsOwner.instance.semanticsEnabled; - - /// A callback that is invoked when the value of [semanticsEnabled] changes. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. + bool get semanticsEnabled => engine.EngineSemanticsOwner.instance.semanticsEnabled; VoidCallback? get onSemanticsEnabledChanged; set onSemanticsEnabledChanged(VoidCallback? callback); - - /// A callback that is invoked whenever the user requests an action to be - /// performed. - /// - /// This callback is used when the user expresses the action they wish to - /// perform based on the semantics supplied by [updateSemantics]. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. SemanticsActionCallback? get onSemanticsAction; set onSemanticsAction(SemanticsActionCallback? callback); - - /// A callback that is invoked when the value of [accessibilityFlags] changes. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. VoidCallback? get onAccessibilityFeaturesChanged; set onAccessibilityFeaturesChanged(VoidCallback? callback); - - /// Called whenever this window receives a message from a platform-specific - /// plugin. - /// - /// The `name` parameter determines which plugin sent the message. The `data` - /// parameter is the payload and is typically UTF-8 encoded JSON but can be - /// arbitrary data. - /// - /// Message handlers must call the function given in the `callback` parameter. - /// If the handler does not need to respond, the handler should pass null to - /// the callback. - /// - /// The framework invokes this callback in the same zone in which the - /// callback was set. PlatformMessageCallback? get onPlatformMessage; set onPlatformMessage(PlatformMessageCallback? callback); - - /// Change the retained semantics data about this window. - /// - /// If [semanticsEnabled] is true, the user has requested that this funciton - /// be called whenever the semantic content of this window changes. - /// - /// In either case, this function disposes the given update, which means the - /// semantics update cannot be used further. void updateSemantics(SemanticsUpdate update) { engine.EngineSemanticsOwner.instance.updateSemantics(update); } - /// Sends a message to a platform-specific plugin. - /// - /// The `name` parameter determines which plugin receives the message. The - /// `data` parameter contains the message payload and is typically UTF-8 - /// encoded JSON but can be arbitrary data. If the plugin replies to the - /// message, `callback` will be called with the response. - /// - /// The framework invokes [callback] in the same zone in which this method - /// was called. void sendPlatformMessage( String name, ByteData? data, PlatformMessageResponseCallback? callback, ); - - /// Additional accessibility features that may be enabled by the platform. AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; AccessibilityFeatures _accessibilityFeatures = AccessibilityFeatures._(0); - - /// Updates the application's rendering on the GPU with the newly provided - /// [Scene]. This function must be called within the scope of the - /// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function - /// is called a second time during a single [onBeginFrame]/[onDrawFrame] - /// callback sequence or called outside the scope of those callbacks, the call - /// will be ignored. - /// - /// To record graphical operations, first create a [PictureRecorder], then - /// construct a [Canvas], passing that [PictureRecorder] to its constructor. - /// After issuing all the graphical operations, call the - /// [PictureRecorder.endRecording] function on the [PictureRecorder] to obtain - /// the final [Picture] that represents the issued graphical operations. - /// - /// Next, create a [SceneBuilder], and add the [Picture] to it using - /// [SceneBuilder.addPicture]. With the [SceneBuilder.build] method you can - /// then obtain a [Scene] object, which you can display to the user via this - /// [render] function. - /// - /// See also: - /// - /// * [SchedulerBinding], the Flutter framework class which manages the - /// scheduling of frames. - /// * [RendererBinding], the Flutter framework class which manages layout and - /// painting. void render(Scene scene); String get initialLifecycleState => 'AppLifecycleState.resumed'; @@ -852,11 +259,6 @@ abstract class Window { ByteData? getPersistentIsolateData() => null; } -/// Additional accessibility features that may be enabled by the platform. -/// -/// It is not possible to enable these settings from Flutter, instead they are -/// used by the platform to indicate that additional accessibility features are -/// enabled. class AccessibilityFeatures { const AccessibilityFeatures._(this._index); @@ -869,33 +271,11 @@ class AccessibilityFeatures { // A bitfield which represents each enabled feature. final int _index; - - /// Whether there is a running accessibility service which is changing the - /// interaction model of the device. - /// - /// For example, TalkBack on Android and VoiceOver on iOS enable this flag. bool get accessibleNavigation => _kAccessibleNavigation & _index != 0; - - /// The platform is inverting the colors of the application. bool get invertColors => _kInvertColorsIndex & _index != 0; - - /// The platform is requesting that animations be disabled or simplified. bool get disableAnimations => _kDisableAnimationsIndex & _index != 0; - - /// The platform is requesting that text be rendered at a bold font weight. - /// - /// Only supported on iOS. bool get boldText => _kBoldTextIndex & _index != 0; - - /// The platform is requesting that certain animations be simplified and - /// parallax effects removed. - /// - /// Only supported on iOS. bool get reduceMotion => _kReduceMotionIndex & _index != 0; - - /// The platform is requesting that UI be rendered with darker colors. - /// - /// Only supported on iOS. bool get highContrast => _kHighContrastIndex & _index != 0; @override @@ -935,18 +315,8 @@ class AccessibilityFeatures { int get hashCode => _index.hashCode; } -/// Describes the contrast of a theme or color palette. enum Brightness { - /// The color is dark and will require a light text color to achieve readable - /// contrast. - /// - /// For example, the color might be dark grey, requiring white text. dark, - - /// The color is light and will require a dark text color to achieve readable - /// contrast. - /// - /// For example, the color might be bright white, requiring black text. light, } @@ -1001,95 +371,41 @@ class IsolateNameServer { } } -/// Various important time points in the lifetime of a frame. -/// -/// [FrameTiming] records a timestamp of each phase for performance analysis. enum FramePhase { - /// When the UI thread starts building a frame. - /// - /// See also [FrameTiming.buildDuration]. + vsyncStart, buildStart, - - /// When the UI thread finishes building a frame. - /// - /// See also [FrameTiming.buildDuration]. buildFinish, - - /// When the raster thread starts rasterizing a frame. - /// - /// See also [FrameTiming.rasterDuration]. rasterStart, - - /// When the raster thread finishes rasterizing a frame. - /// - /// See also [FrameTiming.rasterDuration]. rasterFinish, } -/// Time-related performance metrics of a frame. -/// -/// See [Window.onReportTimings] for how to get this. -/// -/// The metrics in debug mode (`flutter run` without any flags) may be very -/// different from those in profile and release modes due to the debug overhead. -/// Therefore it's recommended to only monitor and analyze performance metrics -/// in profile and release modes. class FrameTiming { - /// Construct [FrameTiming] with raw timestamps in microseconds. - /// - /// List [timestamps] must have the same number of elements as - /// [FramePhase.values]. - /// - /// This constructor is usually only called by the Flutter engine, or a test. - /// To get the [FrameTiming] of your app, see [Window.onReportTimings]. - FrameTiming(List timestamps) + factory FrameTiming({ + required int vsyncStart, + required int buildStart, + required int buildFinish, + required int rasterStart, + required int rasterFinish, + }) { + return FrameTiming._([ + vsyncStart, + buildStart, + buildFinish, + rasterStart, + rasterFinish + ]); + } + FrameTiming._(List timestamps) : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; - /// This is a raw timestamp in microseconds from some epoch. The epoch in all - /// [FrameTiming] is the same, but it may not match [DateTime]'s epoch. int timestampInMicroseconds(FramePhase phase) => _timestamps[phase.index]; - Duration _rawDuration(FramePhase phase) => - Duration(microseconds: _timestamps[phase.index]); - - /// The duration to build the frame on the UI thread. - /// - /// The build starts approximately when [Window.onBeginFrame] is called. The - /// [Duration] in the [Window.onBeginFrame] callback is exactly the - /// `Duration(microseconds: timestampInMicroseconds(FramePhase.buildStart))`. - /// - /// The build finishes when [Window.render] is called. - /// - /// {@template dart.ui.FrameTiming.fps_smoothness_milliseconds} - /// To ensure smooth animations of X fps, this should not exceed 1000/X - /// milliseconds. - /// {@endtemplate} - /// {@template dart.ui.FrameTiming.fps_milliseconds} - /// That's about 16ms for 60fps, and 8ms for 120fps. - /// {@endtemplate} - Duration get buildDuration => - _rawDuration(FramePhase.buildFinish) - - _rawDuration(FramePhase.buildStart); - - /// The duration to rasterize the frame on the raster thread. - /// - /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds} - /// {@macro dart.ui.FrameTiming.fps_milliseconds} - Duration get rasterDuration => - _rawDuration(FramePhase.rasterFinish) - - _rawDuration(FramePhase.rasterStart); - - /// The timespan between build start and raster finish. - /// - /// To achieve the lowest latency on an X fps display, this should not exceed - /// 1000/X milliseconds. - /// {@macro dart.ui.FrameTiming.fps_milliseconds} - /// - /// See also [buildDuration] and [rasterDuration]. - Duration get totalSpan => - _rawDuration(FramePhase.rasterFinish) - - _rawDuration(FramePhase.buildStart); + Duration _rawDuration(FramePhase phase) => Duration(microseconds: _timestamps[phase.index]); + Duration get buildDuration => _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart); + Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart); + Duration get vsyncOverhead => _rawDuration(FramePhase.buildStart) - _rawDuration(FramePhase.vsyncStart); + Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.vsyncStart); final List _timestamps; // in microseconds @@ -1097,11 +413,8 @@ class FrameTiming { @override String toString() { - return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, totalSpan: ${_formatMS(totalSpan)})'; + return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, vsyncOverhead: ${_formatMS(vsyncOverhead)}, totalSpan: ${_formatMS(totalSpan)})'; } } -/// The [Window] singleton. This object exposes the size of the display, the -/// core scheduler API, the input event callback, the graphics drawing API, and -/// other such core services. Window get window => engine.window; diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 34a4ee540ec85..6defcc3037efe 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: dev_dependencies: analyzer: 0.39.15 + archive: 2.0.13 http: 0.12.1 image: 2.1.13 js: 0.6.1+1 diff --git a/lib/web_ui/test/alarm_clock_test.dart b/lib/web_ui/test/alarm_clock_test.dart index 3a90f955b4db5..b56d0dc27d798 100644 --- a/lib/web_ui/test/alarm_clock_test.dart +++ b/lib/web_ui/test/alarm_clock_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:quiver/testing/async.dart'; import 'package:quiver/time.dart'; @@ -10,6 +11,10 @@ import 'package:quiver/time.dart'; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group(AlarmClock, () { _alarmClockTests(); }); diff --git a/lib/web_ui/test/canvas_test.dart b/lib/web_ui/test/canvas_test.dart index 7ce7143150451..53635e0e3ccee 100644 --- a/lib/web_ui/test/canvas_test.dart +++ b/lib/web_ui/test/canvas_test.dart @@ -6,11 +6,16 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'mock_engine_canvas.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUpAll(() { WebExperiments.ensureInitialized(); }); @@ -26,7 +31,6 @@ void main() { test(description, () { testFn(BitmapCanvas(canvasSize)); testFn(DomCanvas()); - testFn(HoudiniCanvas(canvasSize)); testFn(mockCanvas = MockEngineCanvas()); if (whenDone != null) { whenDone(); diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 4787834f19899..8770120ac712c 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -5,14 +5,20 @@ // @dart = 2.6 import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import 'common.dart'; +import 'test_data.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('CanvasKit API', () { setUpAll(() async { await ui.webOnlyInitializePlatform(); @@ -266,7 +272,14 @@ void _imageTests() { expect(frame.height(), 1); expect(nonAnimated.decodeNextFrame(), -1); - expect(frame.makeShader(canvasKit.TileMode.Repeat, canvasKit.TileMode.Mirror), isNotNull); + expect( + frame.makeShader( + canvasKit.TileMode.Repeat, + canvasKit.TileMode.Mirror, + toSkMatrixFromFloat32(Matrix4.identity().storage), + ), + isNotNull, + ); }); test('MakeAnimatedImageFromEncoded makes an animated image', () { @@ -602,7 +615,7 @@ void _pathTests() { }); test('arcTo', () { - path.arcTo( + path.arcToOval( SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4), 5, 40, @@ -611,7 +624,7 @@ void _pathTests() { }); test('overloaded arcTo (used for arcToPoint)', () { - (path as SkPathArcToPointOverload).arcTo( + path.arcToRotated( 1, 2, 3, @@ -1175,26 +1188,29 @@ void _canvasTests() { 20, ); }); -} -final Uint8List kTransparentImage = Uint8List.fromList([ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, - 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, - 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, - 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, - 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, -]); - -/// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue -/// frames). The GIF animates forever, and each frame has a 100ms delay. -final Uint8List kAnimatedGif = Uint8List.fromList( [ - 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xa1, 0x03, 0x00, - 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x21, - 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, - 0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c, - 0x01, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x54, 0x01, 0x00, 0x21, - 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b, -]); + test('toImage.toByteData', () async { + final SkPictureRecorder otherRecorder = SkPictureRecorder(); + final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + )); + otherCanvas.drawRect( + SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + ), + SkPaint(), + ); + final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture(), null); + final CkImage image = await picture.toImage(1, 1); + final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + expect(rawData, isNotNull); + final ByteData pngData = await image.toByteData(format: ui.ImageByteFormat.png); + expect(pngData, isNotNull); + }); +} diff --git a/lib/web_ui/test/canvaskit/image_test.dart b/lib/web_ui/test/canvaskit/image_test.dart new file mode 100644 index 0000000000000..d8e3995da43d3 --- /dev/null +++ b/lib/web_ui/test/canvaskit/image_test.dart @@ -0,0 +1,45 @@ +// 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. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; +import 'test_data.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit image', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); + + test('CkAnimatedImage can be explicitly disposed of', () { + final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage); + expect(image.box.isDeleted, false); + image.dispose(); + expect(image.box.isDeleted, true); + image.dispose(); + expect(image.box.isDeleted, true); + }); + + test('CkImage can be explicitly disposed of', () { + final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame(); + final CkImage image = CkImage(skImage); + expect(image.box.isDeleted, false); + image.dispose(); + expect(image.box.isDeleted, true); + image.dispose(); + expect(image.box.isDeleted, true); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} diff --git a/lib/web_ui/test/canvaskit/path_metrics_test.dart b/lib/web_ui/test/canvaskit/path_metrics_test.dart index 62bde94a8678b..eece12c15b572 100644 --- a/lib/web_ui/test/canvaskit/path_metrics_test.dart +++ b/lib/web_ui/test/canvaskit/path_metrics_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -11,6 +12,10 @@ import 'package:ui/ui.dart' as ui; import 'common.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Path Metrics', () { setUpAll(() async { await ui.webOnlyInitializePlatform(); diff --git a/lib/web_ui/test/canvaskit/shader_test.dart b/lib/web_ui/test/canvaskit/shader_test.dart new file mode 100644 index 0000000000000..dfa3f2dd12b04 --- /dev/null +++ b/lib/web_ui/test/canvaskit/shader_test.dart @@ -0,0 +1,81 @@ +// 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. + +// @dart = 2.6 +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; +import 'test_data.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit shaders', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); + + test('Sweep gradient', () { + final CkGradientSweep gradient = ui.Gradient.sweep( + ui.Offset.zero, + testColors, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Linear gradient', () { + final CkGradientLinear gradient = ui.Gradient.linear( + ui.Offset.zero, + const ui.Offset(0, 1), + testColors, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Radial gradient', () { + final CkGradientRadial gradient = ui.Gradient.radial( + ui.Offset.zero, + 10, + testColors, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Conical gradient', () { + final CkGradientConical gradient = ui.Gradient.radial( + ui.Offset.zero, + 10, + testColors, + null, + ui.TileMode.clamp, + null, + const ui.Offset(10, 10), + 40, + ); + expect(gradient.createDefault(), isNotNull); + }); + + test('Image shader', () { + final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame(); + final CkImage image = CkImage(skImage); + final CkImageShader imageShader = ui.ImageShader( + image, + ui.TileMode.clamp, + ui.TileMode.repeated, + Float64List.fromList(Matrix4.diagonal3Values(1, 2, 3).storage), + ); + expect(imageShader, isA()); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} + +const List testColors = [ui.Color(0xFFFFFF00), ui.Color(0xFFFFFFFF)]; diff --git a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart index d38a447eaa554..08e0cb3d55837 100644 --- a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart +++ b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'package:mockito/mockito.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; @@ -13,6 +14,10 @@ import 'package:ui/src/engine.dart'; import 'common.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('skia_objects_cache', () { _tests(); // TODO: https://github.com/flutter/flutter/issues/60040 @@ -21,12 +26,22 @@ void main() { void _tests() { SkiaObjects.maximumCacheSize = 4; + bool originalBrowserSupportsFinalizationRegistry; setUpAll(() async { await ui.webOnlyInitializePlatform(); + + // Pretend the browser does not support FinalizationRegistry so we can test the + // resurrection logic. + originalBrowserSupportsFinalizationRegistry = browserSupportsFinalizationRegistry; + browserSupportsFinalizationRegistry = false; + }); + + tearDownAll(() { + browserSupportsFinalizationRegistry = originalBrowserSupportsFinalizationRegistry; }); - group(ResurrectableSkiaObject, () { + group(ManagedSkiaObject, () { test('implements create, cache, delete, resurrect, delete lifecycle', () { int addPostFrameCallbackCount = 0; @@ -152,7 +167,7 @@ class TestOneShotSkiaObject extends OneShotSkiaObject { } } -class TestSkiaObject extends ResurrectableSkiaObject { +class TestSkiaObject extends ManagedSkiaObject { int createDefaultCount = 0; int resurrectCount = 0; int deleteCount = 0; diff --git a/lib/web_ui/test/canvaskit/test_data.dart b/lib/web_ui/test/canvaskit/test_data.dart new file mode 100644 index 0000000000000..9ce6313d714ae --- /dev/null +++ b/lib/web_ui/test/canvaskit/test_data.dart @@ -0,0 +1,29 @@ +// 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. + +// @dart = 2.10 + +import 'dart:typed_data'; + +final Uint8List kTransparentImage = Uint8List.fromList([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, + 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, +]); + +/// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue +/// frames). The GIF animates forever, and each frame has a 100ms delay. +final Uint8List kAnimatedGif = Uint8List.fromList( [ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xa1, 0x03, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x21, + 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, + 0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c, + 0x01, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x54, 0x01, 0x00, 0x21, + 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b, +]); diff --git a/lib/web_ui/test/canvaskit/vertices_test.dart b/lib/web_ui/test/canvaskit/vertices_test.dart new file mode 100644 index 0000000000000..0f0263a04eb7b --- /dev/null +++ b/lib/web_ui/test/canvaskit/vertices_test.dart @@ -0,0 +1,62 @@ +// 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. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('Vertices', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); + + test('can be constructed, drawn, and deleted', () { + final CkVertices vertices = _testVertices(); + expect(vertices, isA()); + expect(vertices.createDefault(), isNotNull); + expect(vertices.resurrect(), isNotNull); + + final recorder = CkPictureRecorder(); + final canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 100, 100)); + canvas.drawVertices( + vertices, + ui.BlendMode.srcOver, + ui.Paint(), + ); + vertices.delete(); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} + +ui.Vertices _testVertices() { + return ui.Vertices( + ui.VertexMode.triangles, + [ + ui.Offset(0, 0), + ui.Offset(10, 10), + ui.Offset(0, 20), + ], + textureCoordinates: [ + ui.Offset(0, 0), + ui.Offset(10, 10), + ui.Offset(0, 20), + ], + colors: [ + ui.Color.fromRGBO(255, 0, 0, 1.0), + ui.Color.fromRGBO(0, 255, 0, 1.0), + ui.Color.fromRGBO(0, 0, 255, 1.0), + ], + indices: [0, 1, 2], + ); +} diff --git a/lib/web_ui/test/clipboard_test.dart b/lib/web_ui/test/clipboard_test.dart index eb67e5beff575..e0c526176e831 100644 --- a/lib/web_ui/test/clipboard_test.dart +++ b/lib/web_ui/test/clipboard_test.dart @@ -7,11 +7,16 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:mockito/mockito.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -Future main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('message handler', () { const String testText = 'test text'; diff --git a/lib/web_ui/test/color_test.dart b/lib/web_ui/test/color_test.dart index 8d53b77f73659..d48ea0bef83cb 100644 --- a/lib/web_ui/test/color_test.dart +++ b/lib/web_ui/test/color_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'package:ui/ui.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +void main() { + internalBootstrapBrowserTest(() => testMain); +} + class NotAColor extends Color { const NotAColor(int value) : super(value); } -void main() { +void testMain() { test('color accessors should work', () { const Color foo = Color(0x12345678); expect(foo.alpha, equals(0x12)); diff --git a/lib/web_ui/test/dom_renderer_test.dart b/lib/web_ui/test/dom_renderer_test.dart index ca9d6804a47c8..514c54d4160bc 100644 --- a/lib/web_ui/test/dom_renderer_test.dart +++ b/lib/web_ui/test/dom_renderer_test.dart @@ -5,10 +5,15 @@ // @dart = 2.6 import 'dart:html' as html; -import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('creating elements works', () { final DomRenderer renderer = DomRenderer(); final html.Element element = renderer.createElement('div'); diff --git a/lib/web_ui/test/engine/frame_reference_test.dart b/lib/web_ui/test/engine/frame_reference_test.dart index 6ccd91c57b207..0658f8258ba7c 100644 --- a/lib/web_ui/test/engine/frame_reference_test.dart +++ b/lib/web_ui/test/engine/frame_reference_test.dart @@ -4,9 +4,14 @@ // @dart = 2.6 import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('CrossFrameCache', () { test('Reuse returns no object when cache empty', () { final CrossFrameCache cache = CrossFrameCache(); diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index 58d536aff4785..4d621117a633c 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -10,6 +10,7 @@ import 'dart:async'; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -29,6 +30,10 @@ const MethodCodec codec = JSONMethodCodec(); void emptyCallback(ByteData date) {} void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$BrowserHistory', () { final PlatformMessagesSpy spy = PlatformMessagesSpy(); diff --git a/lib/web_ui/test/engine/image/html_image_codec_test.dart b/lib/web_ui/test/engine/image/html_image_codec_test.dart index 38cd5602d32f4..d29df24ff44f7 100644 --- a/lib/web_ui/test/engine/image/html_image_codec_test.dart +++ b/lib/web_ui/test/engine/image/html_image_codec_test.dart @@ -3,11 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -Future main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('HtmCodec', () { test('loads sample image', () async { @@ -19,7 +24,27 @@ Future main() async { test('provides image loading progress', () async { StringBuffer buffer = new StringBuffer(); final HtmlCodec codec = HtmlCodec('sample_image1.png', - chunkCallback: (int loaded, int total) { + chunkCallback: (int loaded, int total) { + buffer.write('$loaded/$total,'); + }); + await codec.getNextFrame(); + expect(buffer.toString(), '0/100,100/100,'); + }); + }); + + group('ImageCodecUrl', () { + test('loads sample image from web', () async { + final Uri uri = Uri.base.resolve('sample_image1.png'); + final HtmlCodec codec = await ui.webOnlyInstantiateImageCodecFromUrl(uri); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + expect(frameInfo.image, isNotNull); + expect(frameInfo.image.width, 100); + }); + test('provides image loading progress from web', () async { + final Uri uri = Uri.base.resolve('sample_image1.png'); + StringBuffer buffer = new StringBuffer(); + final HtmlCodec codec = await ui.webOnlyInstantiateImageCodecFromUrl(uri, + chunkCallback: (int loaded, int total) { buffer.write('$loaded/$total,'); }); await codec.getNextFrame(); diff --git a/lib/web_ui/test/engine/navigation_test.dart b/lib/web_ui/test/engine/navigation_test.dart index 8266fd7ec9904..d4ac941d3de59 100644 --- a/lib/web_ui/test/engine/navigation_test.dart +++ b/lib/web_ui/test/engine/navigation_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart' as engine; @@ -15,6 +16,10 @@ const engine.MethodCodec codec = engine.JSONMethodCodec(); void emptyCallback(ByteData date) {} void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { engine.window.locationStrategy = _strategy = engine.TestLocationStrategy(); }); diff --git a/lib/web_ui/test/engine/path_metrics_test.dart b/lib/web_ui/test/engine/path_metrics_test.dart index f4181f3917a49..a8938f913d99f 100644 --- a/lib/web_ui/test/engine/path_metrics_test.dart +++ b/lib/web_ui/test/engine/path_metrics_test.dart @@ -4,14 +4,19 @@ import 'dart:math' as math; -import 'package:ui/ui.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; import '../matchers.dart'; const double kTolerance = 0.001; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('PathMetric length', () { test('empty path', () { Path path = Path(); diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 5f6ac793c38ff..091cde33fc52f 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -6,11 +6,11 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; - const int _kNoButtonChange = -1; const PointerSupportDetector _defaultSupportDetector = PointerSupportDetector(); @@ -40,6 +40,10 @@ bool get isIosSafari => (browserEngine == BrowserEngine.webkit && operatingSystem == OperatingSystem.iOs); void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { html.Element glassPane = domRenderer.glassPaneElement; setUp(() { diff --git a/lib/web_ui/test/engine/profiler_test.dart b/lib/web_ui/test/engine/profiler_test.dart index 0314ee9c8ef6a..49f11242c860d 100644 --- a/lib/web_ui/test/engine/profiler_test.dart +++ b/lib/web_ui/test/engine/profiler_test.dart @@ -6,11 +6,16 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { Profiler.isBenchmarkMode = true; Profiler.ensureInitialized(); diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart index d997f811b862c..7bd1a9810eef4 100644 --- a/lib/web_ui/test/engine/recording_canvas_test.dart +++ b/lib/web_ui/test/engine/recording_canvas_test.dart @@ -3,13 +3,18 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../mock_engine_canvas.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { RecordingCanvas underTest; MockEngineCanvas mockCanvas; final Rect screenRect = Rect.largest; diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index 767d4742ad0ea..6eb3366668752 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -6,8 +6,9 @@ import 'dart:async' show Future; import 'dart:html'; -import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; const MessageCodec codec = StandardMessageCodec(); const String testMessage = 'This is an tooltip.'; @@ -16,6 +17,10 @@ const Map testInput = { }; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { AccessibilityAnnouncements accessibilityAnnouncements; group('$AccessibilityAnnouncements', () { diff --git a/lib/web_ui/test/engine/semantics/semantics_helper_test.dart b/lib/web_ui/test/engine/semantics/semantics_helper_test.dart index cd3e0e9e561d9..56fb691d117a1 100644 --- a/lib/web_ui/test/engine/semantics/semantics_helper_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_helper_test.dart @@ -5,11 +5,15 @@ // @dart = 2.6 import 'dart:html' as html; -import 'package:ui/src/engine.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$DesktopSemanticsEnabler', () { DesktopSemanticsEnabler desktopSemanticsEnabler; html.Element _placeholder; diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 016c94f0c5f7c..d3b0e52eb0180 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -12,6 +12,7 @@ import 'dart:typed_data'; import 'package:mockito/mockito.dart'; import 'package:quiver/testing/async.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -24,6 +25,10 @@ DateTime _testTime = DateTime(2018, 12, 17); EngineSemanticsOwner semantics() => EngineSemanticsOwner.instance; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { EngineSemanticsOwner.debugResetSemantics(); }); diff --git a/lib/web_ui/test/engine/services/serialization_test.dart b/lib/web_ui/test/engine/services/serialization_test.dart index f0f0b58fd4900..e7eb72b7f73c9 100644 --- a/lib/web_ui/test/engine/services/serialization_test.dart +++ b/lib/web_ui/test/engine/services/serialization_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:typed_data'; +import 'package:test/test.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Write and read buffer round-trip', () { test('of single byte', () { final WriteBuffer write = WriteBuffer(); diff --git a/lib/web_ui/test/engine/surface/path/path_iterator_test.dart b/lib/web_ui/test/engine/surface/path/path_iterator_test.dart new file mode 100644 index 0000000000000..3e4c21e029930 --- /dev/null +++ b/lib/web_ui/test/engine/surface/path/path_iterator_test.dart @@ -0,0 +1,90 @@ +// 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:typed_data'; + +import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + final Float32List points = Float32List(PathIterator.kMaxBufferSize); + + group('PathIterator', () { + test('Should return done verb for empty path', () { + final SurfacePath path = SurfacePath(); + final PathIterator iter = PathIterator(path.pathRef, false); + expect(iter.peek(), SPath.kDoneVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should return done when moveTo is last instruction', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + final PathIterator iter = PathIterator(path.pathRef, false); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should return lineTo', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + final PathIterator iter = PathIterator(path.pathRef, false); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kMoveVerb); + expect(points[0], 10); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 20); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should return extra lineTo if iteration is closed', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + final PathIterator iter = PathIterator(path.pathRef, true); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kMoveVerb); + expect(points[0], 10); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 20); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 10); + expect(iter.next(points), SPath.kCloseVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should not return extra lineTo if last point is starting point', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + path.lineTo(10, 10); + final PathIterator iter = PathIterator(path.pathRef, true); + expect(iter.peek(), SPath.kMoveVerb); + expect(iter.next(points), SPath.kMoveVerb); + expect(points[0], 10); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 20); + expect(iter.next(points), SPath.kLineVerb); + expect(points[2], 10); + expect(iter.next(points), SPath.kCloseVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('peek should return lineTo if iteration is closed', () { + final SurfacePath path = SurfacePath(); + path.moveTo(10, 10); + path.lineTo(20, 20); + final PathIterator iter = PathIterator(path.pathRef, true); + expect(iter.next(points), SPath.kMoveVerb); + expect(iter.next(points), SPath.kLineVerb); + expect(iter.peek(), SPath.kLineVerb); + }); + }); +} diff --git a/lib/web_ui/test/path_winding_test.dart b/lib/web_ui/test/engine/surface/path/path_winding_test.dart similarity index 99% rename from lib/web_ui/test/path_winding_test.dart rename to lib/web_ui/test/engine/surface/path/path_winding_test.dart index 905fc6c8f153f..32133e648285b 100644 --- a/lib/web_ui/test/path_winding_test.dart +++ b/lib/web_ui/test/engine/surface/path/path_winding_test.dart @@ -5,12 +5,17 @@ // @dart = 2.10 import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; -/// Test winding and convexity of a path. void main() { + internalBootstrapBrowserTest(() => testMain); +} + +/// Test winding and convexity of a path. +void testMain() { group('Convexity', () { test('Empty path should be convex', () { final SurfacePath path = SurfacePath(); 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 f01864fde9f0a..da2bdb275215e 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -8,14 +8,21 @@ import 'dart:async'; import 'dart:html' as html; import 'dart:js_util' as js_util; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' hide window; -import 'package:test/test.dart'; + import '../../matchers.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUpAll(() async { await webOnlyInitializeEngine(); }); @@ -56,7 +63,8 @@ void main() { testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) { return sceneBuilder.pushClipRRect( RRect.fromLTRBR(10, 20, 30, 40, const Radius.circular(3)), - oldLayer: oldLayer); + oldLayer: oldLayer, + clipBehavior: Clip.none); }, () { return ''' diff --git a/lib/web_ui/test/engine/surface/surface_test.dart b/lib/web_ui/test/engine/surface/surface_test.dart index 9da82f026821e..d7dbe9c827afd 100644 --- a/lib/web_ui/test/engine/surface/surface_test.dart +++ b/lib/web_ui/test/engine/surface/surface_test.dart @@ -8,9 +8,14 @@ import 'dart:html' as html; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Surface', () { setUp(() { SurfaceSceneBuilder.debugForgetFrameScene(); diff --git a/lib/web_ui/test/engine/ulps_test.dart b/lib/web_ui/test/engine/ulps_test.dart index 9d993e92eb262..d0a26c45faad7 100644 --- a/lib/web_ui/test/engine/ulps_test.dart +++ b/lib/web_ui/test/engine/ulps_test.dart @@ -3,10 +3,15 @@ // found in the LICENSE file. import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Float Int conversions', (){ test('Should convert signbit to 2\'s compliment', () { expect(signBitTo2sCompliment(0), 0); diff --git a/lib/web_ui/test/engine/util_test.dart b/lib/web_ui/test/engine/util_test.dart index 51d58c2b11810..9ff47eb2b3e14 100644 --- a/lib/web_ui/test/engine/util_test.dart +++ b/lib/web_ui/test/engine/util_test.dart @@ -5,9 +5,9 @@ // @dart = 2.6 import 'dart:typed_data'; -import 'package:ui/src/engine.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; final Float32List identityTransform = Matrix4.identity().storage; final Float32List xTranslation = (Matrix4.identity()..translate(10)).storage; @@ -17,6 +17,10 @@ final Float32List scaleAndTranslate2d = (Matrix4.identity()..scale(2, 3, 1)..tra final Float32List rotation2d = (Matrix4.identity()..rotateZ(0.2)).storage; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('transformKindOf and isIdentityFloat32ListTransform identify matrix kind', () { expect(transformKindOf(identityTransform), TransformKind.identity); expect(isIdentityFloat32ListTransform(identityTransform), isTrue); diff --git a/lib/web_ui/test/engine/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart index 7f06f9639641b..c0f0a4f7f05f2 100644 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -6,12 +6,17 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; const bool _defaultUseCanvasText = true; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUp(() { WebExperiments.ensureInitialized(); }); diff --git a/lib/web_ui/test/engine/window_test.dart b/lib/web_ui/test/engine/window_test.dart index d73fcfdf94e94..ae97dceb8678b 100644 --- a/lib/web_ui/test/engine/window_test.dart +++ b/lib/web_ui/test/engine/window_test.dart @@ -7,11 +7,16 @@ import 'dart:async'; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('onTextScaleFactorChanged preserves the zone', () { final Zone innerZone = Zone.current.fork(); diff --git a/lib/web_ui/test/geometry_test.dart b/lib/web_ui/test/geometry_test.dart index ba2272a3a387c..07ad7601d8d5d 100644 --- a/lib/web_ui/test/geometry_test.dart +++ b/lib/web_ui/test/geometry_test.dart @@ -6,11 +6,16 @@ import 'dart:math' as math show sqrt; import 'dart:math' show pi; -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; + void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('Offset.direction', () { expect(const Offset(0.0, 0.0).direction, 0.0); expect(const Offset(0.0, 1.0).direction, pi / 2.0); diff --git a/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart b/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart index 5b0cc417e1397..797b1cf6b34d1 100644 --- a/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/backdrop_filter_golden_test.dart @@ -5,15 +5,20 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; final Rect region = Rect.fromLTWH(0, 0, 500, 500); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { setUp(() async { debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart index 88e85ab9c95da..888a3f4f1e27a 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 400, 600); BitmapCanvas canvas; @@ -107,4 +112,4 @@ void paintArc(BitmapCanvas canvas, Offset offset, ..strokeWidth = 2 ..color = Color(0x61000000) // black38 ..style = PaintingStyle.stroke); -} \ No newline at end of file +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart index 8407e48766b33..39cfcb4b27feb 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart @@ -6,13 +6,17 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; +void main() { + internalBootstrapBrowserTest(() => testMain); +} -void main() async { +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart index d01620921c1d7..1436e934a59fa 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart' as engine; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart index 927bf2a300fbd..226b8c8a61095 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart @@ -5,14 +5,19 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart' as engine; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; +void main() { + internalBootstrapBrowserTest(() => testMain); +} + /// Tests context save/restore. -void main() async { +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart index 81f1163a4431e..16efb34e3fe78 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart @@ -7,15 +7,20 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:js_util' as js_util; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; import 'scuba.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); @@ -374,6 +379,28 @@ void main() async { sceneElement.remove(); } }); + + // Regression test for https://github.com/flutter/flutter/issues/61691 + // + // The bug in bitmap_canvas.dart was that when we transformed and clipped + // the image we did not apply `transform-origin: 0 0 0` to the clipping + // element which resulted in an undesirable offset. + test('Paints clipped and transformed image', () async { + final Rect region = const Rect.fromLTRB(0, 0, 60, 70); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.translate(10, 10); + canvas.transform(Matrix4.rotationZ(0.4).storage); + canvas.clipPath(Path() + ..moveTo(10, 10) + ..lineTo(50, 10) + ..lineTo(50, 30) + ..lineTo(10, 30) + ..close() + ); + canvas.drawImage(createNineSliceImage(), Offset.zero, Paint()); + await _checkScreenshot(canvas, 'draw_clipped_and_transformed_image', region: region, + maxDiffRatePercent: 1.0); + }); } // 9 slice test image that has a shiny/glass look. diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart index de4c52cebb1d2..b70666711f5ba 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart @@ -5,15 +5,20 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; final Rect region = Rect.fromLTWH(0, 0, 500, 100); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { setUp(() async { debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart index 8688140b290f2..c3e6adaa0469c 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_points_test.dart @@ -6,14 +6,18 @@ import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; - import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 400, 600); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart index ce55498eba86a..a072d025974fd 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart @@ -6,15 +6,20 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; import 'scuba.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 500, 100); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart index ca30387f99319..962d930d8884f 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_image_blend_mode_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart index 215d51da5c29e..8447c26806d47 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 300, 300); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart index a808a57a28598..1f3184c67a097 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart @@ -7,13 +7,18 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { // Commit a recording canvas to a bitmap, and compare with the expected Future _checkScreenshot(RecordingCanvas rc, String fileName, ui.Rect screenRect, {bool write = false}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart index ea77b27ccd802..07d19e51dc902 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 150, 420); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart index 469cc2f88faf8..2bd62f258fa20 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart index 95d977993704c..1506419d20e74 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old scuba tester padding BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart index 0a85cc25f96aa..8ae75205bfb47 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 300, 300); BitmapCanvas canvas; @@ -68,4 +73,4 @@ void paintStrokeJoins(BitmapCanvas canvas) { end = end.translate(0, 20); } } -} \ No newline at end of file +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart index 447f9706030ec..4a81d2fe52665 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 300, 300); BitmapCanvas canvas; @@ -63,4 +68,4 @@ void paintSideBySideRects(BitmapCanvas canvas) { ..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Color(0x7fffff00)); -} \ No newline at end of file +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart new file mode 100644 index 0000000000000..3c8d8c64210f9 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -0,0 +1,49 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +void main() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Canvas to Picture', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = await _drawTestPictureWithCircle(region); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + //await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + }); +} + +Picture _drawTestPictureWithCircle(Rect region) { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00)); + return recorder.endRecording(); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart index 556705953bc23..e48001071a301 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_winding_rule_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 500, 500); BitmapCanvas canvas; diff --git a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart index 2c270a0a0f938..04dce71d0b986 100644 --- a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart @@ -6,16 +6,21 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../../matchers.dart'; import 'package:web_engine_tester/golden_tester.dart'; final Rect region = Rect.fromLTWH(0, 0, 500, 100); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { setUp(() async { debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); @@ -133,7 +138,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, 0, 500, 100)); }, skip: '''TODO(https://github.com/flutter/flutter/issues/40395) Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500'''); @@ -147,7 +152,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, 0, 20, 20)); }); @@ -161,7 +166,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, Rect.zero); expect(picture.debugExactGlobalCullRect, Rect.zero); }); @@ -176,7 +181,7 @@ void _testCullRectComputation() { }); builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 60, 60)); }); @@ -195,7 +200,7 @@ void _testCullRectComputation() { builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 60, 60)); }); @@ -213,7 +218,7 @@ void _testCullRectComputation() { builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect( picture.debugExactGlobalCullRect, const Rect.fromLTRB(0, 70, 20, 100)); expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, -20, 20, 10)); @@ -244,7 +249,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_cull_rect_fills_layer_clip.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 70, 70)); }); @@ -274,7 +279,7 @@ void _testCullRectComputation() { 'compositing_cull_rect_intersects_clip_and_paint_bounds.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(50, 40, 70, 70)); }); @@ -305,7 +310,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_cull_rect_offset_inside_layer_clip.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, const Rect.fromLTRB(-15.0, -20.0, 15.0, 0.0)); }); @@ -335,7 +340,7 @@ void _testCullRectComputation() { builder.build(); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect(picture.optimalLocalCullRect, Rect.zero); expect(picture.debugExactGlobalCullRect, Rect.zero); }); @@ -378,7 +383,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_cull_rect_rotated.png', region: region); - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; expect( picture.optimalLocalCullRect, within( @@ -510,7 +515,7 @@ void _testCullRectComputation() { await matchGoldenFile('compositing_3d_rotate1.png', region: region); // ignore: unused_local_variable - final PersistedStandardPicture picture = enumeratePictures().single; + final PersistedPicture picture = enumeratePictures().single; // TODO(https://github.com/flutter/flutter/issues/40395): // Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500. // expect( diff --git a/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart b/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart index bcf69c155e212..9362cccebd3b9 100644 --- a/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/conic_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding Future testPath(Path path, String scubaFileName) async { diff --git a/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart b/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart index 765c614ae4cfc..68a2136343791 100644 --- a/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart index cbeadad84a958..6eccec76d3234 100644 --- a/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart b/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart index cd3378b569896..fd9efc8bfb44d 100644 --- a/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/multiline_text_clipping_golden_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; @@ -12,7 +13,11 @@ import 'scuba.dart'; typedef PaintTest = void Function(RecordingCanvas recordingCanvas); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { // Scuba doesn't give us viewport smaller than 472px wide. final EngineScubaTester scuba = await EngineScubaTester.initialize( viewportSize: const Size(600, 600), diff --git a/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart index 8695296ab9dbd..380036ba18dde 100644 --- a/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart @@ -5,14 +5,19 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../../matchers.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart b/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart index f657f36a5ae02..03d9f0c1ad2e7 100644 --- a/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding Future testPath(Path path, String scubaFileName, {Paint paint, double maxDiffRatePercent = null}) async { diff --git a/lib/web_ui/test/golden_tests/engine/path_transform_test.dart b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart index 789f0259eeee6..0f968ba72b056 100644 --- a/lib/web_ui/test/golden_tests/engine/path_transform_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart @@ -6,13 +6,18 @@ import 'dart:html' as html; import 'dart:math' as math; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart b/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart index af959cd4fe6c8..1c61fff196a09 100644 --- a/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/picture_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Picture', () { test('toImage produces an image', () async { final EnginePictureRecorder recorder = ui.PictureRecorder(); diff --git a/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart index 4fb58ebc3df94..098233fe1cfb3 100644 --- a/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart @@ -5,13 +5,18 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart index 618c7e9ac973e..4d69f8955ad6a 100644 --- a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart @@ -7,14 +7,19 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import '../../matchers.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double screenWidth = 600.0; const double screenHeight = 800.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); diff --git a/lib/web_ui/test/golden_tests/engine/scuba.dart b/lib/web_ui/test/golden_tests/engine/scuba.dart index 821b9dddd0078..e735967a93bf5 100644 --- a/lib/web_ui/test/golden_tests/engine/scuba.dart +++ b/lib/web_ui/test/golden_tests/engine/scuba.dart @@ -91,7 +91,7 @@ typedef CanvasTest = FutureOr Function(EngineCanvas canvas); /// Runs the given test [body] with each type of canvas. void testEachCanvas(String description, CanvasTest body, - {double maxDiffRate, bool bSkipHoudini = false}) { + {double maxDiffRate}) { const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800); test('$description (bitmap)', () { try { @@ -123,18 +123,6 @@ void testEachCanvas(String description, CanvasTest body, TextMeasurementService.clearCache(); } }); - if (!bSkipHoudini) { - test('$description (houdini)', () { - try { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - WebExperiments.instance.useCanvasText = false; - return body(HoudiniCanvas(bounds)); - } finally { - WebExperiments.instance.useCanvasText = null; - TextMeasurementService.clearCache(); - } - }); - } } final ui.TextStyle _defaultTextStyle = ui.TextStyle( diff --git a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart index c6f98e4e5d91b..307cdb583caad 100644 --- a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart @@ -5,9 +5,10 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; import 'package:web_engine_tester/golden_tester.dart'; @@ -15,7 +16,11 @@ import 'scuba.dart'; const Color _kShadowColor = Color.fromARGB(255, 0, 0, 0); -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 550, 300); SurfaceSceneBuilder builder; diff --git a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart index 507be4fd33df9..dc243b2ab711d 100644 --- a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:async'; +import 'package:test/bootstrap/browser.dart'; import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; @@ -21,7 +22,11 @@ const String veryLong = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; const String longUnbreakable = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final EngineScubaTester scuba = await EngineScubaTester.initialize( viewportSize: const Size(800, 800), ); diff --git a/lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart b/lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart new file mode 100644 index 0000000000000..e89bc7ee69a7a --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart @@ -0,0 +1,79 @@ +// 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. + +// @dart = 2.6 +// import 'package:image/image.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart'; + +import 'scuba.dart'; + +typedef PaintTest = void Function(RecordingCanvas recordingCanvas); + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + final EngineScubaTester scuba = await EngineScubaTester.initialize( + viewportSize: const Size(600, 600), + ); + + setUpStableTestFonts(); + + testEachCanvas('draws paragraphs with placeholders', (EngineCanvas canvas) { + final Rect screenRect = const Rect.fromLTWH(0, 0, 600, 600); + final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect); + + Offset offset = Offset.zero; + for (PlaceholderAlignment alignment in PlaceholderAlignment.values) { + _paintTextWithPlaceholder(recordingCanvas, offset, alignment); + offset = offset.translate(0.0, 80.0); + } + recordingCanvas.endRecording(); + recordingCanvas.apply(canvas, screenRect); + return scuba.diffCanvasScreenshot(canvas, 'text_with_placeholders'); + }); +} + +const Color black = Color(0xFF000000); +const Color blue = Color(0xFF0000FF); +const Color red = Color(0xFFFF0000); + +const Size placeholderSize = Size(80.0, 50.0); + +void _paintTextWithPlaceholder( + RecordingCanvas canvas, + Offset offset, + PlaceholderAlignment alignment, +) { + // First let's draw the paragraph. + final Paragraph paragraph = _createParagraphWithPlaceholder(alignment); + canvas.drawParagraph(paragraph, offset); + + // Then fill the placeholders. + final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single; + canvas.drawRect( + placeholderBox.toRect().shift(offset), + Paint()..color = red, + ); +} + +Paragraph _createParagraphWithPlaceholder(PlaceholderAlignment alignment) { + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); + builder + .pushStyle(TextStyle(color: black, fontFamily: 'Roboto', fontSize: 14)); + builder.addText('Lorem ipsum'); + builder.addPlaceholder( + placeholderSize.width, + placeholderSize.height, + alignment, + baselineOffset: 40.0, + baseline: TextBaseline.alphabetic, + ); + builder.pushStyle(TextStyle(color: blue, fontFamily: 'Roboto', fontSize: 14)); + builder.addText('dolor sit amet, consectetur.'); + return builder.build()..layout(ParagraphConstraints(width: 200.0)); +} diff --git a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart index c8ec261e8b72a..5f31ca6ada828 100644 --- a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart @@ -3,12 +3,17 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; import 'scuba.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final EngineScubaTester scuba = await EngineScubaTester.initialize( viewportSize: const Size(800, 800), ); @@ -220,7 +225,7 @@ void main() async { testEachCanvas('draws text with a shadow', (EngineCanvas canvas) { drawTextWithShadow(canvas); return scuba.diffCanvasScreenshot(canvas, 'text_shadow', maxDiffRatePercent: 0.2); - }, bSkipHoudini: true); + }); testEachCanvas('Handles disabled strut style', (EngineCanvas canvas) { // Flutter uses [StrutStyle.disabled] for the [SelectableText] widget. This diff --git a/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart b/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart index 882bdb60e3f75..7a0cfc7c621e0 100644 --- a/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart +++ b/lib/web_ui/test/golden_tests/golden_failure_smoke_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:web_engine_tester/golden_tester.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('screenshot test reports failure', () async { html.document.body.innerHtml = 'Text that does not appear on the screenshot!'; await matchGoldenFile('__local__/smoke_test.png', region: Rect.fromLTWH(0, 0, 320, 200)); diff --git a/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart b/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart index 97d505511a709..563e3e92c7269 100644 --- a/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart +++ b/lib/web_ui/test/golden_tests/golden_success_smoke_test.dart @@ -5,12 +5,17 @@ // @dart = 2.6 import 'dart:html' as html; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; import 'package:web_engine_tester/golden_tester.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { debugEmulateFlutterTesterEnvironment = true; await webOnlyInitializePlatform(assetManager: WebOnlyMockAssetManager()); diff --git a/lib/web_ui/test/gradient_test.dart b/lib/web_ui/test/gradient_test.dart index a0df9ebdde051..b4335dd353228 100644 --- a/lib/web_ui/test/gradient_test.dart +++ b/lib/web_ui/test/gradient_test.dart @@ -3,11 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; + void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('Gradient.radial with no focal point', () { expect( Gradient.radial( diff --git a/lib/web_ui/test/hash_codes_test.dart b/lib/web_ui/test/hash_codes_test.dart index 7349dc7398edc..cca96671b711a 100644 --- a/lib/web_ui/test/hash_codes_test.dart +++ b/lib/web_ui/test/hash_codes_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; @@ -13,6 +14,10 @@ import 'package:ui/ui.dart'; const int _kBiggestExactJavaScriptInt = 9007199254740992; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('hashValues can hash lots of huge values effectively', () { expect( hashValues( diff --git a/lib/web_ui/test/keyboard_test.dart b/lib/web_ui/test/keyboard_test.dart index 3a819d6ffbf90..4b68cec6e2e13 100644 --- a/lib/web_ui/test/keyboard_test.dart +++ b/lib/web_ui/test/keyboard_test.dart @@ -8,12 +8,16 @@ import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:quiver/testing/async.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; - void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Keyboard', () { /// Used to save and restore [ui.window.onPlatformMessage] after each test. ui.PlatformMessageCallback savedCallback; diff --git a/lib/web_ui/test/locale_test.dart b/lib/web_ui/test/locale_test.dart index eb9331b465e59..316a42ae1101f 100644 --- a/lib/web_ui/test/locale_test.dart +++ b/lib/web_ui/test/locale_test.dart @@ -3,11 +3,15 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('Locale', () { const Null $null = null; expect(const Locale('en').toString(), 'en'); diff --git a/lib/web_ui/test/paragraph_builder_test.dart b/lib/web_ui/test/paragraph_builder_test.dart index 54a3bcf53b046..fe3041bc1539f 100644 --- a/lib/web_ui/test/paragraph_builder_test.dart +++ b/lib/web_ui/test/paragraph_builder_test.dart @@ -3,12 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; -import 'package:test/test.dart'; - void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { setUpAll(() { WebExperiments.ensureInitialized(); }); diff --git a/lib/web_ui/test/paragraph_test.dart b/lib/web_ui/test/paragraph_test.dart index 0bb402651fae4..df3f45edf2e6f 100644 --- a/lib/web_ui/test/paragraph_test.dart +++ b/lib/web_ui/test/paragraph_test.dart @@ -3,10 +3,11 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' hide window; -import 'package:test/test.dart'; void testEachMeasurement(String description, VoidCallback body, {bool skip}) { test('$description (dom measurement)', () async { @@ -31,7 +32,11 @@ void testEachMeasurement(String description, VoidCallback body, {bool skip}) { }, skip: skip); } -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await webOnlyInitializeTestDomRenderer(); // Ahem font uses a constant ideographic/alphabetic baseline ratio. @@ -801,6 +806,46 @@ void main() async { ); }); + testEachMeasurement('getBoxesForRange includes trailing spaces', () { + const String text = 'abcd abcde '; + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'Ahem', + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 10, + )); + builder.addText(text); + final Paragraph paragraph = builder.build(); + paragraph.layout(const ParagraphConstraints(width: double.infinity)); + expect( + paragraph.getBoxesForRange(0, text.length), + [ + TextBox.fromLTRBD(0.0, 0.0, 120.0, 10.0, TextDirection.ltr), + ], + ); + }); + + testEachMeasurement('getBoxesForRange multi-line includes trailing spaces', () { + const String text = 'abcd\nabcde \nabc'; + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'Ahem', + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 10, + )); + builder.addText(text); + final Paragraph paragraph = builder.build(); + paragraph.layout(const ParagraphConstraints(width: double.infinity)); + expect( + paragraph.getBoxesForRange(0, text.length), + [ + TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr), + TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), + TextBox.fromLTRBD(0.0, 20.0, 30.0, 30.0, TextDirection.ltr), + ], + ); + }); + test('longestLine', () { // [Paragraph.longestLine] is only supported by canvas-based measurement. WebExperiments.instance.useCanvasText = true; diff --git a/lib/web_ui/test/path_test.dart b/lib/web_ui/test/path_test.dart index 04461410cae5d..6f19c9cc8f439 100644 --- a/lib/web_ui/test/path_test.dart +++ b/lib/web_ui/test/path_test.dart @@ -7,6 +7,7 @@ import 'dart:js_util' as js_util; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; @@ -14,6 +15,10 @@ import 'package:ui/src/engine.dart'; import 'matchers.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('Path', () { test('Should have no subpaths when created', () { final SurfacePath path = SurfacePath(); diff --git a/lib/web_ui/test/rect_test.dart b/lib/web_ui/test/rect_test.dart index e5719e711603f..a526b4590e9dc 100644 --- a/lib/web_ui/test/rect_test.dart +++ b/lib/web_ui/test/rect_test.dart @@ -3,11 +3,15 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('rect accessors', () { const Rect r = Rect.fromLTRB(1.0, 3.0, 5.0, 7.0); expect(r.left, equals(1.0)); diff --git a/lib/web_ui/test/rrect_test.dart b/lib/web_ui/test/rrect_test.dart index b9d1f59e07ae1..02943b8413adc 100644 --- a/lib/web_ui/test/rrect_test.dart +++ b/lib/web_ui/test/rrect_test.dart @@ -3,11 +3,15 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:ui/ui.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; +import 'package:ui/ui.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('RRect.contains()', () { final RRect rrect = RRect.fromRectAndCorners( const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), diff --git a/lib/web_ui/test/text/font_collection_test.dart b/lib/web_ui/test/text/font_collection_test.dart index ec5590f008a23..b6423378ef6b0 100644 --- a/lib/web_ui/test/text/font_collection_test.dart +++ b/lib/web_ui/test/text/font_collection_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:html' as html; -import 'package:ui/src/engine.dart'; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; + void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$FontManager', () { FontManager fontManager; const String _testFontUrl = 'packages/ui/assets/ahem.ttf'; diff --git a/lib/web_ui/test/text/font_loading_test.dart b/lib/web_ui/test/text/font_loading_test.dart index 5b34d9d9346ae..81ff90eb568b8 100644 --- a/lib/web_ui/test/text/font_loading_test.dart +++ b/lib/web_ui/test/text/font_loading_test.dart @@ -8,11 +8,16 @@ import 'dart:convert'; import 'dart:html' as html; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -Future main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('loadFontFromList', () { const String _testFontUrl = 'packages/ui/assets/ahem.ttf'; diff --git a/lib/web_ui/test/text/line_breaker_test.dart b/lib/web_ui/test/text/line_breaker_test.dart index bfffabb769135..f536729faeeaa 100644 --- a/lib/web_ui/test/text/line_breaker_test.dart +++ b/lib/web_ui/test/text/line_breaker_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.10 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -11,6 +12,10 @@ import 'package:ui/ui.dart'; import 'line_breaker_test_data.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('nextLineBreak', () { test('Does not go beyond the ends of a string', () { expect(split('foo'), [ @@ -162,6 +167,62 @@ void main() { ]); }); + test('trailing spaces and new lines', () { + expect( + findBreaks('foo bar '), + [ + LineBreakResult(4, 4, 3, LineBreakType.opportunity), + LineBreakResult(9, 9, 7, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks('foo \nbar\nbaz \n'), + [ + LineBreakResult(6, 5, 3, LineBreakType.mandatory), + LineBreakResult(10, 9, 9, LineBreakType.mandatory), + LineBreakResult(17, 16, 13, LineBreakType.mandatory), + LineBreakResult(17, 17, 17, LineBreakType.endOfText), + ], + ); + }); + + test('leading spaces', () { + expect( + findBreaks(' foo'), + [ + LineBreakResult(1, 1, 0, LineBreakType.opportunity), + LineBreakResult(4, 4, 4, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks(' foo'), + [ + LineBreakResult(3, 3, 0, LineBreakType.opportunity), + LineBreakResult(6, 6, 6, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks(' foo bar'), + [ + LineBreakResult(2, 2, 0, LineBreakType.opportunity), + LineBreakResult(8, 8, 5, LineBreakType.opportunity), + LineBreakResult(11, 11, 11, LineBreakType.endOfText), + ], + ); + + expect( + findBreaks(' \n foo'), + [ + LineBreakResult(3, 2, 0, LineBreakType.mandatory), + LineBreakResult(6, 6, 3, LineBreakType.opportunity), + LineBreakResult(9, 9, 9, LineBreakType.endOfText), + ], + ); + }); + test('comprehensive test', () { for (int t = 0; t < data.length; t++) { final TestCase testCase = data[t]; @@ -220,9 +281,7 @@ class Line { @override bool operator ==(Object other) { - return other is Line - && other.text == text - && other.breakType == breakType; + return other is Line && other.text == text && other.breakType == breakType; } String get escapedText { @@ -245,14 +304,22 @@ class Line { List split(String text) { final List lines = []; - int i = 0; - LineBreakType? breakType; - while (breakType != LineBreakType.endOfText) { - final LineBreakResult result = nextLineBreak(text, i); - lines.add(Line(text.substring(i, result.index), result.type)); - - i = result.index; - breakType = result.type; + int lastIndex = 0; + for (LineBreakResult brk in findBreaks(text)) { + lines.add(Line(text.substring(lastIndex, brk.index), brk.type)); + lastIndex = brk.index; } return lines; } + +List findBreaks(String text) { + final List breaks = []; + + LineBreakResult brk = nextLineBreak(text, 0); + breaks.add(brk); + while (brk.type != LineBreakType.endOfText) { + brk = nextLineBreak(text, brk.index); + breaks.add(brk); + } + return breaks; +} diff --git a/lib/web_ui/test/text/measurement_test.dart b/lib/web_ui/test/text/measurement_test.dart index 5e5e61d65b2e5..ca4258c89cc77 100644 --- a/lib/web_ui/test/text/measurement_test.dart +++ b/lib/web_ui/test/text/measurement_test.dart @@ -5,10 +5,12 @@ // @dart = 2.6 @TestOn('chrome || firefox') +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; final ui.ParagraphStyle ahemStyle = ui.ParagraphStyle( fontFamily: 'ahem', @@ -47,8 +49,11 @@ void testMeasurements(String description, MeasurementTestBody body, { skip: skipCanvas, ); } +void main() { + internalBootstrapBrowserTest(() => testMain); +} -void main() async { +void testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('$RulerManager', () { @@ -1157,5 +1162,6 @@ EngineLineMetrics line( lineNumber: lineNumber, left: left, endIndexWithoutNewlines: -1, + widthWithTrailingSpaces: width, ); } diff --git a/lib/web_ui/test/text/word_breaker_test.dart b/lib/web_ui/test/text/word_breaker_test.dart index 54ae7dd33c6cb..15966c9bce2ec 100644 --- a/lib/web_ui/test/text/word_breaker_test.dart +++ b/lib/web_ui/test/text/word_breaker_test.dart @@ -3,11 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { group('$WordBreaker', () { test('Does not go beyond the ends of a string', () { expect(WordBreaker.prevBreakIndex('foo', 0), 0); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index dfe51f28e90a5..ec511cf4f98a2 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -3,14 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'dart:async'; import 'dart:html'; import 'dart:js_util' as js_util; import 'dart:typed_data'; -import 'package:ui/src/engine.dart' hide window; - +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart' hide window; + import 'matchers.dart'; import 'spy.dart'; @@ -28,22 +30,14 @@ String lastInputAction; final InputConfiguration singlelineConfig = InputConfiguration( inputType: EngineInputType.text, - obscureText: false, - inputAction: 'TextInputAction.done', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none'), ); final Map flutterSinglelineConfig = createFlutterConfig('text'); final InputConfiguration multilineConfig = InputConfiguration( - inputType: EngineInputType.multiline, - obscureText: false, - inputAction: 'TextInputAction.newline', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + inputType: EngineInputType.multiline, + inputAction: 'TextInputAction.newline', +); final Map flutterMultilineConfig = createFlutterConfig('multiline'); @@ -56,6 +50,10 @@ void trackInputAction(String inputAction) { } void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { tearDown(() { lastEditingState = null; lastInputAction = null; @@ -109,14 +107,23 @@ void main() { expect(document.activeElement, document.body); }); + test('Respects read-only config', () { + final InputConfiguration config = InputConfiguration(readOnly: true); + editingElement.enable( + config, + onChange: trackEditingState, + onAction: trackInputAction, + ); + expect(document.getElementsByTagName('input'), hasLength(1)); + final InputElement input = document.getElementsByTagName('input')[0]; + expect(editingElement.domElement, input); + expect(input.getAttribute('readonly'), 'readonly'); + + editingElement.disable(); + }); + test('Knows how to create password fields', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - inputAction: 'TextInputAction.done', - obscureText: true, - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(obscureText: true); editingElement.enable( config, onChange: trackEditingState, @@ -131,13 +138,7 @@ void main() { }); test('Knows to turn autocorrect off', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - inputAction: 'TextInputAction.done', - obscureText: false, - autocorrect: false, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(autocorrect: false); editingElement.enable( config, onChange: trackEditingState, @@ -152,13 +153,7 @@ void main() { }); test('Knows to turn autocorrect on', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - inputAction: 'TextInputAction.done', - obscureText: false, - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(autocorrect: true); editingElement.enable( config, onChange: trackEditingState, @@ -292,13 +287,7 @@ void main() { }); test('Triggers input action', () { - final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.text, - obscureText: false, - inputAction: 'TextInputAction.done', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + final InputConfiguration config = InputConfiguration(inputAction: 'TextInputAction.done'); editingElement.enable( config, onChange: trackEditingState, @@ -320,12 +309,9 @@ void main() { test('Does not trigger input action in multi-line mode', () { final InputConfiguration config = InputConfiguration( - inputType: EngineInputType.multiline, - obscureText: false, - inputAction: 'TextInputAction.done', - autocorrect: true, - textCapitalization: TextCapitalizationConfig.fromInputConfiguration( - 'TextCapitalization.none')); + inputType: EngineInputType.multiline, + inputAction: 'TextInputAction.done', + ); editingElement.enable( config, onChange: trackEditingState, @@ -719,6 +705,204 @@ void main() { skip: (browserEngine == BrowserEngine.webkit || browserEngine == BrowserEngine.edge)); + test('finishAutofillContext closes connection no autofill element', + () async { + final MethodCall setClient = MethodCall( + 'TextInput.setClient', [123, flutterSinglelineConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); + + // Editing shouldn't have started yet. + expect(document.activeElement, document.body); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', false); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + expect(spy.messages, hasLength(1)); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.onConnectionClosed'); + expect( + spy.messages[0].methodArguments, + [ + 123, // Client ID + ], + ); + spy.messages.clear(); + // Input element is removed from DOM. + expect(document.getElementsByTagName('input'), hasLength(0)); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + + test('finishAutofillContext removes form from DOM', () async { + // Create a configuration with an AutofillGroup of four text fields. + final Map flutterMultiAutofillElementConfig = + createFlutterConfig('text', + autofillHint: 'username', + autofillHintsForFields: [ + 'username', + 'email', + 'name', + 'telephoneNumber' + ]); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterMultiAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // Form is added to DOM. + expect(document.getElementsByTagName('form'), isNotEmpty); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + + // Confirm that [HybridTextEditing] didn't send any messages. + expect(spy.messages, isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); + + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', false); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + // Form element is removed from DOM. + expect(document.getElementsByTagName('form'), hasLength(0)); + expect(formsOnTheDom, hasLength(0)); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + + test('finishAutofillContext with save submits forms', () async { + // Create a configuration with an AutofillGroup of four text fields. + final Map flutterMultiAutofillElementConfig = + createFlutterConfig('text', + autofillHint: 'username', + autofillHintsForFields: [ + 'username', + 'email', + 'name', + 'telephoneNumber' + ]); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterMultiAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // Form is added to DOM. + expect(document.getElementsByTagName('form'), isNotEmpty); + FormElement formElement = document.getElementsByTagName('form')[0]; + final Completer submittedForm = Completer(); + formElement.addEventListener( + 'submit', (event) => submittedForm.complete(true)); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', true); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + // `submit` action is called on form. + await expectLater(await submittedForm.future, true); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + + test('forms submits for focused input', () async { + // Create a configuration with an AutofillGroup of four text fields. + final Map flutterMultiAutofillElementConfig = + createFlutterConfig('text', + autofillHint: 'username', + autofillHintsForFields: [ + 'username', + 'email', + 'name', + 'telephoneNumber' + ]); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterMultiAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // Form is added to DOM. + expect(document.getElementsByTagName('form'), isNotEmpty); + FormElement formElement = document.getElementsByTagName('form')[0]; + final Completer submittedForm = Completer(); + formElement.addEventListener( + 'submit', (event) => submittedForm.complete(true)); + + // Clear client is not called. The used requested context to be finalized. + const MethodCall finishAutofillContext = + MethodCall('TextInput.finishAutofillContext', true); + sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext)); + + // Connection is closed by the engine. + expect(spy.messages, hasLength(1)); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.onConnectionClosed'); + + // `submit` action is called on form. + await expectLater(await submittedForm.future, true); + // Form element is removed from DOM. + expect(document.getElementsByTagName('form'), hasLength(0)); + expect(formsOnTheDom, hasLength(0)); + }, + // TODO(nurhan): https://github.com/flutter/flutter/issues/50590 + // TODO(nurhan): https://github.com/flutter/flutter/issues/50769 + skip: (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.edge)); + test('setClient, setEditingState, show, setClient', () { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); @@ -816,14 +1000,66 @@ void main() { textEditing.editingElement.domElement, 'abcd', 2, 3); final FormElement formElement = document.getElementsByTagName('form')[0]; - expect(formElement.childNodes, hasLength(1)); + // The form has one input element and one submit button. + expect(formElement.childNodes, hasLength(2)); const MethodCall clearClient = MethodCall('TextInput.clearClient'); sendFrameworkMessage(codec.encodeMethodCall(clearClient)); // Confirm that [HybridTextEditing] didn't send any messages. expect(spy.messages, isEmpty); - expect(document.getElementsByTagName('form'), isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); + }); + + test( + 'singleTextField Autofill setEditableSizeAndTransform preserves' + 'editing state', () { + // Create a configuration with focused element has autofil hint. + final Map flutterSingleAutofillElementConfig = + createFlutterConfig('text', autofillHint: 'username'); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterSingleAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // The second [setEditingState] should override the first one. + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + // The transform is changed. For example after a validation error, red + // line appeared under the input field. + final MethodCall setSizeAndTransform = + configureSetSizeAndTransformMethodCall(150, 50, + Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); + sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + + // Check the element still has focus. User can keep editing. + expect(document.activeElement, textEditing.editingElement.domElement); + + // Check the cursor location is the same. + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + + // Confirm that [HybridTextEditing] didn't send any messages. + expect(spy.messages, isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); }); test( @@ -859,14 +1095,17 @@ void main() { textEditing.editingElement.domElement, 'abcd', 2, 3); final FormElement formElement = document.getElementsByTagName('form')[0]; - expect(formElement.childNodes, hasLength(4)); + // The form has 4 input elements and one submit button. + expect(formElement.childNodes, hasLength(5)); const MethodCall clearClient = MethodCall('TextInput.clearClient'); sendFrameworkMessage(codec.encodeMethodCall(clearClient)); // Confirm that [HybridTextEditing] didn't send any messages. expect(spy.messages, isEmpty); - expect(document.getElementsByTagName('form'), isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); }); test('No capitilization: setClient, setEditingState, show', () { @@ -1228,7 +1467,8 @@ void main() { textEditing.editingElement.domElement, 'abcd', 2, 3); final FormElement formElement = document.getElementsByTagName('form')[0]; - expect(formElement.childNodes, hasLength(4)); + // The form has 4 input elements and one submit button. + expect(formElement.childNodes, hasLength(5)); // Autofill one of the form elements. InputElement element = formElement.childNodes.first; @@ -1450,13 +1690,17 @@ void main() { // And default behavior of keyboard event shouldn't have been prevented. expect(event.defaultPrevented, isFalse); }); + + tearDown(() { + clearForms(); + }); }); group('EngineAutofillForm', () { test('validate multi element form', () { final List fields = createFieldValues( ['username', 'password', 'newPassword'], - ['field1', 'fields2', 'field3']); + ['field1', 'field2', 'field3']); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( createAutofillInfo('username', 'field1'), fields); @@ -1467,14 +1711,19 @@ void main() { expect(autofillForm.items, hasLength(2)); expect(autofillForm.formElement, isNotNull); + expect(autofillForm.formIdentifier, 'field1*field2*field3'); + final FormElement form = autofillForm.formElement; - expect(form.childNodes, hasLength(2)); + // Note that we also add a submit button. Therefore the form element has + // 3 child nodes. + expect(form.childNodes, hasLength(3)); final InputElement firstElement = form.childNodes.first; // Autofill value is applied to the element. expect(firstElement.name, BrowserAutofillHints.instance.flutterToEngine('password')); - expect(firstElement.id, 'fields2'); + expect(firstElement.id, + BrowserAutofillHints.instance.flutterToEngine('password')); expect(firstElement.type, 'password'); if (browserEngine == BrowserEngine.firefox) { expect(firstElement.name, @@ -1495,7 +1744,20 @@ void main() { expect(css.backgroundColor, 'transparent'); }); - test('place remove form', () { + test('validate multi element form ids sorted for form id', () { + final List fields = createFieldValues( + ['username', 'password', 'newPassword'], + ['zzyyxx', 'aabbcc', 'jjkkll']); + final EngineAutofillForm autofillForm = + EngineAutofillForm.fromFrameworkMessage( + createAutofillInfo('username', 'field1'), fields); + + expect(autofillForm.formIdentifier, 'aabbcc*jjkkll*zzyyxx'); + }); + + test('place and store form', () { + expect(document.getElementsByTagName('form'), isEmpty); + final List fields = createFieldValues( ['username', 'password', 'newPassword'], ['field1', 'fields2', 'field3']); @@ -1506,16 +1768,18 @@ void main() { final InputElement testInputElement = InputElement(); autofillForm.placeForm(testInputElement); - // The focused element is appended to the form, + // The focused element is appended to the form, form also has the button + // so in total it shoould have 4 elements. final FormElement form = autofillForm.formElement; - expect(form.childNodes, hasLength(3)); + expect(form.childNodes, hasLength(4)); final FormElement formOnDom = document.getElementsByTagName('form')[0]; // Form is attached to the DOM. expect(form, equals(formOnDom)); - autofillForm.removeForm(); - expect(document.getElementsByTagName('form'), isEmpty); + autofillForm.storeForm(); + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); }); test('Validate single element form', () { @@ -1531,7 +1795,13 @@ void main() { expect(autofillForm.formElement, isNotNull); final FormElement form = autofillForm.formElement; - expect(form.childNodes, isEmpty); + // Submit button is added to the form. + expect(form.childNodes, isNotEmpty); + final InputElement inputElement = form.childNodes.first; + expect(inputElement.type, 'submit'); + + // The submit button should have class `submitBtn`. + expect(inputElement.className, 'submitBtn'); }); test('Return null if no focused element', () { @@ -1541,6 +1811,10 @@ void main() { expect(autofillForm, isNull); }); + + tearDown(() { + clearForms(); + }); }); group('AutofillInfo', () { @@ -1570,7 +1844,8 @@ void main() { // browsers. expect(testInputElement.name, BrowserAutofillHints.instance.flutterToEngine(testHint)); - expect(testInputElement.id, testId); + expect(testInputElement.id, + BrowserAutofillHints.instance.flutterToEngine(testHint)); expect(testInputElement.type, 'text'); if (browserEngine == BrowserEngine.firefox) { expect(testInputElement.name, @@ -1592,7 +1867,8 @@ void main() { // browsers. expect(testInputElement.name, BrowserAutofillHints.instance.flutterToEngine(testHint)); - expect(testInputElement.id, testId); + expect(testInputElement.id, + BrowserAutofillHints.instance.flutterToEngine(testHint)); expect(testInputElement.getAttribute('autocomplete'), BrowserAutofillHints.instance.flutterToEngine(testHint)); }); @@ -1608,7 +1884,8 @@ void main() { // browsers. expect(testInputElement.name, BrowserAutofillHints.instance.flutterToEngine(testPasswordHint)); - expect(testInputElement.id, testId); + expect(testInputElement.id, + BrowserAutofillHints.instance.flutterToEngine(testPasswordHint)); expect(testInputElement.type, 'password'); expect(testInputElement.getAttribute('autocomplete'), BrowserAutofillHints.instance.flutterToEngine(testPasswordHint)); @@ -1756,7 +2033,7 @@ MethodCall configureSetSizeAndTransformMethodCall( /// Will disable editing element which will also clean the backup DOM /// element from the page. void cleanTextEditingElement() { - if (editingElement.isEnabled) { + if (editingElement != null && editingElement.isEnabled) { // Clean up all the DOM elements and event listeners. editingElement.disable(); } @@ -1867,3 +2144,11 @@ Map createOneFieldValue(String hint, String uniqueId) => 'textCapitalization': 'TextCapitalization.none', 'autofill': createAutofillInfo(hint, uniqueId) }; + +/// In order to not leak test state, clean up the forms from dom if any remains. +void clearForms() { + while (document.getElementsByTagName('form').length > 0) { + document.getElementsByTagName('form').last.remove(); + } + formsOnTheDom.clear(); +} diff --git a/lib/web_ui/test/text_test.dart b/lib/web_ui/test/text_test.dart index 266d88d1eecae..35861b2eda6c0 100644 --- a/lib/web_ui/test/text_test.dart +++ b/lib/web_ui/test/text_test.dart @@ -5,6 +5,7 @@ // @dart = 2.6 import 'dart:html'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart'; @@ -12,7 +13,11 @@ import 'package:ui/src/engine.dart'; import 'matchers.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { const double baselineRatio = 1.1662499904632568; await webOnlyInitializeTestDomRenderer(); diff --git a/lib/web_ui/test/title_test.dart b/lib/web_ui/test/title_test.dart index c076e2acdd138..af366760e49b9 100644 --- a/lib/web_ui/test/title_test.dart +++ b/lib/web_ui/test/title_test.dart @@ -5,11 +5,16 @@ // @dart = 2.6 import 'dart:html'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:test/test.dart'; void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { const MethodCodec codec = JSONMethodCodec(); group('Title', () { diff --git a/lib/web_ui/test/window_test.dart b/lib/web_ui/test/window_test.dart index 8924a06798d5f..44894d8bc8dfe 100644 --- a/lib/web_ui/test/window_test.dart +++ b/lib/web_ui/test/window_test.dart @@ -8,6 +8,7 @@ import 'dart:html' as html; import 'dart:js_util' as js_util; import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -16,6 +17,10 @@ const MethodCodec codec = JSONMethodCodec(); void emptyCallback(ByteData date) {} void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { test('window.defaultRouteName should not change', () { window.locationStrategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')); expect(window.defaultRouteName, '/initial'); diff --git a/runtime/BUILD.gn b/runtime/BUILD.gn index 9bd85d53d581f..58ddf06941a6d 100644 --- a/runtime/BUILD.gn +++ b/runtime/BUILD.gn @@ -35,7 +35,7 @@ group("libdart") { } } -source_set_maybe_fuchsia_legacy("runtime") { +source_set("runtime") { sources = [ "dart_isolate.cc", "dart_isolate.h", @@ -53,6 +53,8 @@ source_set_maybe_fuchsia_legacy("runtime") { "dart_vm_lifecycle.h", "embedder_resources.cc", "embedder_resources.h", + "platform_data.cc", + "platform_data.h", "ptrace_ios.cc", "ptrace_ios.h", "runtime_controller.cc", @@ -63,8 +65,6 @@ source_set_maybe_fuchsia_legacy("runtime") { "service_protocol.h", "skia_concurrent_executor.cc", "skia_concurrent_executor.h", - "window_data.cc", - "window_data.h", ] public_deps = [ "//third_party/rapidjson" ] @@ -75,8 +75,10 @@ source_set_maybe_fuchsia_legacy("runtime") { ":test_font", "//flutter/assets", "//flutter/common", + "//flutter/flow", "//flutter/fml", "//flutter/lib/io", + "//flutter/lib/ui", "//flutter/third_party/tonic", "//flutter/third_party/txt", "//third_party/dart/runtime:dart_api", @@ -91,11 +93,6 @@ source_set_maybe_fuchsia_legacy("runtime") { "//third_party/dart/runtime/observatory:embedded_observatory_archive", ] } - - deps_legacy_and_next = [ - "//flutter/flow:flow", - "//flutter/lib/ui:ui", - ] } if (enable_unittests) { @@ -103,7 +100,7 @@ if (enable_unittests) { dart_main = "fixtures/runtime_test.dart" } - source_set_maybe_fuchsia_legacy("runtime_unittests_common") { + executable("runtime_unittests") { testonly = true sources = [ @@ -117,40 +114,17 @@ if (enable_unittests) { public_deps = [ ":libdart", + ":runtime", ":runtime_fixtures", "//flutter/common", "//flutter/fml", "//flutter/lib/snapshot", "//flutter/testing", + "//flutter/testing:dart", + "//flutter/testing:fixture_test", "//flutter/third_party/tonic", "//third_party/dart/runtime/bin:elf_loader", "//third_party/skia", ] - - deps_legacy_and_next = [ - ":runtime", - "//flutter/testing:dart", - "//flutter/testing:fixture_test", - ] - } - - if (is_fuchsia) { - executable("runtime_unittests") { - testonly = true - - deps = [ ":runtime_unittests_common_fuchsia_legacy" ] - } - - executable("runtime_unittests_next") { - testonly = true - - deps = [ ":runtime_unittests_common" ] - } - } else { - executable("runtime_unittests") { - testonly = true - - deps = [ ":runtime_unittests_common" ] - } } } diff --git a/runtime/dart_isolate.cc b/runtime/dart_isolate.cc index 53459a9415b75..06ef693b4b46b 100644 --- a/runtime/dart_isolate.cc +++ b/runtime/dart_isolate.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/runtime/dart_isolate.h" @@ -44,7 +43,7 @@ class DartErrorString { } char** error() { return &str_; } const char* str() const { return str_; } - operator bool() const { return str_ != nullptr; } + explicit operator bool() const { return str_ != nullptr; } private: FML_DISALLOW_COPY_AND_ASSIGN(DartErrorString); @@ -57,7 +56,7 @@ std::weak_ptr DartIsolate::CreateRootIsolate( const Settings& settings, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - std::unique_ptr window, + std::unique_ptr platform_configuration, fml::WeakPtr snapshot_delegate, fml::WeakPtr io_manager, fml::RefPtr unref_queue, @@ -112,7 +111,8 @@ std::weak_ptr DartIsolate::CreateRootIsolate( std::shared_ptr* root_isolate_data = static_cast*>(Dart_IsolateData(vm_isolate)); - (*root_isolate_data)->SetWindow(std::move(window)); + (*root_isolate_data) + ->SetPlatformConfiguration(std::move(platform_configuration)); return (*root_isolate_data)->GetWeakIsolatePtr(); } @@ -139,7 +139,9 @@ DartIsolate::DartIsolate(const Settings& settings, settings.unhandled_exception_callback, DartVMRef::GetIsolateNameServer(), is_root_isolate), - disable_http_(settings.disable_http) { + may_insecurely_connect_to_all_domains_( + settings.may_insecurely_connect_to_all_domains), + domain_network_policy_(settings.domain_network_policy) { phase_ = Phase::Uninitialized; } @@ -263,7 +265,8 @@ bool DartIsolate::LoadLibraries() { tonic::DartState::Scope scope(this); - DartIO::InitForIsolate(disable_http_); + DartIO::InitForIsolate(may_insecurely_connect_to_all_domains_, + domain_network_policy_); DartUI::InitForIsolate(); @@ -384,7 +387,7 @@ bool DartIsolate::LoadKernel(std::shared_ptr mapping, if (GetIsolateGroupData().GetChildIsolatePreparer() == nullptr) { GetIsolateGroupData().SetChildIsolatePreparer( [buffers = kernel_buffers_](DartIsolate* isolate) { - for (unsigned long i = 0; i < buffers.size(); i++) { + for (uint64_t i = 0; i < buffers.size(); i++) { bool last_piece = i + 1 == buffers.size(); const std::shared_ptr& buffer = buffers.at(i); if (!isolate->PrepareForRunningFromKernel(buffer, last_piece)) { @@ -598,7 +601,7 @@ Dart_Isolate DartIsolate::DartCreateAndStartServiceIsolate( vm_data->GetSettings(), // settings vm_data->GetIsolateSnapshot(), // isolate snapshot null_task_runners, // task runners - nullptr, // window + nullptr, // platform_configuration {}, // snapshot delegate {}, // IO Manager {}, // Skia unref queue @@ -675,6 +678,10 @@ Dart_Isolate DartIsolate::DartIsolateGroupCreateCallback( ); } + if (!parent_isolate_data) { + return nullptr; + } + DartIsolateGroupData& parent_group_data = (*parent_isolate_data)->GetIsolateGroupData(); @@ -689,7 +696,8 @@ Dart_Isolate DartIsolate::DartIsolateGroupCreateCallback( parent_group_data.GetIsolateShutdownCallback()))); TaskRunners null_task_runners(advisory_script_uri, - /* platform= */ nullptr, /* raster= */ nullptr, + /* platform= */ nullptr, + /* raster= */ nullptr, /* ui= */ nullptr, /* io= */ nullptr); @@ -731,7 +739,8 @@ bool DartIsolate::DartIsolateInitializeCallback(void** child_callback_data, Dart_CurrentIsolateGroupData()); TaskRunners null_task_runners((*isolate_group_data)->GetAdvisoryScriptURI(), - /* platform= */ nullptr, /* raster= */ nullptr, + /* platform= */ nullptr, + /* raster= */ nullptr, /* ui= */ nullptr, /* io= */ nullptr); diff --git a/runtime/dart_isolate.h b/runtime/dart_isolate.h index 7f59aa7dc28d0..d3862d577ba73 100644 --- a/runtime/dart_isolate.h +++ b/runtime/dart_isolate.h @@ -16,7 +16,7 @@ #include "flutter/lib/ui/io_manager.h" #include "flutter/lib/ui/snapshot_delegate.h" #include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/runtime/dart_snapshot.h" #include "third_party/dart/runtime/include/dart_api.h" #include "third_party/tonic/dart_state.h" @@ -192,7 +192,7 @@ class DartIsolate : public UIDartState { const Settings& settings, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - std::unique_ptr window, + std::unique_ptr platform_configuration, fml::WeakPtr snapshot_delegate, fml::WeakPtr io_manager, fml::RefPtr skia_unref_queue, @@ -398,7 +398,8 @@ class DartIsolate : public UIDartState { std::vector> kernel_buffers_; std::vector> shutdown_callbacks_; fml::RefPtr message_handling_task_runner_; - const bool disable_http_; + const bool may_insecurely_connect_to_all_domains_; + std::string domain_network_policy_; DartIsolate(const Settings& settings, TaskRunners task_runners, diff --git a/runtime/dart_isolate_unittests.cc b/runtime/dart_isolate_unittests.cc index e751100f071b2..0e9e4cf59521c 100644 --- a/runtime/dart_isolate_unittests.cc +++ b/runtime/dart_isolate_unittests.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/fml/mapping.h" #include "flutter/fml/synchronization/count_down_latch.h" @@ -19,7 +18,19 @@ namespace flutter { namespace testing { -using DartIsolateTest = FixtureTest; +class DartIsolateTest : public FixtureTest { + public: + DartIsolateTest() {} + + void Wait() { latch_.Wait(); } + + void Signal() { latch_.Signal(); } + + private: + fml::AutoResetWaitableEvent latch_; + + FML_DISALLOW_COPY_AND_ASSIGN(DartIsolateTest); +}; TEST_F(DartIsolateTest, RootIsolateCreationAndShutdown) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); @@ -153,11 +164,10 @@ TEST_F(DartIsolateTest, CanRunDartCodeCodeSynchronously) { TEST_F(DartIsolateTest, CanRegisterNativeCallback) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); - fml::AutoResetWaitableEvent latch; AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { FML_LOG(ERROR) << "Hello from Dart!"; - latch.Signal(); + Signal(); }))); const auto settings = CreateSettingsForFixture(); auto vm_ref = DartVMRef::Create(settings); @@ -173,7 +183,7 @@ TEST_F(DartIsolateTest, CanRegisterNativeCallback) { "canRegisterNativeCallback", {}, GetFixturesPath()); ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - latch.Wait(); + Wait(); } TEST_F(DartIsolateTest, CanSaveCompilationTrace) { @@ -182,12 +192,11 @@ TEST_F(DartIsolateTest, CanSaveCompilationTrace) { GTEST_SKIP(); return; } - fml::AutoResetWaitableEvent latch; AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { ASSERT_TRUE(tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 0))); - latch.Signal(); + Signal(); }))); const auto settings = CreateSettingsForFixture(); @@ -205,31 +214,52 @@ TEST_F(DartIsolateTest, CanSaveCompilationTrace) { ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - latch.Wait(); + Wait(); } -TEST_F(DartIsolateTest, CanLaunchSecondaryIsolates) { - fml::CountDownLatch latch(3); - fml::AutoResetWaitableEvent child_shutdown_latch; - fml::AutoResetWaitableEvent root_isolate_shutdown_latch; +class DartSecondaryIsolateTest : public FixtureTest { + public: + DartSecondaryIsolateTest() : latch_(3) {} + + void LatchCountDown() { latch_.CountDown(); } + + void LatchWait() { latch_.Wait(); } + + void ChildShutdownSignal() { child_shutdown_latch_.Signal(); } + + void ChildShutdownWait() { child_shutdown_latch_.Wait(); } + + void RootIsolateShutdownSignal() { root_isolate_shutdown_latch_.Signal(); } + + bool RootIsolateIsSignaled() { + return root_isolate_shutdown_latch_.IsSignaledForTest(); + } + + private: + fml::CountDownLatch latch_; + fml::AutoResetWaitableEvent child_shutdown_latch_; + fml::AutoResetWaitableEvent root_isolate_shutdown_latch_; + + FML_DISALLOW_COPY_AND_ASSIGN(DartSecondaryIsolateTest); +}; + +TEST_F(DartSecondaryIsolateTest, CanLaunchSecondaryIsolates) { AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { - latch.CountDown(); + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { + LatchCountDown(); }))); AddNativeCallback( - "PassMessage", CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + "PassMessage", CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { auto message = tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 0)); ASSERT_EQ("Hello from code is secondary isolate.", message); - latch.CountDown(); + LatchCountDown(); }))); auto settings = CreateSettingsForFixture(); - settings.root_isolate_shutdown_callback = [&root_isolate_shutdown_latch]() { - root_isolate_shutdown_latch.Signal(); - }; - settings.isolate_shutdown_callback = [&child_shutdown_latch]() { - child_shutdown_latch.Signal(); + settings.root_isolate_shutdown_callback = [this]() { + RootIsolateShutdownSignal(); }; + settings.isolate_shutdown_callback = [this]() { ChildShutdownSignal(); }; auto vm_ref = DartVMRef::Create(settings); auto thread = CreateNewThread(); TaskRunners task_runners(GetCurrentTestName(), // @@ -243,19 +273,18 @@ TEST_F(DartIsolateTest, CanLaunchSecondaryIsolates) { GetFixturesPath()); ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - child_shutdown_latch.Wait(); // wait for child isolate to shutdown first - ASSERT_FALSE(root_isolate_shutdown_latch.IsSignaledForTest()); - latch.Wait(); // wait for last NotifyNative called by main isolate + ChildShutdownWait(); // wait for child isolate to shutdown first + ASSERT_FALSE(RootIsolateIsSignaled()); + LatchWait(); // wait for last NotifyNative called by main isolate // root isolate will be auto-shutdown } TEST_F(DartIsolateTest, CanRecieveArguments) { - fml::AutoResetWaitableEvent latch; AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { + CREATE_NATIVE_ENTRY(([this](Dart_NativeArguments args) { ASSERT_TRUE(tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 0))); - latch.Signal(); + Signal(); }))); const auto settings = CreateSettingsForFixture(); @@ -273,7 +302,7 @@ TEST_F(DartIsolateTest, CanRecieveArguments) { ASSERT_TRUE(isolate); ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); - latch.Wait(); + Wait(); } } // namespace testing diff --git a/runtime/dart_service_isolate.cc b/runtime/dart_service_isolate.cc index 9ad95767234da..9c1bbce2b016f 100644 --- a/runtime/dart_service_isolate.cc +++ b/runtime/dart_service_isolate.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/runtime/dart_service_isolate.h" @@ -202,6 +201,7 @@ bool DartServiceIsolate::Startup(std::string server_ip, result = Dart_SetField( library, Dart_NewStringFromCString("_enableServicePortFallback"), Dart_NewBoolean(enable_service_port_fallback)); + SHUTDOWN_ON_ERROR(result); return true; } diff --git a/runtime/dart_vm.cc b/runtime/dart_vm.cc index 47d2149dc4790..1824ea709ce97 100644 --- a/runtime/dart_vm.cc +++ b/runtime/dart_vm.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/runtime/dart_vm.h" @@ -129,13 +128,15 @@ bool DartFileModifiedCallback(const char* source_url, int64_t since_ms) { const char* path = source_url + kFileUriPrefixLength; struct stat info; - if (stat(path, &info) < 0) + if (stat(path, &info) < 0) { return true; + } // If st_mtime is zero, it's more likely that the file system doesn't support // mtime than that the file was actually modified in the 1970s. - if (!info.st_mtime) + if (!info.st_mtime) { return true; + } // It's very unclear what time bases we're with here. The Dart API doesn't // document the time base for since_ms. Reading the code, the value varies by @@ -383,8 +384,9 @@ DartVM::DartVM(std::shared_ptr vm_data, PushBackAll(&args, kDartTraceStreamsArgs, fml::size(kDartTraceStreamsArgs)); #endif - for (size_t i = 0; i < settings_.dart_flags.size(); i++) + for (size_t i = 0; i < settings_.dart_flags.size(); i++) { args.push_back(settings_.dart_flags[i].c_str()); + } char* flags_error = Dart_SetVMFlags(args.size(), args.data()); if (flags_error) { diff --git a/runtime/window_data.cc b/runtime/platform_data.cc similarity index 63% rename from runtime/window_data.cc rename to runtime/platform_data.cc index a92839d5e8a5d..15b9628599b47 100644 --- a/runtime/window_data.cc +++ b/runtime/platform_data.cc @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" namespace flutter { -WindowData::WindowData() = default; +PlatformData::PlatformData() = default; -WindowData::~WindowData() = default; +PlatformData::~PlatformData() = default; } // namespace flutter diff --git a/runtime/window_data.h b/runtime/platform_data.h similarity index 81% rename from runtime/window_data.h rename to runtime/platform_data.h index e234d2f558162..bb7fdd95fb6bb 100644 --- a/runtime/window_data.h +++ b/runtime/platform_data.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_RUNTIME_WINDOW_DATA_H_ -#define FLUTTER_RUNTIME_WINDOW_DATA_H_ +#ifndef FLUTTER_RUNTIME_PLATFORM_DATA_H_ +#define FLUTTER_RUNTIME_PLATFORM_DATA_H_ #include "flutter/lib/ui/window/viewport_metrics.h" @@ -23,12 +23,12 @@ namespace flutter { /// /// See also: /// -/// * flutter::Shell::Create, which takes a window_data to initialize the +/// * flutter::Shell::Create, which takes a platform_data to initialize the /// ui.Window attached to it. -struct WindowData { - WindowData(); +struct PlatformData { + PlatformData(); - ~WindowData(); + ~PlatformData(); ViewportMetrics viewport_metrics; std::string language_code; @@ -45,4 +45,4 @@ struct WindowData { } // namespace flutter -#endif // FLUTTER_RUNTIME_WINDOW_DATA_H_ +#endif // FLUTTER_RUNTIME_PLATFORM_DATA_H_ diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index aa557d2abf716..9c81d6a759cdb 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -8,12 +8,18 @@ #include "flutter/fml/trace_event.h" #include "flutter/lib/ui/compositing/scene.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" +#include "flutter/lib/ui/window/viewport_metrics.h" #include "flutter/lib/ui/window/window.h" #include "flutter/runtime/runtime_delegate.h" #include "third_party/tonic/dart_message_handler.h" namespace flutter { +RuntimeController::RuntimeController(RuntimeDelegate& client, + TaskRunners p_task_runners) + : client_(client), vm_(nullptr), task_runners_(p_task_runners) {} + RuntimeController::RuntimeController( RuntimeDelegate& p_client, DartVM* p_vm, @@ -26,7 +32,7 @@ RuntimeController::RuntimeController( std::string p_advisory_script_uri, std::string p_advisory_script_entrypoint, const std::function& idle_notification_callback, - const WindowData& p_window_data, + const PlatformData& p_platform_data, const fml::closure& p_isolate_create_callback, const fml::closure& p_isolate_shutdown_callback, std::shared_ptr p_persistent_isolate_data) @@ -41,7 +47,7 @@ RuntimeController::RuntimeController( advisory_script_uri_(p_advisory_script_uri), advisory_script_entrypoint_(p_advisory_script_entrypoint), idle_notification_callback_(idle_notification_callback), - window_data_(std::move(p_window_data)), + platform_data_(std::move(p_platform_data)), isolate_create_callback_(p_isolate_create_callback), isolate_shutdown_callback_(p_isolate_shutdown_callback), persistent_isolate_data_(std::move(p_persistent_isolate_data)) { @@ -49,20 +55,21 @@ RuntimeController::RuntimeController( // It will be run at a later point when the engine provides a run // configuration and then runs the isolate. auto strong_root_isolate = - DartIsolate::CreateRootIsolate(vm_->GetVMData()->GetSettings(), // - isolate_snapshot_, // - task_runners_, // - std::make_unique(this), // - snapshot_delegate_, // - io_manager_, // - unref_queue_, // - image_decoder_, // - p_advisory_script_uri, // - p_advisory_script_entrypoint, // - nullptr, // - isolate_create_callback_, // - isolate_shutdown_callback_ // - ) + DartIsolate::CreateRootIsolate( + vm_->GetVMData()->GetSettings(), // + isolate_snapshot_, // + task_runners_, // + std::make_unique(this), // + snapshot_delegate_, // + io_manager_, // + unref_queue_, // + image_decoder_, // + p_advisory_script_uri, // + p_advisory_script_entrypoint, // + nullptr, // + isolate_create_callback_, // + isolate_shutdown_callback_ // + ) .lock(); FML_CHECK(strong_root_isolate) << "Could not create root isolate."; @@ -74,9 +81,9 @@ RuntimeController::RuntimeController( root_isolate_return_code_ = {true, code}; }); - if (auto* window = GetWindowIfAvailable()) { + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { tonic::DartState::Scope scope(strong_root_isolate); - window->DidCreateIsolate(); + platform_configuration->DidCreateIsolate(); if (!FlushRuntimeStateToIsolate()) { FML_DLOG(ERROR) << "Could not setup initial isolate state."; } @@ -121,7 +128,7 @@ std::unique_ptr RuntimeController::Clone() const { advisory_script_uri_, // advisory_script_entrypoint_, // idle_notification_callback_, // - window_data_, // + platform_data_, // isolate_create_callback_, // isolate_shutdown_callback_, // persistent_isolate_data_ // @@ -129,30 +136,32 @@ std::unique_ptr RuntimeController::Clone() const { } bool RuntimeController::FlushRuntimeStateToIsolate() { - return SetViewportMetrics(window_data_.viewport_metrics) && - SetLocales(window_data_.locale_data) && - SetSemanticsEnabled(window_data_.semantics_enabled) && - SetAccessibilityFeatures(window_data_.accessibility_feature_flags_) && - SetUserSettingsData(window_data_.user_settings_data) && - SetLifecycleState(window_data_.lifecycle_state); + return SetViewportMetrics(platform_data_.viewport_metrics) && + SetLocales(platform_data_.locale_data) && + SetSemanticsEnabled(platform_data_.semantics_enabled) && + SetAccessibilityFeatures( + platform_data_.accessibility_feature_flags_) && + SetUserSettingsData(platform_data_.user_settings_data) && + SetLifecycleState(platform_data_.lifecycle_state); } bool RuntimeController::SetViewportMetrics(const ViewportMetrics& metrics) { - window_data_.viewport_metrics = metrics; + platform_data_.viewport_metrics = metrics; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateWindowMetrics(metrics); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->window()->UpdateWindowMetrics(metrics); return true; } + return false; } bool RuntimeController::SetLocales( const std::vector& locale_data) { - window_data_.locale_data = locale_data; + platform_data_.locale_data = locale_data; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateLocales(locale_data); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateLocales(locale_data); return true; } @@ -160,10 +169,11 @@ bool RuntimeController::SetLocales( } bool RuntimeController::SetUserSettingsData(const std::string& data) { - window_data_.user_settings_data = data; + platform_data_.user_settings_data = data; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateUserSettingsData(window_data_.user_settings_data); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateUserSettingsData( + platform_data_.user_settings_data); return true; } @@ -171,10 +181,11 @@ bool RuntimeController::SetUserSettingsData(const std::string& data) { } bool RuntimeController::SetLifecycleState(const std::string& data) { - window_data_.lifecycle_state = data; + platform_data_.lifecycle_state = data; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateLifecycleState(window_data_.lifecycle_state); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateLifecycleState( + platform_data_.lifecycle_state); return true; } @@ -182,10 +193,11 @@ bool RuntimeController::SetLifecycleState(const std::string& data) { } bool RuntimeController::SetSemanticsEnabled(bool enabled) { - window_data_.semantics_enabled = enabled; + platform_data_.semantics_enabled = enabled; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateSemanticsEnabled(window_data_.semantics_enabled); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateSemanticsEnabled( + platform_data_.semantics_enabled); return true; } @@ -193,10 +205,10 @@ bool RuntimeController::SetSemanticsEnabled(bool enabled) { } bool RuntimeController::SetAccessibilityFeatures(int32_t flags) { - window_data_.accessibility_feature_flags_ = flags; - if (auto* window = GetWindowIfAvailable()) { - window->UpdateAccessibilityFeatures( - window_data_.accessibility_feature_flags_); + platform_data_.accessibility_feature_flags_ = flags; + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->UpdateAccessibilityFeatures( + platform_data_.accessibility_feature_flags_); return true; } @@ -204,22 +216,24 @@ bool RuntimeController::SetAccessibilityFeatures(int32_t flags) { } bool RuntimeController::BeginFrame(fml::TimePoint frame_time) { - if (auto* window = GetWindowIfAvailable()) { - window->BeginFrame(frame_time); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->BeginFrame(frame_time); return true; } + return false; } bool RuntimeController::ReportTimings(std::vector timings) { - if (auto* window = GetWindowIfAvailable()) { - window->ReportTimings(std::move(timings)); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->ReportTimings(std::move(timings)); return true; } + return false; } -bool RuntimeController::NotifyIdle(int64_t deadline) { +bool RuntimeController::NotifyIdle(int64_t deadline, size_t freed_hint) { std::shared_ptr root_isolate = root_isolate_.lock(); if (!root_isolate) { return false; @@ -227,6 +241,9 @@ bool RuntimeController::NotifyIdle(int64_t deadline) { tonic::DartState::Scope scope(root_isolate); + // Dart will use the freed hint at the next idle notification. Make sure to + // Update it with our latest value before calling NotifyIdle. + Dart_HintFreed(freed_hint); Dart_NotifyIdle(deadline); // Idle notifications being in isolate scope are part of the contract. @@ -239,23 +256,25 @@ bool RuntimeController::NotifyIdle(int64_t deadline) { bool RuntimeController::DispatchPlatformMessage( fml::RefPtr message) { - if (auto* window = GetWindowIfAvailable()) { + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { TRACE_EVENT1("flutter", "RuntimeController::DispatchPlatformMessage", "mode", "basic"); - window->DispatchPlatformMessage(std::move(message)); + platform_configuration->DispatchPlatformMessage(std::move(message)); return true; } + return false; } bool RuntimeController::DispatchPointerDataPacket( const PointerDataPacket& packet) { - if (auto* window = GetWindowIfAvailable()) { + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { TRACE_EVENT1("flutter", "RuntimeController::DispatchPointerDataPacket", "mode", "basic"); - window->DispatchPointerDataPacket(packet); + platform_configuration->window()->DispatchPointerDataPacket(packet); return true; } + return false; } @@ -264,69 +283,72 @@ bool RuntimeController::DispatchSemanticsAction(int32_t id, std::vector args) { TRACE_EVENT1("flutter", "RuntimeController::DispatchSemanticsAction", "mode", "basic"); - if (auto* window = GetWindowIfAvailable()) { - window->DispatchSemanticsAction(id, action, std::move(args)); + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + platform_configuration->DispatchSemanticsAction(id, action, + std::move(args)); return true; } + return false; } -Window* RuntimeController::GetWindowIfAvailable() { +PlatformConfiguration* +RuntimeController::GetPlatformConfigurationIfAvailable() { std::shared_ptr root_isolate = root_isolate_.lock(); - return root_isolate ? root_isolate->window() : nullptr; + return root_isolate ? root_isolate->platform_configuration() : nullptr; } -// |WindowClient| +// |PlatformConfigurationClient| std::string RuntimeController::DefaultRouteName() { return client_.DefaultRouteName(); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::ScheduleFrame() { client_.ScheduleFrame(); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::Render(Scene* scene) { client_.Render(scene->takeLayerTree()); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::UpdateSemantics(SemanticsUpdate* update) { - if (window_data_.semantics_enabled) { + if (platform_data_.semantics_enabled) { client_.UpdateSemantics(update->takeNodes(), update->takeActions()); } } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::HandlePlatformMessage( fml::RefPtr message) { client_.HandlePlatformMessage(std::move(message)); } -// |WindowClient| +// |PlatformConfigurationClient| FontCollection& RuntimeController::GetFontCollection() { return client_.GetFontCollection(); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) { client_.UpdateIsolateDescription(isolate_name, isolate_port); } -// |WindowClient| +// |PlatformConfigurationClient| void RuntimeController::SetNeedsReportTimings(bool value) { client_.SetNeedsReportTimings(value); } -// |WindowClient| +// |PlatformConfigurationClient| std::shared_ptr RuntimeController::GetPersistentIsolateData() { return persistent_isolate_data_; } -// |WindowClient| +// |PlatformConfigurationClient| std::unique_ptr> RuntimeController::ComputePlatformResolvedLocale( const std::vector& supported_locale_data) { diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index ad89cbeae064b..e1b29f127a79f 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -14,10 +14,10 @@ #include "flutter/lib/ui/io_manager.h" #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/pointer_data_packet.h" -#include "flutter/lib/ui/window/window.h" #include "flutter/runtime/dart_vm.h" -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" #include "rapidjson/document.h" #include "rapidjson/stringbuffer.h" @@ -38,7 +38,7 @@ class Window; /// used by the engine to copy the currently accumulated window state so it can /// be referenced by the new runtime controller. /// -class RuntimeController final : public WindowClient { +class RuntimeController : public PlatformConfigurationClient { public: //---------------------------------------------------------------------------- /// @brief Creates a new instance of a runtime controller. This is @@ -90,7 +90,7 @@ class RuntimeController final : public WindowClient { /// code in isolate scope when the VM /// is about to be notified that the /// engine is going to be idle. - /// @param[in] window_data The window data (if exists). + /// @param[in] platform_data The window data (if exists). /// @param[in] isolate_create_callback The isolate create callback. This /// allows callers to run native code /// in isolate scope on the UI task @@ -117,12 +117,12 @@ class RuntimeController final : public WindowClient { std::string advisory_script_uri, std::string advisory_script_entrypoint, const std::function& idle_notification_callback, - const WindowData& window_data, + const PlatformData& platform_data, const fml::closure& isolate_create_callback, const fml::closure& isolate_shutdown_callback, std::shared_ptr persistent_isolate_data); - // |WindowClient| + // |PlatformConfigurationClient| ~RuntimeController() override; //---------------------------------------------------------------------------- @@ -136,11 +136,11 @@ class RuntimeController final : public WindowClient { std::unique_ptr Clone() const; //---------------------------------------------------------------------------- - /// @brief Forward the specified window metrics to the running isolate. + /// @brief Forward the specified viewport metrics to the running isolate. /// If the isolate is not running, these metrics will be saved and /// flushed to the isolate when it starts. /// - /// @param[in] metrics The metrics. + /// @param[in] metrics The viewport metrics. /// /// @return If the window metrics were forwarded to the running isolate. /// @@ -329,9 +329,12 @@ class RuntimeController final : public WindowClient { /// system's monotonic time. The clock can be accessed via /// `Dart_TimelineGetMicros`. /// + /// @param[in] freed_hint A hint of the number of bytes potentially freed + /// since the last call to NotifyIdle if a GC were run. + /// /// @return If the idle notification was forwarded to the running isolate. /// - bool NotifyIdle(int64_t deadline); + bool NotifyIdle(int64_t deadline, size_t freed_hint); //---------------------------------------------------------------------------- /// @brief Returns if the root isolate is running. The isolate must be @@ -340,7 +343,7 @@ class RuntimeController final : public WindowClient { /// /// @return True if root isolate running, False otherwise. /// - bool IsRootIsolateRunning() const; + virtual bool IsRootIsolateRunning() const; //---------------------------------------------------------------------------- /// @brief Dispatch the specified platform message to running root @@ -351,7 +354,7 @@ class RuntimeController final : public WindowClient { /// @return If the message was dispatched to the running root isolate. /// This may fail is an isolate is not running. /// - bool DispatchPlatformMessage(fml::RefPtr message); + virtual bool DispatchPlatformMessage(fml::RefPtr message); //---------------------------------------------------------------------------- /// @brief Dispatch the specified pointer data message to the running @@ -440,6 +443,10 @@ class RuntimeController final : public WindowClient { /// std::pair GetRootIsolateReturnCode(); + protected: + /// Constructor for Mocks. + RuntimeController(RuntimeDelegate& client, TaskRunners p_task_runners); + private: struct Locale { Locale(std::string language_code_, @@ -466,46 +473,46 @@ class RuntimeController final : public WindowClient { std::string advisory_script_uri_; std::string advisory_script_entrypoint_; std::function idle_notification_callback_; - WindowData window_data_; + PlatformData platform_data_; std::weak_ptr root_isolate_; std::pair root_isolate_return_code_ = {false, 0}; const fml::closure isolate_create_callback_; const fml::closure isolate_shutdown_callback_; std::shared_ptr persistent_isolate_data_; - Window* GetWindowIfAvailable(); + PlatformConfiguration* GetPlatformConfigurationIfAvailable(); bool FlushRuntimeStateToIsolate(); - // |WindowClient| + // |PlatformConfigurationClient| std::string DefaultRouteName() override; - // |WindowClient| + // |PlatformConfigurationClient| void ScheduleFrame() override; - // |WindowClient| + // |PlatformConfigurationClient| void Render(Scene* scene) override; - // |WindowClient| + // |PlatformConfigurationClient| void UpdateSemantics(SemanticsUpdate* update) override; - // |WindowClient| + // |PlatformConfigurationClient| void HandlePlatformMessage(fml::RefPtr message) override; - // |WindowClient| + // |PlatformConfigurationClient| FontCollection& GetFontCollection() override; - // |WindowClient| + // |PlatformConfigurationClient| void UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) override; - // |WindowClient| + // |PlatformConfigurationClient| void SetNeedsReportTimings(bool value) override; - // |WindowClient| + // |PlatformConfigurationClient| std::shared_ptr GetPersistentIsolateData() override; - // |WindowClient| + // |PlatformConfigurationClient| std::unique_ptr> ComputePlatformResolvedLocale( const std::vector& supported_locale_data) override; diff --git a/runtime/service_protocol.cc b/runtime/service_protocol.cc index 331e25e03fc73..ace3039f2f959 100644 --- a/runtime/service_protocol.cc +++ b/runtime/service_protocol.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #define RAPIDJSON_HAS_STDSTRING 1 @@ -36,6 +35,9 @@ const std::string_view ServiceProtocol::kGetDisplayRefreshRateExtensionName = "_flutter.getDisplayRefreshRate"; const std::string_view ServiceProtocol::kGetSkSLsExtensionName = "_flutter.getSkSLs"; +const std::string_view + ServiceProtocol::kEstimateRasterCacheMemoryExtensionName = + "_flutter.estimateRasterCacheMemory"; static constexpr std::string_view kViewIdPrefx = "_flutterView/"; static constexpr std::string_view kListViewsExtensionName = @@ -54,6 +56,7 @@ ServiceProtocol::ServiceProtocol() kSetAssetBundlePathExtensionName, kGetDisplayRefreshRateExtensionName, kGetSkSLsExtensionName, + kEstimateRasterCacheMemoryExtensionName, }), handlers_mutex_(fml::SharedMutex::Create()) {} @@ -76,8 +79,9 @@ void ServiceProtocol::SetHandlerDescription(Handler* handler, Handler::Description description) { fml::SharedLock lock(*handlers_mutex_); auto it = handlers_.find(handler); - if (it != handlers_.end()) + if (it != handlers_.end()) { it->second.Store(description); + } } void ServiceProtocol::ToggleHooks(bool set) { @@ -90,13 +94,13 @@ void ServiceProtocol::ToggleHooks(bool set) { } } -static void WriteServerErrorResponse(rapidjson::Document& document, +static void WriteServerErrorResponse(rapidjson::Document* document, const char* message) { - document.SetObject(); - document.AddMember("code", -32000, document.GetAllocator()); + document->SetObject(); + document->AddMember("code", -32000, document->GetAllocator()); rapidjson::Value message_value; - message_value.SetString(message, document.GetAllocator()); - document.AddMember("message", message_value, document.GetAllocator()); + message_value.SetString(message, document->GetAllocator()); + document->AddMember("message", message_value, document->GetAllocator()); } bool ServiceProtocol::HandleMessage(const char* method, @@ -123,7 +127,7 @@ bool ServiceProtocol::HandleMessage(const char* method, bool result = HandleMessage(std::string_view{method}, // params, // static_cast(user_data), // - document // + &document // ); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); @@ -141,7 +145,7 @@ bool ServiceProtocol::HandleMessage(const char* method, bool ServiceProtocol::HandleMessage(std::string_view method, const Handler::ServiceProtocolMap& params, ServiceProtocol* service_protocol, - rapidjson::Document& response) { + rapidjson::Document* response) { if (service_protocol == nullptr) { WriteServerErrorResponse(response, "Service protocol unavailable."); return false; @@ -154,7 +158,7 @@ bool ServiceProtocol::HandleMessage(std::string_view method, ServiceProtocol::Handler* handler, std::string_view method, const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& document) { + rapidjson::Document* document) { FML_DCHECK(handler); fml::AutoResetWaitableEvent latch; bool result = false; @@ -177,7 +181,7 @@ bool ServiceProtocol::HandleMessage(std::string_view method, bool ServiceProtocol::HandleMessage(std::string_view method, const Handler::ServiceProtocolMap& params, - rapidjson::Document& response) const { + rapidjson::Document* response) const { if (method == kListViewsExtensionName) { // So far, this is the only built-in method that does not forward to the // dynamic set of handlers. @@ -254,7 +258,7 @@ void ServiceProtocol::Handler::Description::Write( } bool ServiceProtocol::HandleListViewsMethod( - rapidjson::Document& response) const { + rapidjson::Document* response) const { fml::SharedLock lock(*handlers_mutex_); std::vector> descriptions; for (const auto& handler : handlers_) { @@ -262,11 +266,11 @@ bool ServiceProtocol::HandleListViewsMethod( handler.second.Load()); } - auto& allocator = response.GetAllocator(); + auto& allocator = response->GetAllocator(); // Construct the response objects. - response.SetObject(); - response.AddMember("type", "FlutterViewList", allocator); + response->SetObject(); + response->AddMember("type", "FlutterViewList", allocator); rapidjson::Value viewsList(rapidjson::Type::kArrayType); for (const auto& description : descriptions) { @@ -276,7 +280,7 @@ bool ServiceProtocol::HandleListViewsMethod( viewsList.PushBack(view, allocator); } - response.AddMember("views", viewsList, allocator); + response->AddMember("views", viewsList, allocator); return true; } diff --git a/runtime/service_protocol.h b/runtime/service_protocol.h index c66a836a2c52e..e809b7d833fc4 100644 --- a/runtime/service_protocol.h +++ b/runtime/service_protocol.h @@ -28,6 +28,7 @@ class ServiceProtocol { static const std::string_view kSetAssetBundlePathExtensionName; static const std::string_view kGetDisplayRefreshRateExtensionName; static const std::string_view kGetSkSLsExtensionName; + static const std::string_view kEstimateRasterCacheMemoryExtensionName; class Handler { public: @@ -56,7 +57,7 @@ class ServiceProtocol { virtual bool HandleServiceProtocolMessage( std::string_view method, // one if the extension names specified above. const ServiceProtocolMap& params, - rapidjson::Document& response) = 0; + rapidjson::Document* response) = 0; }; ServiceProtocol(); @@ -87,12 +88,12 @@ class ServiceProtocol { std::string_view method, const Handler::ServiceProtocolMap& params, ServiceProtocol* service_protocol, - rapidjson::Document& response); + rapidjson::Document* response); [[nodiscard]] bool HandleMessage(std::string_view method, const Handler::ServiceProtocolMap& params, - rapidjson::Document& response) const; + rapidjson::Document* response) const; - [[nodiscard]] bool HandleListViewsMethod(rapidjson::Document& response) const; + [[nodiscard]] bool HandleListViewsMethod(rapidjson::Document* response) const; FML_DISALLOW_COPY_AND_ASSIGN(ServiceProtocol); }; diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index 28ca87584f6db..62277b8b35e7c 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -58,7 +58,7 @@ template("dart_embedder_resources") { } } -source_set_maybe_fuchsia_legacy("common") { +source_set("common") { sources = [ "animator.cc", "animator.h", @@ -110,27 +110,17 @@ source_set_maybe_fuchsia_legacy("common") { deps = [ "//flutter/assets", "//flutter/common", + "//flutter/flow", "//flutter/fml", + "//flutter/lib/ui", + "//flutter/runtime", "//flutter/shell/profiling", "//third_party/dart/runtime:dart_api", "//third_party/skia", ] - - deps_legacy_and_next = [ - "//flutter/flow:flow", - "//flutter/lib/ui:ui", - "//flutter/runtime:runtime", - ] } template("shell_host_executable") { - common_dep = ":common" - if (defined(invoker.fuchsia_legacy)) { - if (invoker.fuchsia_legacy) { - common_dep += "_fuchsia_legacy" - } - } - executable(target_name) { testonly = true @@ -141,9 +131,9 @@ template("shell_host_executable") { forward_variables_from(invoker, "*") deps += [ + ":common", "//flutter/lib/snapshot", "//flutter/runtime:libdart", - common_dep, ] public_configs = [ "//flutter:export_dynamic_symbols" ] @@ -158,6 +148,14 @@ if (enable_unittests) { test_enable_metal = false } + config("test_enable_gl_config") { + defines = [ "SHELL_ENABLE_GL" ] + } + + config("test_enable_vulkan_config") { + defines = [ "SHELL_ENABLE_VULKAN" ] + } + shell_gpu_configuration("shell_unittests_gpu_configuration") { enable_software = test_enable_software enable_vulkan = test_enable_vulkan @@ -177,12 +175,13 @@ if (enable_unittests) { deps = [ ":shell_unittests_fixtures", "//flutter/benchmarking", + "//flutter/flow", "//flutter/testing:dart", "//flutter/testing:testing_lib", ] } - source_set_maybe_fuchsia_legacy("shell_test_fixture_sources") { + source_set("shell_test_fixture_sources") { testonly = true sources = [ @@ -197,18 +196,25 @@ if (enable_unittests) { ] public_deps = [ + "//flutter/flow", "//flutter/fml/dart", + "//flutter/runtime", + "//flutter/shell/common", "//flutter/testing", ] deps = [ + ":shell_unittests_gpu_configuration", "//flutter/assets", "//flutter/common", + "//flutter/lib/ui", + "//flutter/testing:dart", + "//flutter/testing:fixture_test", "//third_party/rapidjson", "//third_party/skia", ] - defines = [] + public_configs = [] # SwiftShader only supports x86/x64_64 if (target_cpu == "x86" || target_cpu == "x64") { @@ -218,9 +224,9 @@ if (enable_unittests) { "shell_test_platform_view_gl.h", ] - public_deps += [ "//flutter/testing:opengl" ] + public_configs += [ ":test_enable_gl_config" ] - defines += [ "SHELL_ENABLE_GL" ] + public_deps += [ "//flutter/testing:opengl" ] } } @@ -230,34 +236,22 @@ if (enable_unittests) { "shell_test_platform_view_vulkan.h", ] + public_configs += [ ":test_enable_vulkan_config" ] + public_deps += [ "//flutter/testing:vulkan", "//flutter/vulkan", ] - - defines += [ "SHELL_ENABLE_VULKAN" ] } - - public_deps_legacy_and_next = [ - "//flutter/shell/common:common", - "//flutter/flow:flow", - "//flutter/runtime:runtime", - ] - - deps_legacy_and_next = [ - ":shell_unittests_gpu_configuration", - "//flutter/lib/ui:ui", - "//flutter/testing:dart", - "//flutter/testing:fixture_test", - ] } - source_set_maybe_fuchsia_legacy("shell_unittests_common") { + shell_host_executable("shell_unittests") { testonly = true sources = [ "animator_unittests.cc", "canvas_spy_unittests.cc", + "engine_unittests.cc", "input_events_unittests.cc", "persistent_cache_unittests.cc", "pipeline_unittests.cc", @@ -266,35 +260,11 @@ if (enable_unittests) { ] deps = [ + ":shell_test_fixture_sources", + ":shell_unittests_fixtures", "//flutter/assets", "//flutter/shell/version", + "//third_party/googletest:gmock", ] - - public_deps_legacy_and_next = [ ":shell_test_fixture_sources" ] - } - - if (is_fuchsia) { - shell_host_executable("shell_unittests") { - deps = [ - ":shell_unittests_common_fuchsia_legacy", - ":shell_unittests_fixtures", - ] - - fuchsia_legacy = true - } - - shell_host_executable("shell_unittests_next") { - deps = [ - ":shell_unittests_common", - ":shell_unittests_fixtures", - ] - } - } else { - shell_host_executable("shell_unittests") { - deps = [ - ":shell_unittests_common", - ":shell_unittests_fixtures", - ] - } } } diff --git a/shell/common/animator.cc b/shell/common/animator.cc index 7e581c07ca6e5..3ca1a83788773 100644 --- a/shell/common/animator.cc +++ b/shell/common/animator.cc @@ -26,6 +26,7 @@ Animator::Animator(Delegate& delegate, task_runners_(std::move(task_runners)), waiter_(std::move(waiter)), last_frame_begin_time_(), + last_vsync_start_time_(), last_frame_target_time_(), dart_frame_deadline_(0), #if FLUTTER_SHELL_ENABLE_METAL @@ -98,7 +99,7 @@ static int64_t FxlToDartOrEarlier(fml::TimePoint time) { return (time - fxl_now).ToMicroseconds() + dart_now; } -void Animator::BeginFrame(fml::TimePoint frame_start_time, +void Animator::BeginFrame(fml::TimePoint vsync_start_time, fml::TimePoint frame_target_time) { TRACE_EVENT_ASYNC_END0("flutter", "Frame Request Pending", frame_number_++); @@ -133,7 +134,11 @@ void Animator::BeginFrame(fml::TimePoint frame_start_time, // to service potential frame. FML_DCHECK(producer_continuation_); - last_frame_begin_time_ = frame_start_time; + last_frame_begin_time_ = fml::TimePoint::Now(); + last_vsync_start_time_ = vsync_start_time; + fml::tracing::TraceEventAsyncComplete("flutter", "VsyncSchedulingOverhead", + last_vsync_start_time_, + last_frame_begin_time_); last_frame_target_time_ = frame_target_time; dart_frame_deadline_ = FxlToDartOrEarlier(frame_target_time); { @@ -178,11 +183,9 @@ void Animator::Render(std::unique_ptr layer_tree) { } last_layer_tree_size_ = layer_tree->frame_size(); - if (layer_tree) { - // Note the frame time for instrumentation. - layer_tree->RecordBuildTime(last_frame_begin_time_, - last_frame_target_time_); - } + // Note the frame time for instrumentation. + layer_tree->RecordBuildTime(last_vsync_start_time_, last_frame_begin_time_, + last_frame_target_time_); // Commit the pending continuation. bool result = producer_continuation_.Complete(std::move(layer_tree)); @@ -236,13 +239,13 @@ void Animator::RequestFrame(bool regenerate_layer_tree) { void Animator::AwaitVSync() { waiter_->AsyncWaitForVsync( - [self = weak_factory_.GetWeakPtr()](fml::TimePoint frame_start_time, + [self = weak_factory_.GetWeakPtr()](fml::TimePoint vsync_start_time, fml::TimePoint frame_target_time) { if (self) { if (self->CanReuseLastLayerTree()) { self->DrawLastLayerTree(); } else { - self->BeginFrame(frame_start_time, frame_target_time); + self->BeginFrame(vsync_start_time, frame_target_time); } } }); diff --git a/shell/common/animator.h b/shell/common/animator.h index 0bab57730015a..a6508fe24fa1a 100644 --- a/shell/common/animator.h +++ b/shell/common/animator.h @@ -98,6 +98,7 @@ class Animator final { std::shared_ptr waiter_; fml::TimePoint last_frame_begin_time_; + fml::TimePoint last_vsync_start_time_; fml::TimePoint last_frame_target_time_; int64_t dart_frame_deadline_; fml::RefPtr layer_tree_pipeline_; diff --git a/shell/common/animator_unittests.cc b/shell/common/animator_unittests.cc index e0d73499c0c01..322ea7e70275f 100644 --- a/shell/common/animator_unittests.cc +++ b/shell/common/animator_unittests.cc @@ -58,11 +58,7 @@ TEST_F(ShellTest, VSyncTargetTime) { std::move(create_vsync_waiter), ShellTestPlatformView::BackendType::kDefaultBackend, nullptr); }, - [](Shell& shell) { - return std::make_unique( - shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); ASSERT_TRUE(DartVMRef::IsInstanceRunning()); auto configuration = RunConfiguration::InferFromSettings(settings); diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 438a1945b9f50..dbbac6e583b15 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -35,30 +35,46 @@ static constexpr char kLocalizationChannel[] = "flutter/localization"; static constexpr char kSettingsChannel[] = "flutter/settings"; static constexpr char kIsolateChannel[] = "flutter/isolate"; +Engine::Engine( + Delegate& delegate, + const PointerDataDispatcherMaker& dispatcher_maker, + std::shared_ptr image_decoder_task_runner, + TaskRunners task_runners, + Settings settings, + std::unique_ptr animator, + fml::WeakPtr io_manager, + std::unique_ptr runtime_controller) + : delegate_(delegate), + settings_(std::move(settings)), + animator_(std::move(animator)), + runtime_controller_(std::move(runtime_controller)), + activity_running_(true), + have_surface_(false), + image_decoder_(task_runners, image_decoder_task_runner, io_manager), + task_runners_(std::move(task_runners)), + weak_factory_(this) { + pointer_data_dispatcher_ = dispatcher_maker(*this); +} + Engine::Engine(Delegate& delegate, const PointerDataDispatcherMaker& dispatcher_maker, DartVM& vm, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, std::unique_ptr animator, fml::WeakPtr io_manager, fml::RefPtr unref_queue, fml::WeakPtr snapshot_delegate) - : delegate_(delegate), - settings_(std::move(settings)), - animator_(std::move(animator)), - activity_running_(true), - have_surface_(false), - image_decoder_(task_runners, - vm.GetConcurrentWorkerTaskRunner(), - io_manager), - task_runners_(std::move(task_runners)), - weak_factory_(this) { - // Runtime controller is initialized here because it takes a reference to this - // object as its delegate. The delegate may be called in the constructor and - // we want to be fully initilazed by that point. + : Engine(delegate, + dispatcher_maker, + vm.GetConcurrentWorkerTaskRunner(), + task_runners, + settings, + std::move(animator), + io_manager, + nullptr) { runtime_controller_ = std::make_unique( *this, // runtime delegate &vm, // VM @@ -71,13 +87,11 @@ Engine::Engine(Delegate& delegate, settings_.advisory_script_uri, // advisory script uri settings_.advisory_script_entrypoint, // advisory script entrypoint settings_.idle_notification_callback, // idle notification callback - window_data, // window data + platform_data, // platform data settings_.isolate_create_callback, // isolate create callback settings_.isolate_shutdown_callback, // isolate shutdown callback settings_.persistent_isolate_data // persistent isolate data ); - - pointer_data_dispatcher_ = dispatcher_maker(*this); } Engine::~Engine() = default; @@ -234,11 +248,16 @@ void Engine::ReportTimings(std::vector timings) { runtime_controller_->ReportTimings(std::move(timings)); } +void Engine::HintFreed(size_t size) { + hint_freed_bytes_since_last_idle_ += size; +} + void Engine::NotifyIdle(int64_t deadline) { auto trace_event = std::to_string(deadline - Dart_TimelineGetMicros()); TRACE_EVENT1("flutter", "Engine::NotifyIdle", "deadline_now_delta", trace_event.c_str()); - runtime_controller_->NotifyIdle(deadline); + runtime_controller_->NotifyIdle(deadline, hint_freed_bytes_since_last_idle_); + hint_freed_bytes_since_last_idle_ = 0; } std::pair Engine::GetUIIsolateReturnCode() { @@ -276,7 +295,7 @@ void Engine::SetViewportMetrics(const ViewportMetrics& metrics) { bool dimensions_changed = viewport_metrics_.physical_height != metrics.physical_height || viewport_metrics_.physical_width != metrics.physical_width || - viewport_metrics_.physical_depth != metrics.physical_depth; + viewport_metrics_.device_pixel_ratio != metrics.device_pixel_ratio; viewport_metrics_ = metrics; runtime_controller_->SetViewportMetrics(viewport_metrics_); if (animator_) { @@ -302,7 +321,8 @@ void Engine::DispatchPlatformMessage(fml::RefPtr message) { } else if (channel == kSettingsChannel) { HandleSettingsPlatformMessage(message.get()); return; - } else if (channel == kNavigationChannel) { + } else if (!runtime_controller_->IsRootIsolateRunning() && + channel == kNavigationChannel) { // If there's no runtime_, we may still need to set the initial route. HandleNavigationPlatformMessage(std::move(message)); return; @@ -460,8 +480,7 @@ void Engine::Render(std::unique_ptr layer_tree) { // Ensure frame dimensions are sane. if (layer_tree->frame_size().isEmpty() || - layer_tree->frame_physical_depth() <= 0.0f || - layer_tree->frame_device_pixel_ratio() <= 0.0f) { + layer_tree->device_pixel_ratio() <= 0.0f) { return; } diff --git a/shell/common/engine.h b/shell/common/engine.h index d7e516617afc1..97165f42a69fa 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -248,6 +248,20 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { const std::vector& supported_locale_data) = 0; }; + //---------------------------------------------------------------------------- + /// @brief Creates an instance of the engine with a supplied + /// `RuntimeController`. Use the other constructor except for + /// tests. + /// + Engine(Delegate& delegate, + const PointerDataDispatcherMaker& dispatcher_maker, + std::shared_ptr image_decoder_task_runner, + TaskRunners task_runners, + Settings settings, + std::unique_ptr animator, + fml::WeakPtr io_manager, + std::unique_ptr runtime_controller); + //---------------------------------------------------------------------------- /// @brief Creates an instance of the engine. This is done by the Shell /// on the UI task runner. @@ -295,7 +309,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { DartVM& vm, fml::RefPtr isolate_snapshot, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, std::unique_ptr animator, fml::WeakPtr io_manager, @@ -421,7 +435,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// one frame interval from this point, the Flutter application /// will jank. /// - /// If an root isolate is running, this method calls the + /// If a root isolate is running, this method calls the /// `::_beginFrame` method in `hooks.dart`. If a root isolate is /// not running, this call does nothing. /// @@ -451,6 +465,14 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// void BeginFrame(fml::TimePoint frame_time); + //---------------------------------------------------------------------------- + /// @brief Notifies the engine that native bytes might be freed if a + /// garbage collection ran now. + /// + /// @param[in] size The number of bytes freed. + /// + void HintFreed(size_t size); + //---------------------------------------------------------------------------- /// @brief Notifies the engine that the UI task runner is not expected to /// undertake a new frame workload till a specified timepoint. The @@ -701,9 +723,10 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { //---------------------------------------------------------------------------- /// @brief Notifies the engine that the embedder has expressed an opinion - /// about where the accessibility tree should be generated or not. - /// This call originates in the platform view and is forwarded to - /// the engine here on the UI task runner by the shell. + /// about whether the accessibility tree should be generated or + /// not. This call originates in the platform view and is + /// forwarded to the engine here on the UI task runner by the + /// shell. /// /// @param[in] enabled Whether the accessibility tree is enabled or /// disabled. @@ -755,6 +778,12 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// const std::string& GetLastEntrypointLibrary() const; + //---------------------------------------------------------------------------- + /// @brief Getter for the initial route. This can be set with a platform + /// message. + /// + const std::string& InitialRoute() const { return initial_route_; } + private: Engine::Delegate& delegate_; const Settings settings_; @@ -776,6 +805,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { FontCollection font_collection_; ImageDecoder image_decoder_; TaskRunners task_runners_; + size_t hint_freed_bytes_since_last_idle_ = 0; fml::WeakPtrFactory weak_factory_; // |RuntimeDelegate| diff --git a/shell/common/engine_unittests.cc b/shell/common/engine_unittests.cc new file mode 100644 index 0000000000000..54f6f00d7a8f6 --- /dev/null +++ b/shell/common/engine_unittests.cc @@ -0,0 +1,234 @@ +// 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. +// FLUTTER_NOLINT + +#include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/shell/common/engine.h" +#include "flutter/shell/common/thread_host.h" +#include "flutter/testing/testing.h" +#include "gmock/gmock.h" +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" + +///\note Deprecated MOCK_METHOD macros used until this issue is resolved: +// https://github.com/google/googletest/issues/2490 + +namespace flutter { + +namespace { +class MockDelegate : public Engine::Delegate { + MOCK_METHOD2(OnEngineUpdateSemantics, + void(SemanticsNodeUpdates, CustomAccessibilityActionUpdates)); + MOCK_METHOD1(OnEngineHandlePlatformMessage, + void(fml::RefPtr)); + MOCK_METHOD0(OnPreEngineRestart, void()); + MOCK_METHOD2(UpdateIsolateDescription, void(const std::string, int64_t)); + MOCK_METHOD1(SetNeedsReportTimings, void(bool)); + MOCK_METHOD1(ComputePlatformResolvedLocale, + std::unique_ptr>( + const std::vector&)); +}; + +class MockResponse : public PlatformMessageResponse { + public: + MOCK_METHOD1(Complete, void(std::unique_ptr data)); + MOCK_METHOD0(CompleteEmpty, void()); +}; + +class MockRuntimeDelegate : public RuntimeDelegate { + public: + MOCK_METHOD0(DefaultRouteName, std::string()); + MOCK_METHOD1(ScheduleFrame, void(bool)); + MOCK_METHOD1(Render, void(std::unique_ptr)); + MOCK_METHOD2(UpdateSemantics, + void(SemanticsNodeUpdates, CustomAccessibilityActionUpdates)); + MOCK_METHOD1(HandlePlatformMessage, void(fml::RefPtr)); + MOCK_METHOD0(GetFontCollection, FontCollection&()); + MOCK_METHOD2(UpdateIsolateDescription, void(const std::string, int64_t)); + MOCK_METHOD1(SetNeedsReportTimings, void(bool)); + MOCK_METHOD1(ComputePlatformResolvedLocale, + std::unique_ptr>( + const std::vector&)); +}; + +class MockRuntimeController : public RuntimeController { + public: + MockRuntimeController(RuntimeDelegate& client, TaskRunners p_task_runners) + : RuntimeController(client, p_task_runners) {} + MOCK_CONST_METHOD0(IsRootIsolateRunning, bool()); + MOCK_METHOD1(DispatchPlatformMessage, bool(fml::RefPtr)); +}; + +fml::RefPtr MakePlatformMessage( + const std::string& channel, + const std::map& values, + fml::RefPtr response) { + rapidjson::Document document; + auto& allocator = document.GetAllocator(); + document.SetObject(); + + for (const auto& pair : values) { + rapidjson::Value key(pair.first.c_str(), strlen(pair.first.c_str()), + allocator); + rapidjson::Value value(pair.second.c_str(), strlen(pair.second.c_str()), + allocator); + document.AddMember(key, value, allocator); + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + const uint8_t* data = reinterpret_cast(buffer.GetString()); + + fml::RefPtr message = fml::MakeRefCounted( + channel, std::vector(data, data + buffer.GetSize()), response); + return message; +} + +class EngineTest : public ::testing::Test { + public: + EngineTest() + : thread_host_("EngineTest", + ThreadHost::Type::Platform | ThreadHost::Type::IO | + ThreadHost::Type::UI | ThreadHost::Type::GPU), + task_runners_({ + "EngineTest", + thread_host_.platform_thread->GetTaskRunner(), // platform + thread_host_.raster_thread->GetTaskRunner(), // raster + thread_host_.ui_thread->GetTaskRunner(), // ui + thread_host_.io_thread->GetTaskRunner() // io + }) {} + + void PostUITaskSync(const std::function& function) { + fml::AutoResetWaitableEvent latch; + task_runners_.GetUITaskRunner()->PostTask([&] { + function(); + latch.Signal(); + }); + latch.Wait(); + } + + protected: + void SetUp() override { + dispatcher_maker_ = [](PointerDataDispatcher::Delegate&) { + return nullptr; + }; + } + + MockDelegate delegate_; + PointerDataDispatcherMaker dispatcher_maker_; + ThreadHost thread_host_; + TaskRunners task_runners_; + Settings settings_; + std::unique_ptr animator_; + fml::WeakPtr io_manager_; + std::unique_ptr runtime_controller_; + std::shared_ptr image_decoder_task_runner_; +}; +} // namespace + +TEST_F(EngineTest, Create) { + PostUITaskSync([this] { + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(runtime_controller_)); + EXPECT_TRUE(engine); + }); +} + +TEST_F(EngineTest, DispatchPlatformMessageUnknown) { + PostUITaskSync([this] { + MockRuntimeDelegate client; + auto mock_runtime_controller = + std::make_unique(client, task_runners_); + EXPECT_CALL(*mock_runtime_controller, IsRootIsolateRunning()) + .WillRepeatedly(::testing::Return(false)); + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(mock_runtime_controller)); + + fml::RefPtr response = + fml::MakeRefCounted(); + fml::RefPtr message = + fml::MakeRefCounted("foo", response); + engine->DispatchPlatformMessage(message); + }); +} + +TEST_F(EngineTest, DispatchPlatformMessageInitialRoute) { + PostUITaskSync([this] { + MockRuntimeDelegate client; + auto mock_runtime_controller = + std::make_unique(client, task_runners_); + EXPECT_CALL(*mock_runtime_controller, IsRootIsolateRunning()) + .WillRepeatedly(::testing::Return(false)); + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(mock_runtime_controller)); + + fml::RefPtr response = + fml::MakeRefCounted(); + std::map values{ + {"method", "setInitialRoute"}, + {"args", "test_initial_route"}, + }; + fml::RefPtr message = + MakePlatformMessage("flutter/navigation", values, response); + engine->DispatchPlatformMessage(message); + EXPECT_EQ(engine->InitialRoute(), "test_initial_route"); + }); +} + +TEST_F(EngineTest, DispatchPlatformMessageInitialRouteIgnored) { + PostUITaskSync([this] { + MockRuntimeDelegate client; + auto mock_runtime_controller = + std::make_unique(client, task_runners_); + EXPECT_CALL(*mock_runtime_controller, IsRootIsolateRunning()) + .WillRepeatedly(::testing::Return(true)); + EXPECT_CALL(*mock_runtime_controller, DispatchPlatformMessage(::testing::_)) + .WillRepeatedly(::testing::Return(true)); + auto engine = std::make_unique( + /*delegate=*/delegate_, + /*dispatcher_maker=*/dispatcher_maker_, + /*image_decoder_task_runner=*/image_decoder_task_runner_, + /*task_runners=*/task_runners_, + /*settings=*/settings_, + /*animator=*/std::move(animator_), + /*io_manager=*/io_manager_, + /*runtime_controller=*/std::move(mock_runtime_controller)); + + fml::RefPtr response = + fml::MakeRefCounted(); + std::map values{ + {"method", "setInitialRoute"}, + {"args", "test_initial_route"}, + }; + fml::RefPtr message = + MakePlatformMessage("flutter/navigation", values, response); + engine->DispatchPlatformMessage(message); + EXPECT_EQ(engine->InitialRoute(), ""); + }); +} + +} // namespace flutter diff --git a/shell/common/pointer_data_dispatcher.h b/shell/common/pointer_data_dispatcher.h index c222bee8d699e..208c8b0d2878e 100644 --- a/shell/common/pointer_data_dispatcher.h +++ b/shell/common/pointer_data_dispatcher.h @@ -23,7 +23,7 @@ class PointerDataDispatcher; /// delivered packets, and dispatches them in sync with the VSYNC signal. /// /// This object will be owned by the engine because it relies on the engine's -/// `Animator` (which owns `VsyncWaiter`) and `RuntomeController` to do the +/// `Animator` (which owns `VsyncWaiter`) and `RuntimeController` to do the /// filtering. This object is currently designed to be only called from the UI /// thread (no thread safety is guaranteed). /// diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index d43867876fd87..682d3a6d2672f 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -25,38 +25,17 @@ namespace flutter { // used within this interval. static constexpr std::chrono::milliseconds kSkiaCleanupExpiration(15000); -// TODO(dnfield): Remove this once internal embedders have caught up. -static Rasterizer::DummyDelegate dummy_delegate_; -Rasterizer::Rasterizer( - TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch) - : Rasterizer(dummy_delegate_, - std::move(task_runners), - std::move(compositor_context), - is_gpu_disabled_sync_switch) {} - -Rasterizer::Rasterizer( - Delegate& delegate, - TaskRunners task_runners, - std::shared_ptr is_gpu_disabled_sync_switch) +Rasterizer::Rasterizer(Delegate& delegate) : Rasterizer(delegate, - std::move(task_runners), - std::make_unique( - delegate.GetFrameBudget()), - is_gpu_disabled_sync_switch) {} + std::make_unique(delegate)) {} Rasterizer::Rasterizer( Delegate& delegate, - TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch) + std::unique_ptr compositor_context) : delegate_(delegate), - task_runners_(std::move(task_runners)), compositor_context_(std::move(compositor_context)), user_override_resource_cache_bytes_(false), - weak_factory_(this), - is_gpu_disabled_sync_switch_(is_gpu_disabled_sync_switch) { + weak_factory_(this) { FML_DCHECK(compositor_context_); } @@ -81,10 +60,12 @@ void Rasterizer::Setup(std::unique_ptr surface) { #if !defined(OS_FUCHSIA) // TODO(sanjayc77): https://github.com/flutter/flutter/issues/53179. Add // support for raster thread merger for Fuchsia. - if (surface_->GetExternalViewEmbedder()) { + if (surface_->GetExternalViewEmbedder() && + surface_->GetExternalViewEmbedder()->SupportsDynamicThreadMerging()) { const auto platform_id = - task_runners_.GetPlatformTaskRunner()->GetTaskQueueId(); - const auto gpu_id = task_runners_.GetRasterTaskRunner()->GetTaskQueueId(); + delegate_.GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId(); + const auto gpu_id = + delegate_.GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(); raster_thread_merger_ = fml::MakeRefCounted(platform_id, gpu_id); } @@ -95,19 +76,25 @@ void Rasterizer::Teardown() { compositor_context_->OnGrContextDestroyed(); surface_.reset(); last_layer_tree_.reset(); + if (raster_thread_merger_.get() != nullptr && + raster_thread_merger_.get()->IsMerged()) { + raster_thread_merger_->UnMergeNow(); + } } void Rasterizer::NotifyLowMemoryWarning() const { if (!surface_) { - FML_DLOG(INFO) << "Rasterizer::PurgeCaches called with no surface."; + FML_DLOG(INFO) + << "Rasterizer::NotifyLowMemoryWarning called with no surface."; return; } auto context = surface_->GetContext(); if (!context) { - FML_DLOG(INFO) << "Rasterizer::PurgeCaches called with no GrContext."; + FML_DLOG(INFO) + << "Rasterizer::NotifyLowMemoryWarning called with no GrContext."; return; } - context->freeGpuResources(); + context->performDeferredCleanup(std::chrono::milliseconds(0)); } flutter::TextureRegistry* Rasterizer::GetTextureRegistry() { @@ -132,7 +119,9 @@ void Rasterizer::Draw(fml::RefPtr> pipeline) { // we yield and let this frame be serviced on the right thread. return; } - FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(delegate_.GetTaskRunners() + .GetRasterTaskRunner() + ->RunsTasksOnCurrentThread()); RasterStatus raster_status = RasterStatus::kFailed; Pipeline::Consumer consumer = @@ -166,7 +155,7 @@ void Rasterizer::Draw(fml::RefPtr> pipeline) { // between successive tries. switch (consume_result) { case PipelineConsumeResult::MoreAvailable: { - task_runners_.GetRasterTaskRunner()->PostTask( + delegate_.GetTaskRunners().GetRasterTaskRunner()->PostTask( [weak_this = weak_factory_.GetWeakPtr(), pipeline]() { if (weak_this) { weak_this->Draw(pipeline); @@ -224,7 +213,7 @@ sk_sp Rasterizer::DoMakeRasterSnapshot( sk_sp surface = SkSurface::MakeRaster(image_info); result = DrawSnapshot(surface, draw_callback); } else { - is_gpu_disabled_sync_switch_->Execute( + delegate_.GetIsGpuDisabledSyncSwitch()->Execute( fml::SyncSwitch::Handlers() .SetIfTrue([&] { sk_sp surface = SkSurface::MakeRaster(image_info); @@ -280,7 +269,9 @@ sk_sp Rasterizer::ConvertToRasterImage(sk_sp image) { RasterStatus Rasterizer::DoDraw( std::unique_ptr layer_tree) { - FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(delegate_.GetTaskRunners() + .GetRasterTaskRunner() + ->RunsTasksOnCurrentThread()); if (!layer_tree || !surface_) { return RasterStatus::kFailed; @@ -290,6 +281,7 @@ RasterStatus Rasterizer::DoDraw( #if !defined(OS_FUCHSIA) const fml::TimePoint frame_target_time = layer_tree->target_time(); #endif + timing.Set(FrameTiming::kVsyncStart, layer_tree->vsync_start()); timing.Set(FrameTiming::kBuildStart, layer_tree->build_start()); timing.Set(FrameTiming::kBuildFinish, layer_tree->build_finish()); timing.Set(FrameTiming::kRasterStart, fml::TimePoint::Now()); @@ -670,6 +662,24 @@ std::optional Rasterizer::GetResourceCacheMaxBytes() const { return std::nullopt; } +bool Rasterizer::EnsureThreadsAreMerged() { + if (surface_ == nullptr || raster_thread_merger_.get() == nullptr) { + return false; + } + fml::TaskRunner::RunNowOrPostTask( + delegate_.GetTaskRunners().GetRasterTaskRunner(), + [weak_this = weak_factory_.GetWeakPtr(), + thread_merger = raster_thread_merger_]() { + if (weak_this->surface_ == nullptr) { + return; + } + thread_merger->MergeWithLease(10); + }); + raster_thread_merger_->WaitUntilMerged(); + FML_DCHECK(raster_thread_merger_->IsMerged()); + return true; +} + Rasterizer::Screenshot::Screenshot() {} Rasterizer::Screenshot::Screenshot(sk_sp p_data, SkISize p_size) diff --git a/shell/common/rasterizer.h b/shell/common/rasterizer.h index 68701d0c0a75b..963a0998db465 100644 --- a/shell/common/rasterizer.h +++ b/shell/common/rasterizer.h @@ -50,7 +50,7 @@ class Rasterizer final : public SnapshotDelegate { /// are made on the GPU task runner. Any delegate must ensure that /// they can handle the threading implications. /// - class Delegate { + class Delegate : public CompositorContext::Delegate { public: //-------------------------------------------------------------------------- /// @brief Notifies the delegate that a frame has been rendered. The @@ -74,41 +74,18 @@ class Rasterizer final : public SnapshotDelegate { /// Target time for the latest frame. See also `Shell::OnAnimatorBeginFrame` /// for when this time gets updated. virtual fml::TimePoint GetLatestFrameTargetTime() const = 0; - }; - // TODO(dnfield): remove once embedders have caught up. - class DummyDelegate : public Delegate { - void OnFrameRasterized(const FrameTiming&) override {} - fml::Milliseconds GetFrameBudget() override { - return fml::kDefaultFrameBudget; - } - // Returning a time in the past so we don't add additional trace - // events when exceeding the frame budget for other embedders. - fml::TimePoint GetLatestFrameTargetTime() const override { - return fml::TimePoint::FromEpochDelta(fml::TimeDelta::Zero()); - } - }; + /// Task runners used by the shell. + virtual const TaskRunners& GetTaskRunners() const = 0; - //---------------------------------------------------------------------------- - /// @brief Creates a new instance of a rasterizer. Rasterizers may only - /// be created on the GPU task runner. Rasterizers are currently - /// only created by the shell. Usually, the shell also sets itself - /// up as the rasterizer delegate. But, this constructor sets up a - /// dummy rasterizer delegate. - /// - // TODO(chinmaygarde): The rasterizer does not use the task runners for - // anything other than thread checks. Remove the same as an argument. - /// - /// @param[in] task_runners The task runners used by the shell. - /// @param[in] compositor_context The compositor context used to hold all - /// the GPU state used by the rasterizer. - /// @param[in] is_gpu_disabled_sync_switch - /// A `SyncSwitch` for handling disabling of the GPU (typically happens - /// when an app is backgrounded) - /// - Rasterizer(TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch); + /// Accessor for the shell's GPU sync switch, which determines whether GPU + /// operations are allowed on the current thread. + /// + /// For example, on some platforms when the application is backgrounded it + /// is critical that GPU operations are not processed. + virtual std::shared_ptr GetIsGpuDisabledSyncSwitch() + const = 0; + }; //---------------------------------------------------------------------------- /// @brief Creates a new instance of a rasterizer. Rasterizers may only @@ -116,18 +93,9 @@ class Rasterizer final : public SnapshotDelegate { /// only created by the shell (which also sets itself up as the /// rasterizer delegate). /// - // TODO(chinmaygarde): The rasterizer does not use the task runners for - // anything other than thread checks. Remove the same as an argument. - /// /// @param[in] delegate The rasterizer delegate. - /// @param[in] task_runners The task runners used by the shell. - /// @param[in] is_gpu_disabled_sync_switch - /// A `SyncSwitch` for handling disabling of the GPU (typically happens - /// when an app is backgrounded) /// - Rasterizer(Delegate& delegate, - TaskRunners task_runners, - std::shared_ptr is_gpu_disabled_sync_switch); + Rasterizer(Delegate& delegate); //---------------------------------------------------------------------------- /// @brief Creates a new instance of a rasterizer. Rasterizers may only @@ -135,21 +103,12 @@ class Rasterizer final : public SnapshotDelegate { /// only created by the shell (which also sets itself up as the /// rasterizer delegate). /// - // TODO(chinmaygarde): The rasterizer does not use the task runners for - // anything other than thread checks. Remove the same as an argument. - /// /// @param[in] delegate The rasterizer delegate. - /// @param[in] task_runners The task runners used by the shell. /// @param[in] compositor_context The compositor context used to hold all /// the GPU state used by the rasterizer. - /// @param[in] is_gpu_disabled_sync_switch - /// A `SyncSwitch` for handling disabling of the GPU (typically happens - /// when an app is backgrounded) /// Rasterizer(Delegate& delegate, - TaskRunners task_runners, - std::unique_ptr compositor_context, - std::shared_ptr is_gpu_disabled_sync_switch); + std::unique_ptr compositor_context); //---------------------------------------------------------------------------- /// @brief Destroys the rasterizer. This must happen on the GPU task @@ -430,9 +389,24 @@ class Rasterizer final : public SnapshotDelegate { /// std::optional GetResourceCacheMaxBytes() const; + //---------------------------------------------------------------------------- + /// @brief Makes sure the raster task runner and the platform task runner + /// are merged. + /// + /// @attention If raster and platform task runners are not the same or not + /// merged, this method will try to merge the task runners, + /// blocking the current thread until the 2 task runners are + /// merged. + /// + /// @return `true` if raster and platform task runners are the same. + /// `true` if/when raster and platform task runners are merged. + /// `false` if the surface or the |RasterThreadMerger| has not + /// been initialized. + /// + bool EnsureThreadsAreMerged(); + private: Delegate& delegate_; - TaskRunners task_runners_; std::unique_ptr surface_; std::unique_ptr compositor_context_; // This is the last successfully rasterized layer tree. @@ -446,7 +420,6 @@ class Rasterizer final : public SnapshotDelegate { std::optional max_cache_bytes_; fml::TaskRunnerAffineWeakPtrFactory weak_factory_; fml::RefPtr raster_thread_merger_; - std::shared_ptr is_gpu_disabled_sync_switch_; // |SnapshotDelegate| sk_sp MakeRasterSnapshot(sk_sp picture, diff --git a/shell/common/serialization_callbacks.cc b/shell/common/serialization_callbacks.cc index b482f5fab9f42..bd334b7bf80e5 100644 --- a/shell/common/serialization_callbacks.cc +++ b/shell/common/serialization_callbacks.cc @@ -11,11 +11,11 @@ namespace flutter { sk_sp SerializeTypefaceWithoutData(SkTypeface* typeface, void* ctx) { - return typeface->serialize(SkTypeface::SerializeBehavior::kDoIncludeData); + return typeface->serialize(SkTypeface::SerializeBehavior::kDontIncludeData); } sk_sp SerializeTypefaceWithData(SkTypeface* typeface, void* ctx) { - return typeface->serialize(SkTypeface::SerializeBehavior::kDontIncludeData); + return typeface->serialize(SkTypeface::SerializeBehavior::kDoIncludeData); } struct ImageMetaData { diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 9137ebcd945a8..f4651184c0e08 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -43,7 +43,7 @@ constexpr char kFontChange[] = "fontsChange"; std::unique_ptr Shell::CreateShellOnPlatformThread( DartVMRef vm, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const Shell::CreateCallback& on_create_platform_view, @@ -134,7 +134,7 @@ std::unique_ptr Shell::CreateShellOnPlatformThread( fml::MakeCopyable([&engine_promise, // shell = shell.get(), // &dispatcher_maker, // - &window_data, // + &platform_data, // isolate_snapshot = std::move(isolate_snapshot), // vsync_waiter = std::move(vsync_waiter), // &weak_io_manager_future, // @@ -155,7 +155,7 @@ std::unique_ptr Shell::CreateShellOnPlatformThread( *shell->GetDartVM(), // std::move(isolate_snapshot), // task_runners, // - window_data, // + platform_data, // shell->GetSettings(), // std::move(animator), // weak_io_manager_future.get(), // @@ -242,17 +242,17 @@ std::unique_ptr Shell::Create( Settings settings, const Shell::CreateCallback& on_create_platform_view, const Shell::CreateCallback& on_create_rasterizer) { - return Shell::Create(std::move(task_runners), // - WindowData{/* default window data */}, // - std::move(settings), // - std::move(on_create_platform_view), // - std::move(on_create_rasterizer) // + return Shell::Create(std::move(task_runners), // + PlatformData{/* default platform data */}, // + std::move(settings), // + std::move(on_create_platform_view), // + std::move(on_create_rasterizer) // ); } std::unique_ptr Shell::Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, Shell::CreateCallback on_create_platform_view, Shell::CreateCallback on_create_rasterizer) { @@ -267,7 +267,7 @@ std::unique_ptr Shell::Create( auto vm_data = vm->GetVMData(); return Shell::Create(std::move(task_runners), // - std::move(window_data), // + std::move(platform_data), // std::move(settings), // vm_data->GetIsolateSnapshot(), // isolate snapshot on_create_platform_view, // @@ -278,7 +278,7 @@ std::unique_ptr Shell::Create( std::unique_ptr Shell::Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const Shell::CreateCallback& on_create_platform_view, @@ -302,7 +302,7 @@ std::unique_ptr Shell::Create( vm = std::move(vm), // &shell, // task_runners = std::move(task_runners), // - window_data, // + platform_data, // settings, // isolate_snapshot = std::move(isolate_snapshot), // on_create_platform_view, // @@ -310,7 +310,7 @@ std::unique_ptr Shell::Create( ]() mutable { shell = CreateShellOnPlatformThread(std::move(vm), std::move(task_runners), // - window_data, // + platform_data, // settings, // std::move(isolate_snapshot), // on_create_platform_view, // @@ -375,6 +375,11 @@ Shell::Shell(DartVMRef vm, TaskRunners task_runners, Settings settings) task_runners_.GetIOTaskRunner(), std::bind(&Shell::OnServiceProtocolGetSkSLs, this, std::placeholders::_1, std::placeholders::_2)}; + service_protocol_handlers_ + [ServiceProtocol::kEstimateRasterCacheMemoryExtensionName] = { + task_runners_.GetRasterTaskRunner(), + std::bind(&Shell::OnServiceProtocolEstimateRasterCacheMemory, this, + std::placeholders::_1, std::placeholders::_2)}; } Shell::~Shell() { @@ -731,47 +736,21 @@ void Shell::OnPlatformViewDestroyed() { fml::TaskRunner::RunNowOrPostTask(io_task_runner, io_task); }; - // The normal flow executed by this method is that the platform thread is - // starting the sequence and waiting on the latch. Later the UI thread posts - // raster_task to the raster thread triggers signaling the latch(on the IO - // thread). If the raster and the platform threads are the same this results - // in a deadlock as the raster_task will never be posted to the plaform/raster - // thread that is blocked on a latch. To avoid the described deadlock, if the - // raster and the platform threads are the same, should_post_raster_task will - // be false, and then instead of posting a task to the raster thread, the ui - // thread just signals the latch and the platform/raster thread follows with - // executing raster_task. - bool should_post_raster_task = task_runners_.GetRasterTaskRunner() != - task_runners_.GetPlatformTaskRunner(); - - auto ui_task = [engine = engine_->GetWeakPtr(), - raster_task_runner = task_runners_.GetRasterTaskRunner(), - raster_task, should_post_raster_task, &latch]() { + auto ui_task = [engine = engine_->GetWeakPtr(), &latch]() { if (engine) { engine->OnOutputSurfaceDestroyed(); } - // Step 1: Next, tell the raster thread that its rasterizer should suspend - // access to the underlying surface. - if (should_post_raster_task) { - fml::TaskRunner::RunNowOrPostTask(raster_task_runner, raster_task); - } else { - // See comment on should_post_raster_task, in this case we just unblock - // the platform thread. - latch.Signal(); - } + latch.Signal(); }; // Step 0: Post a task onto the UI thread to tell the engine that its output // surface is about to go away. fml::TaskRunner::RunNowOrPostTask(task_runners_.GetUITaskRunner(), ui_task); latch.Wait(); - if (!should_post_raster_task) { - // See comment on should_post_raster_task, in this case the raster_task - // wasn't executed, and we just run it here as the platform thread - // is the raster thread. - raster_task(); - latch.Wait(); - } + rasterizer_->EnsureThreadsAreMerged(); + fml::TaskRunner::RunNowOrPostTask(task_runners_.GetRasterTaskRunner(), + raster_task); + latch.Wait(); } // |PlatformView::Delegate| @@ -1200,6 +1179,16 @@ void Shell::OnFrameRasterized(const FrameTiming& timing) { } } +void Shell::OnCompositorEndFrame(size_t freed_hint) { + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + task_runners_.GetUITaskRunner()->PostTask( + [engine = weak_engine_, freed_hint = freed_hint]() { + if (engine) { + engine->HintFreed(freed_hint); + } + }); +} + fml::Milliseconds Shell::GetFrameBudget() { if (display_refresh_rate_ > 0) { return fml::RefreshRateToFrameBudget(display_refresh_rate_.load()); @@ -1230,7 +1219,7 @@ fml::RefPtr Shell::GetServiceProtocolHandlerTaskRunner( bool Shell::HandleServiceProtocolMessage( std::string_view method, // one if the extension names specified above. const ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { auto found = service_protocol_handlers_.find(method); if (found != service_protocol_handlers_.end()) { return found->second.second(params, response); @@ -1247,44 +1236,44 @@ ServiceProtocol::Handler::Description Shell::GetServiceProtocolDescription() }; } -static void ServiceProtocolParameterError(rapidjson::Document& response, +static void ServiceProtocolParameterError(rapidjson::Document* response, std::string error_details) { - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); const int64_t kInvalidParams = -32602; - response.AddMember("code", kInvalidParams, allocator); - response.AddMember("message", "Invalid params", allocator); + response->AddMember("code", kInvalidParams, allocator); + response->AddMember("message", "Invalid params", allocator); { rapidjson::Value details(rapidjson::kObjectType); details.AddMember("details", error_details, allocator); - response.AddMember("data", details, allocator); + response->AddMember("data", details, allocator); } } -static void ServiceProtocolFailureError(rapidjson::Document& response, +static void ServiceProtocolFailureError(rapidjson::Document* response, std::string message) { - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); const int64_t kJsonServerError = -32000; - response.AddMember("code", kJsonServerError, allocator); - response.AddMember("message", message, allocator); + response->AddMember("code", kJsonServerError, allocator); + response->AddMember("message", message, allocator); } // Service protocol handler bool Shell::OnServiceProtocolScreenshot( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); auto screenshot = rasterizer_->ScreenshotLastLayerTree( Rasterizer::ScreenshotType::CompressedImage, true); if (screenshot.data) { - response.SetObject(); - auto& allocator = response.GetAllocator(); - response.AddMember("type", "Screenshot", allocator); + response->SetObject(); + auto& allocator = response->GetAllocator(); + response->AddMember("type", "Screenshot", allocator); rapidjson::Value image; image.SetString(static_cast(screenshot.data->data()), screenshot.data->size(), allocator); - response.AddMember("screenshot", image, allocator); + response->AddMember("screenshot", image, allocator); return true; } ServiceProtocolFailureError(response, "Could not capture image screenshot."); @@ -1294,18 +1283,18 @@ bool Shell::OnServiceProtocolScreenshot( // Service protocol handler bool Shell::OnServiceProtocolScreenshotSKP( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); auto screenshot = rasterizer_->ScreenshotLastLayerTree( Rasterizer::ScreenshotType::SkiaPicture, true); if (screenshot.data) { - response.SetObject(); - auto& allocator = response.GetAllocator(); - response.AddMember("type", "ScreenshotSkp", allocator); + response->SetObject(); + auto& allocator = response->GetAllocator(); + response->AddMember("type", "ScreenshotSkp", allocator); rapidjson::Value skp; skp.SetString(static_cast(screenshot.data->data()), screenshot.data->size(), allocator); - response.AddMember("skp", skp, allocator); + response->AddMember("skp", skp, allocator); return true; } ServiceProtocolFailureError(response, "Could not capture SKP screenshot."); @@ -1315,7 +1304,7 @@ bool Shell::OnServiceProtocolScreenshotSKP( // Service protocol handler bool Shell::OnServiceProtocolRunInView( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); if (params.count("mainScript") == 0) { @@ -1351,14 +1340,14 @@ bool Shell::OnServiceProtocolRunInView( std::make_unique(fml::OpenDirectory( asset_directory_path.c_str(), false, fml::FilePermission::kRead))); - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); if (engine_->Restart(std::move(configuration))) { - response.AddMember("type", "Success", allocator); + response->AddMember("type", "Success", allocator); auto new_description = GetServiceProtocolDescription(); rapidjson::Value view(rapidjson::kObjectType); new_description.Write(this, view, allocator); - response.AddMember("view", view, allocator); + response->AddMember("view", view, allocator); return true; } else { FML_DLOG(ERROR) << "Could not run configuration in engine."; @@ -1374,7 +1363,7 @@ bool Shell::OnServiceProtocolRunInView( // Service protocol handler bool Shell::OnServiceProtocolFlushUIThreadTasks( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); // This API should not be invoked by production code. // It can potentially starve the service isolate if the main isolate pauses @@ -1382,28 +1371,28 @@ bool Shell::OnServiceProtocolFlushUIThreadTasks( // // It should be invoked from the VM Service and and blocks it until UI thread // tasks are processed. - response.SetObject(); - response.AddMember("type", "Success", response.GetAllocator()); + response->SetObject(); + response->AddMember("type", "Success", response->GetAllocator()); return true; } bool Shell::OnServiceProtocolGetDisplayRefreshRate( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); - response.SetObject(); - response.AddMember("type", "DisplayRefreshRate", response.GetAllocator()); - response.AddMember("fps", engine_->GetDisplayRefreshRate(), - response.GetAllocator()); + response->SetObject(); + response->AddMember("type", "DisplayRefreshRate", response->GetAllocator()); + response->AddMember("fps", engine_->GetDisplayRefreshRate(), + response->GetAllocator()); return true; } bool Shell::OnServiceProtocolGetSkSLs( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetIOTaskRunner()->RunsTasksOnCurrentThread()); - response.SetObject(); - response.AddMember("type", "GetSkSLs", response.GetAllocator()); + response->SetObject(); + response->AddMember("type", "GetSkSLs", response->GetAllocator()); rapidjson::Value shaders_json(rapidjson::kObjectType); PersistentCache* persistent_cache = PersistentCache::GetCacheForProcess(); @@ -1415,19 +1404,36 @@ bool Shell::OnServiceProtocolGetSkSLs( char* b64_char = static_cast(b64_data->writable_data()); SkBase64::Encode(sksl.second->data(), sksl.second->size(), b64_char); b64_char[b64_size] = 0; // make it null terminated for printing - rapidjson::Value shader_value(b64_char, response.GetAllocator()); + rapidjson::Value shader_value(b64_char, response->GetAllocator()); rapidjson::Value shader_key(PersistentCache::SkKeyToFilePath(*sksl.first), - response.GetAllocator()); - shaders_json.AddMember(shader_key, shader_value, response.GetAllocator()); + response->GetAllocator()); + shaders_json.AddMember(shader_key, shader_value, response->GetAllocator()); } - response.AddMember("SkSLs", shaders_json, response.GetAllocator()); + response->AddMember("SkSLs", shaders_json, response->GetAllocator()); + return true; +} + +bool Shell::OnServiceProtocolEstimateRasterCacheMemory( + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document* response) { + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + const auto& raster_cache = rasterizer_->compositor_context()->raster_cache(); + response->SetObject(); + response->AddMember("type", "EstimateRasterCacheMemory", + response->GetAllocator()); + response->AddMember("layerBytes", + raster_cache.EstimateLayerCacheByteSize(), + response->GetAllocator()); + response->AddMember("pictureBytes", + raster_cache.EstimatePictureCacheByteSize(), + response->GetAllocator()); return true; } // Service protocol handler bool Shell::OnServiceProtocolSetAssetBundlePath( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); if (params.count("assetDirectory") == 0) { @@ -1436,8 +1442,8 @@ bool Shell::OnServiceProtocolSetAssetBundlePath( return false; } - auto& allocator = response.GetAllocator(); - response.SetObject(); + auto& allocator = response->GetAllocator(); + response->SetObject(); auto asset_manager = std::make_shared(); @@ -1446,11 +1452,11 @@ bool Shell::OnServiceProtocolSetAssetBundlePath( fml::FilePermission::kRead))); if (engine_->UpdateAssetManager(std::move(asset_manager))) { - response.AddMember("type", "Success", allocator); + response->AddMember("type", "Success", allocator); auto new_description = GetServiceProtocolDescription(); rapidjson::Value view(rapidjson::kObjectType); new_description.Write(this, view, allocator); - response.AddMember("view", view, allocator); + response->AddMember("view", view, allocator); return true; } else { FML_DLOG(ERROR) << "Could not update asset directory."; diff --git a/shell/common/shell.h b/shell/common/shell.h index fd1a30d3f4e35..aa9b64ae1ee32 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -12,6 +12,7 @@ #include "flutter/common/settings.h" #include "flutter/common/task_runners.h" +#include "flutter/flow/compositor_context.h" #include "flutter/flow/surface.h" #include "flutter/flow/texture.h" #include "flutter/fml/closure.h" @@ -138,7 +139,7 @@ class Shell final : public PlatformView::Delegate, /// the Dart VM. /// /// @param[in] task_runners The task runners - /// @param[in] window_data The default data for setting up + /// @param[in] platform_data The default data for setting up /// ui.Window that attached to this /// intance. /// @param[in] settings The settings @@ -161,7 +162,7 @@ class Shell final : public PlatformView::Delegate, /// static std::unique_ptr Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, CreateCallback on_create_platform_view, CreateCallback on_create_rasterizer); @@ -176,7 +177,7 @@ class Shell final : public PlatformView::Delegate, /// requires the specification of a running VM instance. /// /// @param[in] task_runners The task runners - /// @param[in] window_data The default data for setting up + /// @param[in] platform_data The default data for setting up /// ui.Window that attached to this /// intance. /// @param[in] settings The settings @@ -203,7 +204,7 @@ class Shell final : public PlatformView::Delegate, /// static std::unique_ptr Create( TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const CreateCallback& on_create_platform_view, @@ -248,7 +249,7 @@ class Shell final : public PlatformView::Delegate, /// /// @return The task runners current in use by the shell. /// - const TaskRunners& GetTaskRunners() const; + const TaskRunners& GetTaskRunners() const override; //---------------------------------------------------------------------------- /// @brief Rasterizers may only be accessed on the GPU task runner. @@ -352,7 +353,7 @@ class Shell final : public PlatformView::Delegate, //---------------------------------------------------------------------------- /// @brief Accessor for the disable GPU SyncSwitch - std::shared_ptr GetIsGpuDisabledSyncSwitch() const; + std::shared_ptr GetIsGpuDisabledSyncSwitch() const override; //---------------------------------------------------------------------------- /// @brief Get a pointer to the Dart VM used by this running shell @@ -365,7 +366,7 @@ class Shell final : public PlatformView::Delegate, private: using ServiceProtocolHandler = std::function; + rapidjson::Document*)>; const TaskRunners task_runners_; const Settings settings_; @@ -426,7 +427,7 @@ class Shell final : public PlatformView::Delegate, static std::unique_ptr CreateShellOnPlatformThread( DartVMRef vm, TaskRunners task_runners, - const WindowData window_data, + const PlatformData platform_data, Settings settings, fml::RefPtr isolate_snapshot, const Shell::CreateCallback& on_create_platform_view, @@ -528,10 +529,14 @@ class Shell final : public PlatformView::Delegate, void OnFrameRasterized(const FrameTiming&) override; // |Rasterizer::Delegate| - fml::Milliseconds GetFrameBudget() override; + fml::TimePoint GetLatestFrameTargetTime() const override; // |Rasterizer::Delegate| - fml::TimePoint GetLatestFrameTargetTime() const override; + // |CompositorContext::Delegate| + fml::Milliseconds GetFrameBudget() override; + + // |CompositorContext::Delegate| + void OnCompositorEndFrame(size_t freed_hint) override; // |ServiceProtocol::Handler| fml::RefPtr GetServiceProtocolHandlerTaskRunner( @@ -541,7 +546,7 @@ class Shell final : public PlatformView::Delegate, bool HandleServiceProtocolMessage( std::string_view method, // one if the extension names specified above. const ServiceProtocolMap& params, - rapidjson::Document& response) override; + rapidjson::Document* response) override; // |ServiceProtocol::Handler| ServiceProtocol::Handler::Description GetServiceProtocolDescription() @@ -550,39 +555,44 @@ class Shell final : public PlatformView::Delegate, // Service protocol handler bool OnServiceProtocolScreenshot( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolScreenshotSKP( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolRunInView( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolFlushUIThreadTasks( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolSetAssetBundlePath( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler bool OnServiceProtocolGetDisplayRefreshRate( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); // Service protocol handler // // The returned SkSLs are base64 encoded. Decode before storing them to files. bool OnServiceProtocolGetSkSLs( const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); + + // Service protocol handler + bool OnServiceProtocolEstimateRasterCacheMemory( + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document* response); fml::WeakPtrFactory weak_factory_; diff --git a/shell/common/shell_benchmarks.cc b/shell/common/shell_benchmarks.cc index 7808130659959..26e4dc9f08d6d 100644 --- a/shell/common/shell_benchmarks.cc +++ b/shell/common/shell_benchmarks.cc @@ -58,11 +58,7 @@ static void StartupAndShutdownShell(benchmark::State& state, [](Shell& shell) { return std::make_unique(shell, shell.GetTaskRunners()); }, - [](Shell& shell) { - return std::make_unique( - shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); } FML_CHECK(shell); diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index c8ebf642e056f..e80652e33c059 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -49,6 +49,16 @@ void ShellTest::PlatformViewNotifyCreated(Shell* shell) { latch.Wait(); } +void ShellTest::PlatformViewNotifyDestroyed(Shell* shell) { + fml::AutoResetWaitableEvent latch; + fml::TaskRunner::RunNowOrPostTask( + shell->GetTaskRunners().GetPlatformTaskRunner(), [shell, &latch]() { + shell->GetPlatformView()->NotifyDestroyed(); + latch.Signal(); + }); + latch.Wait(); +} + void ShellTest::RunEngine(Shell* shell, RunConfiguration configuration) { fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( @@ -96,14 +106,45 @@ void ShellTest::VSyncFlush(Shell* shell, bool& will_draw_new_frame) { latch.Wait(); } +void ShellTest::SetViewportMetrics(Shell* shell, double width, double height) { + flutter::ViewportMetrics viewport_metrics = { + 1, // device pixel ratio + width, // physical width + height, // physical height + 0, // padding top + 0, // padding right + 0, // padding bottom + 0, // padding left + 0, // view inset top + 0, // view inset right + 0, // view inset bottom + 0, // view inset left + 0, // gesture inset top + 0, // gesture inset right + 0, // gesture inset bottom + 0 // gesture inset left + }; + // Set viewport to nonempty, and call Animator::BeginFrame to make the layer + // tree pipeline nonempty. Without either of this, the layer tree below + // won't be rasterized. + fml::AutoResetWaitableEvent latch; + shell->GetTaskRunners().GetUITaskRunner()->PostTask( + [&latch, engine = shell->weak_engine_, viewport_metrics]() { + engine->SetViewportMetrics(std::move(viewport_metrics)); + const auto frame_begin_time = fml::TimePoint::Now(); + const auto frame_end_time = + frame_begin_time + fml::TimeDelta::FromSecondsF(1.0 / 60.0); + engine->animator_->BeginFrame(frame_begin_time, frame_end_time); + latch.Signal(); + }); + latch.Wait(); +} + void ShellTest::PumpOneFrame(Shell* shell, double width, double height, LayerTreeBuilder builder) { - PumpOneFrame(shell, - flutter::ViewportMetrics{1, width, height, flutter::kUnsetDepth, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - std::move(builder)); + PumpOneFrame(shell, {1.0, width, height}, std::move(builder)); } void ShellTest::PumpOneFrame(Shell* shell, @@ -132,7 +173,6 @@ void ShellTest::PumpOneFrame(Shell* shell, auto layer_tree = std::make_unique( SkISize::Make(viewport_metrics.physical_width, viewport_metrics.physical_height), - static_cast(viewport_metrics.physical_depth), static_cast(viewport_metrics.device_pixel_ratio)); SkMatrix identity; identity.setIdentity(); @@ -181,14 +221,17 @@ void ShellTest::OnServiceProtocol( ServiceProtocolEnum some_protocol, fml::RefPtr task_runner, const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response) { + rapidjson::Document* response) { std::promise finished; fml::TaskRunner::RunNowOrPostTask( - task_runner, [shell, some_protocol, params, &response, &finished]() { + task_runner, [shell, some_protocol, params, response, &finished]() { switch (some_protocol) { case ServiceProtocolEnum::kGetSkSLs: shell->OnServiceProtocolGetSkSLs(params, response); break; + case ServiceProtocolEnum::kEstimateRasterCacheMemory: + shell->OnServiceProtocolEstimateRasterCacheMemory(params, response); + break; case ServiceProtocolEnum::kSetAssetBundlePath: shell->OnServiceProtocolSetAssetBundlePath(params, response); break; @@ -271,10 +314,7 @@ std::unique_ptr ShellTest::CreateShell( ShellTestPlatformView::BackendType::kDefaultBackend, shell_test_external_view_embedder); }, - [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); } void ShellTest::DestroyShell(std::unique_ptr shell) { DestroyShell(std::move(shell), GetTaskRunnersForFixture()); diff --git a/shell/common/shell_test.h b/shell/common/shell_test.h index 2b46881c3c68f..5b626289cd766 100644 --- a/shell/common/shell_test.h +++ b/shell/common/shell_test.h @@ -50,6 +50,8 @@ class ShellTest : public FixtureTest { static void PlatformViewNotifyCreated( Shell* shell); // This creates the surface + static void PlatformViewNotifyDestroyed( + Shell* shell); // This destroys the surface static void RunEngine(Shell* shell, RunConfiguration configuration); static void RestartEngine(Shell* shell, RunConfiguration configuration); @@ -57,6 +59,7 @@ class ShellTest : public FixtureTest { /// the `will_draw_new_frame` to true. static void VSyncFlush(Shell* shell, bool& will_draw_new_frame); + static void SetViewportMetrics(Shell* shell, double width, double height); /// Given the root layer, this callback builds the layer tree to be rasterized /// in PumpOneFrame. using LayerTreeBuilder = @@ -80,6 +83,7 @@ class ShellTest : public FixtureTest { enum ServiceProtocolEnum { kGetSkSLs, + kEstimateRasterCacheMemory, kSetAssetBundlePath, kRunInView, }; @@ -92,7 +96,7 @@ class ShellTest : public FixtureTest { ServiceProtocolEnum some_protocol, fml::RefPtr task_runner, const ServiceProtocol::Handler::ServiceProtocolMap& params, - rapidjson::Document& response); + rapidjson::Document* response); std::shared_ptr GetFontCollection(Shell* shell); diff --git a/shell/common/shell_test_external_view_embedder.cc b/shell/common/shell_test_external_view_embedder.cc index f3a5a0a37d27d..07557768a46b7 100644 --- a/shell/common/shell_test_external_view_embedder.cc +++ b/shell/common/shell_test_external_view_embedder.cc @@ -2,6 +2,25 @@ namespace flutter { +ShellTestExternalViewEmbedder::ShellTestExternalViewEmbedder( + const EndFrameCallBack& end_frame_call_back, + PostPrerollResult post_preroll_result, + bool support_thread_merging) + : end_frame_call_back_(end_frame_call_back), + post_preroll_result_(post_preroll_result), + support_thread_merging_(support_thread_merging) { + resubmit_once_ = false; +} + +void ShellTestExternalViewEmbedder::UpdatePostPrerollResult( + PostPrerollResult post_preroll_result) { + post_preroll_result_ = post_preroll_result; +} + +void ShellTestExternalViewEmbedder::SetResubmitOnce() { + resubmit_once_ = true; +} + // |ExternalViewEmbedder| void ShellTestExternalViewEmbedder::CancelFrame() {} @@ -21,6 +40,10 @@ void ShellTestExternalViewEmbedder::PrerollCompositeEmbeddedView( PostPrerollResult ShellTestExternalViewEmbedder::PostPrerollAction( fml::RefPtr raster_thread_merger) { FML_DCHECK(raster_thread_merger); + if (resubmit_once_) { + resubmit_once_ = false; + return PostPrerollResult::kResubmitFrame; + } return post_preroll_result_; } @@ -45,7 +68,7 @@ void ShellTestExternalViewEmbedder::SubmitFrame( void ShellTestExternalViewEmbedder::EndFrame( bool should_resubmit_frame, fml::RefPtr raster_thread_merger) { - end_frame_call_back_(should_resubmit_frame); + end_frame_call_back_(should_resubmit_frame, raster_thread_merger); } // |ExternalViewEmbedder| @@ -53,4 +76,8 @@ SkCanvas* ShellTestExternalViewEmbedder::GetRootCanvas() { return nullptr; } +bool ShellTestExternalViewEmbedder::SupportsDynamicThreadMerging() { + return support_thread_merging_; +} + } // namespace flutter diff --git a/shell/common/shell_test_external_view_embedder.h b/shell/common/shell_test_external_view_embedder.h index c67772df74f9a..6d90879c74e17 100644 --- a/shell/common/shell_test_external_view_embedder.h +++ b/shell/common/shell_test_external_view_embedder.h @@ -15,15 +15,23 @@ namespace flutter { /// class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { public: - using EndFrameCallBack = std::function; + using EndFrameCallBack = + std::function)>; ShellTestExternalViewEmbedder(const EndFrameCallBack& end_frame_call_back, - PostPrerollResult post_preroll_result) - : end_frame_call_back_(end_frame_call_back), - post_preroll_result_(post_preroll_result) {} + PostPrerollResult post_preroll_result, + bool support_thread_merging); ~ShellTestExternalViewEmbedder() = default; + // Updates the post preroll result so the |PostPrerollAction| after always + // returns the new `post_preroll_result`. + void UpdatePostPrerollResult(PostPrerollResult post_preroll_result); + + // Updates the post preroll result to `PostPrerollResult::kResubmitFrame` for + // only the next frame. + void SetResubmitOnce(); + private: // |ExternalViewEmbedder| void CancelFrame() override; @@ -62,8 +70,14 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { // |ExternalViewEmbedder| SkCanvas* GetRootCanvas() override; + // |ExternalViewEmbedder| + bool SupportsDynamicThreadMerging() override; + const EndFrameCallBack end_frame_call_back_; - const PostPrerollResult post_preroll_result_; + PostPrerollResult post_preroll_result_; + bool resubmit_once_; + + bool support_thread_merging_; FML_DISALLOW_COPY_AND_ASSIGN(ShellTestExternalViewEmbedder); }; diff --git a/shell/common/shell_test_platform_view_gl.cc b/shell/common/shell_test_platform_view_gl.cc index 0b7b1f588c71d..2adcbe6b5dfda 100644 --- a/shell/common/shell_test_platform_view_gl.cc +++ b/shell/common/shell_test_platform_view_gl.cc @@ -60,7 +60,7 @@ bool ShellTestPlatformViewGL::GLContextPresent() { } // |GPUSurfaceGLDelegate| -intptr_t ShellTestPlatformViewGL::GLContextFBO() const { +intptr_t ShellTestPlatformViewGL::GLContextFBO(GLFrameInfo frame_info) const { return gl_surface_.GetFramebuffer(); } diff --git a/shell/common/shell_test_platform_view_gl.h b/shell/common/shell_test_platform_view_gl.h index e33b84fed7814..5ca969531751d 100644 --- a/shell/common/shell_test_platform_view_gl.h +++ b/shell/common/shell_test_platform_view_gl.h @@ -56,7 +56,7 @@ class ShellTestPlatformViewGL : public ShellTestPlatformView, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| GLProcResolver GetGLProcResolver() const override; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index 5f82ee64b5b50..30e03d1818fc7 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -32,10 +32,14 @@ #include "flutter/shell/common/vsync_waiter_fallback.h" #include "flutter/shell/version/version.h" #include "flutter/testing/testing.h" -#include "rapidjson/writer.h" +#include "third_party/rapidjson/include/rapidjson/writer.h" #include "third_party/skia/include/core/SkPictureRecorder.h" #include "third_party/tonic/converter/dart_converter.h" +#ifdef SHELL_ENABLE_VULKAN +#include "flutter/vulkan/vulkan_application.h" // nogncheck +#endif + namespace flutter { namespace testing { @@ -63,6 +67,32 @@ static bool ValidateShell(Shell* shell) { return true; } +static bool RasterizerHasLayerTree(Shell* shell) { + fml::AutoResetWaitableEvent latch; + bool has_layer_tree = false; + fml::TaskRunner::RunNowOrPostTask( + shell->GetTaskRunners().GetRasterTaskRunner(), + [shell, &latch, &has_layer_tree]() { + has_layer_tree = shell->GetRasterizer()->GetLastLayerTree() != nullptr; + latch.Signal(); + }); + latch.Wait(); + return has_layer_tree; +} + +static void ValidateDestroyPlatformView(Shell* shell) { + ASSERT_TRUE(shell != nullptr); + ASSERT_TRUE(shell->IsSetup()); + + // To validate destroy platform view, we must ensure the rasterizer has a + // layer tree before the platform view is destroyed. + ASSERT_TRUE(RasterizerHasLayerTree(shell)); + + ShellTest::PlatformViewNotifyDestroyed(shell); + // Validate the layer tree is destroyed + ASSERT_FALSE(RasterizerHasLayerTree(shell)); +} + TEST_F(ShellTest, InitializeWithInvalidThreads) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); Settings settings = CreateSettingsForFixture(); @@ -145,10 +175,7 @@ TEST_F(ShellTest, }, ShellTestPlatformView::BackendType::kDefaultBackend, nullptr); }, - [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }); + [](Shell& shell) { return std::make_unique(shell); }); ASSERT_TRUE(ValidateShell(shell.get())); ASSERT_TRUE(DartVMRef::IsInstanceRunning()); DestroyShell(std::move(shell), std::move(task_runners)); @@ -453,7 +480,6 @@ TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) { CREATE_NATIVE_ENTRY(nativeOnBeginFrame)); RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get()); // Check that timing is properly set. This implies that @@ -474,18 +500,70 @@ TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) { #if !defined(OS_FUCHSIA) // TODO(sanjayc77): https://github.com/flutter/flutter/issues/53179. Add // support for raster thread merger for Fuchsia. +TEST_F(ShellTest, ExternalEmbedderNoThreadMerger) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + bool end_frame_called = false; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + ASSERT_TRUE(raster_thread_merger.get() == nullptr); + ASSERT_FALSE(should_resubmit_frame); + end_frame_called = true; + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kResubmitFrame, false); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + + ASSERT_TRUE(end_frame_called); + + DestroyShell(std::move(shell)); +} + TEST_F(ShellTest, ExternalEmbedderEndFrameIsCalledWhenPostPrerollResultIsResubmit) { auto settings = CreateSettingsForFixture(); - fml::AutoResetWaitableEvent endFrameLatch; + fml::AutoResetWaitableEvent end_frame_latch; bool end_frame_called = false; - auto end_frame_callback = [&](bool should_resubmit_frame) { - ASSERT_TRUE(should_resubmit_frame); - end_frame_called = true; - endFrameLatch.Signal(); - }; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + ASSERT_TRUE(raster_thread_merger.get() != nullptr); + ASSERT_TRUE(should_resubmit_frame); + end_frame_called = true; + end_frame_latch.Signal(); + }; auto external_view_embedder = std::make_shared( - end_frame_callback, PostPrerollResult::kResubmitFrame); + end_frame_callback, PostPrerollResult::kResubmitFrame, true); auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), false, external_view_embedder); @@ -508,19 +586,323 @@ TEST_F(ShellTest, this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); auto picture_layer = std::make_shared( SkPoint::Make(10, 10), - flutter::SkiaGPUObject({sk_picture, queue}), false, false); + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); root->Add(picture_layer); }; PumpOneFrame(shell.get(), 100, 100, builder); - endFrameLatch.Wait(); + end_frame_latch.Wait(); ASSERT_TRUE(end_frame_called); DestroyShell(std::move(shell)); } + +TEST_F(ShellTest, OnPlatformViewDestroyAfterMergingThreads) { + const size_t ThreadMergingLease = 10; + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + if (should_resubmit_frame && !raster_thread_merger->IsMerged()) { + raster_thread_merger->MergeWithLease(ThreadMergingLease); + } + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + // Set resubmit once to trigger thread merging. + external_view_embedder->SetResubmitOnce(); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + // Pump one frame to trigger thread merging. + end_frame_latch.Wait(); + // Pump another frame to ensure threads are merged and a regular layer tree is + // submitted. + PumpOneFrame(shell.get(), 100, 100, builder); + // Threads are merged here. PlatformViewNotifyDestroy should be executed + // successfully. + ASSERT_TRUE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, OnPlatformViewDestroyWhenThreadsAreMerging) { + const size_t ThreadMergingLease = 10; + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + if (should_resubmit_frame && !raster_thread_merger->IsMerged()) { + raster_thread_merger->MergeWithLease(ThreadMergingLease); + } + end_frame_latch.Signal(); + }; + // Start with a regular layer tree with `PostPrerollResult::kSuccess` so we + // can later check if the rasterizer is tore down using + // |ValidateDestroyPlatformView| + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + // Pump one frame and threads aren't merged + end_frame_latch.Wait(); + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Pump a frame with `PostPrerollResult::kResubmitFrame` to start merging + // threads + external_view_embedder->SetResubmitOnce(); + PumpOneFrame(shell.get(), 100, 100, builder); + + // Now destroy the platform view immediately. + // Two things can happen here: + // 1. Threads haven't merged. 2. Threads has already merged. + // |Shell:OnPlatformViewDestroy| should be able to handle both cases. + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, + OnPlatformViewDestroyWithThreadMergerWhileThreadsAreUnmerged) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + + // Threads should not be merged. + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, OnPlatformViewDestroyWithoutRasterThreadMerger) { + auto settings = CreateSettingsForFixture(); + + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, nullptr); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + PumpOneFrame(shell.get(), 100, 100, builder); + + // Threads should not be merged. + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + ValidateDestroyPlatformView(shell.get()); + + // Ensure threads are unmerged after platform view destroy + ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( + shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(), + shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId())); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell)); +} #endif +TEST_F(ShellTest, OnPlatformViewDestroyWithStaticThreadMerging) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kSuccess, true); + ThreadHost thread_host( + "io.flutter.test." + GetCurrentTestName() + ".", + ThreadHost::Type::Platform | ThreadHost::Type::IO | ThreadHost::Type::UI); + TaskRunners task_runners( + "test", + thread_host.platform_thread->GetTaskRunner(), // platform + thread_host.platform_thread->GetTaskRunner(), // raster + thread_host.ui_thread->GetTaskRunner(), // ui + thread_host.io_thread->GetTaskRunner() // io + ); + auto shell = CreateShell(std::move(settings), std::move(task_runners), false, + external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); + root->Add(picture_layer); + }; + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + + ValidateDestroyPlatformView(shell.get()); + + // Validate the platform view can be recreated and destroyed again + ValidateShell(shell.get()); + + DestroyShell(std::move(shell), std::move(task_runners)); +} + TEST(SettingsTest, FrameTimingSetsAndGetsProperly) { // Ensure that all phases are in kPhases. ASSERT_EQ(sizeof(FrameTiming::kPhases), @@ -576,16 +958,23 @@ TEST_F(ShellTest, ReportTimingsIsCalledSoonerInNonReleaseMode) { DestroyShell(std::move(shell)); fml::TimePoint finish = fml::TimePoint::Now(); - fml::TimeDelta ellapsed = finish - start; + fml::TimeDelta elapsed = finish - start; #if FLUTTER_RELEASE // Our batch time is 1000ms. Hopefully the 800ms limit is relaxed enough to // make it not too flaky. - ASSERT_TRUE(ellapsed >= fml::TimeDelta::FromMilliseconds(800)); + ASSERT_TRUE(elapsed >= fml::TimeDelta::FromMilliseconds(800)); #else // Our batch time is 100ms. Hopefully the 500ms limit is relaxed enough to // make it not too flaky. - ASSERT_TRUE(ellapsed <= fml::TimeDelta::FromMilliseconds(500)); + // + // TODO(https://github.com/flutter/flutter/issues/64087): Fuchsia uses a + // 2000ms timeout to handle slowdowns in FEMU. +#if OS_FUCHSIA + ASSERT_TRUE(elapsed <= fml::TimeDelta::FromMilliseconds(2000)); +#else + ASSERT_TRUE(elapsed <= fml::TimeDelta::FromMilliseconds(500)); +#endif #endif } @@ -684,7 +1073,7 @@ TEST_F(ShellTest, WaitForFirstFrameZeroSizeFrame) { configuration.SetEntrypoint("emptyMain"); RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get(), {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + PumpOneFrame(shell.get(), {1.0, 0.0, 0.0}); fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::FromMilliseconds(1000)); ASSERT_FALSE(result.ok()); @@ -797,13 +1186,19 @@ TEST_F(ShellTest, SetResourceCacheSize) { RunEngine(shell.get(), std::move(configuration)); PumpOneFrame(shell.get()); + // The Vulkan and GL backends set different default values for the resource + // cache size. +#ifdef SHELL_ENABLE_VULKAN + EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), + vulkan::kGrCacheMaxByteSize); +#else EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), static_cast(24 * (1 << 20))); +#endif fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 400, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 400, 200}); }); PumpOneFrame(shell.get()); @@ -822,8 +1217,7 @@ TEST_F(ShellTest, SetResourceCacheSize) { fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 800, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 800, 400}); }); PumpOneFrame(shell.get()); @@ -841,8 +1235,7 @@ TEST_F(ShellTest, SetResourceCacheSizeEarly) { fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 400, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 400, 200}); }); PumpOneFrame(shell.get()); @@ -870,8 +1263,7 @@ TEST_F(ShellTest, SetResourceCacheSizeNotifiesDart) { fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { - shell->GetPlatformView()->SetViewportMetrics( - {1.0, 400, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + shell->GetPlatformView()->SetViewportMetrics({1.0, 400, 200}); }); PumpOneFrame(shell.get()); @@ -1075,7 +1467,8 @@ TEST_F(ShellTest, Screenshot) { this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); auto picture_layer = std::make_shared( SkPoint::Make(10, 10), - flutter::SkiaGPUObject({sk_picture, queue}), false, false); + flutter::SkiaGPUObject({sk_picture, queue}), false, false, + 0); root->Add(picture_layer); }; @@ -1235,15 +1628,17 @@ TEST_F(ShellTest, CanDecompressImageFromAsset) { } TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) { - // Create 2 dummpy SkSL cache file IE (base32 encoding of A), II (base32 - // encoding of B) with content x and y. - fml::ScopedTemporaryDirectory temp_dir; - PersistentCache::SetCacheDirectoryPath(temp_dir.path()); + fml::ScopedTemporaryDirectory base_dir; + ASSERT_TRUE(base_dir.fd().is_valid()); + PersistentCache::SetCacheDirectoryPath(base_dir.path()); PersistentCache::ResetCacheForProcess(); - std::vector components = {"flutter_engine", - GetFlutterEngineVersion(), "skia", - GetSkiaVersion(), "sksl"}; - auto sksl_dir = fml::CreateDirectory(temp_dir.fd(), components, + + // Create 2 dummy SkSL cache file IE (base32 encoding of A), II (base32 + // encoding of B) with content x and y. + std::vector components = { + "flutter_engine", GetFlutterEngineVersion(), "skia", GetSkiaVersion(), + PersistentCache::kSkSLSubdirName}; + auto sksl_dir = fml::CreateDirectory(base_dir.fd(), components, fml::FilePermission::kReadWrite); const std::string x = "x"; const std::string y = "y"; @@ -1260,7 +1655,7 @@ TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) { rapidjson::Document document; OnServiceProtocol(shell.get(), ServiceProtocolEnum::kGetSkSLs, shell->GetTaskRunners().GetIOTaskRunner(), empty_params, - document); + &document); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); document.Accept(writer); @@ -1274,9 +1669,6 @@ TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) { (expected_json2 == buffer.GetString()); ASSERT_TRUE(json_is_expected) << buffer.GetString() << " is not equal to " << expected_json1 << " or " << expected_json2; - - // Cleanup files - fml::RemoveFilesInDirectory(temp_dir.fd()); } TEST_F(ShellTest, RasterizerScreenshot) { @@ -1342,5 +1734,91 @@ TEST_F(ShellTest, RasterizerMakeRasterSnapshot) { DestroyShell(std::move(shell), std::move(task_runners)); } +static sk_sp MakeSizedPicture(int width, int height) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, width, height)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, width, height), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + return recorder.finishRecordingAsPicture(); +} + +TEST_F(ShellTest, OnServiceProtocolEstimateRasterCacheMemoryWorks) { + Settings settings = CreateSettingsForFixture(); + std::unique_ptr shell = CreateShell(settings); + + // 1. Construct a picture and a picture layer to be raster cached. + sk_sp picture = MakeSizedPicture(10, 10); + fml::RefPtr queue = fml::MakeRefCounted( + GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(0, 0), + flutter::SkiaGPUObject({MakeSizedPicture(100, 100), queue}), + false, false, 0); + picture_layer->set_paint_bounds(SkRect::MakeWH(100, 100)); + + // 2. Rasterize the picture and the picture layer in the raster cache. + std::promise rasterized; + shell->GetTaskRunners().GetRasterTaskRunner()->PostTask( + [&shell, &rasterized, &picture, &picture_layer] { + auto* compositor_context = shell->GetRasterizer()->compositor_context(); + auto& raster_cache = compositor_context->raster_cache(); + // 2.1. Rasterize the picture. Call Draw multiple times to pass the + // access threshold (default to 3) so a cache can be generated. + SkCanvas dummy_canvas; + bool picture_cache_generated; + for (int i = 0; i < 4; i += 1) { + picture_cache_generated = + raster_cache.Prepare(nullptr, // GrDirectContext + picture.get(), SkMatrix::I(), + nullptr, // SkColorSpace + true, // isComplex + false // willChange + ); + raster_cache.Draw(*picture, dummy_canvas); + } + ASSERT_TRUE(picture_cache_generated); + + // 2.2. Rasterize the picture layer. + Stopwatch raster_time; + Stopwatch ui_time; + MutatorsStack mutators_stack; + TextureRegistry texture_registry; + PrerollContext preroll_context = { + nullptr, /* raster_cache */ + nullptr, /* gr_context */ + nullptr, /* external_view_embedder */ + mutators_stack, nullptr, /* color_space */ + kGiantRect, /* cull_rect */ + false, /* layer reads from surface */ + raster_time, ui_time, texture_registry, + false, /* checkerboard_offscreen_layers */ + 1.0f, /* frame_device_pixel_ratio */ + false, /* has_platform_view */ + }; + raster_cache.Prepare(&preroll_context, picture_layer.get(), + SkMatrix::I()); + rasterized.set_value(true); + }); + rasterized.get_future().wait(); + + // 3. Call the service protocol and check its output. + ServiceProtocol::Handler::ServiceProtocolMap empty_params; + rapidjson::Document document; + OnServiceProtocol( + shell.get(), ServiceProtocolEnum::kEstimateRasterCacheMemory, + shell->GetTaskRunners().GetRasterTaskRunner(), empty_params, &document); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + std::string expected_json = + "{\"type\":\"EstimateRasterCacheMemory\",\"layerBytes\":40000,\"picture" + "Bytes\":400}"; + std::string actual_json = buffer.GetString(); + ASSERT_EQ(actual_json, expected_json); + + DestroyShell(std::move(shell)); +} + } // namespace testing } // namespace flutter diff --git a/shell/common/skp_shader_warmup_unittests.cc b/shell/common/skp_shader_warmup_unittests.cc index 689f2d9322fcc..ec81003c8f04f 100644 --- a/shell/common/skp_shader_warmup_unittests.cc +++ b/shell/common/skp_shader_warmup_unittests.cc @@ -153,7 +153,8 @@ class SkpWarmupTest : public ShellTest { auto picture_layer = std::make_shared( SkPoint::Make(0, 0), SkiaGPUObject(picture, queue), /* is_complex */ false, - /* will_change */ false); + /* will_change */ false, + /* external_size */ 0); root->Add(picture_layer); }; PumpOneFrame(shell.get(), picture->cullRect().width(), @@ -235,7 +236,8 @@ TEST_F(SkpWarmupTest, Image) { auto picture_layer = std::make_shared( SkPoint::Make(0, 0), SkiaGPUObject(picture, queue), /* is_complex */ false, - /* will_change */ false); + /* will_change */ false, + /* external_size */ 0); root->Add(picture_layer); }; diff --git a/shell/common/switches.cc b/shell/common/switches.cc index e2f83f3f9c748..16494204ddf81 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -242,8 +242,11 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { } } - settings.disable_http = - command_line.HasOption(FlagForSwitch(Switch::DisableHttp)); + settings.may_insecurely_connect_to_all_domains = !command_line.HasOption( + FlagForSwitch(Switch::DisallowInsecureConnections)); + + command_line.GetOptionValue(FlagForSwitch(Switch::DomainNetworkPolicy), + &settings.domain_network_policy); // Disable need for authentication codes for VM service communication, if // specified. diff --git a/shell/common/switches.h b/shell/common/switches.h index 9b91355c33856..1110261f767c3 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -181,12 +181,15 @@ DEF_SWITCH(DisableDartAsserts, "disabled. This flag may be specified if the user wishes to run " "with assertions disabled in the debug product mode (i.e. with JIT " "or DBC).") -DEF_SWITCH(DisableHttp, - "disable-http", - "Dart VM has a master switch that can be set to disable insecure " - "HTTP and WebSocket protocols. Localhost or loopback addresses are " - "exempted. This flag can be specified if the embedder wants this " - "for a particular platform.") +DEF_SWITCH(DisallowInsecureConnections, + "disallow-insecure-connections", + "By default, dart:io allows all socket connections. If this switch " + "is set, all insecure connections are rejected.") +DEF_SWITCH(DomainNetworkPolicy, + "domain-network-policy", + "JSON encoded network policy per domain. This overrides the " + "DisallowInsecureConnections switch. Embedder can specify whether " + "to allow or disallow insecure connections at a domain level.") DEF_SWITCH( ForceMultithreading, "force-multithreading", diff --git a/shell/common/vsync_waiter.cc b/shell/common/vsync_waiter.cc index ae86fc547ea3b..7947d42cf6125 100644 --- a/shell/common/vsync_waiter.cc +++ b/shell/common/vsync_waiter.cc @@ -122,9 +122,6 @@ void VsyncWaiter::FireCallback(fml::TimePoint frame_start_time, [callback, flow_identifier, frame_start_time, frame_target_time]() { FML_TRACE_EVENT("flutter", kVsyncTraceName, "StartTime", frame_start_time, "TargetTime", frame_target_time); - fml::tracing::TraceEventAsyncComplete( - "flutter", "VsyncSchedulingOverhead", fml::TimePoint::Now(), - frame_start_time); callback(frame_start_time, frame_target_time); TRACE_FLOW_END("flutter", kVsyncFlowName, flow_identifier); }, diff --git a/shell/gpu/BUILD.gn b/shell/gpu/BUILD.gn index 7e9295218d370..a4c59e28b765f 100644 --- a/shell/gpu/BUILD.gn +++ b/shell/gpu/BUILD.gn @@ -5,69 +5,56 @@ import("//flutter/common/config.gni") import("//flutter/shell/config.gni") -gpu_dir = "//flutter/shell/gpu" - gpu_common_deps = [ "//flutter/common", + "//flutter/flow", "//flutter/fml", + "//flutter/shell/common", "//third_party/skia", ] -gpu_common_deps_legacy_and_next = [ - "//flutter/flow:flow", - "//flutter/shell/common:common", -] - -source_set_maybe_fuchsia_legacy("gpu_surface_software") { +source_set("gpu_surface_software") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_software.cc", - "$gpu_dir/gpu_surface_software.h", - "$gpu_dir/gpu_surface_software_delegate.cc", - "$gpu_dir/gpu_surface_software_delegate.h", + "gpu_surface_delegate.h", + "gpu_surface_software.cc", + "gpu_surface_software.h", + "gpu_surface_software_delegate.cc", + "gpu_surface_software_delegate.h", ] deps = gpu_common_deps - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } -source_set_maybe_fuchsia_legacy("gpu_surface_gl") { +source_set("gpu_surface_gl") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_gl.cc", - "$gpu_dir/gpu_surface_gl.h", - "$gpu_dir/gpu_surface_gl_delegate.cc", - "$gpu_dir/gpu_surface_gl_delegate.h", + "gpu_surface_delegate.h", + "gpu_surface_gl.cc", + "gpu_surface_gl.h", + "gpu_surface_gl_delegate.cc", + "gpu_surface_gl_delegate.h", ] deps = gpu_common_deps - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } -source_set_maybe_fuchsia_legacy("gpu_surface_vulkan") { +source_set("gpu_surface_vulkan") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_vulkan.cc", - "$gpu_dir/gpu_surface_vulkan.h", - "$gpu_dir/gpu_surface_vulkan_delegate.cc", - "$gpu_dir/gpu_surface_vulkan_delegate.h", + "gpu_surface_delegate.h", + "gpu_surface_vulkan.cc", + "gpu_surface_vulkan.h", + "gpu_surface_vulkan_delegate.cc", + "gpu_surface_vulkan_delegate.h", ] deps = gpu_common_deps + [ "//flutter/vulkan" ] - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } -source_set_maybe_fuchsia_legacy("gpu_surface_metal") { +source_set("gpu_surface_metal") { sources = [ - "$gpu_dir/gpu_surface_delegate.h", - "$gpu_dir/gpu_surface_metal.h", - "$gpu_dir/gpu_surface_metal.mm", + "gpu_surface_delegate.h", + "gpu_surface_metal.h", + "gpu_surface_metal.mm", ] deps = gpu_common_deps - - deps_legacy_and_next = gpu_common_deps_legacy_and_next } diff --git a/shell/gpu/gpu.gni b/shell/gpu/gpu.gni index e064e1ea4b984..65464c8f6499b 100644 --- a/shell/gpu/gpu.gni +++ b/shell/gpu/gpu.gni @@ -33,30 +33,4 @@ template("shell_gpu_configuration") { public_deps += [ "//flutter/shell/gpu:gpu_surface_metal" ] } } - - if (is_fuchsia) { - legagcy_suffix = "_fuchsia_legacy" - group(target_name + legagcy_suffix) { - public_deps = [] - - if (invoker.enable_software) { - public_deps += - [ "//flutter/shell/gpu:gpu_surface_software" + legagcy_suffix ] - } - - if (invoker.enable_gl) { - public_deps += [ "//flutter/shell/gpu:gpu_surface_gl" + legagcy_suffix ] - } - - if (invoker.enable_vulkan) { - public_deps += - [ "//flutter/shell/gpu:gpu_surface_vulkan" + legagcy_suffix ] - } - - if (invoker.enable_metal) { - public_deps += - [ "//flutter/shell/gpu:gpu_surface_metal" + legagcy_suffix ] - } - } - } } diff --git a/shell/gpu/gpu_surface_gl.cc b/shell/gpu/gpu_surface_gl.cc index c492a456bb812..5386683a27474 100644 --- a/shell/gpu/gpu_surface_gl.cc +++ b/shell/gpu/gpu_surface_gl.cc @@ -204,10 +204,12 @@ bool GPUSurfaceGL::CreateOrUpdateSurfaces(const SkISize& size) { sk_sp onscreen_surface; + GLFrameInfo frame_info = {static_cast(size.width()), + static_cast(size.height())}; onscreen_surface = - WrapOnscreenSurface(context_.get(), // GL context - size, // root surface size - delegate_->GLContextFBO() // window FBO ID + WrapOnscreenSurface(context_.get(), // GL context + size, // root surface size + delegate_->GLContextFBO(frame_info) // window FBO ID ); if (onscreen_surface == nullptr) { @@ -287,13 +289,16 @@ bool GPUSurfaceGL::PresentSurface(SkCanvas* canvas) { auto current_size = SkISize::Make(onscreen_surface_->width(), onscreen_surface_->height()); + GLFrameInfo frame_info = {static_cast(current_size.width()), + static_cast(current_size.height())}; + // The FBO has changed, ask the delegate for the new FBO and do a surface // re-wrap. - auto new_onscreen_surface = - WrapOnscreenSurface(context_.get(), // GL context - current_size, // root surface size - delegate_->GLContextFBO() // window FBO ID - ); + auto new_onscreen_surface = WrapOnscreenSurface( + context_.get(), // GL context + current_size, // root surface size + delegate_->GLContextFBO(frame_info) // window FBO ID + ); if (!new_onscreen_surface) { return false; diff --git a/shell/gpu/gpu_surface_gl_delegate.h b/shell/gpu/gpu_surface_gl_delegate.h index 9fb9765ac9be2..7fd111dd3ebe5 100644 --- a/shell/gpu/gpu_surface_gl_delegate.h +++ b/shell/gpu/gpu_surface_gl_delegate.h @@ -14,6 +14,13 @@ namespace flutter { +// A structure to represent the frame information which is passed to the +// embedder when requesting a frame buffer object. +struct GLFrameInfo { + uint32_t width; + uint32_t height; +}; + class GPUSurfaceGLDelegate : public GPUSurfaceDelegate { public: ~GPUSurfaceGLDelegate() override; @@ -33,13 +40,13 @@ class GPUSurfaceGLDelegate : public GPUSurfaceDelegate { virtual bool GLContextPresent() = 0; // The ID of the main window bound framebuffer. Typically FBO0. - virtual intptr_t GLContextFBO() const = 0; + virtual intptr_t GLContextFBO(GLFrameInfo frame_info) const = 0; // The rendering subsystem assumes that the ID of the main window bound // framebuffer remains constant throughout. If this assumption in incorrect, // embedders are required to return true from this method. In such cases, - // GLContextFBO() will be called again to acquire the new FBO ID for rendering - // subsequent frames. + // GLContextFBO(frame_info) will be called again to acquire the new FBO ID for + // rendering subsequent frames. virtual bool GLContextFBOResetAfterPresent() const; // Indicates whether or not the surface supports pixel readback as used in diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 308c70af691e4..eed010506b3b9 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -153,6 +153,8 @@ android_java_sources = [ "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", + "io/flutter/embedding/engine/loader/ApplicationInfoLoader.java", + "io/flutter/embedding/engine/loader/FlutterApplicationInfo.java", "io/flutter/embedding/engine/loader/FlutterLoader.java", "io/flutter/embedding/engine/loader/ResourceExtractor.java", "io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java", @@ -430,8 +432,10 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/PluginComponentTest.java", "test/io/flutter/embedding/engine/RenderingComponentTest.java", "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", + "test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java", "test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java", "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", + "test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java", "test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java", "test/io/flutter/external/FlutterLaunchTests.java", "test/io/flutter/plugin/common/StandardMessageCodecTest.java", diff --git a/shell/platform/android/android_external_texture_gl.cc b/shell/platform/android/android_external_texture_gl.cc index 8e071ffba2420..ae5101012f78b 100644 --- a/shell/platform/android/android_external_texture_gl.cc +++ b/shell/platform/android/android_external_texture_gl.cc @@ -7,6 +7,7 @@ #include #include "third_party/skia/include/gpu/GrBackendSurface.h" +#include "third_party/skia/include/gpu/GrDirectContext.h" namespace flutter { @@ -54,8 +55,8 @@ void AndroidExternalTextureGL::Paint(SkCanvas& canvas, GL_RGBA8_OES}; GrBackendTexture backendTexture(1, 1, GrMipMapped::kNo, textureInfo); sk_sp image = SkImage::MakeFromTexture( - canvas.getGrContext(), backendTexture, kTopLeft_GrSurfaceOrigin, - kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr); + context, backendTexture, kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType, + kPremul_SkAlphaType, nullptr); if (image) { SkAutoCanvasRestore autoRestore(&canvas, true); canvas.translate(bounds.x(), bounds.y()); diff --git a/shell/platform/android/android_shell_holder.cc b/shell/platform/android/android_shell_holder.cc index c9667dcb7edf3..7272571d71a4f 100644 --- a/shell/platform/android/android_shell_holder.cc +++ b/shell/platform/android/android_shell_holder.cc @@ -22,10 +22,10 @@ namespace flutter { -static WindowData GetDefaultWindowData() { - WindowData window_data; - window_data.lifecycle_state = "AppLifecycleState.detached"; - return window_data; +static PlatformData GetDefaultPlatformData() { + PlatformData platform_data; + platform_data.lifecycle_state = "AppLifecycleState.detached"; + return platform_data; } bool AndroidShellHolder::use_embedded_view; @@ -81,8 +81,7 @@ AndroidShellHolder::AndroidShellHolder( }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); + return std::make_unique(shell); }; // The current thread will be used as the platform thread. Ensure that the @@ -121,9 +120,9 @@ AndroidShellHolder::AndroidShellHolder( ); shell_ = - Shell::Create(task_runners, // task runners - GetDefaultWindowData(), // window data - settings_, // settings + Shell::Create(task_runners, // task runners + GetDefaultPlatformData(), // window data + settings_, // settings on_create_platform_view, // platform view create callback on_create_rasterizer // rasterizer create callback ); @@ -137,9 +136,9 @@ AndroidShellHolder::AndroidShellHolder( ); shell_ = - Shell::Create(task_runners, // task runners - GetDefaultWindowData(), // window data - settings_, // settings + Shell::Create(task_runners, // task runners + GetDefaultPlatformData(), // window data + settings_, // settings on_create_platform_view, // platform view create callback on_create_rasterizer // rasterizer create callback ); diff --git a/shell/platform/android/android_shell_holder.h b/shell/platform/android/android_shell_holder.h index 6fb6695801733..28ad8610882f7 100644 --- a/shell/platform/android/android_shell_holder.h +++ b/shell/platform/android/android_shell_holder.h @@ -10,7 +10,7 @@ #include "flutter/fml/macros.h" #include "flutter/fml/unique_fd.h" #include "flutter/lib/ui/window/viewport_metrics.h" -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" #include "flutter/shell/common/run_configuration.h" #include "flutter/shell/common/shell.h" #include "flutter/shell/common/thread_host.h" diff --git a/shell/platform/android/android_surface_gl.cc b/shell/platform/android/android_surface_gl.cc index a3356653df9a8..7f692f007634a 100644 --- a/shell/platform/android/android_surface_gl.cc +++ b/shell/platform/android/android_surface_gl.cc @@ -115,7 +115,7 @@ bool AndroidSurfaceGL::GLContextPresent() { return onscreen_surface_->SwapBuffers(); } -intptr_t AndroidSurfaceGL::GLContextFBO() const { +intptr_t AndroidSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { FML_DCHECK(IsValid()); // The default window bound framebuffer on Android. return 0; diff --git a/shell/platform/android/android_surface_gl.h b/shell/platform/android/android_surface_gl.h index 281a273351908..1ebbf8f60b1a5 100644 --- a/shell/platform/android/android_surface_gl.h +++ b/shell/platform/android/android_surface_gl.h @@ -59,7 +59,7 @@ class AndroidSurfaceGL final : public GPUSurfaceGLDelegate, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| ExternalViewEmbedder* GetExternalViewEmbedder() override; diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.cc b/shell/platform/android/external_view_embedder/external_view_embedder.cc index 9a32dc171eabf..b5479284aa0ef 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder.cc @@ -295,4 +295,9 @@ void AndroidExternalViewEmbedder::EndFrame( } } +// |ExternalViewEmbedder| +bool AndroidExternalViewEmbedder::SupportsDynamicThreadMerging() { + return true; +} + } // namespace flutter diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.h b/shell/platform/android/external_view_embedder/external_view_embedder.h index 9e827b83b0ee0..ce755f252a8ac 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.h +++ b/shell/platform/android/external_view_embedder/external_view_embedder.h @@ -70,6 +70,8 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { bool should_resubmit_frame, fml::RefPtr raster_thread_merger) override; + bool SupportsDynamicThreadMerging() override; + // Gets the rect based on the device pixel ratio of a platform view displayed // on the screen. SkRect GetViewRect(int view_id) const; diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index 93356347ae322..bdb8ca1c6c286 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -556,5 +556,13 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { raster_thread_merger); } +TEST(AndroidExternalViewEmbedder, SupportsDynamicThreadMerging) { + auto jni_mock = std::make_shared(); + + auto embedder = + std::make_unique(nullptr, jni_mock, nullptr); + ASSERT_TRUE(embedder->SupportsDynamicThreadMerging()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 92d7586a32a70..0d0c0d203bc8b 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -397,10 +397,9 @@ private void ensureFlutterFragmentCreated() { */ @NonNull protected FlutterFragment createFlutterFragment() { - BackgroundMode backgroundMode = getBackgroundMode(); - RenderMode renderMode = - backgroundMode == BackgroundMode.opaque ? RenderMode.surface : RenderMode.texture; - TransparencyMode transparencyMode = + final BackgroundMode backgroundMode = getBackgroundMode(); + final RenderMode renderMode = getRenderMode(); + final TransparencyMode transparencyMode = backgroundMode == BackgroundMode.opaque ? TransparencyMode.opaque : TransparencyMode.transparent; @@ -690,6 +689,19 @@ protected BackgroundMode getBackgroundMode() { } } + /** + * Returns the desired {@link RenderMode} for the {@link FlutterView} displayed in this {@code + * FlutterFragmentActivity}. + * + *

That is, {@link RenderMode#surface} if {@link FlutterFragmentActivity#getBackgroundMode()} + * is {@link BackgroundMode.opaque} or {@link RenderMode#texture} otherwise. + */ + @NonNull + protected RenderMode getRenderMode() { + final BackgroundMode backgroundMode = getBackgroundMode(); + return backgroundMode == BackgroundMode.opaque ? RenderMode.surface : RenderMode.texture; + } + /** * Returns true if Flutter is running in "debug mode", and false otherwise. * diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 76b0f2461141c..860d9c4786025 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1063,7 +1063,7 @@ public FlutterEngine getAttachedFlutterEngine() { } /** - * Adds a {@link FlutterEngineAttachmentListener}, which is notifed whenever this {@code + * Adds a {@link FlutterEngineAttachmentListener}, which is notified whenever this {@code * FlutterView} attached to/detaches from a {@link FlutterEngine}. */ @VisibleForTesting diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java new file mode 100644 index 0000000000000..c29ddd9f21a9c --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java @@ -0,0 +1,161 @@ +// 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. + +package io.flutter.embedding.engine.loader; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.security.NetworkSecurityPolicy; +import androidx.annotation.NonNull; +import java.io.IOException; +import org.json.JSONArray; +import org.xmlpull.v1.XmlPullParserException; + +/** Loads application information given a Context. */ +final class ApplicationInfoLoader { + // XML Attribute keys supported in AndroidManifest.xml + static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = + FlutterLoader.class.getName() + '.' + FlutterLoader.AOT_SHARED_LIBRARY_NAME; + static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.VM_SNAPSHOT_DATA_KEY; + static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.ISOLATE_SNAPSHOT_DATA_KEY; + static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.FLUTTER_ASSETS_DIR_KEY; + static final String NETWORK_POLICY_METADATA_KEY = "io.flutter.network-policy"; + + @NonNull + private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { + try { + return applicationContext + .getPackageManager() + .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + private static String getString(Bundle metadata, String key) { + if (metadata == null) { + return null; + } + return metadata.getString(key, null); + } + + private static String getNetworkPolicy(ApplicationInfo appInfo, Context context) { + // We cannot use reflection to look at networkSecurityConfigRes because + // Android throws an error when we try to access fields marked as @hide. + // Instead we rely on metadata. + Bundle metadata = appInfo.metaData; + if (metadata == null) { + return null; + } + + int networkSecurityConfigRes = metadata.getInt(NETWORK_POLICY_METADATA_KEY, 0); + if (networkSecurityConfigRes <= 0) { + return null; + } + + JSONArray output = new JSONArray(); + try { + XmlResourceParser xrp = context.getResources().getXml(networkSecurityConfigRes); + xrp.next(); + int eventType = xrp.getEventType(); + while (eventType != XmlResourceParser.END_DOCUMENT) { + if (eventType == XmlResourceParser.START_TAG) { + if (xrp.getName().equals("domain-config")) { + parseDomainConfig(xrp, output, false); + } + } + eventType = xrp.next(); + } + } catch (IOException | XmlPullParserException e) { + return null; + } + return output.toString(); + } + + private static boolean getUseEmbeddedView(ApplicationInfo appInfo) { + Bundle bundle = appInfo.metaData; + return bundle != null && bundle.getBoolean("io.flutter.embedded_views_preview"); + } + + private static void parseDomainConfig( + XmlResourceParser xrp, JSONArray output, boolean inheritedCleartextPermitted) + throws IOException, XmlPullParserException { + boolean cleartextTrafficPermitted = + xrp.getAttributeBooleanValue( + null, "cleartextTrafficPermitted", inheritedCleartextPermitted); + while (true) { + int eventType = xrp.next(); + if (eventType == XmlResourceParser.START_TAG) { + if (xrp.getName().equals("domain")) { + // There can be multiple domains. + parseDomain(xrp, output, cleartextTrafficPermitted); + } else if (xrp.getName().equals("domain-config")) { + parseDomainConfig(xrp, output, cleartextTrafficPermitted); + } else { + skipTag(xrp); + } + } else if (eventType == XmlResourceParser.END_TAG) { + break; + } + } + } + + private static void skipTag(XmlResourceParser xrp) throws IOException, XmlPullParserException { + String name = xrp.getName(); + int eventType = xrp.getEventType(); + while (eventType != XmlResourceParser.END_TAG || xrp.getName() != name) { + eventType = xrp.next(); + } + } + + private static void parseDomain( + XmlResourceParser xrp, JSONArray output, boolean cleartextPermitted) + throws IOException, XmlPullParserException { + boolean includeSubDomains = xrp.getAttributeBooleanValue(null, "includeSubdomains", false); + xrp.next(); + if (xrp.getEventType() != XmlResourceParser.TEXT) { + throw new IllegalStateException("Expected text"); + } + String domain = xrp.getText().trim(); + JSONArray outputArray = new JSONArray(); + outputArray.put(domain); + outputArray.put(includeSubDomains); + outputArray.put(cleartextPermitted); + output.put(outputArray); + xrp.next(); + if (xrp.getEventType() != XmlResourceParser.END_TAG) { + throw new IllegalStateException("Expected end of domain tag"); + } + } + + /** + * Initialize our Flutter config values by obtaining them from the manifest XML file, falling back + * to default values. + */ + @NonNull + public static FlutterApplicationInfo load(@NonNull Context applicationContext) { + ApplicationInfo appInfo = getApplicationInfo(applicationContext); + // Prior to API 23, cleartext traffic is allowed. + boolean clearTextPermitted = true; + if (android.os.Build.VERSION.SDK_INT >= 23) { + clearTextPermitted = NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(); + } + + return new FlutterApplicationInfo( + getString(appInfo.metaData, PUBLIC_AOT_SHARED_LIBRARY_NAME), + getString(appInfo.metaData, PUBLIC_VM_SNAPSHOT_DATA_KEY), + getString(appInfo.metaData, PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY), + getString(appInfo.metaData, PUBLIC_FLUTTER_ASSETS_DIR_KEY), + getNetworkPolicy(appInfo, applicationContext), + appInfo.nativeLibraryDir, + clearTextPermitted, + getUseEmbeddedView(appInfo)); + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java new file mode 100644 index 0000000000000..3d5c2b10c40d6 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java @@ -0,0 +1,46 @@ +// 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. + +package io.flutter.embedding.engine.loader; + +/** Encapsulates all the information that Flutter needs from application manifest. */ +public final class FlutterApplicationInfo { + private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; + private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; + private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; + private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; + + final String aotSharedLibraryName; + final String vmSnapshotData; + final String isolateSnapshotData; + final String flutterAssetsDir; + final String domainNetworkPolicy; + final String nativeLibraryDir; + final boolean clearTextPermitted; + // TODO(cyanlaz): Remove this when dynamic thread merging is done. + // https://github.com/flutter/flutter/issues/59930 + final boolean useEmbeddedView; + + public FlutterApplicationInfo( + String aotSharedLibraryName, + String vmSnapshotData, + String isolateSnapshotData, + String flutterAssetsDir, + String domainNetworkPolicy, + String nativeLibraryDir, + boolean clearTextPermitted, + boolean useEmbeddedView) { + this.aotSharedLibraryName = + aotSharedLibraryName == null ? DEFAULT_AOT_SHARED_LIBRARY_NAME : aotSharedLibraryName; + this.vmSnapshotData = vmSnapshotData == null ? DEFAULT_VM_SNAPSHOT_DATA : vmSnapshotData; + this.isolateSnapshotData = + isolateSnapshotData == null ? DEFAULT_ISOLATE_SNAPSHOT_DATA : isolateSnapshotData; + this.flutterAssetsDir = + flutterAssetsDir == null ? DEFAULT_FLUTTER_ASSETS_DIR : flutterAssetsDir; + this.nativeLibraryDir = nativeLibraryDir; + this.domainNetworkPolicy = domainNetworkPolicy == null ? "" : domainNetworkPolicy; + this.clearTextPermitted = clearTextPermitted; + this.useEmbeddedView = useEmbeddedView; + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 9156a64ef061e..2aee56dee94e3 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -5,10 +5,8 @@ package io.flutter.embedding.engine.loader; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; -import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -31,35 +29,15 @@ public class FlutterLoader { private static final String TAG = "FlutterLoader"; // Must match values in flutter::switches - private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; - private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; - private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; - private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; - private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; - - // XML Attribute keys supported in AndroidManifest.xml - private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = - FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; - private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; - private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY; - private static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = - FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY; + static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; + static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; + static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; + static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; + static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; // Resource names used for components of the precompiled snapshot. - private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; - private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; - private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; private static final String DEFAULT_LIBRARY = "libflutter.so"; private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; - private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; - - // Mutable because default values can be overridden via config properties - private String aotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; - private String vmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; - private String isolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; - private String flutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; private static FlutterLoader instance; @@ -78,9 +56,17 @@ public static FlutterLoader getInstance() { return instance; } + @NonNull + public static FlutterLoader getInstanceForTest(FlutterApplicationInfo flutterApplicationInfo) { + FlutterLoader loader = new FlutterLoader(); + loader.flutterApplicationInfo = flutterApplicationInfo; + return loader; + } + private boolean initialized = false; @Nullable private Settings settings; private long initStartTimestampMillis; + private FlutterApplicationInfo flutterApplicationInfo; private static class InitResult { final String appStoragePath; @@ -131,7 +117,7 @@ public void startInitialization(@NonNull Context applicationContext, @NonNull Se this.settings = settings; initStartTimestampMillis = SystemClock.uptimeMillis(); - initConfig(appContext); + flutterApplicationInfo = ApplicationInfoLoader.load(appContext); VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE)) .init(); @@ -195,10 +181,9 @@ public void ensureInitializationComplete( List shellArgs = new ArrayList<>(); shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); - ApplicationInfo applicationInfo = getApplicationInfo(applicationContext); shellArgs.add( "--icu-native-lib-path=" - + applicationInfo.nativeLibraryDir + + flutterApplicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); if (args != null) { @@ -207,13 +192,16 @@ public void ensureInitializationComplete( String kernelPath = null; if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { - String snapshotAssetPath = result.dataDirPath + File.separator + flutterAssetsDir; + String snapshotAssetPath = + result.dataDirPath + File.separator + flutterApplicationInfo.flutterAssetsDir; kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); - shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData); - shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData); + shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.vmSnapshotData); + shellArgs.add( + "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.isolateSnapshotData); } else { - shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName); + shellArgs.add( + "--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName); // Most devices can load the AOT shared library based on the library name // with no directory path. Provide a fully qualified path to the library @@ -222,28 +210,28 @@ public void ensureInitializationComplete( "--" + AOT_SHARED_LIBRARY_NAME + "=" - + applicationInfo.nativeLibraryDir + + flutterApplicationInfo.nativeLibraryDir + File.separator - + aotSharedLibraryName); + + flutterApplicationInfo.aotSharedLibraryName); } shellArgs.add("--cache-dir-path=" + result.engineCachesPath); + // TODO(mehmetf): Announce this since it is a breaking change then enable it. + // if (!flutterApplicationInfo.clearTextPermitted) { + // shellArgs.add("--disallow-insecure-connections"); + // } + if (flutterApplicationInfo.domainNetworkPolicy != null) { + shellArgs.add("--domain-network-policy=" + flutterApplicationInfo.domainNetworkPolicy); + } + if (flutterApplicationInfo.useEmbeddedView) { + shellArgs.add("--use-embedded-view"); + } if (settings.getLogTag() != null) { shellArgs.add("--log-tag=" + settings.getLogTag()); } long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; - // TODO(cyanlaz): Remove this when dynamic thread merging is done. - // https://github.com/flutter/flutter/issues/59930 - Bundle bundle = applicationInfo.metaData; - if (bundle != null) { - boolean use_embedded_view = bundle.getBoolean("io.flutter.embedded_views_preview"); - if (use_embedded_view) { - shellArgs.add("--use-embedded-view"); - } - } - FlutterJNI.nativeInit( applicationContext, shellArgs.toArray(new String[0]), @@ -306,40 +294,6 @@ public void run() { }); } - @NonNull - private ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { - try { - return applicationContext - .getPackageManager() - .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException(e); - } - } - - /** - * Initialize our Flutter config values by obtaining them from the manifest XML file, falling back - * to default values. - */ - private void initConfig(@NonNull Context applicationContext) { - Bundle metadata = getApplicationInfo(applicationContext).metaData; - - // There isn't a `` tag as a direct child of `` in - // `AndroidManifest.xml`. - if (metadata == null) { - return; - } - - aotSharedLibraryName = - metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); - flutterAssetsDir = - metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); - - vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); - isolateSnapshotData = - metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); - } - /** Extract assets out of the APK that need to be cached as uncompressed files on disk. */ private ResourceExtractor initResources(@NonNull Context applicationContext) { ResourceExtractor resourceExtractor = null; @@ -354,8 +308,8 @@ private ResourceExtractor initResources(@NonNull Context applicationContext) { // In debug/JIT mode these assets will be written to disk and then // mapped into memory so they can be provided to the Dart VM. resourceExtractor - .addResource(fullAssetPathFrom(vmSnapshotData)) - .addResource(fullAssetPathFrom(isolateSnapshotData)) + .addResource(fullAssetPathFrom(flutterApplicationInfo.vmSnapshotData)) + .addResource(fullAssetPathFrom(flutterApplicationInfo.isolateSnapshotData)) .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB)); resourceExtractor.start(); @@ -365,7 +319,7 @@ private ResourceExtractor initResources(@NonNull Context applicationContext) { @NonNull public String findAppBundlePath() { - return flutterAssetsDir; + return flutterApplicationInfo.flutterAssetsDir; } /** @@ -396,7 +350,7 @@ public String getLookupKeyForAsset(@NonNull String asset, @NonNull String packag @NonNull private String fullAssetPathFrom(@NonNull String filePath) { - return flutterAssetsDir + File.separator + filePath; + return flutterApplicationInfo.flutterAssetsDir + File.separator + filePath; } public static class Settings { diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java index ddb39fd0e5093..06833cbe2c225 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -30,7 +30,7 @@ public class PlatformChannel { @Nullable private PlatformMessageHandler platformMessageHandler; @NonNull @VisibleForTesting - protected final MethodChannel.MethodCallHandler parsingMethodCallHandler = + final MethodChannel.MethodCallHandler parsingMethodCallHandler = new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { @@ -155,6 +155,14 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result.success(null); break; } + case "Clipboard.hasStrings": + { + boolean hasStrings = platformMessageHandler.clipboardHasStrings(); + JSONObject response = new JSONObject(); + response.put("value", hasStrings); + result.success(response); + break; + } default: result.notImplemented(); break; @@ -426,6 +434,12 @@ public interface PlatformMessageHandler { * {@code text}. */ void setClipboardData(@NonNull String text); + + /** + * The Flutter application would like to know if the clipboard currently contains a string that + * can be pasted. + */ + boolean clipboardHasStrings(); } /** Types of sounds the Android OS can play on behalf of an application. */ diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index 8cb778daf1167..b05921c84bb1b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -1,6 +1,7 @@ package io.flutter.embedding.engine.systemchannels; import android.os.Build; +import android.os.Bundle; import android.view.View; import android.view.inputmethod.EditorInfo; import androidx.annotation.NonNull; @@ -13,6 +14,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -112,6 +114,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result textInputMethodHandler.clearClient(); result.success(null); break; + case "TextInput.sendAppPrivateCommand": + try { + final JSONObject arguments = (JSONObject) args; + final String action = arguments.getString("action"); + final String data = arguments.getString("data"); + Bundle bundle = null; + if (data != null && !data.isEmpty()) { + bundle = new Bundle(); + bundle.putString("data", data); + } + textInputMethodHandler.sendAppPrivateCommand(action, bundle); + result.success(null); + } catch (JSONException exception) { + result.error("error", exception.getMessage(), null); + } + break; case "TextInput.finishAutofillContext": textInputMethodHandler.finishAutofillContext((boolean) args); result.success(null); @@ -266,6 +284,38 @@ public void unspecifiedAction(int inputClientId) { Arrays.asList(inputClientId, "TextInputAction.unspecified")); } + public void performPrivateCommand(int inputClientId, String action, Bundle data) { + HashMap json = new HashMap<>(); + json.put("action", action); + if (data != null) { + HashMap dataMap = new HashMap<>(); + Set keySet = data.keySet(); + for (String key : keySet) { + Object value = data.get(key); + if (value instanceof byte[]) { + dataMap.put(key, data.getByteArray(key)); + } else if (value instanceof Byte) { + dataMap.put(key, data.getByte(key)); + } else if (value instanceof char[]) { + dataMap.put(key, data.getCharArray(key)); + } else if (value instanceof Character) { + dataMap.put(key, data.getChar(key)); + } else if (value instanceof CharSequence[]) { + dataMap.put(key, data.getCharSequenceArray(key)); + } else if (value instanceof CharSequence) { + dataMap.put(key, data.getCharSequence(key)); + } else if (value instanceof float[]) { + dataMap.put(key, data.getFloatArray(key)); + } else if (value instanceof Float) { + dataMap.put(key, data.getFloat(key)); + } + } + json.put("data", dataMap); + } + channel.invokeMethod( + "TextInputClient.performPrivateCommand", Arrays.asList(inputClientId, json)); + } + /** * Sets the {@link TextInputMethodHandler} which receives all events and requests that are parsed * from the underlying platform channel. @@ -328,6 +378,17 @@ public interface TextInputMethodHandler { // TODO(mattcarroll): javadoc void clearClient(); + + /** + * Sends client app private command to the current text input client(input method). The app + * private command result will be informed through {@code performPrivateCommand}. + * + * @param action Name of the command to be performed. This must be a scoped name. i.e. prefixed + * with a package name you own, so that different developers will not create conflicting + * commands. + * @param data Any data to include with the command. + */ + void sendAppPrivateCommand(String action, Bundle data); } /** A text editing configuration. */ diff --git a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java index 7b0acfae88747..c1a9a51c00596 100644 --- a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java @@ -70,6 +70,17 @@ public ByteBuffer encodeErrorEnvelope( .put(JSONUtil.wrap(errorDetails))); } + @Override + public ByteBuffer encodeErrorEnvelopeWithStacktrace( + String errorCode, String errorMessage, Object errorDetails, String errorStacktrace) { + return JSONMessageCodec.INSTANCE.encodeMessage( + new JSONArray() + .put(errorCode) + .put(JSONUtil.wrap(errorMessage)) + .put(JSONUtil.wrap(errorDetails)) + .put(JSONUtil.wrap(errorStacktrace))); + } + @Override public Object decodeEnvelope(ByteBuffer envelope) { try { diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index 41dbae9c9f8dd..81e50e3b938c3 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -11,6 +11,9 @@ import io.flutter.BuildConfig; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; import java.nio.ByteBuffer; /** @@ -90,7 +93,7 @@ public void invokeMethod(@NonNull String method, @Nullable Object arguments) { * @param callback a {@link Result} callback for the invocation result, or null. */ @UiThread - public void invokeMethod(String method, @Nullable Object arguments, Result callback) { + public void invokeMethod(String method, @Nullable Object arguments, @Nullable Result callback) { messenger.send( name, codec.encodeMethodCall(new MethodCall(method, arguments)), @@ -247,8 +250,16 @@ public void notImplemented() { }); } catch (RuntimeException e) { Log.e(TAG + name, "Failed to handle method call", e); - reply.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); + reply.reply( + codec.encodeErrorEnvelopeWithStacktrace( + "error", e.getMessage(), null, getStackTrace(e))); } } + + private String getStackTrace(Exception e) { + Writer result = new StringWriter(); + e.printStackTrace(new PrintWriter(result)); + return result.toString(); + } } } diff --git a/shell/platform/android/io/flutter/plugin/common/MethodCodec.java b/shell/platform/android/io/flutter/plugin/common/MethodCodec.java index fba950f9a499c..f958a5307ae9c 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodCodec.java @@ -55,6 +55,20 @@ public interface MethodCodec { */ ByteBuffer encodeErrorEnvelope(String errorCode, String errorMessage, Object errorDetails); + /** + * Encodes an error result into a binary envelope message with the native stacktrace. + * + * @param errorCode An error code String. + * @param errorMessage An error message String, possibly null. + * @param errorDetails Error details, possibly null. Consider supporting {@link Throwable} in your + * codec. This is the most common value passed to this field. + * @param errorStacktrace Platform stacktrace for the error. possibly null. + * @return a {@link ByteBuffer} containing the encoding between position 0 and the current + * position. + */ + ByteBuffer encodeErrorEnvelopeWithStacktrace( + String errorCode, String errorMessage, Object errorDetails, String errorStacktrace); + /** * Decodes a result envelope from binary. * diff --git a/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java b/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java index 913001f5b2bcf..942bd0e99bf29 100644 --- a/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java @@ -79,6 +79,24 @@ public ByteBuffer encodeErrorEnvelope( return buffer; } + @Override + public ByteBuffer encodeErrorEnvelopeWithStacktrace( + String errorCode, String errorMessage, Object errorDetails, String errorStacktrace) { + final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream(); + stream.write(1); + messageCodec.writeValue(stream, errorCode); + messageCodec.writeValue(stream, errorMessage); + if (errorDetails instanceof Throwable) { + messageCodec.writeValue(stream, getStackTrace((Throwable) errorDetails)); + } else { + messageCodec.writeValue(stream, errorDetails); + } + messageCodec.writeValue(stream, errorStacktrace); + final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size()); + buffer.put(stream.buffer(), 0, stream.size()); + return buffer; + } + @Override public Object decodeEnvelope(ByteBuffer envelope) { envelope.order(ByteOrder.nativeOrder()); diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 29b3fb859e1ef..ab4ed5c6b1d82 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -9,6 +9,7 @@ import android.content.ClipboardManager; import android.content.Context; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.text.DynamicLayout; import android.text.Editable; @@ -477,6 +478,12 @@ public boolean performContextMenuAction(int id) { return false; } + @Override + public boolean performPrivateCommand(String action, Bundle data) { + textInputChannel.performPrivateCommand(mClient, action, data); + return true; + } + @Override public boolean performEditorAction(int actionCode) { markDirty(); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 1aea8fbb279e3..1a635f13acdc5 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -8,6 +8,7 @@ import android.content.Context; import android.graphics.Rect; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.text.Editable; import android.text.InputType; @@ -119,6 +120,11 @@ public void setEditableSizeAndTransform(double width, double height, double[] tr public void clearClient() { clearTextInputClient(); } + + @Override + public void sendAppPrivateCommand(String action, Bundle data) { + sendTextInputAppPrivateCommand(action, data); + } }); textInputChannel.requestExistingInputState(); @@ -303,6 +309,10 @@ public void clearPlatformViewClient(int platformViewId) { } } + public void sendTextInputAppPrivateCommand(String action, Bundle data) { + mImm.sendAppPrivateCommand(mView, action, data); + } + private void showTextInput(View view) { view.requestFocus(); mImm.showSoftInput(view, 0); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 38f2b275fd275..cc87aa2eaf254 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -30,7 +30,8 @@ public class PlatformPlugin { private PlatformChannel.SystemChromeStyle currentTheme; private int mEnabledOverlays; - private final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = + @VisibleForTesting + final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = new PlatformChannel.PlatformMessageHandler() { @Override public void playSystemSound(@NonNull PlatformChannel.SoundType soundType) { @@ -85,6 +86,14 @@ public CharSequence getClipboardData( public void setClipboardData(@NonNull String text) { PlatformPlugin.this.setClipboardData(text); } + + @Override + public boolean clipboardHasStrings() { + CharSequence data = + PlatformPlugin.this.getClipboardData( + PlatformChannel.ClipboardContentFormat.PLAIN_TEXT); + return data != null && data.length() > 0; + } }; public PlatformPlugin(Activity activity, PlatformChannel platformChannel) { diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 7c064a6f90818..75d724605c69d 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -17,6 +17,7 @@ import android.view.View; import android.widget.FrameLayout; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.android.AndroidTouchProcessor; @@ -79,9 +80,20 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // it is associated with(e.g if a platform view creates other views in the same virtual display. private final HashMap contextToPlatformView; - private final SparseArray platformViewRequests; + // The views returned by `PlatformView#getView()`. + // + // This only applies to hybrid composition. private final SparseArray platformViews; - private final SparseArray mutatorViews; + + // The platform view parents that are appended to `FlutterView`. + // If an entry in `platformViews` doesn't have an entry in this array, the platform view isn't + // in the view hierarchy. + // + // This view provides a wrapper that applies scene builder operations to the platform view. + // For example, a transform matrix, or setting opacity on the platform view layer. + // + // This is only applies to hybrid composition. + private final SparseArray platformViewParent; // Map of unique IDs to views that render overlay layers. private final SparseArray overlayLayerViews; @@ -107,25 +119,57 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega @Override public void createAndroidViewForPlatformView( @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { - // API level 19 is required for android.graphics.ImageReader. + // API level 19 is required for `android.graphics.ImageReader`. ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT); - platformViewRequests.put(request.viewId, request); + + if (!validateDirection(request.direction)) { + throw new IllegalStateException( + "Trying to create a view with unknown direction value: " + + request.direction + + "(view id: " + + request.viewId + + ")"); + } + + final PlatformViewFactory factory = registry.getFactory(request.viewType); + if (factory == null) { + throw new IllegalStateException( + "Trying to create a platform view of unregistered type: " + request.viewType); + } + + Object createParams = null; + if (request.params != null) { + createParams = factory.getCreateArgsCodec().decodeMessage(request.params); + } + + final PlatformView platformView = factory.create(context, request.viewId, createParams); + final View view = platformView.getView(); + if (view == null) { + throw new IllegalStateException( + "PlatformView#getView() returned null, but an Android view reference was expected."); + } + if (view.getParent() != null) { + throw new IllegalStateException( + "The Android view returned from PlatformView#getView() was already added to a parent view."); + } + platformViews.put(request.viewId, view); } @Override public void disposeAndroidViewForPlatformView(int viewId) { // Hybrid view. - if (platformViewRequests.get(viewId) != null) { - platformViewRequests.remove(viewId); - } - final View platformView = platformViews.get(viewId); + final FlutterMutatorView parentView = platformViewParent.get(viewId); if (platformView != null) { - final FlutterMutatorView mutatorView = mutatorViews.get(viewId); - mutatorView.removeView(platformView); - ((FlutterView) flutterView).removeView(mutatorView); + if (parentView != null) { + parentView.removeView(platformView); + } platformViews.remove(viewId); - mutatorViews.remove(viewId); + } + + if (parentView != null) { + ((FlutterView) flutterView).removeView(parentView); + platformViewParent.remove(viewId); } } @@ -378,9 +422,8 @@ public PlatformViewsController() { currentFrameUsedOverlayLayerIds = new HashSet<>(); currentFrameUsedPlatformViewIds = new HashSet<>(); - platformViewRequests = new SparseArray<>(); platformViews = new SparseArray<>(); - mutatorViews = new SparseArray<>(); + platformViewParent = new SparseArray<>(); motionEventTracker = MotionEventTracker.getInstance(); } @@ -489,7 +532,12 @@ public void detachTextInputPlugin() { * if the view was created in a platform view's VD, delegates the decision to the platform view's * {@link View#checkInputConnectionProxy(View)} method. Else returns false. */ - public boolean checkInputConnectionProxy(View view) { + public boolean checkInputConnectionProxy(@Nullable View view) { + // View can be null on some devices + // See: https://github.com/flutter/flutter/issues/36517 + if (view == null) { + return false; + } if (!contextToPlatformView.containsKey(view.getContext())) { return false; } @@ -651,55 +699,20 @@ private void initializeRootImageViewIfNeeded() { @VisibleForTesting void initializePlatformViewIfNeeded(int viewId) { - if (platformViews.get(viewId) != null) { - return; - } - - PlatformViewsChannel.PlatformViewCreationRequest request = platformViewRequests.get(viewId); - if (request == null) { - throw new IllegalStateException( - "Platform view hasn't been initialized from the platform view channel."); - } - - if (!validateDirection(request.direction)) { - throw new IllegalStateException( - "Trying to create a view with unknown direction value: " - + request.direction - + "(view id: " - + viewId - + ")"); - } - - PlatformViewFactory factory = registry.getFactory(request.viewType); - if (factory == null) { - throw new IllegalStateException( - "Trying to create a platform view of unregistered type: " + request.viewType); - } - - Object createParams = null; - if (request.params != null) { - createParams = factory.getCreateArgsCodec().decodeMessage(request.params); - } - - PlatformView platformView = factory.create(context, viewId, createParams); - View view = platformView.getView(); - + final View view = platformViews.get(viewId); if (view == null) { throw new IllegalStateException( - "PlatformView#getView() returned null, but an Android view reference was expected."); + "Platform view hasn't been initialized from the platform view channel."); } - if (view.getParent() != null) { - throw new IllegalStateException( - "The Android view returned from PlatformView#getView() was already added to a parent view."); + if (platformViewParent.get(viewId) != null) { + return; } - platformViews.put(viewId, view); - - FlutterMutatorView mutatorView = + final FlutterMutatorView parentView = new FlutterMutatorView( context, context.getResources().getDisplayMetrics().density, androidTouchProcessor); - mutatorViews.put(viewId, mutatorView); - mutatorView.addView(view); - ((FlutterView) flutterView).addView(mutatorView); + platformViewParent.put(viewId, parentView); + parentView.addView(view); + ((FlutterView) flutterView).addView(parentView); } public void attachToFlutterRenderer(FlutterRenderer flutterRenderer) { @@ -718,13 +731,14 @@ public void onDisplayPlatformView( initializeRootImageViewIfNeeded(); initializePlatformViewIfNeeded(viewId); - FlutterMutatorView mutatorView = mutatorViews.get(viewId); - mutatorView.readyToDisplay(mutatorsStack, x, y, width, height); - mutatorView.setVisibility(View.VISIBLE); - mutatorView.bringToFront(); + final FlutterMutatorView parentView = platformViewParent.get(viewId); + parentView.readyToDisplay(mutatorsStack, x, y, width, height); + parentView.setVisibility(View.VISIBLE); + parentView.bringToFront(); - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, viewHeight); - View platformView = platformViews.get(viewId); + final FrameLayout.LayoutParams layoutParams = + new FrameLayout.LayoutParams(viewWidth, viewHeight); + final View platformView = platformViews.get(viewId); platformView.setLayoutParams(layoutParams); platformView.bringToFront(); currentFrameUsedPlatformViewIds.add(viewId); @@ -733,7 +747,7 @@ public void onDisplayPlatformView( public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) { initializeRootImageViewIfNeeded(); - FlutterImageView overlayView = overlayLayerViews.get(id); + final FlutterImageView overlayView = overlayLayerViews.get(id); if (overlayView.getParent() == null) { ((FlutterView) flutterView).addView(overlayView); } @@ -776,19 +790,19 @@ public void onEndFrame() { // If one of the surfaces doesn't have an image, the frame may be incomplete and must be // dropped. // For example, a toolbar widget painted by Flutter may not be rendered. - boolean isFrameRenderedUsingImageReaders = + final boolean isFrameRenderedUsingImageReaders = flutterViewConvertedToImageView && view.acquireLatestImageViewFrame(); finishFrame(isFrameRenderedUsingImageReaders); } private void finishFrame(boolean isFrameRenderedUsingImageReaders) { for (int i = 0; i < overlayLayerViews.size(); i++) { - int overlayId = overlayLayerViews.keyAt(i); - FlutterImageView overlayView = overlayLayerViews.valueAt(i); + final int overlayId = overlayLayerViews.keyAt(i); + final FlutterImageView overlayView = overlayLayerViews.valueAt(i); if (currentFrameUsedOverlayLayerIds.contains(overlayId)) { ((FlutterView) flutterView).attachOverlaySurfaceToRender(overlayView); - boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage(); + final boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage(); isFrameRenderedUsingImageReaders &= didAcquireOverlaySurfaceImage; } else { // If the background surface isn't rendered by the image view, then the @@ -802,22 +816,20 @@ private void finishFrame(boolean isFrameRenderedUsingImageReaders) { } } - for (int i = 0; i < platformViews.size(); i++) { - int viewId = platformViews.keyAt(i); - View platformView = platformViews.get(viewId); - View mutatorView = mutatorViews.get(viewId); + for (int i = 0; i < platformViewParent.size(); i++) { + final int viewId = platformViewParent.keyAt(i); + final View parentView = platformViewParent.get(viewId); // Show platform views only if the surfaces have images available in this frame, // and if the platform view is rendered in this frame. + // The platform view is appended to a mutator view. // // Otherwise, hide the platform view, but don't remove it from the view hierarchy yet as // they are removed when the framework diposes the platform view widget. if (isFrameRenderedUsingImageReaders && currentFrameUsedPlatformViewIds.contains(viewId)) { - platformView.setVisibility(View.VISIBLE); - mutatorView.setVisibility(View.VISIBLE); + parentView.setVisibility(View.VISIBLE); } else { - platformView.setVisibility(View.GONE); - mutatorView.setVisibility(View.GONE); + parentView.setVisibility(View.GONE); } } } diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index f6ed0179be89a..b54b76b02b886 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1548,6 +1548,17 @@ private void sendWindowChangeEvent(@NonNull SemanticsNode route) { AccessibilityEvent event = obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); String routeName = route.getRouteName(); + if (routeName == null) { + // The routeName will be null when there is no semantics node that represnets namesRoute in + // the scopeRoute. The TYPE_WINDOW_STATE_CHANGED only works the route name is not null and not + // empty. Gives it a whitespace will make it focus the first semantics node without + // pronouncing any word. + // + // The other way to trigger a focus change is to send a TYPE_VIEW_FOCUSED to the + // rootAccessibilityView. However, it is less predictable which semantics node it will focus + // next. + routeName = " "; + } event.getText().add(routeName); sendAccessibilityEvent(event); } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index f472b07c0753c..36f74850fad2f 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -432,6 +432,7 @@ public void destroy() { if (!isAttached()) return; getHolder().removeCallback(mSurfaceCallback); + releaseAccessibilityNodeProvider(); mNativeView.destroy(); mNativeView = null; @@ -749,9 +750,7 @@ protected void onAttachedToWindow() { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - - mAccessibilityNodeProvider.release(); - mAccessibilityNodeProvider = null; + releaseAccessibilityNodeProvider(); } // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise @@ -776,6 +775,13 @@ public AccessibilityNodeProvider getAccessibilityNodeProvider() { } } + private void releaseAccessibilityNodeProvider() { + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.release(); + mAccessibilityNodeProvider = null; + } + } + @Override @TargetApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N) diff --git a/shell/platform/android/surface/android_surface_mock.cc b/shell/platform/android/surface/android_surface_mock.cc index 5b0e7adad8cdd..f7098f7c56b74 100644 --- a/shell/platform/android/surface/android_surface_mock.cc +++ b/shell/platform/android/surface/android_surface_mock.cc @@ -18,7 +18,7 @@ bool AndroidSurfaceMock::GLContextPresent() { return true; } -intptr_t AndroidSurfaceMock::GLContextFBO() const { +intptr_t AndroidSurfaceMock::GLContextFBO(GLFrameInfo frame_info) const { return 0; } diff --git a/shell/platform/android/surface/android_surface_mock.h b/shell/platform/android/surface/android_surface_mock.h index 688681e01c8c0..7d5fddbf3d922 100644 --- a/shell/platform/android/surface/android_surface_mock.h +++ b/shell/platform/android/surface/android_surface_mock.h @@ -48,7 +48,7 @@ class AndroidSurfaceMock final : public GPUSurfaceGLDelegate, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| ExternalViewEmbedder* GetExternalViewEmbedder() override; diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 075e33c24b53e..3436a14eb601e 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -17,6 +17,7 @@ import io.flutter.embedding.engine.RenderingComponentTest; import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest; import io.flutter.embedding.engine.renderer.FlutterRendererTest; +import io.flutter.embedding.engine.systemchannels.PlatformChannelTest; import io.flutter.embedding.engine.systemchannels.RestorationChannelTest; import io.flutter.external.FlutterLaunchTests; import io.flutter.plugin.common.StandardMessageCodecTest; @@ -68,6 +69,7 @@ TextInputPluginTest.class, MouseCursorPluginTest.class, AccessibilityBridgeTest.class, + PlatformChannelTest.class, RestorationChannelTest.class, }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java index 39384aca2e716..88ef48cbdd8e9 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java @@ -1,8 +1,10 @@ package io.flutter.embedding.android; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import android.content.Intent; import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -11,17 +13,63 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class FlutterFragmentActivityTest { + + @Test + public void createFlutterFragment__defaultRenderModeSurface() { + final FlutterFragmentActivity activity = new FakeFlutterFragmentActivity(); + assertEquals(activity.createFlutterFragment().getRenderMode(), RenderMode.surface); + } + @Test - public void placeholder() { - // This is just a placeholder since this file only has a compile check currently. - // Delete when adding the first real test. - assertTrue(true); + public void createFlutterFragment__defaultRenderModeTexture() { + final FlutterFragmentActivity activity = + new FakeFlutterFragmentActivity() { + @Override + protected BackgroundMode getBackgroundMode() { + return BackgroundMode.transparent; + } + }; + assertEquals(activity.createFlutterFragment().getRenderMode(), RenderMode.texture); + } + + @Test + public void createFlutterFragment__customRenderMode() { + final FlutterFragmentActivity activity = + new FakeFlutterFragmentActivity() { + @Override + protected RenderMode getRenderMode() { + return RenderMode.texture; + } + }; + assertEquals(activity.createFlutterFragment().getRenderMode(), RenderMode.texture); + } + + private static class FakeFlutterFragmentActivity extends FlutterFragmentActivity { + @Override + public Intent getIntent() { + return new Intent(); + } + + @Override + public String getDartEntrypointFunctionName() { + return ""; + } + + @Override + protected String getInitialRoute() { + return ""; + } + + @Override + protected String getAppBundlePath() { + return ""; + } } // This is just a compile time check to ensure that it's possible for FlutterFragmentActivity // subclasses // to provide their own intent builders which builds their own runtime types. - static class FlutterFragmentActivityWithIntentBuilders extends FlutterFragmentActivity { + private static class FlutterFragmentActivityWithIntentBuilders extends FlutterFragmentActivity { public static NewEngineIntentBuilder withNewEngine() { return new NewEngineIntentBuilder(FlutterFragmentActivityWithIntentBuilders.class); } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java index f4860fd62e845..01d012c38b83a 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterApplicationInfo; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.FlutterPlugin; import org.junit.Test; @@ -26,6 +27,8 @@ public void pluginsCanAccessFlutterAssetPaths() { // Setup test. FlutterJNI flutterJNI = mock(FlutterJNI.class); when(flutterJNI.isAttached()).thenReturn(true); + FlutterApplicationInfo emptyInfo = + new FlutterApplicationInfo(null, null, null, null, null, null, false, false); // FlutterLoader is the object to which the PluginRegistry defers for obtaining // the path to a Flutter asset. Ideally in this component test we would use a @@ -44,7 +47,8 @@ public void pluginsCanAccessFlutterAssetPaths() { public String answer(InvocationOnMock invocation) throws Throwable { // Defer to a real FlutterLoader to return the asset path. String fileNameOrSubpath = (String) invocation.getArguments()[0]; - return FlutterLoader.getInstance().getLookupKeyForAsset(fileNameOrSubpath); + return FlutterLoader.getInstanceForTest(emptyInfo) + .getLookupKeyForAsset(fileNameOrSubpath); } }); when(flutterLoader.getLookupKeyForAsset(any(String.class), any(String.class))) @@ -55,7 +59,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { // Defer to a real FlutterLoader to return the asset path. String fileNameOrSubpath = (String) invocation.getArguments()[0]; String packageName = (String) invocation.getArguments()[1]; - return FlutterLoader.getInstance() + return FlutterLoader.getInstanceForTest(emptyInfo) .getLookupKeyForAsset(fileNameOrSubpath, packageName); } }); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java new file mode 100644 index 0000000000000..769525f7db3c9 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java @@ -0,0 +1,193 @@ +package io.flutter.embedding.engine.loader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.security.NetworkSecurityPolicy; +import java.io.StringReader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class ApplicationInfoLoaderTest { + + @Test + public void itGeneratesCorrectApplicationInfoWithDefaultManifest() { + FlutterApplicationInfo info = ApplicationInfoLoader.load(RuntimeEnvironment.application); + assertNotNull(info); + assertEquals("libapp.so", info.aotSharedLibraryName); + assertEquals("vm_snapshot_data", info.vmSnapshotData); + assertEquals("isolate_snapshot_data", info.isolateSnapshotData); + assertEquals("flutter_assets", info.flutterAssetsDir); + assertEquals("", info.domainNetworkPolicy); + assertNull(info.nativeLibraryDir); + assertEquals(true, info.clearTextPermitted); + assertEquals(false, info.useEmbeddedView); + } + + @Config(shadows = {ApplicationInfoLoaderTest.ShadowNetworkSecurityPolicy.class}) + @Test + public void itVotesAgainstClearTextIfSecurityPolicySaysSo() { + FlutterApplicationInfo info = ApplicationInfoLoader.load(RuntimeEnvironment.application); + assertNotNull(info); + assertEquals(false, info.clearTextPermitted); + } + + @Implements(NetworkSecurityPolicy.class) + public static class ShadowNetworkSecurityPolicy { + @Implementation + public boolean isCleartextTrafficPermitted() { + return false; + } + } + + private Context generateMockContext(Bundle metadata, String networkPolicyXml) throws Exception { + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + ApplicationInfo applicationInfo = mock(ApplicationInfo.class); + applicationInfo.metaData = metadata; + Resources resources = mock(Resources.class); + when(context.getPackageManager()).thenReturn(packageManager); + when(context.getResources()).thenReturn(resources); + when(packageManager.getApplicationInfo(any(String.class), any(int.class))) + .thenReturn(applicationInfo); + if (networkPolicyXml != null) { + metadata.putInt(ApplicationInfoLoader.NETWORK_POLICY_METADATA_KEY, 5); + doAnswer(invocationOnMock -> createMockResourceParser(networkPolicyXml)) + .when(resources) + .getXml(5); + } + return context; + } + + @Test + public void itGeneratesCorrectApplicationInfoWithCustomValues() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString(ApplicationInfoLoader.PUBLIC_AOT_SHARED_LIBRARY_NAME, "testaot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_VM_SNAPSHOT_DATA_KEY, "testvmsnapshot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, "testisolatesnapshot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "testassets"); + bundle.putBoolean("io.flutter.embedded_views_preview", true); + Context context = generateMockContext(bundle, null); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("testaot", info.aotSharedLibraryName); + assertEquals("testvmsnapshot", info.vmSnapshotData); + assertEquals("testisolatesnapshot", info.isolateSnapshotData); + assertEquals("testassets", info.flutterAssetsDir); + assertNull(info.nativeLibraryDir); + assertEquals("", info.domainNetworkPolicy); + assertEquals(true, info.useEmbeddedView); + } + + @Test + public void itGeneratesCorrectNetworkPolicy() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "secure.example.com" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("[[\"secure.example.com\",true,false]]", info.domainNetworkPolicy); + } + + @Test + public void itHandlesBogusInformationInNetworkPolicy() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "secure.example.com" + + "" + + "7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=" + + "" + + "fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=" + + "" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("[[\"secure.example.com\",true,false]]", info.domainNetworkPolicy); + } + + @Test + public void itHandlesNestedSubDomains() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "example.com" + + "" + + "insecure.example.com" + + "" + + "" + + "secure.example.com" + + "" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals( + "[[\"example.com\",true,true],[\"insecure.example.com\",true,true],[\"secure.example.com\",true,false]]", + info.domainNetworkPolicy); + } + + // The following ridiculousness is needed because Android gives no way for us + // to customize XmlResourceParser. We have to mock it and tie each method + // we use to an actual Xml parser. + private XmlResourceParser createMockResourceParser(String xml) throws Exception { + final XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput(new StringReader(xml)); + XmlResourceParser resourceParser = mock(XmlResourceParser.class); + final Answer invokeMethodOnRealParser = + invocation -> invocation.getMethod().invoke(xpp, invocation.getArguments()); + when(resourceParser.next()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getName()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getEventType()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getText()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeCount()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeName(anyInt())).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeValue(anyInt())).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeValue(any(String.class), any(String.class))) + .thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeBooleanValue( + any(String.class), any(String.class), any(Boolean.class))) + .thenAnswer( + invocation -> { + Object[] args = invocation.getArguments(); + String result = xpp.getAttributeValue((String) args[0], (String) args[1]); + if (result == null) { + return (Boolean) args[2]; + } + return Boolean.parseBoolean(result); + }); + return resourceParser; + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java new file mode 100644 index 0000000000000..3a215e3e5507d --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java @@ -0,0 +1,45 @@ +package io.flutter.embedding.engine.systemchannels; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class PlatformChannelTest { + @Test + public void platformChannel_hasStringsMessage() { + MethodChannel rawChannel = mock(MethodChannel.class); + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class)); + PlatformChannel fakePlatformChannel = new PlatformChannel(dartExecutor); + PlatformChannel.PlatformMessageHandler mockMessageHandler = + mock(PlatformChannel.PlatformMessageHandler.class); + fakePlatformChannel.setPlatformMessageHandler(mockMessageHandler); + Boolean returnValue = true; + when(mockMessageHandler.clipboardHasStrings()).thenReturn(returnValue); + MethodCall methodCall = new MethodCall("Clipboard.hasStrings", null); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + fakePlatformChannel.parsingMethodCallHandler.onMethodCall(methodCall, mockResult); + + JSONObject expected = new JSONObject(); + try { + expected.put("value", returnValue); + } catch (JSONException e) { + } + verify(mockResult).success(Matchers.refEq(expected)); + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java b/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java index c99a30f5d7fac..f87815f401f1e 100644 --- a/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java +++ b/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.fail; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -89,4 +90,27 @@ public void encodeErrorEnvelopeWithThrowableTest() { "at io.flutter.plugin.common.StandardMethodCodecTest.encodeErrorEnvelopeWithThrowableTest(StandardMethodCodecTest.java:")); } } + + @Test + public void encodeErrorEnvelopeWithStacktraceTest() { + final Exception e = new IllegalArgumentException("foo"); + final ByteBuffer buffer = + StandardMethodCodec.INSTANCE.encodeErrorEnvelopeWithStacktrace( + "code", e.getMessage(), e, "error stacktrace"); + assertNotNull(buffer); + buffer.flip(); + buffer.order(ByteOrder.nativeOrder()); + final byte flag = buffer.get(); + final Object code = StandardMessageCodec.INSTANCE.readValue(buffer); + final Object message = StandardMessageCodec.INSTANCE.readValue(buffer); + final Object details = StandardMessageCodec.INSTANCE.readValue(buffer); + final Object stacktrace = StandardMessageCodec.INSTANCE.readValue(buffer); + assertEquals("code", (String) code); + assertEquals("foo", (String) message); + String stack = (String) details; + assertTrue( + stack.contains( + "at io.flutter.plugin.common.StandardMethodCodecTest.encodeErrorEnvelopeWithStacktraceTest(StandardMethodCodecTest.java:")); + assertEquals("error stacktrace", (String) stacktrace); + } } diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java index d3afb22e5976d..458e75b7cfb02 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.eq; @@ -14,6 +15,7 @@ import android.content.ClipboardManager; import android.content.res.AssetManager; +import android.os.Bundle; import android.text.Editable; import android.text.Emoji; import android.text.InputType; @@ -26,9 +28,16 @@ import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; import io.flutter.util.FakeKeyEvent; +import java.nio.ByteBuffer; +import org.json.JSONArray; +import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -37,6 +46,21 @@ @Config(manifest = Config.NONE, shadows = ShadowClipboardManager.class) @RunWith(RobolectricTestRunner.class) public class InputConnectionAdaptorTest { + // Verifies the method and arguments for a captured method call. + private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs) + throws JSONException { + buffer.rewind(); + MethodCall methodCall = JSONMethodCodec.INSTANCE.decodeMethodCall(buffer); + assertEquals(methodName, methodCall.method); + if (expectedArgs != null) { + JSONArray args = methodCall.arguments(); + assertEquals(expectedArgs.length, args.length()); + for (int i = 0; i < args.length(); i++) { + assertEquals(expectedArgs[i], args.get(i).toString()); + } + } + } + @Test public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { View testView = new View(RuntimeEnvironment.application); @@ -125,6 +149,294 @@ public void testPerformContextMenuAction_paste() { assertTrue(editable.toString().startsWith(textToBePasted)); } + @Test + public void testPerformPrivateCommand_dataIsNull() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + adaptor.performPrivateCommand("actionCommand", null); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsByteArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + byte[] buffer = new byte[] {'a', 'b', 'c', 'd'}; + bundle.putByteArray("keyboard_layout", buffer); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[97,98,99,100]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsByte() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + byte b = 3; + bundle.putByte("keyboard_layout", b); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":3},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsCharArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + char[] buffer = new char[] {'a', 'b', 'c', 'd'}; + bundle.putCharArray("keyboard_layout", buffer); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", + "{\"data\":{\"keyboard_layout\":[\"a\",\"b\",\"c\",\"d\"]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsChar() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + char b = 'a'; + bundle.putChar("keyboard_layout", b); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":\"a\"},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsCharSequenceArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + CharSequence charSequence1 = new StringBuffer("abc"); + CharSequence charSequence2 = new StringBuffer("efg"); + CharSequence[] value = {charSequence1, charSequence2}; + bundle.putCharSequenceArray("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[\"abc\",\"efg\"]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsCharSequence() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + CharSequence charSequence = new StringBuffer("abc"); + bundle.putCharSequence("keyboard_layout", charSequence); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":\"abc\"},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsFloat() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + float value = 0.5f; + bundle.putFloat("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":0.5},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsFloatArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + float[] value = {0.5f, 0.6f}; + bundle.putFloatArray("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)) + .send( + channelCaptor.capture(), + bufferCaptor.capture(), + any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall( + bufferCaptor.getValue(), + "TextInputClient.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[0.5,0.6]},\"action\":\"actionCommand\"}" + }); + } + @Test public void testSendKeyEvent_shiftKeyUpCancelsSelection() { int selStart = 5; diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index bd4eb6f4b32ec..1a6b53c6b7080 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -18,6 +18,7 @@ import android.content.Context; import android.content.res.AssetManager; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.util.SparseIntArray; import android.view.KeyEvent; @@ -40,6 +41,7 @@ import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -566,6 +568,74 @@ public void respondsToInputChannelMessages() { verify(mockHandler, times(1)).finishAutofillContext(false); } + @Test + public void sendAppPrivateCommand_dataIsEmpty() throws JSONException { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger); + + EventHandler mockEventHandler = mock(EventHandler.class); + TestImm testImm = + Shadow.extract( + RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); + testImm.setEventHandler(mockEventHandler); + + View testView = new View(RuntimeEnvironment.application); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + JSONObject arguments = new JSONObject(); + arguments.put("action", "actionCommand"); + arguments.put("data", ""); + + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + sendToBinaryMessageHandler(binaryMessageHandler, "TextInput.sendAppPrivateCommand", arguments); + verify(mockEventHandler, times(1)) + .sendAppPrivateCommand(any(View.class), eq("actionCommand"), eq(null)); + } + + @Test + public void sendAppPrivateCommand_hasData() throws JSONException { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger); + + EventHandler mockEventHandler = mock(EventHandler.class); + TestImm testImm = + Shadow.extract( + RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); + testImm.setEventHandler(mockEventHandler); + + View testView = new View(RuntimeEnvironment.application); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + JSONObject arguments = new JSONObject(); + arguments.put("action", "actionCommand"); + arguments.put("data", "actionData"); + + ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + sendToBinaryMessageHandler(binaryMessageHandler, "TextInput.sendAppPrivateCommand", arguments); + verify(mockEventHandler, times(1)) + .sendAppPrivateCommand(any(View.class), eq("actionCommand"), bundleCaptor.capture()); + assertEquals("actionData", bundleCaptor.getValue().getCharSequence("data")); + } + + interface EventHandler { + void sendAppPrivateCommand(View view, String action, Bundle data); + } + @Implements(InputMethodManager.class) public static class TestImm extends ShadowInputMethodManager { private InputMethodSubtype currentInputMethodSubtype; @@ -573,6 +643,7 @@ public static class TestImm extends ShadowInputMethodManager { private CursorAnchorInfo cursorAnchorInfo; private ArrayList selectionUpdateValues; private boolean trackSelection = false; + private EventHandler handler; public TestImm() { selectionUpdateValues = new ArrayList(); @@ -597,6 +668,15 @@ public int getRestartCount(View view) { return restartCounter.get(view.hashCode(), /*defaultValue=*/ 0); } + public void setEventHandler(EventHandler eventHandler) { + handler = eventHandler; + } + + @Implementation + public void sendAppPrivateCommand(View view, String action, Bundle data) { + handler.sendAppPrivateCommand(view, action, data); + } + @Implementation public void updateCursorAnchorInfo(View view, CursorAnchorInfo cursorAnchorInfo) { this.cursorAnchorInfo = cursorAnchorInfo; diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 848b19161a3f9..355ec4e9ea5ee 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -1,15 +1,20 @@ package io.flutter.plugin.platform; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.app.Activity; +import android.content.ClipboardManager; +import android.content.Context; import android.view.View; import android.view.Window; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @Config(manifest = Config.NONE) @@ -32,4 +37,25 @@ public void itIgnoresNewHapticEventsOnOldAndroidPlatforms() { // SELECTION_CLICK haptic response is only available on "LOLLIPOP" (21) and later. platformPlugin.vibrateHapticFeedback(PlatformChannel.HapticFeedbackType.SELECTION_CLICK); } + + @Test + public void platformPlugin_hasStrings() { + ClipboardManager clipboardManager = + RuntimeEnvironment.application.getSystemService(ClipboardManager.class); + + View fakeDecorView = mock(View.class); + Window fakeWindow = mock(Window.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + Activity fakeActivity = mock(Activity.class); + when(fakeActivity.getWindow()).thenReturn(fakeWindow); + when(fakeActivity.getSystemService(Context.CLIPBOARD_SERVICE)).thenReturn(clipboardManager); + PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); + PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + + clipboardManager.setText("iamastring"); + assertTrue(platformPlugin.mPlatformMessageHandler.clipboardHasStrings()); + + clipboardManager.setText(""); + assertFalse(platformPlugin.mPlatformMessageHandler.clipboardHasStrings()); + } } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 659056afc5132..0e585b3e508c4 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -7,6 +7,7 @@ import android.content.Context; import android.content.res.AssetManager; +import android.util.SparseArray; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; @@ -28,7 +29,9 @@ import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.common.FlutterException; import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.common.StandardMethodCodec; import io.flutter.plugin.localization.LocalizationPlugin; import java.nio.ByteBuffer; @@ -242,7 +245,29 @@ public void getPlatformViewById__hybridComposition() { @Test @Config(shadows = {ShadowFlutterJNI.class}) - public void initializePlatformViewIfNeeded__throwsIfViewIsNull() { + public void createPlatformViewMessage__initializesAndroidView() { + PlatformViewsController platformViewsController = new PlatformViewsController(); + + int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + PlatformView platformView = mock(PlatformView.class); + when(platformView.getView()).thenReturn(mock(View.class)); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + FlutterJNI jni = new FlutterJNI(); + attach(jni, platformViewsController); + + // Simulate create call from the framework. + createPlatformView(jni, platformViewsController, platformViewId, "testType"); + verify(platformView, times(1)).getView(); + } + + @Test + @Config(shadows = {ShadowFlutterJNI.class}) + public void createPlatformViewMessage__throwsIfViewIsNull() { PlatformViewsController platformViewsController = new PlatformViewsController(); int platformViewId = 0; @@ -259,22 +284,28 @@ public void initializePlatformViewIfNeeded__throwsIfViewIsNull() { // Simulate create call from the framework. createPlatformView(jni, platformViewsController, platformViewId, "testType"); + assertEquals(ShadowFlutterJNI.getResponses().size(), 1); + + final ByteBuffer responseBuffer = ShadowFlutterJNI.getResponses().get(0); + responseBuffer.rewind(); + StandardMethodCodec methodCodec = new StandardMethodCodec(new StandardMessageCodec()); try { - platformViewsController.initializePlatformViewIfNeeded(platformViewId); - } catch (Exception exception) { - assertTrue(exception instanceof IllegalStateException); - assertEquals( - exception.getMessage(), - "PlatformView#getView() returned null, but an Android view reference was expected."); + methodCodec.decodeEnvelope(responseBuffer); + } catch (FlutterException exception) { + assertTrue( + exception + .getMessage() + .contains( + "PlatformView#getView() returned null, but an Android view reference was expected.")); return; } - assertTrue(false); + assertFalse(true); } @Test @Config(shadows = {ShadowFlutterJNI.class}) - public void initializePlatformViewIfNeeded__throwsIfViewHasParent() { + public void createPlatformViewMessage__throwsIfViewHasParent() { PlatformViewsController platformViewsController = new PlatformViewsController(); int platformViewId = 0; @@ -293,16 +324,23 @@ public void initializePlatformViewIfNeeded__throwsIfViewHasParent() { // Simulate create call from the framework. createPlatformView(jni, platformViewsController, platformViewId, "testType"); + assertEquals(ShadowFlutterJNI.getResponses().size(), 1); + + final ByteBuffer responseBuffer = ShadowFlutterJNI.getResponses().get(0); + responseBuffer.rewind(); + + StandardMethodCodec methodCodec = new StandardMethodCodec(new StandardMessageCodec()); try { - platformViewsController.initializePlatformViewIfNeeded(platformViewId); - } catch (Exception exception) { - assertTrue(exception instanceof IllegalStateException); - assertEquals( - exception.getMessage(), - "The Android view returned from PlatformView#getView() was already added to a parent view."); + methodCodec.decodeEnvelope(responseBuffer); + } catch (FlutterException exception) { + assertTrue( + exception + .getMessage() + .contains( + "The Android view returned from PlatformView#getView() was already added to a parent view.")); return; } - assertTrue(false); + assertFalse(true); } @Test @@ -407,6 +445,87 @@ public void onEndFrame__destroysOverlaySurfaceAfterFrameOnFlutterSurfaceView() { verify(overlayImageView, times(1)).detachFromRenderer(); } + @Test + @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + public void onEndFrame__removesPlatformView() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + + final int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + final PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + final PlatformView platformView = mock(PlatformView.class); + final View androidView = mock(View.class); + when(platformView.getView()).thenReturn(androidView); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + final FlutterJNI jni = new FlutterJNI(); + jni.attachToNative(false); + attach(jni, platformViewsController); + + jni.onFirstFrame(); + + // Simulate create call from the framework. + createPlatformView(jni, platformViewsController, platformViewId, "testType"); + + // Simulate first frame from the framework. + jni.onFirstFrame(); + platformViewsController.onBeginFrame(); + + platformViewsController.onEndFrame(); + verify(androidView, never()).setVisibility(View.GONE); + + final ViewParent parentView = mock(ViewParent.class); + when(androidView.getParent()).thenReturn(parentView); + } + + @Test + @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + public void onEndFrame__removesPlatformViewParent() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + + final int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + final PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + final PlatformView platformView = mock(PlatformView.class); + final View androidView = mock(View.class); + when(platformView.getView()).thenReturn(androidView); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + final FlutterJNI jni = new FlutterJNI(); + jni.attachToNative(false); + + final FlutterView flutterView = attach(jni, platformViewsController); + + jni.onFirstFrame(); + + // Simulate create call from the framework. + createPlatformView(jni, platformViewsController, platformViewId, "testType"); + platformViewsController.initializePlatformViewIfNeeded(platformViewId); + assertEquals(flutterView.getChildCount(), 2); + + // Simulate first frame from the framework. + jni.onFirstFrame(); + platformViewsController.onBeginFrame(); + platformViewsController.onEndFrame(); + + // Simulate dispose call from the framework. + disposePlatformView(jni, platformViewsController, platformViewId); + assertEquals(flutterView.getChildCount(), 1); + } + + @Test + public void checkInputConnectionProxy__falseIfViewIsNull() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + boolean shouldProxying = platformViewsController.checkInputConnectionProxy(null); + assertFalse(shouldProxying); + } + private static byte[] encodeMethodCall(MethodCall call) { final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call); buffer.rewind(); @@ -446,7 +565,8 @@ private static void disposePlatformView( "flutter/platform_views", encodeMethodCall(platformDisposeMethodCall), /*replyId=*/ 0); } - private static void attach(FlutterJNI jni, PlatformViewsController platformViewsController) { + private static FlutterView attach( + FlutterJNI jni, PlatformViewsController platformViewsController) { final DartExecutor executor = new DartExecutor(jni, mock(AssetManager.class)); executor.onAttachedToJNI(); @@ -477,10 +597,12 @@ public FlutterImageView createImageView() { view.attachToFlutterEngine(engine); platformViewsController.attachToView(view); + return view; } @Implements(FlutterJNI.class) public static class ShadowFlutterJNI { + private static SparseArray replies = new SparseArray<>(); public ShadowFlutterJNI() {} @@ -527,7 +649,13 @@ public void setViewportMetrics( @Implementation public void invokePlatformMessageResponseCallback( - int responseId, ByteBuffer message, int position) {} + int responseId, ByteBuffer message, int position) { + replies.put(responseId, message); + } + + public static SparseArray getResponses() { + return replies; + } } @Implements(SurfaceView.class) diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 38a6d7e695ed8..96ade8718a9cd 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -132,6 +132,41 @@ public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() { assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); } + @Test + public void itAnnouncesWhiteSpaceWhenNoNamesRoute() { + AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class); + AccessibilityManager mockManager = mock(AccessibilityManager.class); + View mockRootView = mock(View.class); + Context context = mock(Context.class); + when(mockRootView.getContext()).thenReturn(context); + when(context.getPackageName()).thenReturn("test"); + AccessibilityBridge accessibilityBridge = + setUpBridge(mockRootView, mockManager, mockViewEmbedder); + ViewParent mockParent = mock(ViewParent.class); + when(mockRootView.getParent()).thenReturn(mockParent); + when(mockManager.isEnabled()).thenReturn(true); + + // Sent a11y tree with scopeRoute without namesRoute. + TestSemanticsNode root = new TestSemanticsNode(); + root.id = 0; + TestSemanticsNode scopeRoute = new TestSemanticsNode(); + scopeRoute.id = 1; + scopeRoute.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE); + root.children.add(scopeRoute); + TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); + accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings); + + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mockParent, times(2)) + .requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture()); + AccessibilityEvent event = eventCaptor.getAllValues().get(0); + assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + List sentences = event.getText(); + assertEquals(sentences.size(), 1); + assertEquals(sentences.get(0).toString(), " "); + } + @Test public void itHoverOverOutOfBoundsDoesNotCrash() { // SementicsNode.hitTest() returns null when out of bounds. diff --git a/shell/platform/common/cpp/client_wrapper/BUILD.gn b/shell/platform/common/cpp/client_wrapper/BUILD.gn index 0b4f243c215df..6c297103d4360 100644 --- a/shell/platform/common/cpp/client_wrapper/BUILD.gn +++ b/shell/platform/common/cpp/client_wrapper/BUILD.gn @@ -8,7 +8,8 @@ import("core_wrapper_files.gni") # Client library build for internal use by the shell implementation. source_set("client_wrapper") { sources = core_cpp_client_wrapper_sources - public = core_cpp_client_wrapper_includes + public = core_cpp_client_wrapper_includes + + core_cpp_client_wrapper_internal_headers deps = [ "//flutter/shell/platform/common/cpp:common_cpp_library_headers" ] @@ -19,6 +20,24 @@ source_set("client_wrapper") { [ "//flutter/shell/platform/common/cpp:relative_flutter_library_headers" ] } +# Temporary test for the legacy EncodableValue implementation. Remove once the +# legacy version is removed. +source_set("client_wrapper_legacy_encodable_value") { + sources = core_cpp_client_wrapper_sources + public = core_cpp_client_wrapper_includes + + core_cpp_client_wrapper_internal_headers + + deps = [ "//flutter/shell/platform/common/cpp:common_cpp_library_headers" ] + + configs += + [ "//flutter/shell/platform/common/cpp:desktop_library_implementation" ] + + defines = [ "USE_LEGACY_ENCODABLE_VALUE" ] + + public_configs = + [ "//flutter/shell/platform/common/cpp:relative_flutter_library_headers" ] +} + source_set("client_wrapper_library_stubs") { sources = [ "testing/stub_flutter_api.cc", @@ -48,14 +67,17 @@ executable("client_wrapper_unittests") { "plugin_registrar_unittests.cc", "standard_message_codec_unittests.cc", "standard_method_codec_unittests.cc", - "testing/encodable_value_utils.cc", - "testing/encodable_value_utils.h", + "testing/test_codec_extensions.cc", + "testing/test_codec_extensions.h", ] deps = [ ":client_wrapper", ":client_wrapper_fixtures", ":client_wrapper_library_stubs", + + # Build the legacy version as well as a sanity check. + ":client_wrapper_unittests_legacy_encodable_value", "//flutter/testing", # TODO(chunhtai): Consider refactoring flutter_root/testing so that there's a testing @@ -66,3 +88,31 @@ executable("client_wrapper_unittests") { defines = [ "FLUTTER_DESKTOP_LIBRARY" ] } + +# Ensures that the legacy EncodableValue codepath still compiles. +executable("client_wrapper_unittests_legacy_encodable_value") { + testonly = true + + sources = [ + "encodable_value_unittests.cc", + "standard_message_codec_unittests.cc", + "testing/test_codec_extensions.h", + ] + + deps = [ + ":client_wrapper_fixtures", + ":client_wrapper_legacy_encodable_value", + ":client_wrapper_library_stubs", + "//flutter/testing", + + # TODO(chunhtai): Consider refactoring flutter_root/testing so that there's a testing + # target that doesn't require a Dart runtime to be linked in. + # https://github.com/flutter/flutter/issues/41414. + "//third_party/dart/runtime:libdart_jit", + ] + + defines = [ + "FLUTTER_DESKTOP_LIBRARY", + "USE_LEGACY_ENCODABLE_VALUE", + ] +} diff --git a/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc b/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc index 5098e89b30dd6..08f6a138f551b 100644 --- a/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/basic_message_channel.h" - #include #include +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/basic_message_channel.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h" #include "gtest/gtest.h" @@ -56,7 +55,7 @@ TEST(BasicMessageChannelTest, Registration) { callback_called = true; // Ensure that the wrapper recieved a correctly decoded message and a // reply. - EXPECT_EQ(message.StringValue(), message_value); + EXPECT_EQ(std::get(message), message_value); EXPECT_NE(reply, nullptr); }); EXPECT_EQ(messenger.last_message_handler_channel(), channel_name); diff --git a/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h b/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h new file mode 100644 index 0000000000000..28fa475c11633 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h @@ -0,0 +1,50 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BINARY_MESSENGER_IMPL_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BINARY_MESSENGER_IMPL_H_ + +#include + +#include +#include + +#include "include/flutter/binary_messenger.h" + +namespace flutter { + +// Wrapper around a FlutterDesktopMessengerRef that implements the +// BinaryMessenger API. +class BinaryMessengerImpl : public BinaryMessenger { + public: + explicit BinaryMessengerImpl(FlutterDesktopMessengerRef core_messenger); + + virtual ~BinaryMessengerImpl(); + + // Prevent copying. + BinaryMessengerImpl(BinaryMessengerImpl const&) = delete; + BinaryMessengerImpl& operator=(BinaryMessengerImpl const&) = delete; + + // |flutter::BinaryMessenger| + void Send(const std::string& channel, + const uint8_t* message, + size_t message_size, + BinaryReply reply) const override; + + // |flutter::BinaryMessenger| + void SetMessageHandler(const std::string& channel, + BinaryMessageHandler handler) override; + + private: + // Handle for interacting with the C API. + FlutterDesktopMessengerRef messenger_; + + // A map from channel names to the BinaryMessageHandler that should be called + // for incoming messages on that channel. + std::map handlers_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BINARY_MESSENGER_IMPL_H_ diff --git a/shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h b/shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h similarity index 58% rename from shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h rename to shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h index 96ab0b0d06210..e054e00f2d04c 100644 --- a/shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h +++ b/shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h @@ -2,30 +2,31 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_STREAM_WRAPPERS_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_STREAM_WRAPPERS_H_ - -// Utility classes for interacting with a buffer of bytes as a stream, for use -// in message channel codecs. +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_BUFFER_STREAMS_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_BUFFER_STREAMS_H_ +#include #include #include #include #include +#include "include/flutter/byte_streams.h" + namespace flutter { -// Wraps an array of bytes with utility methods for treating it as a readable -// stream. -class ByteBufferStreamReader { +// Implementation of ByteStreamReader base on a byte array. +class ByteBufferStreamReader : public ByteStreamReader { public: // Createa a reader reading from |bytes|, which must have a length of |size|. // |bytes| must remain valid for the lifetime of this object. explicit ByteBufferStreamReader(const uint8_t* bytes, size_t size) : bytes_(bytes), size_(size) {} - // Reads and returns the next byte from the stream. - uint8_t ReadByte() { + virtual ~ByteBufferStreamReader() = default; + + // |ByteStreamReader| + uint8_t ReadByte() override { if (location_ >= size_) { std::cerr << "Invalid read in StandardCodecByteStreamReader" << std::endl; return 0; @@ -33,9 +34,8 @@ class ByteBufferStreamReader { return bytes_[location_++]; } - // Reads the next |length| bytes from the stream into |buffer|. The caller - // is responsible for ensuring that |buffer| is large enough. - void ReadBytes(uint8_t* buffer, size_t length) { + // |ByteStreamReader| + void ReadBytes(uint8_t* buffer, size_t length) override { if (location_ + length > size_) { std::cerr << "Invalid read in StandardCodecByteStreamReader" << std::endl; return; @@ -44,9 +44,8 @@ class ByteBufferStreamReader { location_ += length; } - // Advances the read cursor to the next multiple of |alignment| relative to - // the start of the wrapped byte buffer, unless it is already aligned. - void ReadAlignment(uint8_t alignment) { + // |ByteStreamReader| + void ReadAlignment(uint8_t alignment) override { uint8_t mod = location_ % alignment; if (mod) { location_ += alignment - mod; @@ -62,30 +61,28 @@ class ByteBufferStreamReader { size_t location_ = 0; }; -// Wraps an array of bytes with utility methods for treating it as a writable -// stream. -class ByteBufferStreamWriter { +// Implementation of ByteStreamWriter based on a byte array. +class ByteBufferStreamWriter : public ByteStreamWriter { public: - // Createa a writter that writes into |buffer|. + // Creates a writer that writes into |buffer|. // |buffer| must remain valid for the lifetime of this object. explicit ByteBufferStreamWriter(std::vector* buffer) : bytes_(buffer) { assert(buffer); } - // Writes |byte| to the wrapped buffer. + virtual ~ByteBufferStreamWriter() = default; + + // |ByteStreamWriter| void WriteByte(uint8_t byte) { bytes_->push_back(byte); } - // Writes the next |length| bytes from |bytes| into the wrapped buffer. - // The caller is responsible for ensuring that |buffer| is large enough. + // |ByteStreamWriter| void WriteBytes(const uint8_t* bytes, size_t length) { assert(length > 0); bytes_->insert(bytes_->end(), bytes, bytes + length); } - // Writes 0s until the next multiple of |alignment| relative to - // the start of the wrapped byte buffer, unless the write positition is - // already aligned. + // |ByteStreamWriter| void WriteAlignment(uint8_t alignment) { uint8_t mod = bytes_->size() % alignment; if (mod) { @@ -102,4 +99,4 @@ class ByteBufferStreamWriter { } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_STREAM_WRAPPERS_H_ +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_BYTE_BUFFER_STREAMS_H_ diff --git a/shell/platform/common/cpp/client_wrapper/core_implementations.cc b/shell/platform/common/cpp/client_wrapper/core_implementations.cc new file mode 100644 index 0000000000000..2cafc7f9fe3a5 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/core_implementations.cc @@ -0,0 +1,150 @@ +// 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. + +// This file contains the implementations of any class in the wrapper that +// - is not fully inline, and +// - is necessary for all clients of the wrapper (either app or plugin). +// It exists instead of the usual structure of having some_class_name.cc files +// so that changes to the set of things that need non-header implementations +// are not breaking changes for the template. +// +// If https://github.com/flutter/flutter/issues/57146 is fixed, this can be +// removed in favor of the normal structure since templates will no longer +// manually include files. + +#include + +#include + +#include "binary_messenger_impl.h" +#include "include/flutter/engine_method_result.h" + +namespace flutter { + +// ========== binary_messenger_impl.h ========== + +namespace { +// Passes |message| to |user_data|, which must be a BinaryMessageHandler, along +// with a BinaryReply that will send a response on |message|'s response handle. +// +// This serves as an adaptor between the function-pointer-based message callback +// interface provided by the C API and the std::function-based message handler +// interface of BinaryMessenger. +void ForwardToHandler(FlutterDesktopMessengerRef messenger, + const FlutterDesktopMessage* message, + void* user_data) { + auto* response_handle = message->response_handle; + BinaryReply reply_handler = [messenger, response_handle]( + const uint8_t* reply, + size_t reply_size) mutable { + if (!response_handle) { + std::cerr << "Error: Response can be set only once. Ignoring " + "duplicate response." + << std::endl; + return; + } + FlutterDesktopMessengerSendResponse(messenger, response_handle, reply, + reply_size); + // The engine frees the response handle once + // FlutterDesktopSendMessageResponse is called. + response_handle = nullptr; + }; + + const BinaryMessageHandler& message_handler = + *static_cast(user_data); + + message_handler(message->message, message->message_size, + std::move(reply_handler)); +} +} // namespace + +BinaryMessengerImpl::BinaryMessengerImpl( + FlutterDesktopMessengerRef core_messenger) + : messenger_(core_messenger) {} + +BinaryMessengerImpl::~BinaryMessengerImpl() = default; + +void BinaryMessengerImpl::Send(const std::string& channel, + const uint8_t* message, + size_t message_size, + BinaryReply reply) const { + if (reply == nullptr) { + FlutterDesktopMessengerSend(messenger_, channel.c_str(), message, + message_size); + return; + } + struct Captures { + BinaryReply reply; + }; + auto captures = new Captures(); + captures->reply = reply; + + auto message_reply = [](const uint8_t* data, size_t data_size, + void* user_data) { + auto captures = reinterpret_cast(user_data); + captures->reply(data, data_size); + delete captures; + }; + bool result = FlutterDesktopMessengerSendWithReply( + messenger_, channel.c_str(), message, message_size, message_reply, + captures); + if (!result) { + delete captures; + } +} + +void BinaryMessengerImpl::SetMessageHandler(const std::string& channel, + BinaryMessageHandler handler) { + if (!handler) { + handlers_.erase(channel); + FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), nullptr, + nullptr); + return; + } + // Save the handler, to keep it alive. + handlers_[channel] = std::move(handler); + BinaryMessageHandler* message_handler = &handlers_[channel]; + // Set an adaptor callback that will invoke the handler. + FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), + ForwardToHandler, message_handler); +} + +// ========== engine_method_result.h ========== + +namespace internal { + +ReplyManager::ReplyManager(BinaryReply reply_handler) + : reply_handler_(std::move(reply_handler)) { + assert(reply_handler_); +} + +ReplyManager::~ReplyManager() { + if (reply_handler_) { + // Warn, rather than send a not-implemented response, since the engine may + // no longer be valid at this point. + std::cerr + << "Warning: Failed to respond to a message. This is a memory leak." + << std::endl; + } +} + +void ReplyManager::SendResponseData(const std::vector* data) { + if (!reply_handler_) { + std::cerr + << "Error: Only one of Success, Error, or NotImplemented can be " + "called," + << " and it can be called exactly once. Ignoring duplicate result." + << std::endl; + return; + } + + const uint8_t* message = data && !data->empty() ? data->data() : nullptr; + size_t message_size = data ? data->size() : 0; + reply_handler_(message, message_size); + reply_handler_ = nullptr; +} + +} // namespace internal + +} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni b/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni index 18b0897e2a987..264bf774e6111 100644 --- a/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni +++ b/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni @@ -6,6 +6,7 @@ core_cpp_client_wrapper_includes = get_path_info([ "include/flutter/basic_message_channel.h", "include/flutter/binary_messenger.h", + "include/flutter/byte_streams.h", "include/flutter/encodable_value.h", "include/flutter/engine_method_result.h", "include/flutter/event_channel.h", @@ -20,19 +21,32 @@ core_cpp_client_wrapper_includes = "include/flutter/method_result.h", "include/flutter/plugin_registrar.h", "include/flutter/plugin_registry.h", + "include/flutter/standard_codec_serializer.h", "include/flutter/standard_message_codec.h", "include/flutter/standard_method_codec.h", ], "abspath") +# Headers that aren't public for clients of the wrapper, but are considered +# public for the purpose of BUILD dependencies (e.g., to allow +# windows/client_wrapper implementation files to include them). +core_cpp_client_wrapper_internal_headers = + get_path_info([ + "binary_messenger_impl.h", + "byte_buffer_streams.h", + ], + "abspath") + # TODO: Once the wrapper API is more stable, consolidate to as few files as is # reasonable (without forcing different kinds of clients to take unnecessary # code) to simplify use. core_cpp_client_wrapper_sources = get_path_info([ - "byte_stream_wrappers.h", - "engine_method_result.cc", + "core_implementations.cc", "plugin_registrar.cc", - "standard_codec_serializer.h", "standard_codec.cc", ], "abspath") + +# Temporary shim, published for backwards compatibility. +# See comment in the file for more detail. +temporary_shim_files = get_path_info([ "engine_method_result.cc" ], "abspath") diff --git a/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc b/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc index 480314252255d..e64beae8b3a8f 100644 --- a/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc @@ -3,96 +3,84 @@ // found in the LICENSE file. // FLUTTER_NOLINT -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" - #include +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" #include "gtest/gtest.h" namespace flutter { -// Verifies that value.type() is |type|, and that of all the Is* methods, only -// the one that matches the type is true. -void VerifyType(EncodableValue& value, - EncodableValue::EncodableValue::Type type) { - EXPECT_EQ(value.type(), type); - - EXPECT_EQ(value.IsNull(), type == EncodableValue::Type::kNull); - EXPECT_EQ(value.IsBool(), type == EncodableValue::Type::kBool); - EXPECT_EQ(value.IsInt(), type == EncodableValue::Type::kInt); - EXPECT_EQ(value.IsLong(), type == EncodableValue::Type::kLong); - EXPECT_EQ(value.IsDouble(), type == EncodableValue::Type::kDouble); - EXPECT_EQ(value.IsString(), type == EncodableValue::Type::kString); - EXPECT_EQ(value.IsByteList(), type == EncodableValue::Type::kByteList); - EXPECT_EQ(value.IsIntList(), type == EncodableValue::Type::kIntList); - EXPECT_EQ(value.IsLongList(), type == EncodableValue::Type::kLongList); - EXPECT_EQ(value.IsDoubleList(), type == EncodableValue::Type::kDoubleList); - EXPECT_EQ(value.IsList(), type == EncodableValue::Type::kList); - EXPECT_EQ(value.IsMap(), type == EncodableValue::Type::kMap); -} - TEST(EncodableValueTest, Null) { EncodableValue value; - VerifyType(value, EncodableValue::Type::kNull); + value.IsNull(); } +#ifndef USE_LEGACY_ENCODABLE_VALUE + TEST(EncodableValueTest, Bool) { EncodableValue value(false); - VerifyType(value, EncodableValue::Type::kBool); - EXPECT_FALSE(value.BoolValue()); + EXPECT_FALSE(std::get(value)); value = true; - EXPECT_TRUE(value.BoolValue()); + EXPECT_TRUE(std::get(value)); } TEST(EncodableValueTest, Int) { EncodableValue value(42); - VerifyType(value, EncodableValue::Type::kInt); - EXPECT_EQ(value.IntValue(), 42); + EXPECT_EQ(std::get(value), 42); value = std::numeric_limits::max(); - EXPECT_EQ(value.IntValue(), std::numeric_limits::max()); + EXPECT_EQ(std::get(value), std::numeric_limits::max()); } -TEST(EncodableValueTest, LongValueFromInt) { +// Test the int/long convenience wrapper. +TEST(EncodableValueTest, LongValue) { EncodableValue value(std::numeric_limits::max()); EXPECT_EQ(value.LongValue(), std::numeric_limits::max()); + value = std::numeric_limits::max(); + EXPECT_EQ(value.LongValue(), std::numeric_limits::max()); } TEST(EncodableValueTest, Long) { EncodableValue value(INT64_C(42)); - VerifyType(value, EncodableValue::Type::kLong); - EXPECT_EQ(value.LongValue(), 42); + EXPECT_EQ(std::get(value), 42); value = std::numeric_limits::max(); - EXPECT_EQ(value.LongValue(), std::numeric_limits::max()); + EXPECT_EQ(std::get(value), std::numeric_limits::max()); } TEST(EncodableValueTest, Double) { EncodableValue value(3.14); - VerifyType(value, EncodableValue::Type::kDouble); - EXPECT_EQ(value.DoubleValue(), 3.14); + EXPECT_EQ(std::get(value), 3.14); value = std::numeric_limits::max(); - EXPECT_EQ(value.DoubleValue(), std::numeric_limits::max()); + EXPECT_EQ(std::get(value), std::numeric_limits::max()); } TEST(EncodableValueTest, String) { std::string hello("Hello, world!"); EncodableValue value(hello); - VerifyType(value, EncodableValue::Type::kString); - EXPECT_EQ(value.StringValue(), hello); + EXPECT_EQ(std::get(value), hello); + value = std::string("Goodbye"); + EXPECT_EQ(std::get(value), "Goodbye"); +} + +// Explicitly verify that the overrides to prevent char*->bool conversions work. +TEST(EncodableValueTest, CString) { + const char* hello = "Hello, world!"; + EncodableValue value(hello); + + EXPECT_EQ(std::get(value), hello); value = "Goodbye"; - EXPECT_EQ(value.StringValue(), "Goodbye"); + EXPECT_EQ(std::get(value), "Goodbye"); } TEST(EncodableValueTest, UInt8List) { std::vector data = {0, 2}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kByteList); - std::vector& list_value = value.ByteListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], 0); EXPECT_EQ(list_value[1], 2); @@ -105,9 +93,8 @@ TEST(EncodableValueTest, UInt8List) { TEST(EncodableValueTest, Int32List) { std::vector data = {-10, 2}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kIntList); - std::vector& list_value = value.IntListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], -10); EXPECT_EQ(list_value[1], 2); @@ -120,9 +107,8 @@ TEST(EncodableValueTest, Int32List) { TEST(EncodableValueTest, Int64List) { std::vector data = {-10, 2}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kLongList); - std::vector& list_value = value.LongListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], -10); EXPECT_EQ(list_value[1], 2); @@ -135,9 +121,8 @@ TEST(EncodableValueTest, Int64List) { TEST(EncodableValueTest, DoubleList) { std::vector data = {-10.0, 2.0}; EncodableValue value(data); - VerifyType(value, EncodableValue::Type::kDoubleList); - std::vector& list_value = value.DoubleListValue(); + auto& list_value = std::get>(value); list_value.push_back(std::numeric_limits::max()); EXPECT_EQ(list_value[0], -10.0); EXPECT_EQ(list_value[1], 2.0); @@ -154,18 +139,17 @@ TEST(EncodableValueTest, List) { EncodableValue("Three"), }; EncodableValue value(encodables); - VerifyType(value, EncodableValue::Type::kList); - EncodableList& list_value = value.ListValue(); - EXPECT_EQ(list_value[0].IntValue(), 1); - EXPECT_EQ(list_value[1].DoubleValue(), 2.0); - EXPECT_EQ(list_value[2].StringValue(), "Three"); + auto& list_value = std::get(value); + EXPECT_EQ(std::get(list_value[0]), 1); + EXPECT_EQ(std::get(list_value[1]), 2.0); + EXPECT_EQ(std::get(list_value[2]), "Three"); // Ensure that it's a modifiable copy of the original array. list_value.push_back(EncodableValue(true)); ASSERT_EQ(list_value.size(), 4u); EXPECT_EQ(encodables.size(), 3u); - EXPECT_EQ(value.ListValue()[3].BoolValue(), true); + EXPECT_EQ(std::get(std::get(value)[3]), true); } TEST(EncodableValueTest, Map) { @@ -175,43 +159,19 @@ TEST(EncodableValueTest, Map) { {EncodableValue("two"), EncodableValue(7)}, }; EncodableValue value(encodables); - VerifyType(value, EncodableValue::Type::kMap); - EncodableMap& map_value = value.MapValue(); - EXPECT_EQ(map_value[EncodableValue()].IsIntList(), true); - EXPECT_EQ(map_value[EncodableValue(1)].LongValue(), INT64_C(10000)); - EXPECT_EQ(map_value[EncodableValue("two")].IntValue(), 7); + auto& map_value = std::get(value); + EXPECT_EQ( + std::holds_alternative>(map_value[EncodableValue()]), + true); + EXPECT_EQ(std::get(map_value[EncodableValue(1)]), INT64_C(10000)); + EXPECT_EQ(std::get(map_value[EncodableValue("two")]), 7); // Ensure that it's a modifiable copy of the original map. map_value[EncodableValue(true)] = EncodableValue(false); ASSERT_EQ(map_value.size(), 4u); EXPECT_EQ(encodables.size(), 3u); - EXPECT_EQ(map_value[EncodableValue(true)].BoolValue(), false); -} - -TEST(EncodableValueTest, EmptyTypeConstructor) { - EXPECT_TRUE(EncodableValue(EncodableValue::Type::kNull).IsNull()); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kBool).BoolValue(), false); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kInt).IntValue(), 0); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kLong).LongValue(), - INT64_C(0)); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kDouble).DoubleValue(), 0.0); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kString).StringValue().size(), - 0u); - EXPECT_EQ( - EncodableValue(EncodableValue::Type::kByteList).ByteListValue().size(), - 0u); - EXPECT_EQ( - EncodableValue(EncodableValue::Type::kIntList).IntListValue().size(), 0u); - EXPECT_EQ( - EncodableValue(EncodableValue::Type::kLongList).LongListValue().size(), - 0u); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kDoubleList) - .DoubleListValue() - .size(), - 0u); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kList).ListValue().size(), 0u); - EXPECT_EQ(EncodableValue(EncodableValue::Type::kMap).MapValue().size(), 0u); + EXPECT_EQ(std::get(map_value[EncodableValue(true)]), false); } // Tests that the < operator meets the requirements of using EncodableValue as @@ -248,8 +208,8 @@ TEST(EncodableValueTest, Comparison) { EncodableValue(std::vector{0, INT64_C(1)}), EncodableValue(std::vector{0, INT64_C(100)}), // DoubleList - EncodableValue(std::vector{0, INT64_C(1)}), - EncodableValue(std::vector{0, INT64_C(100)}), + EncodableValue(std::vector{0, INT64_C(1)}), + EncodableValue(std::vector{0, INT64_C(100)}), // List EncodableValue(EncodableList{EncodableValue(), EncodableValue(true)}), EncodableValue(EncodableList{EncodableValue(), EncodableValue(1.0)}), @@ -272,23 +232,19 @@ TEST(EncodableValueTest, Comparison) { } else { // All other comparisons should be consistent, but the direction doesn't // matter. - EXPECT_NE(a < b, b < a); + EXPECT_NE(a < b, b < a) << "Indexes: " << i << ", " << j; } } - // Different non-collection objects with the same value should be equal; - // different collections should always be unequal regardless of contents. - bool is_collection = a.IsByteList() || a.IsIntList() || a.IsLongList() || - a.IsDoubleList() || a.IsList() || a.IsMap(); + // Copies should always be equal. EncodableValue copy(a); - bool is_equal = !(a < copy || copy < a); - EXPECT_EQ(is_equal, !is_collection); + EXPECT_FALSE(a < copy || copy < a); } } // Tests that structures are deep-copied. TEST(EncodableValueTest, DeepCopy) { - EncodableList encodables = { + EncodableList original = { EncodableValue(EncodableMap{ {EncodableValue(), EncodableValue(std::vector{1, 2, 3})}, {EncodableValue(1), EncodableValue(INT64_C(0000))}, @@ -302,30 +258,30 @@ TEST(EncodableValueTest, DeepCopy) { }), }; - EncodableValue value(encodables); - ASSERT_TRUE(value.IsList()); + EncodableValue copy(original); + ASSERT_TRUE(std::holds_alternative(copy)); // Spot-check innermost collection values. - EXPECT_EQ(value.ListValue()[0].MapValue()[EncodableValue("two")].IntValue(), - 7); - EXPECT_EQ(value.ListValue()[1] - .ListValue()[2] - .MapValue()[EncodableValue("a")] - .StringValue(), - "b"); + auto& root_list = std::get(copy); + auto& first_child = std::get(root_list[0]); + EXPECT_EQ(std::get(first_child[EncodableValue("two")]), 7); + auto& second_child = std::get(root_list[1]); + auto& innermost_map = std::get(second_child[2]); + EXPECT_EQ(std::get(innermost_map[EncodableValue("a")]), "b"); // Modify those values in the original structure. - encodables[0].MapValue()[EncodableValue("two")] = EncodableValue(); - encodables[1].ListValue()[2].MapValue()[EncodableValue("a")] = 99; - - // Re-check innermost collection values to ensure that they haven't changed. - EXPECT_EQ(value.ListValue()[0].MapValue()[EncodableValue("two")].IntValue(), - 7); - EXPECT_EQ(value.ListValue()[1] - .ListValue()[2] - .MapValue()[EncodableValue("a")] - .StringValue(), - "b"); + first_child[EncodableValue("two")] = EncodableValue(); + innermost_map[EncodableValue("a")] = 99; + + // Re-check innermost collection values of the original to ensure that they + // haven't changed. + first_child = std::get(original[0]); + EXPECT_EQ(std::get(first_child[EncodableValue("two")]), 7); + second_child = std::get(original[1]); + innermost_map = std::get(second_child[2]); + EXPECT_EQ(std::get(innermost_map[EncodableValue("a")]), "b"); } +#endif // !LEGACY_ENCODABLE_VALUE + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/engine_method_result.cc b/shell/platform/common/cpp/client_wrapper/engine_method_result.cc index 03e17a82e6bd5..65eaf5d4358cb 100644 --- a/shell/platform/common/cpp/client_wrapper/engine_method_result.cc +++ b/shell/platform/common/cpp/client_wrapper/engine_method_result.cc @@ -2,44 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "include/flutter/engine_method_result.h" +// This file is deprecated in favor of core_implementations.cc. This is a +// temporary forwarding implementation so that the switch to +// core_implementations.cc isn't an immediate breaking change, allowing for the +// template to be updated to include it and update the template version before +// removing this file. -#include -#include - -namespace flutter { -namespace internal { - -ReplyManager::ReplyManager(BinaryReply reply_handler) - : reply_handler_(std::move(reply_handler)) { - assert(reply_handler_); -} - -ReplyManager::~ReplyManager() { - if (reply_handler_) { - // Warn, rather than send a not-implemented response, since the engine may - // no longer be valid at this point. - std::cerr - << "Warning: Failed to respond to a message. This is a memory leak." - << std::endl; - } -} - -void ReplyManager::SendResponseData(const std::vector* data) { - if (!reply_handler_) { - std::cerr - << "Error: Only one of Success, Error, or NotImplemented can be " - "called," - << " and it can be called exactly once. Ignoring duplicate result." - << std::endl; - return; - } - - const uint8_t* message = data && !data->empty() ? data->data() : nullptr; - size_t message_size = data ? data->size() : 0; - reply_handler_(message, message_size); - reply_handler_ = nullptr; -} - -} // namespace internal -} // namespace flutter +#include "core_implementations.cc" diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h b/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h new file mode 100644 index 0000000000000..f5314c0b9b575 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h @@ -0,0 +1,85 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_BYTE_STREAMS_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_BYTE_STREAMS_H_ + +// Interfaces for interacting with a stream of bytes, for use in codecs. + +namespace flutter { + +// An interface for a class that reads from a byte stream. +class ByteStreamReader { + public: + explicit ByteStreamReader() = default; + virtual ~ByteStreamReader() = default; + + // Reads and returns the next byte from the stream. + virtual uint8_t ReadByte() = 0; + + // Reads the next |length| bytes from the stream into |buffer|. The caller + // is responsible for ensuring that |buffer| is large enough. + virtual void ReadBytes(uint8_t* buffer, size_t length) = 0; + + // Advances the read cursor to the next multiple of |alignment| relative to + // the start of the stream, unless it is already aligned. + virtual void ReadAlignment(uint8_t alignment) = 0; + + // Reads and returns the next 32-bit integer from the stream. + int32_t ReadInt32() { + int32_t value = 0; + ReadBytes(reinterpret_cast(&value), 4); + return value; + } + + // Reads and returns the next 64-bit integer from the stream. + int64_t ReadInt64() { + int64_t value = 0; + ReadBytes(reinterpret_cast(&value), 8); + return value; + } + + // Reads and returns the next 64-bit floating point number from the stream. + double ReadDouble() { + double value = 0; + ReadBytes(reinterpret_cast(&value), 8); + return value; + } +}; + +// An interface for a class that writes to a byte stream. +class ByteStreamWriter { + public: + explicit ByteStreamWriter() = default; + virtual ~ByteStreamWriter() = default; + + // Writes |byte| to the stream. + virtual void WriteByte(uint8_t byte) = 0; + + // Writes the next |length| bytes from |bytes| to the stream + virtual void WriteBytes(const uint8_t* bytes, size_t length) = 0; + + // Writes 0s until the next multiple of |alignment| relative to the start + // of the stream, unless the write positition is already aligned. + virtual void WriteAlignment(uint8_t alignment) = 0; + + // Writes the given 32-bit int to the stream. + void WriteInt32(int32_t value) { + WriteBytes(reinterpret_cast(&value), 4); + } + + // Writes the given 64-bit int to the stream. + void WriteInt64(int64_t value) { + WriteBytes(reinterpret_cast(&value), 8); + } + + // Writes the given 36-bit double to the stream. + void WriteDouble(double value) { + WriteBytes(reinterpret_cast(&value), 8); + } +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_BYTE_STREAMS_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h b/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h index a63a4335c2b8f..0b021292d0fc8 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h @@ -5,17 +5,207 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_ENCODABLE_VALUE_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_ENCODABLE_VALUE_H_ -#include +#include +#include #include #include #include #include +#include #include +// Unless overridden, attempt to detect the RTTI state from the compiler. +#ifndef FLUTTER_ENABLE_RTTI +#if defined(_MSC_VER) +#ifdef _CPPRTTI +#define FLUTTER_ENABLE_RTTI 1 +#endif +#elif defined(__clang__) +#if __has_feature(cxx_rtti) +#define FLUTTER_ENABLE_RTTI 1 +#endif +#elif defined(__GNUC__) +#ifdef __GXX_RTTI +#define FLUTTER_ENABLE_RTTI 1 +#endif +#endif +#endif // #ifndef FLUTTER_ENABLE_RTTI + namespace flutter { static_assert(sizeof(double) == 8, "EncodableValue requires a 64-bit double"); +// Defining USE_LEGACY_ENCODABLE_VALUE will use the original EncodableValue +// implementation. This is a temporary measure to minimize the impact of the +// breaking change; it will be removed in the future. If you set this, you +// should update your code as soon as possible to use the new std::variant +// version, or it will break when the legacy version is removed. +#ifndef USE_LEGACY_ENCODABLE_VALUE + +// A container for arbitrary types in EncodableValue. +// +// This is used in conjunction with StandardCodecExtension to allow using other +// types with a StandardMethodCodec/StandardMessageCodec. It is implicitly +// convertible to EncodableValue, so constructing an EncodableValue from a +// custom type can generally be written as: +// CustomEncodableValue(MyType(...)) +// rather than: +// EncodableValue(CustomEncodableValue(MyType(...))) +// +// For extracting recieved custom types, it is implicitly convertible to +// std::any. For example: +// const MyType& my_type_value = +// std::any_cast(std::get(value)); +// +// If RTTI is enabled, different extension types can be checked with type(): +// if (custom_value->type() == typeid(SomeData)) { ... } +// Clients that wish to disable RTTI would need to decide on another approach +// for distinguishing types (e.g., in StandardCodecExtension::WriteValueOfType) +// if multiple custom types are needed. For instance, wrapping all of the +// extension types in an EncodableValue-style variant, and only ever storing +// that variant in CustomEncodableValue. +class CustomEncodableValue { + public: + explicit CustomEncodableValue(const std::any& value) : value_(value) {} + ~CustomEncodableValue() = default; + + // Allow implict conversion to std::any to allow direct use of any_cast. + operator std::any &() { return value_; } + operator const std::any &() const { return value_; } + +#if defined(FLUTTER_ENABLE_RTTI) && FLUTTER_ENABLE_RTTI + // Passthrough to std::any's type(). + const std::type_info& type() const noexcept { return value_.type(); } +#endif + + // This operator exists only to provide a stable ordering for use as a + // std::map key, to satisfy the compiler requirements for EncodableValue. + // It does not attempt to provide useful ordering semantics, and using a + // custom value as a map key is not recommended. + bool operator<(const CustomEncodableValue& other) const { + return this < &other; + } + bool operator==(const CustomEncodableValue& other) const { + return this == &other; + } + + private: + std::any value_; +}; + +class EncodableValue; + +// Convenience type aliases. +using EncodableList = std::vector; +using EncodableMap = std::map; + +namespace internal { +// The base class for EncodableValue. Do not use this directly; it exists only +// for EncodableValue to inherit from. +// +// Do not change the order or indexes of the items here; see the comment on +// EncodableValue +using EncodableValueVariant = std::variant, + std::vector, + std::vector, + std::vector, + EncodableList, + EncodableMap, + CustomEncodableValue>; +} // namespace internal + +// An object that can contain any value or collection type supported by +// Flutter's standard method codec. +// +// For details, see: +// https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html +// +// As an example, the following Dart structure: +// { +// 'flag': true, +// 'name': 'Thing', +// 'values': [1, 2.0, 4], +// } +// would correspond to: +// EncodableValue(EncodableMap{ +// {EncodableValue("flag"), EncodableValue(true)}, +// {EncodableValue("name"), EncodableValue("Thing")}, +// {EncodableValue("values"), EncodableValue(EncodableList{ +// EncodableValue(1), +// EncodableValue(2.0), +// EncodableValue(4), +// })}, +// }) +// +// The primary API surface for this object is std::variant. For instance, +// getting a string value from an EncodableValue, with type checking: +// if (std::holds_alternative(value)) { +// std::string some_string = std::get(value); +// } +// +// The order/indexes of the variant types is part of the API surface, and is +// guaranteed not to change. +class EncodableValue : public internal::EncodableValueVariant { + public: + // Rely on std::variant for most of the constructors/operators. + using super = internal::EncodableValueVariant; + using super::super; + using super::operator=; + + explicit EncodableValue() = default; + + // Avoid the C++17 pitfall of conversion from char* to bool. Should not be + // needed for C++20. + explicit EncodableValue(const char* string) : super(std::string(string)) {} + EncodableValue& operator=(const char* other) { + *this = std::string(other); + return *this; + } + + // Allow implicit conversion from CustomEncodableValue; the only reason to + // make a CustomEncodableValue (which can only be constructed explicitly) is + // to use it with EncodableValue, so the risk of unintended conversions is + // minimal, and it avoids the need for the verbose: + // EncodableValue(CustomEncodableValue(...)). + EncodableValue(const CustomEncodableValue& v) : super(v) {} + + // Override the conversion constructors from std::variant to make them + // explicit, to avoid implicit conversion. + // + // While implicit conversion can be convenient in some cases, it can have very + // surprising effects. E.g., calling a function that takes an EncodableValue + // but accidentally passing an EncodableValue* would, instead of failing to + // compile, go through a pointer->bool->EncodableValue(bool) chain and + // silently call the function with a temp-constructed EncodableValue(true). + template + constexpr explicit EncodableValue(T&& t) noexcept : super(t) {} + + // Returns true if the value is null. Convenience wrapper since unlike the + // other types, std::monostate uses aren't self-documenting. + bool IsNull() const { return std::holds_alternative(*this); } + + // Convience method to simplify handling objects received from Flutter where + // the values may be larger than 32-bit, since they have the same type on the + // Dart side, but will be either 32-bit or 64-bit here depending on the value. + // + // Calling this method if the value doesn't contain either an int32_t or an + // int64_t will throw an exception. + int64_t LongValue() { + if (std::holds_alternative(*this)) { + return std::get(*this); + } + return std::get(*this); + } +}; + +#else + class EncodableValue; // Convenience type aliases for list and map EncodableValue types. using EncodableList = std::vector; @@ -564,6 +754,8 @@ class EncodableValue { Type type_ = Type::kNull; }; +#endif + } // namespace flutter #endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_ENCODABLE_VALUE_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h b/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h index 301c73a418195..23fd7940752db 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h @@ -144,14 +144,14 @@ class EventChannel { const MethodCodec* codec_; protected: - void SuccessInternal(T* event = nullptr) override { + void SuccessInternal(const T* event = nullptr) override { auto result = codec_->EncodeSuccessEnvelope(event); messenger_->Send(name_, result->data(), result->size()); } void ErrorInternal(const std::string& error_code, const std::string& error_message, - T* error_details) override { + const T* error_details) override { auto result = codec_->EncodeErrorEnvelope(error_code, error_message, error_details); messenger_->Send(name_, result->data(), result->size()); diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h b/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h index 6223ad7b6a572..764ea9d9f1fb8 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/event_sink.h @@ -19,28 +19,49 @@ class EventSink { EventSink(EventSink const&) = delete; EventSink& operator=(EventSink const&) = delete; + // DEPRECATED. Use the reference version below. This will be removed in the + // near future. + void Success(const T* event) { SuccessInternal(event); } + + // Consumes a successful event + void Success(const T& event) { SuccessInternal(&event); } + // Consumes a successful event. - void Success(T* event = nullptr) { SuccessInternal(event); } + void Success() { SuccessInternal(nullptr); } - // Consumes an error event. + // DEPRECATED. Use the reference version below. This will be removed in the + // near future. void Error(const std::string& error_code, - const std::string& error_message = "", - T* error_details = nullptr) { + const std::string& error_message, + const T* error_details) { ErrorInternal(error_code, error_message, error_details); } + // Consumes an error event. + void Error(const std::string& error_code, + const std::string& error_message, + const T& error_details) { + ErrorInternal(error_code, error_message, &error_details); + } + + // Consumes an error event. + void Error(const std::string& error_code, + const std::string& error_message = "") { + ErrorInternal(error_code, error_message, nullptr); + } + // Consumes end of stream. Ensuing calls to Success() or // Error(), if any, are ignored. void EndOfStream() { EndOfStreamInternal(); } protected: // Implementation of the public interface, to be provided by subclasses. - virtual void SuccessInternal(T* event = nullptr) = 0; + virtual void SuccessInternal(const T* event = nullptr) = 0; // Implementation of the public interface, to be provided by subclasses. virtual void ErrorInternal(const std::string& error_code, const std::string& error_message, - T* error_details) = 0; + const T* error_details) = 0; // Implementation of the public interface, to be provided by subclasses. virtual void EndOfStreamInternal() = 0; diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h index 1e9c822e253e8..c2b2df5791058 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h @@ -22,20 +22,48 @@ class MethodResult { MethodResult(MethodResult const&) = delete; MethodResult& operator=(MethodResult const&) = delete; - // Sends a success response, indicating that the call completed successfully. - // An optional value can be provided as part of the success message. - void Success(const T* result = nullptr) { SuccessInternal(result); } + // DEPRECATED. Use the reference versions below. This will be removed in the + // near future. + void Success(const T* result) { SuccessInternal(result); } - // Sends an error response, indicating that the call was understood but - // handling failed in some way. A string error code must be provided, and in - // addition an optional user-readable error_message and/or details object can - // be included. + // Sends a success response, indicating that the call completed successfully + // with the given result. + void Success(const T& result) { SuccessInternal(&result); } + + // Sends a success response, indicating that the call completed successfully + // with no result. + void Success() { SuccessInternal(nullptr); } + + // DEPRECATED. Use the reference versions below. This will be removed in the + // near future. void Error(const std::string& error_code, - const std::string& error_message = "", - const T* error_details = nullptr) { + const std::string& error_message, + const T* error_details) { ErrorInternal(error_code, error_message, error_details); } + // Sends an error response, indicating that the call was understood but + // handling failed in some way. + // + // error_code: A string error code describing the error. + // error_message: A user-readable error message. + // error_details: Arbitrary extra details about the error. + void Error(const std::string& error_code, + const std::string& error_message, + const T& error_details) { + ErrorInternal(error_code, error_message, &error_details); + } + + // Sends an error response, indicating that the call was understood but + // handling failed in some way. + // + // error_code: A string error code describing the error. + // error_message: A user-readable error message (optional). + void Error(const std::string& error_code, + const std::string& error_message = "") { + ErrorInternal(error_code, error_message, nullptr); + } + // Sends a not-implemented response, indicating that the method either was not // recognized, or has not been implemented. void NotImplemented() { NotImplementedInternal(); } diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h index 647b7cbb48e8b..1ad67a1b41044 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h @@ -5,13 +5,13 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_H_ +#include + #include #include #include #include -#include - #include "binary_messenger.h" namespace flutter { @@ -48,11 +48,8 @@ class PluginRegistrar { // that they stay valid for any registered callbacks. void AddPlugin(std::unique_ptr plugin); - // Enables input blocking on the given channel name. - // - // If set, then the parent window should disable input callbacks - // while waiting for the handler for messages on that channel to run. - void EnableInputBlockingForChannel(const std::string& channel); + protected: + FlutterDesktopPluginRegistrarRef registrar() { return registrar_; } private: // Handle for interacting with the C API's registrar. diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h new file mode 100644 index 0000000000000..f571b3256e360 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h @@ -0,0 +1,76 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_CODEC_SERIALIZER_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_CODEC_SERIALIZER_H_ + +#include "byte_streams.h" +#include "encodable_value.h" + +namespace flutter { + +// Encapsulates the logic for encoding/decoding EncodableValues to/from the +// standard codec binary representation. +// +// This can be subclassed to extend the standard codec with support for new +// types. +class StandardCodecSerializer { + public: + virtual ~StandardCodecSerializer(); + + // Returns the shared serializer instance. + static const StandardCodecSerializer& GetInstance(); + + // Prevent copying. + StandardCodecSerializer(StandardCodecSerializer const&) = delete; + StandardCodecSerializer& operator=(StandardCodecSerializer const&) = delete; + + // Reads and returns the next value from |stream|. + EncodableValue ReadValue(ByteStreamReader* stream) const; + + // Writes the encoding of |value| to |stream|, including the initial type + // discrimination byte. + // + // Can be overridden by a subclass to extend the codec. + virtual void WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const; + + protected: + // Codecs require long-lived serializers, so clients should always use + // GetInstance(). + StandardCodecSerializer(); + + // Reads and returns the next value from |stream|, whose discrimination byte + // was |type|. + // + // The discrimination byte will already have been read from the stream when + // this is called. + // + // Can be overridden by a subclass to extend the codec. + virtual EncodableValue ReadValueOfType(uint8_t type, + ByteStreamReader* stream) const; + + // Reads the variable-length size from the current position in |stream|. + size_t ReadSize(ByteStreamReader* stream) const; + + // Writes the variable-length size encoding to |stream|. + void WriteSize(size_t size, ByteStreamWriter* stream) const; + + private: + // Reads a fixed-type list whose values are of type T from the current + // position in |stream|, and returns it as the corresponding EncodableValue. + // |T| must correspond to one of the supported list value types of + // EncodableValue. + template + EncodableValue ReadVector(ByteStreamReader* stream) const; + + // Writes |vector| to |stream| as a fixed-type list. |T| must correspond to + // one of the supported list value types of EncodableValue. + template + void WriteVector(const std::vector vector, ByteStreamWriter* stream) const; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_CODEC_SERIALIZER_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h index 75644c7a85f31..735dcda580c7e 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h @@ -5,8 +5,11 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_MESSAGE_CODEC_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_MESSAGE_CODEC_H_ +#include + #include "encodable_value.h" #include "message_codec.h" +#include "standard_codec_serializer.h" namespace flutter { @@ -14,8 +17,17 @@ namespace flutter { // Flutter engine via message channels. class StandardMessageCodec : public MessageCodec { public: - // Returns the shared instance of the codec. - static const StandardMessageCodec& GetInstance(); + // Returns an instance of the codec, optionally using a custom serializer to + // add support for more types. + // + // If provided, |serializer| must be long-lived. If no serializer is provided, + // the default will be used. + // + // The instance returned for a given |serializer| will be shared, and + // any instance returned from this will be long-lived, and can be safely + // passed to, e.g., channel constructors. + static const StandardMessageCodec& GetInstance( + const StandardCodecSerializer* serializer = nullptr); ~StandardMessageCodec(); @@ -24,9 +36,6 @@ class StandardMessageCodec : public MessageCodec { StandardMessageCodec& operator=(StandardMessageCodec const&) = delete; protected: - // Instances should be obtained via GetInstance. - StandardMessageCodec(); - // |flutter::MessageCodec| std::unique_ptr DecodeMessageInternal( const uint8_t* binary_message, @@ -35,6 +44,12 @@ class StandardMessageCodec : public MessageCodec { // |flutter::MessageCodec| std::unique_ptr> EncodeMessageInternal( const EncodableValue& message) const override; + + private: + // Instances should be obtained via GetInstance. + explicit StandardMessageCodec(const StandardCodecSerializer* serializer); + + const StandardCodecSerializer* serializer_; }; } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h index ef40897893183..729babc2b62b0 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h @@ -5,28 +5,37 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_METHOD_CODEC_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_STANDARD_METHOD_CODEC_H_ +#include + #include "encodable_value.h" #include "method_call.h" #include "method_codec.h" +#include "standard_codec_serializer.h" namespace flutter { // An implementation of MethodCodec that uses a binary serialization. class StandardMethodCodec : public MethodCodec { public: - // Returns the shared instance of the codec. - static const StandardMethodCodec& GetInstance(); + // Returns an instance of the codec, optionally using a custom serializer to + // add support for more types. + // + // If provided, |serializer| must be long-lived. If no serializer is provided, + // the default will be used. + // + // The instance returned for a given |extension| will be shared, and + // any instance returned from this will be long-lived, and can be safely + // passed to, e.g., channel constructors. + static const StandardMethodCodec& GetInstance( + const StandardCodecSerializer* serializer = nullptr); - ~StandardMethodCodec() = default; + ~StandardMethodCodec(); // Prevent copying. StandardMethodCodec(StandardMethodCodec const&) = delete; StandardMethodCodec& operator=(StandardMethodCodec const&) = delete; protected: - // Instances should be obtained via GetInstance. - StandardMethodCodec() = default; - // |flutter::MethodCodec| std::unique_ptr> DecodeMethodCallInternal( const uint8_t* message, @@ -51,6 +60,12 @@ class StandardMethodCodec : public MethodCodec { const uint8_t* response, size_t response_size, MethodResult* result) const override; + + private: + // Instances should be obtained via GetInstance. + explicit StandardMethodCodec(const StandardCodecSerializer* serializer); + + const StandardCodecSerializer* serializer_; }; } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc b/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc index ee62b526eaf12..13cbb50f88c63 100644 --- a/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h" - #include #include #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" #include "gtest/gtest.h" @@ -68,6 +67,7 @@ TEST(MethodChannelTest, Registration) { // result. EXPECT_EQ(call.method_name(), method_name); EXPECT_NE(result, nullptr); + result->Success(); }); EXPECT_EQ(messenger.last_message_handler_channel(), channel_name); EXPECT_NE(messenger.last_message_handler(), nullptr); @@ -119,7 +119,7 @@ TEST(MethodChannelTest, InvokeWithResponse) { auto result_handler = std::make_unique>( [&received_reply, reply](const EncodableValue* success_value) { received_reply = true; - EXPECT_EQ(success_value->StringValue(), reply); + EXPECT_EQ(std::get(*success_value), reply); }, nullptr, nullptr); diff --git a/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc b/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc index 98750dbba244b..9c32da9774a7c 100644 --- a/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" - #include #include +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" #include "gtest/gtest.h" namespace flutter { @@ -29,7 +28,7 @@ TEST(MethodChannelTest, Success) { EXPECT_EQ(*i, value); }, nullptr, nullptr); - result.Success(&value); + result.Success(value); EXPECT_TRUE(called); } @@ -50,7 +49,7 @@ TEST(MethodChannelTest, Error) { EXPECT_EQ(*details, error_details); }, nullptr); - result.Error(error_code, error_message, &error_details); + result.Error(error_code, error_message, error_details); EXPECT_TRUE(called); } diff --git a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc index 9527dd73ead3d..a779d6e26df53 100644 --- a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc +++ b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc @@ -7,125 +7,12 @@ #include #include +#include "binary_messenger_impl.h" #include "include/flutter/engine_method_result.h" #include "include/flutter/method_channel.h" namespace flutter { -namespace { - -// Passes |message| to |user_data|, which must be a BinaryMessageHandler, along -// with a BinaryReply that will send a response on |message|'s response handle. -// -// This serves as an adaptor between the function-pointer-based message callback -// interface provided by the C API and the std::function-based message handler -// interface of BinaryMessenger. -void ForwardToHandler(FlutterDesktopMessengerRef messenger, - const FlutterDesktopMessage* message, - void* user_data) { - auto* response_handle = message->response_handle; - BinaryReply reply_handler = [messenger, response_handle]( - const uint8_t* reply, - size_t reply_size) mutable { - if (!response_handle) { - std::cerr << "Error: Response can be set only once. Ignoring " - "duplicate response." - << std::endl; - return; - } - FlutterDesktopMessengerSendResponse(messenger, response_handle, reply, - reply_size); - // The engine frees the response handle once - // FlutterDesktopSendMessageResponse is called. - response_handle = nullptr; - }; - - const BinaryMessageHandler& message_handler = - *static_cast(user_data); - - message_handler(message->message, message->message_size, - std::move(reply_handler)); -} - -} // namespace - -// Wrapper around a FlutterDesktopMessengerRef that implements the -// BinaryMessenger API. -class BinaryMessengerImpl : public BinaryMessenger { - public: - explicit BinaryMessengerImpl(FlutterDesktopMessengerRef core_messenger) - : messenger_(core_messenger) {} - - virtual ~BinaryMessengerImpl() = default; - - // Prevent copying. - BinaryMessengerImpl(BinaryMessengerImpl const&) = delete; - BinaryMessengerImpl& operator=(BinaryMessengerImpl const&) = delete; - - // |flutter::BinaryMessenger| - void Send(const std::string& channel, - const uint8_t* message, - size_t message_size, - BinaryReply reply) const override; - - // |flutter::BinaryMessenger| - void SetMessageHandler(const std::string& channel, - BinaryMessageHandler handler) override; - - private: - // Handle for interacting with the C API. - FlutterDesktopMessengerRef messenger_; - - // A map from channel names to the BinaryMessageHandler that should be called - // for incoming messages on that channel. - std::map handlers_; -}; - -void BinaryMessengerImpl::Send(const std::string& channel, - const uint8_t* message, - size_t message_size, - BinaryReply reply) const { - if (reply == nullptr) { - FlutterDesktopMessengerSend(messenger_, channel.c_str(), message, - message_size); - return; - } - struct Captures { - BinaryReply reply; - }; - auto captures = new Captures(); - captures->reply = reply; - - auto message_reply = [](const uint8_t* data, size_t data_size, - void* user_data) { - auto captures = reinterpret_cast(user_data); - captures->reply(data, data_size); - delete captures; - }; - bool result = FlutterDesktopMessengerSendWithReply( - messenger_, channel.c_str(), message, message_size, message_reply, - captures); - if (!result) { - delete captures; - } -} - -void BinaryMessengerImpl::SetMessageHandler(const std::string& channel, - BinaryMessageHandler handler) { - if (!handler) { - handlers_.erase(channel); - FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), nullptr, - nullptr); - return; - } - // Save the handler, to keep it alive. - handlers_[channel] = std::move(handler); - BinaryMessageHandler* message_handler = &handlers_[channel]; - // Set an adaptor callback that will invoke the handler. - FlutterDesktopMessengerSetCallback(messenger_, channel.c_str(), - ForwardToHandler, message_handler); -} - // ===== PluginRegistrar ===== PluginRegistrar::PluginRegistrar(FlutterDesktopPluginRegistrarRef registrar) @@ -140,11 +27,6 @@ void PluginRegistrar::AddPlugin(std::unique_ptr plugin) { plugins_.insert(std::move(plugin)); } -void PluginRegistrar::EnableInputBlockingForChannel( - const std::string& channel) { - FlutterDesktopRegistrarEnableInputBlocking(registrar_, channel.c_str()); -} - // ===== PluginRegistrarManager ===== // static diff --git a/shell/platform/common/cpp/client_wrapper/publish.gni b/shell/platform/common/cpp/client_wrapper/publish.gni index e52bf6a319369..907197b6cced6 100644 --- a/shell/platform/common/cpp/client_wrapper/publish.gni +++ b/shell/platform/common/cpp/client_wrapper/publish.gni @@ -71,7 +71,9 @@ template("publish_client_wrapper_core") { "visibility", ]) public = core_cpp_client_wrapper_includes - sources = core_cpp_client_wrapper_sources + [ _wrapper_readme ] + sources = core_cpp_client_wrapper_sources + + core_cpp_client_wrapper_internal_headers + [ _wrapper_readme ] + + temporary_shim_files } } diff --git a/shell/platform/common/cpp/client_wrapper/standard_codec.cc b/shell/platform/common/cpp/client_wrapper/standard_codec.cc index 609329c8a7376..bb6309844fb9f 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_codec.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_codec.cc @@ -8,17 +8,18 @@ // together to simplify use of the client wrapper, since the common case is // that any client that needs one of these files needs all three. -#include "include/flutter/standard_message_codec.h" -#include "include/flutter/standard_method_codec.h" -#include "standard_codec_serializer.h" - -#include +#include #include #include #include #include #include +#include "byte_buffer_streams.h" +#include "include/flutter/standard_codec_serializer.h" +#include "include/flutter/standard_message_codec.h" +#include "include/flutter/standard_method_codec.h" + namespace flutter { // ===== standard_codec_serializer.h ===== @@ -45,6 +46,7 @@ enum class EncodedType { // Returns the encoded type that should be written when serializing |value|. EncodedType EncodedTypeForValue(const EncodableValue& value) { +#ifdef USE_LEGACY_ENCODABLE_VALUE switch (value.type()) { case EncodableValue::Type::kNull: return EncodedType::kNull; @@ -71,6 +73,34 @@ EncodedType EncodedTypeForValue(const EncodableValue& value) { case EncodableValue::Type::kMap: return EncodedType::kMap; } +#else + switch (value.index()) { + case 0: + return EncodedType::kNull; + case 1: + return std::get(value) ? EncodedType::kTrue : EncodedType::kFalse; + case 2: + return EncodedType::kInt32; + case 3: + return EncodedType::kInt64; + case 4: + return EncodedType::kFloat64; + case 5: + return EncodedType::kString; + case 6: + return EncodedType::kUInt8List; + case 7: + return EncodedType::kInt32List; + case 8: + return EncodedType::kInt64List; + case 9: + return EncodedType::kFloat64List; + case 10: + return EncodedType::kList; + case 11: + return EncodedType::kMap; + } +#endif assert(false); return EncodedType::kNull; } @@ -81,97 +111,36 @@ StandardCodecSerializer::StandardCodecSerializer() = default; StandardCodecSerializer::~StandardCodecSerializer() = default; +const StandardCodecSerializer& StandardCodecSerializer::GetInstance() { + static StandardCodecSerializer sInstance; + return sInstance; +}; + EncodableValue StandardCodecSerializer::ReadValue( - ByteBufferStreamReader* stream) const { - EncodedType type = static_cast(stream->ReadByte()); - switch (type) { - case EncodedType::kNull: - return EncodableValue(); - case EncodedType::kTrue: - return EncodableValue(true); - case EncodedType::kFalse: - return EncodableValue(false); - case EncodedType::kInt32: { - int32_t int_value = 0; - stream->ReadBytes(reinterpret_cast(&int_value), 4); - return EncodableValue(int_value); - } - case EncodedType::kInt64: { - int64_t long_value = 0; - stream->ReadBytes(reinterpret_cast(&long_value), 8); - return EncodableValue(long_value); - } - case EncodedType::kFloat64: { - double double_value = 0; - stream->ReadAlignment(8); - stream->ReadBytes(reinterpret_cast(&double_value), 8); - return EncodableValue(double_value); - } - case EncodedType::kLargeInt: - case EncodedType::kString: { - size_t size = ReadSize(stream); - std::string string_value; - string_value.resize(size); - stream->ReadBytes(reinterpret_cast(&string_value[0]), size); - return EncodableValue(string_value); - } - case EncodedType::kUInt8List: - return ReadVector(stream); - case EncodedType::kInt32List: - return ReadVector(stream); - case EncodedType::kInt64List: - return ReadVector(stream); - case EncodedType::kFloat64List: - return ReadVector(stream); - case EncodedType::kList: { - size_t length = ReadSize(stream); - EncodableList list_value; - list_value.reserve(length); - for (size_t i = 0; i < length; ++i) { - list_value.push_back(ReadValue(stream)); - } - return EncodableValue(list_value); - } - case EncodedType::kMap: { - size_t length = ReadSize(stream); - EncodableMap map_value; - for (size_t i = 0; i < length; ++i) { - EncodableValue key = ReadValue(stream); - EncodableValue value = ReadValue(stream); - map_value.emplace(std::move(key), std::move(value)); - } - return EncodableValue(map_value); - } - } - std::cerr << "Unknown type in StandardCodecSerializer::ReadValue: " - << static_cast(type) << std::endl; - return EncodableValue(); + ByteStreamReader* stream) const { + uint8_t type = stream->ReadByte(); + return ReadValueOfType(type, stream); } void StandardCodecSerializer::WriteValue(const EncodableValue& value, - ByteBufferStreamWriter* stream) const { + ByteStreamWriter* stream) const { stream->WriteByte(static_cast(EncodedTypeForValue(value))); +#ifdef USE_LEGACY_ENCODABLE_VALUE switch (value.type()) { case EncodableValue::Type::kNull: case EncodableValue::Type::kBool: // Null and bool are encoded directly in the type. break; - case EncodableValue::Type::kInt: { - int32_t int_value = value.IntValue(); - stream->WriteBytes(reinterpret_cast(&int_value), 4); + case EncodableValue::Type::kInt: + stream->WriteInt32(value.IntValue()); break; - } - case EncodableValue::Type::kLong: { - int64_t long_value = value.LongValue(); - stream->WriteBytes(reinterpret_cast(&long_value), 8); + case EncodableValue::Type::kLong: + stream->WriteInt64(value.LongValue()); break; - } - case EncodableValue::Type::kDouble: { + case EncodableValue::Type::kDouble: stream->WriteAlignment(8); - double double_value = value.DoubleValue(); - stream->WriteBytes(reinterpret_cast(&double_value), 8); + stream->WriteDouble(value.DoubleValue()); break; - } case EncodableValue::Type::kString: { const auto& string_value = value.StringValue(); size_t size = string_value.size(); @@ -208,9 +177,130 @@ void StandardCodecSerializer::WriteValue(const EncodableValue& value, } break; } +#else + // TODO: Consider replacing this this with a std::visitor. + switch (value.index()) { + case 0: + case 1: + // Null and bool are encoded directly in the type. + break; + case 2: + stream->WriteInt32(std::get(value)); + break; + case 3: + stream->WriteInt64(std::get(value)); + break; + case 4: + stream->WriteAlignment(8); + stream->WriteDouble(std::get(value)); + break; + case 5: { + const auto& string_value = std::get(value); + size_t size = string_value.size(); + WriteSize(size, stream); + if (size > 0) { + stream->WriteBytes( + reinterpret_cast(string_value.data()), size); + } + break; + } + case 6: + WriteVector(std::get>(value), stream); + break; + case 7: + WriteVector(std::get>(value), stream); + break; + case 8: + WriteVector(std::get>(value), stream); + break; + case 9: + WriteVector(std::get>(value), stream); + break; + case 10: { + const auto& list = std::get(value); + WriteSize(list.size(), stream); + for (const auto& item : list) { + WriteValue(item, stream); + } + break; + } + case 11: { + const auto& map = std::get(value); + WriteSize(map.size(), stream); + for (const auto& pair : map) { + WriteValue(pair.first, stream); + WriteValue(pair.second, stream); + } + break; + } + case 12: + std::cerr + << "Unhandled custom type in StandardCodecSerializer::WriteValue. " + << "Custom types require codec extensions." << std::endl; + break; + } +#endif +} + +EncodableValue StandardCodecSerializer::ReadValueOfType( + uint8_t type, + ByteStreamReader* stream) const { + switch (static_cast(type)) { + case EncodedType::kNull: + return EncodableValue(); + case EncodedType::kTrue: + return EncodableValue(true); + case EncodedType::kFalse: + return EncodableValue(false); + case EncodedType::kInt32: + return EncodableValue(stream->ReadInt32()); + case EncodedType::kInt64: + return EncodableValue(stream->ReadInt64()); + case EncodedType::kFloat64: + stream->ReadAlignment(8); + return EncodableValue(stream->ReadDouble()); + case EncodedType::kLargeInt: + case EncodedType::kString: { + size_t size = ReadSize(stream); + std::string string_value; + string_value.resize(size); + stream->ReadBytes(reinterpret_cast(&string_value[0]), size); + return EncodableValue(string_value); + } + case EncodedType::kUInt8List: + return ReadVector(stream); + case EncodedType::kInt32List: + return ReadVector(stream); + case EncodedType::kInt64List: + return ReadVector(stream); + case EncodedType::kFloat64List: + return ReadVector(stream); + case EncodedType::kList: { + size_t length = ReadSize(stream); + EncodableList list_value; + list_value.reserve(length); + for (size_t i = 0; i < length; ++i) { + list_value.push_back(ReadValue(stream)); + } + return EncodableValue(list_value); + } + case EncodedType::kMap: { + size_t length = ReadSize(stream); + EncodableMap map_value; + for (size_t i = 0; i < length; ++i) { + EncodableValue key = ReadValue(stream); + EncodableValue value = ReadValue(stream); + map_value.emplace(std::move(key), std::move(value)); + } + return EncodableValue(map_value); + } + } + std::cerr << "Unknown type in StandardCodecSerializer::ReadValueOfType: " + << static_cast(type) << std::endl; + return EncodableValue(); } -size_t StandardCodecSerializer::ReadSize(ByteBufferStreamReader* stream) const { +size_t StandardCodecSerializer::ReadSize(ByteStreamReader* stream) const { uint8_t byte = stream->ReadByte(); if (byte < 254) { return byte; @@ -226,7 +316,7 @@ size_t StandardCodecSerializer::ReadSize(ByteBufferStreamReader* stream) const { } void StandardCodecSerializer::WriteSize(size_t size, - ByteBufferStreamWriter* stream) const { + ByteStreamWriter* stream) const { if (size < 254) { stream->WriteByte(static_cast(size)); } else if (size <= 0xffff) { @@ -242,7 +332,7 @@ void StandardCodecSerializer::WriteSize(size_t size, template EncodableValue StandardCodecSerializer::ReadVector( - ByteBufferStreamReader* stream) const { + ByteStreamReader* stream) const { size_t count = ReadSize(stream); std::vector vector; vector.resize(count); @@ -256,9 +346,8 @@ EncodableValue StandardCodecSerializer::ReadVector( } template -void StandardCodecSerializer::WriteVector( - const std::vector vector, - ByteBufferStreamWriter* stream) const { +void StandardCodecSerializer::WriteVector(const std::vector vector, + ByteStreamWriter* stream) const { size_t count = vector.size(); WriteSize(count, stream); if (count == 0) { @@ -275,69 +364,115 @@ void StandardCodecSerializer::WriteVector( // ===== standard_message_codec.h ===== // static -const StandardMessageCodec& StandardMessageCodec::GetInstance() { - static StandardMessageCodec sInstance; - return sInstance; +const StandardMessageCodec& StandardMessageCodec::GetInstance( + const StandardCodecSerializer* serializer) { + if (!serializer) { + serializer = &StandardCodecSerializer::GetInstance(); + } + auto* sInstances = new std::map>; + auto it = sInstances->find(serializer); + if (it == sInstances->end()) { + // Uses new due to private constructor (to prevent API clients from + // accidentally passing temporary codec instances to channels). + auto emplace_result = sInstances->emplace( + serializer, std::unique_ptr( + new StandardMessageCodec(serializer))); + it = emplace_result.first; + } + return *(it->second); } -StandardMessageCodec::StandardMessageCodec() = default; +StandardMessageCodec::StandardMessageCodec( + const StandardCodecSerializer* serializer) + : serializer_(serializer) {} StandardMessageCodec::~StandardMessageCodec() = default; std::unique_ptr StandardMessageCodec::DecodeMessageInternal( const uint8_t* binary_message, size_t message_size) const { - StandardCodecSerializer serializer; ByteBufferStreamReader stream(binary_message, message_size); - return std::make_unique(serializer.ReadValue(&stream)); + return std::make_unique(serializer_->ReadValue(&stream)); } std::unique_ptr> StandardMessageCodec::EncodeMessageInternal( const EncodableValue& message) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); - serializer.WriteValue(message, &stream); + serializer_->WriteValue(message, &stream); return encoded; } // ===== standard_method_codec.h ===== // static -const StandardMethodCodec& StandardMethodCodec::GetInstance() { - static StandardMethodCodec sInstance; - return sInstance; +const StandardMethodCodec& StandardMethodCodec::GetInstance( + const StandardCodecSerializer* serializer) { + if (!serializer) { + serializer = &StandardCodecSerializer::GetInstance(); + } + auto* sInstances = new std::map>; + auto it = sInstances->find(serializer); + if (it == sInstances->end()) { + // Uses new due to private constructor (to prevent API clients from + // accidentally passing temporary codec instances to channels). + auto emplace_result = sInstances->emplace( + serializer, std::unique_ptr( + new StandardMethodCodec(serializer))); + it = emplace_result.first; + } + return *(it->second); } +StandardMethodCodec::StandardMethodCodec( + const StandardCodecSerializer* serializer) + : serializer_(serializer) {} + +StandardMethodCodec::~StandardMethodCodec() = default; + std::unique_ptr> StandardMethodCodec::DecodeMethodCallInternal(const uint8_t* message, size_t message_size) const { - StandardCodecSerializer serializer; ByteBufferStreamReader stream(message, message_size); - EncodableValue method_name = serializer.ReadValue(&stream); +#ifdef USE_LEGACY_ENCODABLE_VALUE + EncodableValue method_name = serializer_->ReadValue(&stream); if (!method_name.IsString()) { std::cerr << "Invalid method call; method name is not a string." << std::endl; return nullptr; } auto arguments = - std::make_unique(serializer.ReadValue(&stream)); + std::make_unique(serializer_->ReadValue(&stream)); return std::make_unique>(method_name.StringValue(), std::move(arguments)); +#else + EncodableValue method_name_value = serializer_->ReadValue(&stream); + const auto* method_name = std::get_if(&method_name_value); + if (!method_name) { + std::cerr << "Invalid method call; method name is not a string." + << std::endl; + return nullptr; + } + auto arguments = + std::make_unique(serializer_->ReadValue(&stream)); + return std::make_unique>(*method_name, + std::move(arguments)); +#endif } std::unique_ptr> StandardMethodCodec::EncodeMethodCallInternal( const MethodCall& method_call) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); - serializer.WriteValue(EncodableValue(method_call.method_name()), &stream); + serializer_->WriteValue(EncodableValue(method_call.method_name()), &stream); if (method_call.arguments()) { - serializer.WriteValue(*method_call.arguments(), &stream); + serializer_->WriteValue(*method_call.arguments(), &stream); } else { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } return encoded; } @@ -345,14 +480,13 @@ StandardMethodCodec::EncodeMethodCallInternal( std::unique_ptr> StandardMethodCodec::EncodeSuccessEnvelopeInternal( const EncodableValue* result) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); stream.WriteByte(0); if (result) { - serializer.WriteValue(*result, &stream); + serializer_->WriteValue(*result, &stream); } else { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } return encoded; } @@ -362,20 +496,19 @@ StandardMethodCodec::EncodeErrorEnvelopeInternal( const std::string& error_code, const std::string& error_message, const EncodableValue* error_details) const { - StandardCodecSerializer serializer; auto encoded = std::make_unique>(); ByteBufferStreamWriter stream(encoded.get()); stream.WriteByte(1); - serializer.WriteValue(EncodableValue(error_code), &stream); + serializer_->WriteValue(EncodableValue(error_code), &stream); if (error_message.empty()) { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } else { - serializer.WriteValue(EncodableValue(error_message), &stream); + serializer_->WriteValue(EncodableValue(error_message), &stream); } if (error_details) { - serializer.WriteValue(*error_details, &stream); + serializer_->WriteValue(*error_details, &stream); } else { - serializer.WriteValue(EncodableValue(), &stream); + serializer_->WriteValue(EncodableValue(), &stream); } return encoded; } @@ -384,22 +517,39 @@ bool StandardMethodCodec::DecodeAndProcessResponseEnvelopeInternal( const uint8_t* response, size_t response_size, MethodResult* result) const { - StandardCodecSerializer serializer; ByteBufferStreamReader stream(response, response_size); uint8_t flag = stream.ReadByte(); switch (flag) { case 0: { - EncodableValue value = serializer.ReadValue(&stream); - result->Success(value.IsNull() ? nullptr : &value); + EncodableValue value = serializer_->ReadValue(&stream); + if (value.IsNull()) { + result->Success(); + } else { + result->Success(value); + } return true; } case 1: { - EncodableValue code = serializer.ReadValue(&stream); - EncodableValue message = serializer.ReadValue(&stream); - EncodableValue details = serializer.ReadValue(&stream); - result->Error(code.StringValue(), - message.IsNull() ? "" : message.StringValue(), - details.IsNull() ? nullptr : &details); + EncodableValue code = serializer_->ReadValue(&stream); + EncodableValue message = serializer_->ReadValue(&stream); + EncodableValue details = serializer_->ReadValue(&stream); +#ifdef USE_LEGACY_ENCODABLE_VALUE + if (details.IsNull()) { + result->Error(code.StringValue(), + message.IsNull() ? "" : message.StringValue()); + } else { + result->Error(code.StringValue(), + message.IsNull() ? "" : message.StringValue(), details); + } +#else + const std::string& message_string = + message.IsNull() ? "" : std::get(message); + if (details.IsNull()) { + result->Error(std::get(code), message_string); + } else { + result->Error(std::get(code), message_string, details); + } +#endif return true; } default: diff --git a/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h b/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h deleted file mode 100644 index 89aab3b9988f8..0000000000000 --- a/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h +++ /dev/null @@ -1,54 +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. - -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_ENCODABLE_VALUE_SERIALIZER_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_ENCODABLE_VALUE_SERIALIZER_H_ - -#include "byte_stream_wrappers.h" -#include "include/flutter/encodable_value.h" - -namespace flutter { - -// Encapsulates the logic for encoding/decoding EncodableValues to/from the -// standard codec binary representation. -class StandardCodecSerializer { - public: - StandardCodecSerializer(); - ~StandardCodecSerializer(); - - // Prevent copying. - StandardCodecSerializer(StandardCodecSerializer const&) = delete; - StandardCodecSerializer& operator=(StandardCodecSerializer const&) = delete; - - // Reads and returns the next value from |stream|. - EncodableValue ReadValue(ByteBufferStreamReader* stream) const; - - // Writes the encoding of |value| to |stream|. - void WriteValue(const EncodableValue& value, - ByteBufferStreamWriter* stream) const; - - protected: - // Reads the variable-length size from the current position in |stream|. - size_t ReadSize(ByteBufferStreamReader* stream) const; - - // Writes the variable-length size encoding to |stream|. - void WriteSize(size_t size, ByteBufferStreamWriter* stream) const; - - // Reads a fixed-type list whose values are of type T from the current - // position in |stream|, and returns it as the corresponding EncodableValue. - // |T| must correspond to one of the support list value types of - // EncodableValue. - template - EncodableValue ReadVector(ByteBufferStreamReader* stream) const; - - // Writes |vector| to |stream| as a fixed-type list. |T| must correspond to - // one of the support list value types of EncodableValue. - template - void WriteVector(const std::vector vector, - ByteBufferStreamWriter* stream) const; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_ENCODABLE_VALUE_SERIALIZER_H_ diff --git a/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc b/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc index a0b4437b195f8..03459dc75ddfa 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc @@ -2,27 +2,52 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h" - #include #include -#include "flutter/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h" #include "gtest/gtest.h" +#ifndef USE_LEGACY_ENCODABLE_VALUE +#include "flutter/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h" +#endif + namespace flutter { // Validates round-trip encoding and decoding of |value|, and checks that the // encoded value matches |expected_encoding|. -static void CheckEncodeDecode(const EncodableValue& value, - const std::vector& expected_encoding) { - const StandardMessageCodec& codec = StandardMessageCodec::GetInstance(); +// +// If testing with CustomEncodableValues, |serializer| must be provided to +// handle the encoding/decoding, and |custom_comparator| must be provided to +// validate equality since CustomEncodableValue doesn't define a useful ==. +static void CheckEncodeDecode( + const EncodableValue& value, + const std::vector& expected_encoding, + const StandardCodecSerializer* serializer = nullptr, + std::function + custom_comparator = nullptr) { + const StandardMessageCodec& codec = + StandardMessageCodec::GetInstance(serializer); auto encoded = codec.EncodeMessage(value); ASSERT_TRUE(encoded); EXPECT_EQ(*encoded, expected_encoding); auto decoded = codec.DecodeMessage(*encoded); - EXPECT_TRUE(testing::EncodableValuesAreEqual(value, *decoded)); +#ifdef USE_LEGACY_ENCODABLE_VALUE + // Full equality isn't implemented for the legacy path; just do a sanity test + // of basic types. + if (value.IsNull() || value.IsBool() || value.IsInt() || value.IsLong() || + value.IsDouble() || value.IsString()) { + EXPECT_FALSE(value < *decoded); + EXPECT_FALSE(*decoded < value); + } +#else + if (custom_comparator) { + EXPECT_TRUE(custom_comparator(value, *decoded)); + } else { + EXPECT_EQ(value, *decoded); + } +#endif } // Validates round-trip encoding and decoding of |value|, and checks that the @@ -34,7 +59,11 @@ static void CheckEncodeDecodeWithEncodePrefix( const EncodableValue& value, const std::vector& expected_encoding_prefix, size_t expected_encoding_length) { +#ifdef USE_LEGACY_ENCODABLE_VALUE EXPECT_TRUE(value.IsMap()); +#else + EXPECT_TRUE(std::holds_alternative(value)); +#endif const StandardMessageCodec& codec = StandardMessageCodec::GetInstance(); auto encoded = codec.EncodeMessage(value); ASSERT_TRUE(encoded); @@ -46,7 +75,12 @@ static void CheckEncodeDecodeWithEncodePrefix( expected_encoding_prefix.begin(), expected_encoding_prefix.end())); auto decoded = codec.DecodeMessage(*encoded); - EXPECT_TRUE(testing::EncodableValuesAreEqual(value, *decoded)); + +#ifdef USE_LEGACY_ENCODABLE_VALUE + EXPECT_NE(decoded, nullptr); +#else + EXPECT_EQ(value, *decoded); +#endif } TEST(StandardMessageCodec, CanEncodeAndDecodeNull) { @@ -170,4 +204,43 @@ TEST(StandardMessageCodec, CanEncodeAndDecodeFloat64Array) { CheckEncodeDecode(value, bytes); } +#ifndef USE_LEGACY_ENCODABLE_VALUE + +TEST(StandardMessageCodec, CanEncodeAndDecodeSimpleCustomType) { + std::vector bytes = {0x80, 0x09, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00}; + auto point_comparator = [](const EncodableValue& a, const EncodableValue& b) { + const Point& a_point = + std::any_cast(std::get(a)); + const Point& b_point = + std::any_cast(std::get(b)); + return a_point == b_point; + }; + CheckEncodeDecode(CustomEncodableValue(Point(9, 16)), bytes, + &PointExtensionSerializer::GetInstance(), point_comparator); +} + +TEST(StandardMessageCodec, CanEncodeAndDecodeVariableLengthCustomType) { + std::vector bytes = { + 0x81, // custom type + 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // data + 0x07, 0x04, // string type and length + 0x74, 0x65, 0x73, 0x74 // string characters + }; + auto some_data_comparator = [](const EncodableValue& a, + const EncodableValue& b) { + const SomeData& data_a = + std::any_cast(std::get(a)); + const SomeData& data_b = + std::any_cast(std::get(b)); + return data_a.data() == data_b.data() && data_a.label() == data_b.label(); + }; + CheckEncodeDecode(CustomEncodableValue( + SomeData("test", {0x00, 0x01, 0x02, 0x03, 0x04, 0x05})), + bytes, &SomeDataExtensionSerializer::GetInstance(), + some_data_comparator); +} + +#endif // !USE_LEGACY_ENCODABLE_VALUE + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc b/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc index 4b43b40229038..bb7a6f930f788 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" - #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" -#include "flutter/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h" #include "gtest/gtest.h" namespace flutter { @@ -24,7 +23,11 @@ bool MethodCallsAreEqual(const MethodCall& a, (!b.arguments() || b.arguments()->IsNull())) { return true; } - return testing::EncodableValuesAreEqual(*a.arguments(), *b.arguments()); + // If only one is nullptr, fail early rather than throw below. + if (!a.arguments() || !b.arguments()) { + return false; + } + return *a.arguments() == *b.arguments(); } } // namespace @@ -86,7 +89,7 @@ TEST(StandardMethodCodec, HandlesSuccessEnvelopesWithResult) { MethodResultFunctions result_handler( [&decoded_successfully](const EncodableValue* result) { decoded_successfully = true; - EXPECT_EQ(result->IntValue(), 42); + EXPECT_EQ(std::get(*result), 42); }, nullptr, nullptr); codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), @@ -145,9 +148,10 @@ TEST(StandardMethodCodec, HandlesErrorEnvelopesWithDetails) { decoded_successfully = true; EXPECT_EQ(code, "errorCode"); EXPECT_EQ(message, "something failed"); - EXPECT_TRUE(details->IsList()); - EXPECT_EQ(details->ListValue()[0].StringValue(), "a"); - EXPECT_EQ(details->ListValue()[1].IntValue(), 42); + const auto* details_list = std::get_if(details); + ASSERT_NE(details_list, nullptr); + EXPECT_EQ(std::get((*details_list)[0]), "a"); + EXPECT_EQ(std::get((*details_list)[1]), 42); }, nullptr); codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), @@ -155,4 +159,21 @@ TEST(StandardMethodCodec, HandlesErrorEnvelopesWithDetails) { EXPECT_TRUE(decoded_successfully); } +TEST(StandardMethodCodec, HandlesCustomTypeArguments) { + const StandardMethodCodec& codec = StandardMethodCodec::GetInstance( + &PointExtensionSerializer::GetInstance()); + Point point(7, 9); + MethodCall call( + "hello", std::make_unique(CustomEncodableValue(point))); + auto encoded = codec.EncodeMethodCall(call); + ASSERT_NE(encoded.get(), nullptr); + std::unique_ptr> decoded = + codec.DecodeMethodCall(*encoded); + ASSERT_NE(decoded.get(), nullptr); + + const Point& decoded_point = std::any_cast( + std::get(*decoded->arguments())); + EXPECT_EQ(point, decoded_point); +}; + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc b/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc deleted file mode 100644 index 607d69b18f599..0000000000000 --- a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc +++ /dev/null @@ -1,89 +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. - -#include "flutter/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h" - -#include - -namespace flutter { -namespace testing { - -bool EncodableValuesAreEqual(const EncodableValue& a, const EncodableValue& b) { - if (a.type() != b.type()) { - return false; - } - - switch (a.type()) { - case EncodableValue::Type::kNull: - return true; - case EncodableValue::Type::kBool: - return a.BoolValue() == b.BoolValue(); - case EncodableValue::Type::kInt: - return a.IntValue() == b.IntValue(); - case EncodableValue::Type::kLong: - return a.LongValue() == b.LongValue(); - case EncodableValue::Type::kDouble: - // This is a crude epsilon, but fine for the values in the unit tests. - return std::abs(a.DoubleValue() - b.DoubleValue()) < 0.0001l; - case EncodableValue::Type::kString: - return a.StringValue() == b.StringValue(); - case EncodableValue::Type::kByteList: - return a.ByteListValue() == b.ByteListValue(); - case EncodableValue::Type::kIntList: - return a.IntListValue() == b.IntListValue(); - case EncodableValue::Type::kLongList: - return a.LongListValue() == b.LongListValue(); - case EncodableValue::Type::kDoubleList: - return a.DoubleListValue() == b.DoubleListValue(); - case EncodableValue::Type::kList: { - const auto& a_list = a.ListValue(); - const auto& b_list = b.ListValue(); - if (a_list.size() != b_list.size()) { - return false; - } - for (size_t i = 0; i < a_list.size(); ++i) { - if (!EncodableValuesAreEqual(a_list[0], b_list[0])) { - return false; - } - } - return true; - } - case EncodableValue::Type::kMap: { - const auto& a_map = a.MapValue(); - const auto& b_map = b.MapValue(); - if (a_map.size() != b_map.size()) { - return false; - } - // Store references to all the keys in |b|. - std::vector unmatched_b_keys; - for (auto& pair : b_map) { - unmatched_b_keys.push_back(&pair.first); - } - // For each key,value in |a|, see if any of the not-yet-matched key,value - // pairs in |b| match by value; if so, remove that match and continue. - for (const auto& pair : a_map) { - bool found_match = false; - for (size_t i = 0; i < unmatched_b_keys.size(); ++i) { - const EncodableValue& b_key = *unmatched_b_keys[i]; - if (EncodableValuesAreEqual(pair.first, b_key) && - EncodableValuesAreEqual(pair.second, b_map.at(b_key))) { - found_match = true; - unmatched_b_keys.erase(unmatched_b_keys.begin() + i); - break; - } - } - if (!found_match) { - return false; - } - } - // If all entries had matches, consider the maps equal. - return true; - } - } - assert(false); - return false; -} - -} // namespace testing -} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h b/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h deleted file mode 100644 index 465afb0e96efd..0000000000000 --- a/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h +++ /dev/null @@ -1,22 +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. - -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_ENCODABLE_VALUE_UTILS_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_ENCODABLE_VALUE_UTILS_H_ - -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" - -namespace flutter { -namespace testing { - -// Returns true if |a| and |b| have equivalent values, recursively comparing -// the contents of collections (unlike the < operator defined on EncodableValue, -// which doesn't consider different collections with the same contents to be -// the same). -bool EncodableValuesAreEqual(const EncodableValue& a, const EncodableValue& b); - -} // namespace testing -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_ENCODABLE_VALUE_UTILS_H_ diff --git a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc index 580df48645aa9..1f60548952249 100644 --- a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc +++ b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc @@ -52,14 +52,6 @@ void FlutterDesktopRegistrarSetDestructionHandler( } } -void FlutterDesktopRegistrarEnableInputBlocking( - FlutterDesktopPluginRegistrarRef registrar, - const char* channel) { - if (s_stub_implementation) { - s_stub_implementation->RegistrarEnableInputBlocking(channel); - } -} - bool FlutterDesktopMessengerSend(FlutterDesktopMessengerRef messenger, const char* channel, const uint8_t* message, diff --git a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h index 284d15e974947..ae55cdc9010f0 100644 --- a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h +++ b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h @@ -38,9 +38,6 @@ class StubFlutterApi { virtual void RegistrarSetDestructionHandler( FlutterDesktopOnRegistrarDestroyed callback) {} - // Called for FlutterDesktopRegistrarEnableInputBlocking. - virtual void RegistrarEnableInputBlocking(const char* channel) {} - // Called for FlutterDesktopMessengerSend. virtual bool MessengerSend(const char* channel, const uint8_t* message, diff --git a/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc new file mode 100644 index 0000000000000..53bdfe93a54e8 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc @@ -0,0 +1,80 @@ +// 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. + +#include "flutter/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h" + +namespace flutter { + +PointExtensionSerializer::PointExtensionSerializer() = default; +PointExtensionSerializer::~PointExtensionSerializer() = default; + +// static +const PointExtensionSerializer& PointExtensionSerializer::GetInstance() { + static PointExtensionSerializer sInstance; + return sInstance; +} + +EncodableValue PointExtensionSerializer::ReadValueOfType( + uint8_t type, + ByteStreamReader* stream) const { + if (type == kPointType) { + int32_t x = stream->ReadInt32(); + int32_t y = stream->ReadInt32(); + return CustomEncodableValue(Point(x, y)); + } + return StandardCodecSerializer::ReadValueOfType(type, stream); +} + +void PointExtensionSerializer::WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const { + auto custom_value = std::get_if(&value); + if (!custom_value) { + StandardCodecSerializer::WriteValue(value, stream); + return; + } + stream->WriteByte(kPointType); + const Point& point = std::any_cast(*custom_value); + stream->WriteInt32(point.x()); + stream->WriteInt32(point.y()); +} + +SomeDataExtensionSerializer::SomeDataExtensionSerializer() = default; +SomeDataExtensionSerializer::~SomeDataExtensionSerializer() = default; + +// static +const SomeDataExtensionSerializer& SomeDataExtensionSerializer::GetInstance() { + static SomeDataExtensionSerializer sInstance; + return sInstance; +} + +EncodableValue SomeDataExtensionSerializer::ReadValueOfType( + uint8_t type, + ByteStreamReader* stream) const { + if (type == kSomeDataType) { + size_t size = ReadSize(stream); + std::vector data; + data.resize(size); + stream->ReadBytes(data.data(), size); + EncodableValue label = ReadValue(stream); + return CustomEncodableValue(SomeData(std::get(label), data)); + } + return StandardCodecSerializer::ReadValueOfType(type, stream); +} + +void SomeDataExtensionSerializer::WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const { + auto custom_value = std::get_if(&value); + if (!custom_value) { + StandardCodecSerializer::WriteValue(value, stream); + return; + } + stream->WriteByte(kSomeDataType); + const SomeData& some_data = std::any_cast(*custom_value); + size_t data_size = some_data.data().size(); + WriteSize(data_size, stream); + stream->WriteBytes(some_data.data().data(), data_size); + WriteValue(EncodableValue(some_data.label()), stream); +} + +} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h new file mode 100644 index 0000000000000..cbe01c8a886ff --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h @@ -0,0 +1,89 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_TEST_CODEC_EXTENSIONS_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_TEST_CODEC_EXTENSIONS_H_ + +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h" + +namespace flutter { + +// A representation of a point, for custom type testing of a simple type. +class Point { + public: + Point(int x, int y) : x_(x), y_(y) {} + ~Point() = default; + + int x() const { return x_; } + int y() const { return y_; } + + bool operator==(const Point& other) const { + return x_ == other.x_ && y_ == other.y_; + } + + private: + int x_; + int y_; +}; + +// A typed binary data object with extra fields, for custom type testing of a +// variable-length type that includes types handled by the core standard codec. +class SomeData { + public: + SomeData(const std::string label, const std::vector& data) + : label_(label), data_(data) {} + ~SomeData() = default; + + const std::string& label() const { return label_; } + const std::vector& data() const { return data_; } + + private: + std::string label_; + std::vector data_; +}; + +// Codec extension for Point. +class PointExtensionSerializer : public StandardCodecSerializer { + public: + PointExtensionSerializer(); + virtual ~PointExtensionSerializer(); + + static const PointExtensionSerializer& GetInstance(); + + // |TestCodecSerializer| + EncodableValue ReadValueOfType(uint8_t type, + ByteStreamReader* stream) const override; + + // |TestCodecSerializer| + void WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const override; + + private: + static constexpr uint8_t kPointType = 128; +}; + +// Codec extension for SomeData. +class SomeDataExtensionSerializer : public StandardCodecSerializer { + public: + SomeDataExtensionSerializer(); + virtual ~SomeDataExtensionSerializer(); + + static const SomeDataExtensionSerializer& GetInstance(); + + // |TestCodecSerializer| + EncodableValue ReadValueOfType(uint8_t type, + ByteStreamReader* stream) const override; + + // |TestCodecSerializer| + void WriteValue(const EncodableValue& value, + ByteStreamWriter* stream) const override; + + private: + static constexpr uint8_t kSomeDataType = 129; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_TESTING_TEST_CODEC_EXTENSIONS_H_ diff --git a/shell/platform/common/cpp/json_method_codec.cc b/shell/platform/common/cpp/json_method_codec.cc index 8fae8b7ed9b82..f026061ad235f 100644 --- a/shell/platform/common/cpp/json_method_codec.cc +++ b/shell/platform/common/cpp/json_method_codec.cc @@ -131,7 +131,11 @@ bool JsonMethodCodec::DecodeAndProcessResponseEnvelopeInternal( case 1: { std::unique_ptr value = ExtractElement(json_response.get(), &((*json_response)[0])); - result->Success(value->IsNull() ? nullptr : value.get()); + if (value->IsNull()) { + result->Success(); + } else { + result->Success(*value); + } return true; } case 3: { @@ -139,7 +143,11 @@ bool JsonMethodCodec::DecodeAndProcessResponseEnvelopeInternal( std::string message = (*json_response)[1].GetString(); std::unique_ptr details = ExtractElement(json_response.get(), &((*json_response)[2])); - result->Error(code, message, details->IsNull() ? nullptr : details.get()); + if (details->IsNull()) { + result->Error(code, message); + } else { + result->Error(code, message, *details); + } return true; } default: diff --git a/shell/platform/common/cpp/public/flutter_plugin_registrar.h b/shell/platform/common/cpp/public/flutter_plugin_registrar.h index 95f0abf139608..e27e125530ac9 100644 --- a/shell/platform/common/cpp/public/flutter_plugin_registrar.h +++ b/shell/platform/common/cpp/public/flutter_plugin_registrar.h @@ -31,19 +31,6 @@ FLUTTER_EXPORT void FlutterDesktopRegistrarSetDestructionHandler( FlutterDesktopPluginRegistrarRef registrar, FlutterDesktopOnRegistrarDestroyed callback); -// Enables input blocking on the given channel. -// -// If set, then the Flutter window will disable input callbacks -// while waiting for the handler for messages on that channel to run. This is -// useful if handling the message involves showing a modal window, for instance. -// -// This must be called after FlutterDesktopSetMessageHandler, as setting a -// handler on a channel will reset the input blocking state back to the -// default of disabled. -FLUTTER_EXPORT void FlutterDesktopRegistrarEnableInputBlocking( - FlutterDesktopPluginRegistrarRef registrar, - const char* channel); - #if defined(__cplusplus) } // extern "C" #endif diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 737e72731232f..327dfdd1605ca 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -171,6 +171,7 @@ source_set("ios_test_flutter_mrc") { ] sources = [ "framework/Source/FlutterEnginePlatformViewTest.mm", + "framework/Source/FlutterPlatformPluginTest.mm", "framework/Source/FlutterPlatformViewsTest.mm", "framework/Source/accessibility_bridge_test.mm", ] @@ -207,6 +208,7 @@ shared_library("ios_test_flutter") { ] sources = [ "framework/Source/FlutterBinaryMessengerRelayTest.mm", + "framework/Source/FlutterDartProjectTest.mm", "framework/Source/FlutterEngineTest.mm", "framework/Source/FlutterPluginAppLifeCycleDelegateTest.m", "framework/Source/FlutterTextInputPluginTest.m", diff --git a/shell/platform/darwin/ios/framework/Headers/Flutter.h b/shell/platform/darwin/ios/framework/Headers/Flutter.h index 9135c8200603c..d91eba7576fec 100644 --- a/shell/platform/darwin/ios/framework/Headers/Flutter.h +++ b/shell/platform/darwin/ios/framework/Headers/Flutter.h @@ -5,52 +5,6 @@ #ifndef FLUTTER_FLUTTER_H_ #define FLUTTER_FLUTTER_H_ -/** - BREAKING CHANGES: - - December 17, 2018: - - Changed designated initializer on FlutterEngine - - October 5, 2018: - - Removed FlutterNavigationController.h/.mm - - Changed return signature of `FlutterDartHeadlessCodeRunner.run*` from void - to bool - - Removed HeadlessPlatformViewIOS - - Marked FlutterDartHeadlessCodeRunner deprecated - - August 31, 2018: Marked -[FlutterDartProject - initFromDefaultSourceForConfiguration] and FlutterStandardBigInteger as - unavailable. - - July 26, 2018: Marked -[FlutterDartProject - initFromDefaultSourceForConfiguration] deprecated. - - February 28, 2018: Removed "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot". - - January 15, 2018: Marked "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot" as unavailable following the - deprecation from December 11, 2017. Scheduled to be removed on February - 19, 2018. - - January 09, 2018: Deprecated "FlutterStandardBigInteger" and its use in - "FlutterStandardMessageCodec" and "FlutterStandardMethodCodec". Scheduled to - be marked as unavailable once the deprecation has been available on the - flutter/flutter alpha branch for four weeks. "FlutterStandardBigInteger" was - needed because the Dart 1.0 int type had no size limit. With Dart 2.0, the - int type is a fixed-size, 64-bit signed integer. If you need to communicate - larger integers, use NSString encoding instead. - - December 11, 2017: Deprecated "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot" and scheculed the same to be marked as - unavailable on January 15, 2018. Instead, "initWithFlutterAssets" and - "initWithFlutterAssetsWithScriptSnapshot" should be used. The reason for this - change is that the FLX archive will be deprecated and replaced with a flutter - assets directory containing the same files as the FLX did. - - November 29, 2017: Added a BREAKING CHANGES section. - */ - #include "FlutterAppDelegate.h" #include "FlutterBinaryMessenger.h" #include "FlutterCallbackCache.h" diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index 46980d609a078..87b7753317e73 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -24,6 +24,11 @@ NS_ASSUME_NONNULL_BEGIN */ extern NSString* const FlutterDefaultDartEntrypoint; +/** + * The default Flutter initial route ("/"). + */ +extern NSString* const FlutterDefaultInitialRoute; + /** * The FlutterEngine class coordinates a single instance of execution for a * `FlutterDartProject`. It may have zero or one `FlutterViewController` at a @@ -53,6 +58,24 @@ extern NSString* const FlutterDefaultDartEntrypoint; FLUTTER_EXPORT @interface FlutterEngine : NSObject +/** + * Default initializer for a FlutterEngine. + * + * Threads created by this FlutterEngine will appear as "FlutterEngine #" in + * Instruments. The prefix can be customized using `initWithName`. + * + * The engine will execute the project located in the bundle with the identifier + * "io.flutter.flutter.app" (the default for Flutter projects). + * + * A newly initialized engine will not run until either `-runWithEntrypoint:` or + * `-runWithEntrypoint:libraryURI:` is called. + * + * FlutterEngine created with this method will have allowHeadlessExecution set to `YES`. + * This means that the engine will continue to run regardless of whether a `FlutterViewController` + * is attached to it or not, until `-destroyContext:` is called or the process finishes. + */ +- (instancetype)init; + /** * Initialize this FlutterEngine. * @@ -114,17 +137,12 @@ FLUTTER_EXPORT project:(nullable FlutterDartProject*)project allowHeadlessExecution:(BOOL)allowHeadlessExecution NS_DESIGNATED_INITIALIZER; -/** - * The default initializer is not available for this object. - * Callers must use `-[FlutterEngine initWithName:project:]`. - */ -- (instancetype)init NS_UNAVAILABLE; - + (instancetype)new NS_UNAVAILABLE; /** * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that - * contains `main()`), using `main()` as the entrypoint (the default for Flutter projects). + * contains `main()`), using `main()` as the entrypoint (the default for Flutter projects), + * and using "/" (the default route) as the initial route. * * The first call to this method will create a new Isolate. Subsequent calls will return * immediately and have no effect. @@ -135,7 +153,7 @@ FLUTTER_EXPORT /** * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that - * contains `main()`). + * contains `main()`), using "/" (the default route) as the initial route. * * The first call to this method will create a new Isolate. Subsequent calls will return * immediately and have no effect. @@ -149,6 +167,25 @@ FLUTTER_EXPORT */ - (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint; +/** + * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that + * contains `main()`). + * + * The first call to this method will create a new Isolate. Subsequent calls will return + * immediately and have no effect. + * + * @param entrypoint The name of a top-level function from the same Dart + * library that contains the app's main() function. If this is FlutterDefaultDartEntrypoint (or + * nil), it will default to `main()`. If it is not the app's main() function, that function must + * be decorated with `@pragma(vm:entry-point)` to ensure the method is not tree-shaken by the Dart + * compiler. + * @param initialRoute The name of the initial Flutter `Navigator` `Route` to load. If this is + * FlutterDefaultInitialRoute (or nil), it will default to the "/" route. + * @return YES if the call succeeds in creating and running a Flutter Engine instance; NO otherwise. + */ +- (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint + initialRoute:(nullable NSString*)initialRoute; + /** * Runs a Dart program on an Isolate using the specified entrypoint and Dart library, * which may not be the same as the library containing the Dart program's `main()` function. diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 6f434af1047f7..4468ce7ea770c 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -55,7 +55,7 @@ FLUTTER_EXPORT * * The initialized viewcontroller will attach itself to the engine as part of this process. * - * @param engine The `FlutterEngine` instance to attach to. + * @param engine The `FlutterEngine` instance to attach to. Cannot be nil. * @param nibName The NIB name to initialize this UIViewController with. * @param nibBundle The NIB bundle. */ @@ -78,6 +78,23 @@ FLUTTER_EXPORT nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; +/** + * Initializes a new FlutterViewController and `FlutterEngine` with the specified + * `FlutterDartProject` and `initialRoute`. + * + * This will implicitly create a new `FlutterEngine` which is retrievable via the `engine` property + * after initialization. + * + * @param project The `FlutterDartProject` to initialize the `FlutterEngine` with. + * @param initialRoute The initial `Navigator` route to load. + * @param nibName The NIB name to initialize this UIViewController with. + * @param nibBundle The NIB bundle. + */ +- (instancetype)initWithProject:(nullable FlutterDartProject*)project + initialRoute:(nullable NSString*)initialRoute + nibName:(nullable NSString*)nibName + bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; + /** * Initializer that is called from loading a FlutterViewController from a XIB. * @@ -117,6 +134,8 @@ FLUTTER_EXPORT - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package; /** + * Deprecated API to set initial route. + * * Attempts to set the first route that the Flutter app shows if the Flutter * runtime hasn't yet started. The default is "/". * @@ -127,9 +146,15 @@ FLUTTER_EXPORT * Setting this after the Flutter started running has no effect. See `pushRoute` * and `popRoute` to change the route after Flutter started running. * + * This is deprecated because it needs to be called at the time of initialization + * and thus should just be in the `initWithProject` initializer. If using + * `initWithEngine`, the initial route should be set on the engine's + * initializer. + * * @param route The name of the first route to show. */ -- (void)setInitialRoute:(NSString*)route; +- (void)setInitialRoute:(NSString*)route + FLUTTER_DEPRECATED("Use FlutterViewController initializer to specify initial route"); /** * Instructs the Flutter Navigator (if any) to go back. @@ -138,8 +163,7 @@ FLUTTER_EXPORT /** * Instructs the Flutter Navigator (if any) to push a route on to the navigation - * stack. The setInitialRoute method should be preferred if this is called before the - * FlutterViewController has come into view. + * stack. * * @param route The name of the route to push to the navigation stack. */ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 3e8bc4727b64b..4f6a2acdf7b76 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -132,6 +132,20 @@ } } + // Domain network configuration + NSDictionary* appTransportSecurity = + [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; + settings.may_insecurely_connect_to_all_domains = + [FlutterDartProject allowsArbitraryLoads:appTransportSecurity]; + settings.domain_network_policy = + [FlutterDartProject domainNetworkPolicy:appTransportSecurity].UTF8String; + + // TODO(mehmetf): We need to announce this change since it is breaking. + // Remove these two lines after we announce and we know which release this is + // going to be part of. + settings.may_insecurely_connect_to_all_domains = true; + settings.domain_network_policy = ""; + #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG // There are no ownership concerns here as all mappings are owned by the // embedder and not the engine. @@ -168,12 +182,12 @@ - (instancetype)initWithPrecompiledDartBundle:(nullable NSBundle*)bundle { return self; } -#pragma mark - WindowData accessors +#pragma mark - PlatformData accessors -- (const flutter::WindowData)defaultWindowData { - flutter::WindowData windowData; - windowData.lifecycle_state = std::string("AppLifecycleState.detached"); - return windowData; +- (const flutter::PlatformData)defaultPlatformData { + flutter::PlatformData PlatformData; + PlatformData.lifecycle_state = std::string("AppLifecycleState.detached"); + return PlatformData; } #pragma mark - Settings accessors @@ -219,6 +233,34 @@ + (NSString*)flutterAssetsName:(NSBundle*)bundle { return flutterAssetsName; } ++ (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity { + // https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity/nsexceptiondomains + NSDictionary* exceptionDomains = [appTransportSecurity objectForKey:@"NSExceptionDomains"]; + if (exceptionDomains == nil) { + return @""; + } + NSMutableArray* networkConfigArray = [[NSMutableArray alloc] init]; + for (NSString* domain in exceptionDomains) { + NSDictionary* domainConfiguration = [exceptionDomains objectForKey:domain]; + // Default value is false. + bool includesSubDomains = + [[domainConfiguration objectForKey:@"NSIncludesSubdomains"] boolValue]; + bool allowsCleartextCommunication = + [[domainConfiguration objectForKey:@"NSExceptionAllowsInsecureHTTPLoads"] boolValue]; + [networkConfigArray addObject:@[ + domain, includesSubDomains ? @YES : @NO, allowsCleartextCommunication ? @YES : @NO + ]]; + } + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:networkConfigArray + options:0 + error:NULL]; + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + ++ (bool)allowsArbitraryLoads:(NSDictionary*)appTransportSecurity { + return [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue]; +} + + (NSString*)lookupKeyForAsset:(NSString*)asset { return [self lookupKeyForAsset:asset fromBundle:nil]; } @@ -261,6 +303,6 @@ - (void)setPersistentIsolateData:(NSData*)data { ); } -#pragma mark - windowData utilities +#pragma mark - PlatformData utilities @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm new file mode 100644 index 0000000000000..eed1bd9cc8969 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm @@ -0,0 +1,85 @@ +// 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 +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterDartProjectTest : XCTestCase +@end + +@implementation FlutterDartProjectTest + +- (void)setUp { +} + +- (void)tearDown { +} + +- (void)testMainBundleSettingsAreCorrectlyParsed { + NSBundle* mainBundle = [NSBundle mainBundle]; + NSDictionary* appTransportSecurity = + [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; + XCTAssertTrue([FlutterDartProject allowsArbitraryLoads:appTransportSecurity]); + XCTAssertEqualObjects( + @"[[\"invalid-site.com\",true,false],[\"sub.invalid-site.com\",false,false]]", + [FlutterDartProject domainNetworkPolicy:appTransportSecurity]); +} + +- (void)testEmptySettingsAreCorrect { + XCTAssertFalse([FlutterDartProject allowsArbitraryLoads:[[NSDictionary alloc] init]]); + XCTAssertEqualObjects(@"", [FlutterDartProject domainNetworkPolicy:[[NSDictionary alloc] init]]); +} + +- (void)testAllowsArbitraryLoads { + XCTAssertFalse([FlutterDartProject allowsArbitraryLoads:@{@"NSAllowsArbitraryLoads" : @false}]); + XCTAssertTrue([FlutterDartProject allowsArbitraryLoads:@{@"NSAllowsArbitraryLoads" : @true}]); +} + +- (void)testProperlyFormedExceptionDomains { + NSDictionary* domainInfoOne = @{ + @"NSIncludesSubdomains" : @false, + @"NSExceptionAllowsInsecureHTTPLoads" : @true, + @"NSExceptionMinimumTLSVersion" : @"4.0" + }; + NSDictionary* domainInfoTwo = @{ + @"NSIncludesSubdomains" : @true, + @"NSExceptionAllowsInsecureHTTPLoads" : @false, + @"NSExceptionMinimumTLSVersion" : @"4.0" + }; + NSDictionary* domainInfoThree = @{ + @"NSIncludesSubdomains" : @false, + @"NSExceptionAllowsInsecureHTTPLoads" : @true, + @"NSExceptionMinimumTLSVersion" : @"4.0" + }; + NSDictionary* exceptionDomains = @{ + @"domain.name" : domainInfoOne, + @"sub.domain.name" : domainInfoTwo, + @"sub.two.domain.name" : domainInfoThree + }; + NSDictionary* appTransportSecurity = @{@"NSExceptionDomains" : exceptionDomains}; + XCTAssertEqualObjects(@"[[\"domain.name\",false,true],[\"sub.domain.name\",true,false]," + @"[\"sub.two.domain.name\",false,true]]", + [FlutterDartProject domainNetworkPolicy:appTransportSecurity]); +} + +- (void)testExceptionDomainsWithMissingInfo { + NSDictionary* domainInfoOne = @{@"NSExceptionMinimumTLSVersion" : @"4.0"}; + NSDictionary* domainInfoTwo = @{ + @"NSIncludesSubdomains" : @true, + }; + NSDictionary* domainInfoThree = @{}; + NSDictionary* exceptionDomains = @{ + @"domain.name" : domainInfoOne, + @"sub.domain.name" : domainInfoTwo, + @"sub.two.domain.name" : domainInfoThree + }; + NSDictionary* appTransportSecurity = @{@"NSExceptionDomains" : exceptionDomains}; + XCTAssertEqualObjects(@"[[\"domain.name\",false,false],[\"sub.domain.name\",true,false]," + @"[\"sub.two.domain.name\",false,false]]", + [FlutterDartProject domainNetworkPolicy:appTransportSecurity]); +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h index b21a38a9be6fd..daac68e663786 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h @@ -6,7 +6,7 @@ #define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERDARTPROJECT_INTERNAL_H_ #include "flutter/common/settings.h" -#include "flutter/runtime/window_data.h" +#include "flutter/runtime/platform_data.h" #include "flutter/shell/common/engine.h" #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterDartProject.h" @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FlutterDartProject () - (const flutter::Settings&)settings; -- (const flutter::WindowData)defaultWindowData; +- (const flutter::PlatformData)defaultPlatformData; - (flutter::RunConfiguration)runConfiguration; - (flutter::RunConfiguration)runConfigurationForEntrypoint:(nullable NSString*)entrypointOrNil; @@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN libraryOrNil:(nullable NSString*)dartLibraryOrNil; + (NSString*)flutterAssetsName:(NSBundle*)bundle; ++ (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity; ++ (bool)allowsArbitraryLoads:(NSDictionary*)appTransportSecurity; /** * The embedder can specify data that the isolate can request synchronously on launch. Engines diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 57d531ec7420d..13394a976c92c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -17,6 +17,9 @@ #include "flutter/shell/common/switches.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/platform/darwin/common/command_line.h" +#include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" +#include "flutter/shell/profiling/sampling_profiler.h" + #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h" @@ -28,10 +31,9 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/profiler_metrics_ios.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" -#include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" -#include "flutter/shell/profiling/sampling_profiler.h" NSString* const FlutterDefaultDartEntrypoint = nil; +NSString* const FlutterDefaultInitialRoute = nil; static constexpr int kNumProfilerSamplesPerSec = 5; @interface FlutterEngineRegistrar : NSObject @@ -46,6 +48,7 @@ @interface FlutterEngine () @property(nonatomic, readonly) NSMutableDictionary* registrars; @property(nonatomic, readwrite, copy) NSString* isolateId; +@property(nonatomic, copy) NSString* initialRoute; @property(nonatomic, retain) id flutterViewControllerWillDeallocObserver; @end @@ -82,6 +85,10 @@ @implementation FlutterEngine { std::unique_ptr _connections; } +- (instancetype)init { + return [self initWithName:@"FlutterEngine" project:nil allowHeadlessExecution:YES]; +} + - (instancetype)initWithName:(NSString*)labelPrefix { return [self initWithName:labelPrefix project:nil allowHeadlessExecution:YES]; } @@ -160,6 +167,7 @@ - (void)dealloc { }]; [_labelPrefix release]; + [_initialRoute release]; [_pluginPublications release]; [_registrars release]; _binaryMessenger.parent = nil; @@ -367,6 +375,13 @@ - (void)setupChannels { binaryMessenger:self.binaryMessenger codec:[FlutterJSONMethodCodec sharedInstance]]); + if ([_initialRoute length] > 0) { + // Flutter isn't ready to receive this method call yet but the channel buffer will cache this. + [_navigationChannel invokeMethod:@"setInitialRoute" arguments:_initialRoute]; + [_initialRoute release]; + _initialRoute = nil; + } + _platformChannel.reset([[FlutterMethodChannel alloc] initWithName:@"flutter/platform" binaryMessenger:self.binaryMessenger @@ -436,16 +451,19 @@ - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil { libraryOrNil:libraryOrNil]); } -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute { if (_shell != nullptr) { FML_LOG(WARNING) << "This FlutterEngine was already invoked."; return NO; } static size_t shellCount = 1; + self.initialRoute = initialRoute; auto settings = [_dartProject.get() settings]; - auto windowData = [_dartProject.get() defaultWindowData]; + auto platformData = [_dartProject.get() defaultPlatformData]; if (libraryURI) { FML_DCHECK(entrypoint) << "Must specify entrypoint if specifying library"; @@ -488,48 +506,21 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { }; flutter::Shell::CreateCallback on_create_rasterizer = - [](flutter::Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); - }; - - if (flutter::IsIosEmbeddedViewsPreviewEnabled()) { - // Embedded views requires the gpu and the platform views to be the same. - // The plan is to eventually dynamically merge the threads when there's a - // platform view in the layer tree. - // For now we use a fixed thread configuration with the same thread used as the - // gpu and platform task runner. - // TODO(amirh/chinmaygarde): remove this, and dynamically change the thread configuration. - // https://github.com/flutter/flutter/issues/23975 - - flutter::TaskRunners task_runners(threadLabel.UTF8String, // label - fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform - fml::MessageLoop::GetCurrent().GetTaskRunner(), // raster - _threadHost.ui_thread->GetTaskRunner(), // ui - _threadHost.io_thread->GetTaskRunner() // io - ); - // Create the shell. This is a blocking operation. - _shell = flutter::Shell::Create(std::move(task_runners), // task runners - std::move(windowData), // window data - std::move(settings), // settings - on_create_platform_view, // platform view creation - on_create_rasterizer // rasterzier creation - ); - } else { - flutter::TaskRunners task_runners(threadLabel.UTF8String, // label - fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform - _threadHost.raster_thread->GetTaskRunner(), // raster - _threadHost.ui_thread->GetTaskRunner(), // ui - _threadHost.io_thread->GetTaskRunner() // io - ); - // Create the shell. This is a blocking operation. - _shell = flutter::Shell::Create(std::move(task_runners), // task runners - std::move(windowData), // window data - std::move(settings), // settings - on_create_platform_view, // platform view creation - on_create_rasterizer // rasterzier creation - ); - } + [](flutter::Shell& shell) { return std::make_unique(shell); }; + + flutter::TaskRunners task_runners(threadLabel.UTF8String, // label + fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform + _threadHost.raster_thread->GetTaskRunner(), // raster + _threadHost.ui_thread->GetTaskRunner(), // ui + _threadHost.io_thread->GetTaskRunner() // io + ); + // Create the shell. This is a blocking operation. + _shell = flutter::Shell::Create(std::move(task_runners), // task runners + std::move(platformData), // window data + std::move(settings), // settings + on_create_platform_view, // platform view creation + on_create_rasterizer // rasterzier creation + ); if (_shell == nullptr) { FML_LOG(ERROR) << "Could not start a shell FlutterEngine with entrypoint: " @@ -552,21 +543,35 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { } - (BOOL)run { - return [self runWithEntrypoint:FlutterDefaultDartEntrypoint libraryURI:nil]; + return [self runWithEntrypoint:FlutterDefaultDartEntrypoint + libraryURI:nil + initialRoute:FlutterDefaultInitialRoute]; } - (BOOL)runWithEntrypoint:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { - if ([self createShell:entrypoint libraryURI:libraryURI]) { + return [self runWithEntrypoint:entrypoint + libraryURI:libraryURI + initialRoute:FlutterDefaultInitialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint { + return [self runWithEntrypoint:entrypoint libraryURI:nil initialRoute:FlutterDefaultInitialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint initialRoute:(NSString*)initialRoute { + return [self runWithEntrypoint:entrypoint libraryURI:nil initialRoute:initialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute { + if ([self createShell:entrypoint libraryURI:libraryURI initialRoute:initialRoute]) { [self launchEngine:entrypoint libraryURI:libraryURI]; } return _shell != nullptr; } -- (BOOL)runWithEntrypoint:(NSString*)entrypoint { - return [self runWithEntrypoint:entrypoint libraryURI:nil]; -} - - (void)notifyLowMemory { if (_shell) { _shell->NotifyLowMemoryWarning(); @@ -669,6 +674,15 @@ - (void)showAutocorrectionPromptRectForStart:(NSUInteger)start return _binaryMessenger; } +// For test only. Ideally we should create a dependency injector for all dependencies and +// remove this. +- (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger { + // Discard the previous messenger and keep the new one. + _binaryMessenger.parent = nil; + [_binaryMessenger release]; + _binaryMessenger = [binaryMessenger retain]; +} + #pragma mark - FlutterBinaryMessenger - (void)sendOnChannel:(NSString*)channel message:(NSData*)message { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 7fe3cb16e0775..e7a68f9a7d2bd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -5,7 +5,8 @@ #import #import #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" -#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h" FLUTTER_ASSERT_ARC @@ -79,4 +80,23 @@ - (void)testNotifyPluginOfDealloc { OCMVerify([plugin detachFromEngineForRegistrar:[OCMArg any]]); } +- (void)testRunningInitialRouteSendsNavigationMessage { + id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]); + + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine setBinaryMessenger:mockBinaryMessenger]; + + // Run with an initial route. + [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"]; + + // Now check that an encoded method call has been made on the binary messenger to set the + // initial route to "test". + FlutterMethodCall* setInitialRouteMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"]; + NSData* encodedSetInitialRouteMethod = + [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall]; + OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation" + message:encodedSetInitialRouteMethod]); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 52558eaf71ab3..93e6cbac58514 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -43,7 +43,9 @@ - (flutter::FlutterPlatformViewsController*)platformViewsController; - (FlutterTextInputPlugin*)textInputPlugin; - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryOrNil + initialRoute:(NSString*)initialRoute; - (void)attachView; - (void)notifyLowMemory; - (flutter::PlatformViewIOS*)iosPlatformView; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h new file mode 100644 index 0000000000000..7be2f68d77b50 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h @@ -0,0 +1,10 @@ +// 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 "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" + +// Category to add test-only visibility. +@interface FlutterEngine (Test) +- (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger; +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 5238be44eaef1..87db2313dfb8e 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -88,6 +88,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([method isEqualToString:@"Clipboard.setData"]) { [self setClipboardData:args]; result(nil); + } else if ([method isEqualToString:@"Clipboard.hasStrings"]) { + result([self clipboardHasStrings]); } else { result(FlutterMethodNotImplemented); } @@ -248,4 +250,16 @@ - (void)setClipboardData:(NSDictionary*)data { } } +- (NSDictionary*)clipboardHasStrings { + bool hasStrings = false; + UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; + if (@available(iOS 10, *)) { + hasStrings = pasteboard.hasStrings; + } else { + NSString* stringInPasteboard = pasteboard.string; + hasStrings = stringInPasteboard != nil; + } + return @{@"value" : @(hasStrings)}; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm new file mode 100644 index 0000000000000..01f3ca4e611d4 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -0,0 +1,51 @@ +// 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 + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" +#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" +#import "third_party/ocmock/Source/OCMock/OCMock.h" + +@interface FlutterPlatformPluginTest : XCTestCase +@end + +@implementation FlutterPlatformPluginTest + +- (void)testHasStrings { + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + FlutterPlatformPlugin* plugin = + [[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()]; + + // Set some string to the pasteboard. + __block bool calledSet = false; + FlutterResult resultSet = ^(id result) { + calledSet = true; + }; + FlutterMethodCall* methodCallSet = + [FlutterMethodCall methodCallWithMethodName:@"Clipboard.setClipboardData" + arguments:@{@"text" : @"some string"}]; + [plugin handleMethodCall:methodCallSet result:resultSet]; + XCTAssertEqual(calledSet, true); + + // Call hasStrings and expect it to be true. + __block bool called = false; + __block bool value; + FlutterResult result = ^(id result) { + called = true; + value = result[@"value"]; + }; + FlutterMethodCall* methodCall = + [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil]; + [plugin handleMethodCall:methodCall result:result]; + + XCTAssertEqual(called, true); + XCTAssertEqual(value, true); +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 9c43d355b3210..891f8ed1a86e9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -167,7 +167,11 @@ touch_interceptors_[viewId] = fml::scoped_nsobject([touch_interceptor retain]); - root_views_[viewId] = fml::scoped_nsobject([touch_interceptor retain]); + + ChildClippingView* clipping_view = + [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease]; + [clipping_view addSubview:touch_interceptor]; + root_views_[viewId] = fml::scoped_nsobject([clipping_view retain]); result(nil); } @@ -317,83 +321,60 @@ return clipCount; } -UIView* FlutterPlatformViewsController::ReconstructClipViewsChain(int number_of_clips, - UIView* platform_view, - UIView* head_clip_view) { - NSInteger indexInFlutterView = -1; - if (head_clip_view.superview) { - // TODO(cyanglaz): potentially cache the index of oldPlatformViewRoot to make this a O(1). - // https://github.com/flutter/flutter/issues/35023 - indexInFlutterView = [flutter_view_.get().subviews indexOfObject:head_clip_view]; - [head_clip_view removeFromSuperview]; - } - UIView* head = platform_view; - int clipIndex = 0; - // Re-use as much existing clip views as needed. - while (head != head_clip_view && clipIndex < number_of_clips) { - head = head.superview; - clipIndex++; - } - // If there were not enough existing clip views, add more. - while (clipIndex < number_of_clips) { - ChildClippingView* clippingView = - [[[ChildClippingView alloc] initWithFrame:flutter_view_.get().bounds] autorelease]; - [clippingView addSubview:head]; - head = clippingView; - clipIndex++; - } - [head removeFromSuperview]; - - if (indexInFlutterView > -1) { - // The chain was previously attached; attach it to the same position. - [flutter_view_.get() insertSubview:head atIndex:indexInFlutterView]; - } - return head; -} - void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view) { FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity)); - UIView* head = embedded_view; - ResetAnchor(head.layer); + ResetAnchor(embedded_view.layer); + ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview; - std::vector>::const_reverse_iterator iter = mutators_stack.Bottom(); - while (iter != mutators_stack.Top()) { + // The UIKit frame is set based on the logical resolution instead of physical. + // (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html). + // However, flow is based on the physical resolution. For example, 1000 pixels in flow equals + // 500 points in UIKit. And until this point, we did all the calculation based on the flow + // resolution. So we need to scale down to match UIKit's logical resolution. + CGFloat screenScale = [UIScreen mainScreen].scale; + CATransform3D finalTransform = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1); + + // Mask view needs to be full screen because we might draw platform view pixels outside of the + // `ChildClippingView`. Since the mask view's frame will be based on the `clipView`'s coordinate + // system, we need to convert the flutter_view's frame to the clipView's coordinate system. The + // mask view is not displayed on the screen. + CGRect maskViewFrame = [flutter_view_ convertRect:flutter_view_.get().frame toView:clipView]; + FlutterClippingMaskView* maskView = + [[[FlutterClippingMaskView alloc] initWithFrame:maskViewFrame] autorelease]; + auto iter = mutators_stack.Begin(); + while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { case transform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); - head.layer.transform = CATransform3DConcat(head.layer.transform, transform); + finalTransform = CATransform3DConcat(transform, finalTransform); break; } case clip_rect: + [maskView clipRect:(*iter)->GetRect() matrix:finalTransform]; + break; case clip_rrect: - case clip_path: { - ChildClippingView* clipView = (ChildClippingView*)head.superview; - clipView.layer.transform = CATransform3DIdentity; - [clipView setClip:(*iter)->GetType() - rect:(*iter)->GetRect() - rrect:(*iter)->GetRRect() - path:(*iter)->GetPath()]; - ResetAnchor(clipView.layer); - head = clipView; + [maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform]; + break; + case clip_path: + [maskView clipPath:(*iter)->GetPath() matrix:finalTransform]; break; - } case opacity: embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; } ++iter; } - // Reverse scale based on screen scale. + // Reverse the offset of the clipView. + // The clipView's frame includes the final translate of the final transform matrix. + // So we need to revese this translate so the platform view can layout at the correct offset. // - // The UIKit frame is set based on the logical resolution instead of physical. - // (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html). - // However, flow is based on the physical resolution. For example, 1000 pixels in flow equals - // 500 points in UIKit. And until this point, we did all the calculation based on the flow - // resolution. So we need to scale down to match UIKit's logical resolution. - CGFloat screenScale = [UIScreen mainScreen].scale; - head.layer.transform = CATransform3DConcat( - head.layer.transform, CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1)); + // Note that we don't apply this transform matrix the clippings because clippings happen on the + // mask view, whose origin is alwasy (0,0) to the flutter_view. + CATransform3D reverseTranslate = + CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0); + embedded_view.layer.transform = CATransform3DConcat(finalTransform, reverseTranslate); + clipView.maskView = maskView; } void FlutterPlatformViewsController::CompositeWithParams(int view_id, @@ -406,17 +387,15 @@ touchInterceptor.alpha = 1; const MutatorsStack& mutatorStack = params.mutatorsStack(); - int currentClippingCount = CountClips(mutatorStack); - int previousClippingCount = clip_count_[view_id]; - if (currentClippingCount != previousClippingCount) { - clip_count_[view_id] = currentClippingCount; - // If we have a different clipping count in this frame, we need to reconstruct the - // ClippingChildView chain to prepare for `ApplyMutators`. - UIView* oldPlatformViewRoot = root_views_[view_id].get(); - UIView* newPlatformViewRoot = - ReconstructClipViewsChain(currentClippingCount, touchInterceptor, oldPlatformViewRoot); - root_views_[view_id] = fml::scoped_nsobject([newPlatformViewRoot retain]); - } + UIView* clippingView = root_views_[view_id].get(); + // The frame of the clipping view should be the final bounding rect. + // Because the translate matrix in the Mutator Stack also includes the offset, + // when we apply the transforms matrix in |ApplyMutators|, we need + // to remember to do a reverse translate. + const SkRect& rect = params.finalBoundingRect(); + CGFloat screenScale = [UIScreen mainScreen].scale; + clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale, + rect.width() / screenScale, rect.height() / screenScale); ApplyMutators(mutatorStack, touchInterceptor); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index e2a0088dfc96b..0e8397e1146b4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -14,6 +14,7 @@ FLUTTER_ASSERT_NOT_ARC @class FlutterPlatformViewsTestMockPlatformView; static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil; +const float kFloatCompareEpsilon = 0.001; @interface FlutterPlatformViewsTestMockPlatformView : UIView @end @@ -143,4 +144,385 @@ - (void)testCanCreatePlatformViewWithoutFlutterView { flutterPlatformViewsController->Reset(); } +- (void)testChildClippingViewHitTests { + ChildClippingView* childClippingView = + [[[ChildClippingView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease]; + UIView* childView = [[[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)] autorelease]; + [childClippingView addSubview:childView]; + + XCTAssertFalse([childClippingView pointInside:CGPointMake(50, 50) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(99, 100) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(100, 99) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(201, 200) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(200, 201) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(99, 200) withEvent:nil]); + XCTAssertFalse([childClippingView pointInside:CGPointMake(200, 299) withEvent:nil]); + + XCTAssertTrue([childClippingView pointInside:CGPointMake(150, 150) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(100, 100) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 100) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(100, 199) withEvent:nil]); + XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]); +} + +- (void)testCompositePlatformView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a translate matrix + SkMatrix translateMatrix = SkMatrix::MakeTrans(100, 100); + stack.PushTransform(translateMatrix); + SkMatrix finalMatrix; + finalMatrix.setConcat(screenScaleMatrix, translateMatrix); + + auto embeddedViewParams = + std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds + toView:mockFlutterView]; + XCTAssertTrue(CGRectEqualToRect(platformViewRectInFlutterView, CGRectMake(100, 100, 300, 300))); + flutterPlatformViewsController->Reset(); +} + +- (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a rotate matrix + SkMatrix rotateMatrix; + rotateMatrix.setRotate(10); + stack.PushTransform(rotateMatrix); + SkMatrix finalMatrix; + finalMatrix.setConcat(screenScaleMatrix, rotateMatrix); + + auto embeddedViewParams = + std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds + toView:mockFlutterView]; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + // The childclippingview's frame is set based on flow, but the platform view's frame is set based + // on quartz. Although they should be the same, but we should tolerate small floating point + // errors. + XCTAssertLessThan(fabs(platformViewRectInFlutterView.origin.x - childClippingView.frame.origin.x), + kFloatCompareEpsilon); + XCTAssertLessThan(fabs(platformViewRectInFlutterView.origin.y - childClippingView.frame.origin.y), + kFloatCompareEpsilon); + XCTAssertLessThan( + fabs(platformViewRectInFlutterView.size.width - childClippingView.frame.size.width), + kFloatCompareEpsilon); + XCTAssertLessThan( + fabs(platformViewRectInFlutterView.size.height - childClippingView.frame.size.height), + kFloatCompareEpsilon); + + flutterPlatformViewsController->Reset(); +} + +- (void)testClipRect { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a clip rect + SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3); + stack.PushClipRect(rect); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + gMockPlatformView.backgroundColor = UIColor.redColor; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + CGPoint point = CGPointMake(i, j); + int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; + // Edges of the clipping might have a semi transparent pixel, we only check the pixels that + // are fully inside the clipped area. + CGRect insideClipping = CGRectMake(3, 3, 1, 1); + if (CGRectContainsPoint(insideClipping, point)) { + XCTAssertEqual(alpha, 255); + } else { + XCTAssertLessThan(alpha, 255); + } + } + } + flutterPlatformViewsController->Reset(); +} + +- (void)testClipRRect { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a clip rrect + SkRRect rrect = SkRRect::MakeRectXY(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1); + stack.PushClipRRect(rrect); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + gMockPlatformView.backgroundColor = UIColor.redColor; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + CGPoint point = CGPointMake(i, j); + int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; + // Edges of the clipping might have a semi transparent pixel, we only check the pixels that + // are fully inside the clipped area. + CGRect insideClipping = CGRectMake(3, 3, 4, 4); + if (CGRectContainsPoint(insideClipping, point)) { + XCTAssertEqual(alpha, 255); + } else { + XCTAssertLessThan(alpha, 255); + } + } + } + flutterPlatformViewsController->Reset(); +} + +- (void)testClipPath { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a clip path + SkPath path; + path.addRoundRect(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1); + stack.PushClipPath(path); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + gMockPlatformView.backgroundColor = UIColor.redColor; + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + CGPoint point = CGPointMake(i, j); + int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; + // Edges of the clipping might have a semi transparent pixel, we only check the pixels that + // are fully inside the clipped area. + CGRect insideClipping = CGRectMake(3, 3, 4, 4); + if (CGRectContainsPoint(insideClipping, point)) { + XCTAssertEqual(alpha, 255); + } else { + XCTAssertLessThan(alpha, 255); + } + } + } + flutterPlatformViewsController->Reset(); +} + +- (int)alphaOfPoint:(CGPoint)point onView:(UIView*)view { + unsigned char pixel[4] = {0}; + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + + // Draw the pixel on `point` in the context. + CGContextRef context = CGBitmapContextCreate( + pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast); + CGContextTranslateCTM(context, -point.x, -point.y); + [view.layer renderInContext:context]; + + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + // Get the alpha from the pixel that we just rendered. + return pixel[3]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 2b6bcf961310c..311414b9d682d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -16,6 +16,33 @@ #include "flutter/shell/platform/darwin/ios/ios_context.h" #include "third_party/skia/include/core/SkPictureRecorder.h" +// A UIView that acts as a clipping mask for the |ChildClippingView|. +// +// On the [UIView drawRect:] method, this view performs a series of clipping operations and sets the +// alpha channel to the final resulting area to be 1; it also sets the "clipped out" area's alpha +// channel to be 0. +// +// When a UIView sets a |FlutterClippingMaskView| as its `maskView`, the alpha channel of the UIView +// is replaced with the alpha channel of the |FlutterClippingMaskView|. +@interface FlutterClippingMaskView : UIView + +// Adds a clip rect operation to the queue. +// +// The `clipSkRect` is transformed with the `matrix` before adding to the queue. +- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix; + +// Adds a clip rrect operation to the queue. +// +// The `clipSkRRect` is transformed with the `matrix` before adding to the queue. +- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix; + +// Adds a clip path operation to the queue. +// +// The `path` is transformed with the `matrix` before adding to the queue. +- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix; + +@end + // A UIView that is used as the parent for embedded UIViews. // // This view has 2 roles: @@ -37,14 +64,6 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView -// Performs the clipping based on the type. -// -// The `type` must be one of the 3: clip_rect, clip_rrect, clip_path. -- (void)setClip:(flutter::MutatorType)type - rect:(const SkRect&)rect - rrect:(const SkRRect&)rrect - path:(const SkPath&)path; - @end namespace flutter { @@ -253,20 +272,6 @@ class FlutterPlatformViewsController { // Traverse the `mutators_stack` and return the number of clip operations. int CountClips(const MutatorsStack& mutators_stack); - // Make sure that platform_view has exactly clip_count ChildClippingView ancestors. - // - // Existing ChildClippingViews are re-used. If there are currently more ChildClippingView - // ancestors than needed, the extra views are detached. If there are less ChildClippingView - // ancestors than needed, new ChildClippingViews will be added. - // - // If head_clip_view was attached as a subview to FlutterView, the head of the newly constructed - // ChildClippingViews chain is attached to FlutterView in the same position. - // - // Returns the new head of the clip views chain. - UIView* ReconstructClipViewsChain(int number_of_clips, - UIView* platform_view, - UIView* head_clip_view); - // Applies the mutators in the mutators_stack to the UIView chain that was constructed by // `ReconstructClipViewsChain` // diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 551535a2c7faf..5e9ed80279975 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -53,32 +53,72 @@ void ResetAnchor(CALayer* layer) { @implementation ChildClippingView -+ (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect { - return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft, - clipSkRect.fBottom - clipSkRect.fTop); +// The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to +// be hit tested and consumed by this view if they are inside the embedded platform view which could +// be smaller the embedded platform view is rotated. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { + for (UIView* view in self.subviews) { + if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) { + return YES; + } + } + return NO; +} + +@end + +@interface FlutterClippingMaskView () + +- (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix; +- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect; + +@end + +@implementation FlutterClippingMaskView { + std::vector> paths_; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if ([super initWithFrame:frame]) { + self.backgroundColor = UIColor.clearColor; + } + return self; } -- (void)clipRect:(const SkRect&)clipSkRect { - CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRect]; - fml::CFRef pathRef(CGPathCreateWithRect(clipRect, nil)); - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; +- (void)drawRect:(CGRect)rect { + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSaveGState(context); + + // For mask view, only the alpha channel is used. + CGContextSetAlpha(context, 1); + + for (size_t i = 0; i < paths_.size(); i++) { + CGContextAddPath(context, paths_.at(i)); + CGContextClip(context); + } + CGContextFillRect(context, rect); + CGContextRestoreGState(context); +} + +- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix { + CGRect clipRect = [self getCGRectFromSkRect:clipSkRect]; + CGPathRef path = CGPathCreateWithRect(clipRect, nil); + paths_.push_back([self getTransformedPath:path matrix:matrix]); } -- (void)clipRRect:(const SkRRect&)clipSkRRect { +- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix { CGPathRef pathRef = nullptr; switch (clipSkRRect.getType()) { case SkRRect::kEmpty_Type: { break; } case SkRRect::kRect_Type: { - [self clipRect:clipSkRRect.rect()]; + [self clipRect:clipSkRRect.rect() matrix:matrix]; return; } case SkRRect::kOval_Type: case SkRRect::kSimple_Type: { - CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRRect.rect()]; + CGRect clipRect = [self getCGRectFromSkRect:clipSkRRect.rect()]; pathRef = CGPathCreateWithRoundedRect(clipRect, clipSkRRect.getSimpleRadii().x(), clipSkRRect.getSimpleRadii().y(), nil); break; @@ -129,23 +169,17 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect { // TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that // the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge // clipping on iOS. - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; - CGPathRelease(pathRef); + paths_.push_back([self getTransformedPath:pathRef matrix:matrix]); } -- (void)clipPath:(const SkPath&)path { +- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix { if (!path.isValid()) { return; } - fml::CFRef pathRef(CGPathCreateMutable()); if (path.isEmpty()) { - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; return; } + CGMutablePathRef pathRef = CGPathCreateMutable(); // Loop through all verbs and translate them into CGPath SkPath::Iter iter(path, true); @@ -197,42 +231,20 @@ - (void)clipPath:(const SkPath&)path { } verb = iter.next(pts); } - - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; + paths_.push_back([self getTransformedPath:pathRef matrix:matrix]); } -- (void)setClip:(flutter::MutatorType)type - rect:(const SkRect&)rect - rrect:(const SkRRect&)rrect - path:(const SkPath&)path { - FML_CHECK(type == flutter::clip_rect || type == flutter::clip_rrect || - type == flutter::clip_path); - switch (type) { - case flutter::clip_rect: - [self clipRect:rect]; - break; - case flutter::clip_rrect: - [self clipRRect:rrect]; - break; - case flutter::clip_path: - [self clipPath:path]; - break; - default: - break; - } +- (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix { + CGAffineTransform affine = + CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); + CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine); + CGPathRelease(path); + return fml::CFRef(transformedPath); } -// The ChildClippingView is as big as the FlutterView, we only want touches to be hit tested and -// consumed by this view if they are inside the smaller child view. -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { - for (UIView* view in self.subviews) { - if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) { - return YES; - } - } - return NO; +- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect { + return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft, + clipSkRect.fBottom - clipSkRect.fTop); } @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 1e04170da2159..77b65e63554ca 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -461,7 +461,6 @@ - (void)configureWithDictionary:(NSDictionary*)configuration { self.secureTextEntry = [configuration[kSecureTextEntry] boolValue]; self.keyboardType = ToUIKeyboardType(inputType); - self.keyboardType = UIKeyboardTypeNamePhonePad; self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]); self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration); @@ -538,35 +537,21 @@ - (BOOL)setTextInputState:(NSDictionary*)state { FlutterTextRange* newMarkedRange = composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil; needsEditingStateUpdate = - needsEditingStateUpdate || newMarkedRange == nil - ? self.markedTextRange == nil - : [newMarkedRange isEqualTo:(FlutterTextRange*)self.markedTextRange]; + needsEditingStateUpdate || + (!newMarkedRange ? self.markedTextRange != nil + : ![newMarkedRange isEqualTo:(FlutterTextRange*)self.markedTextRange]); self.markedTextRange = newMarkedRange; - NSInteger selectionBase = [state[@"selectionBase"] intValue]; - NSInteger selectionExtent = [state[@"selectionExtent"] intValue]; - NSRange selectedRange = [self clampSelection:NSMakeRange(MIN(selectionBase, selectionExtent), - ABS(selectionBase - selectionExtent)) - forText:self.text]; + NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue] + extent:[state[@"selectionExtent"] intValue] + forText:self.text]; + NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range]; - if (selectedRange.location != oldSelectedRange.location || - selectedRange.length != oldSelectedRange.length) { + if (!NSEqualRanges(selectedRange, oldSelectedRange)) { needsEditingStateUpdate = YES; [self.inputDelegate selectionWillChange:self]; - // The state may contain an invalid selection, such as when no selection was - // explicitly set in the framework. This is handled here by setting the - // selection to (0,0). In contrast, Android handles this situation by - // clearing the selection, but the result in both cases is that the cursor - // is placed at the beginning of the field. - bool selectionBaseIsValid = selectionBase > 0 && selectionBase <= ((NSInteger)self.text.length); - bool selectionExtentIsValid = - selectionExtent > 0 && selectionExtent <= ((NSInteger)self.text.length); - if (selectionBaseIsValid && selectionExtentIsValid) { - [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; - } else { - [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 0)]]; - } + [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; _selectionAffinity = _kTextAffinityDownstream; if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) @@ -582,6 +567,22 @@ - (BOOL)setTextInputState:(NSDictionary*)state { return needsEditingStateUpdate; } +// Extracts the selection information from the editing state dictionary. +// +// The state may contain an invalid selection, such as when no selection was +// explicitly set in the framework. This is handled here by setting the +// selection to (0,0). In contrast, Android handles this situation by +// clearing the selection, but the result in both cases is that the cursor +// is placed at the beginning of the field. +- (NSRange)clampSelectionFromBase:(int)selectionBase + extent:(int)selectionExtent + forText:(NSString*)text { + int loc = MIN(selectionBase, selectionExtent); + int len = ABS(selectionExtent - selectionBase); + return loc < 0 ? NSMakeRange(0, 0) + : [self clampSelection:NSMakeRange(loc, len) forText:self.text]; +} + - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text { int start = MIN(MAX(range.location, 0), text.length); int length = MIN(range.length, text.length - start); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 7f69a19664de1..a6d10993de755 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -13,7 +13,8 @@ @interface FlutterTextInputView () @property(nonatomic, copy) NSString* autofillId; -- (void)setTextInputState:(NSDictionary*)state; +- (BOOL)setTextInputState:(NSDictionary*)state; +- (void)updateEditingState; - (BOOL)isVisibleToAutofill; @end @@ -63,23 +64,6 @@ - (void)setClientId:(int)clientId configuration:(NSDictionary*)config { }]; } -- (void)commitAutofillContextAndVerify { - FlutterMethodCall* methodCall = - [FlutterMethodCall methodCallWithMethodName:@"TextInput.finishAutofillContext" - arguments:@YES]; - [textInputPlugin handleMethodCall:methodCall - result:^(id _Nullable result){ - }]; - - XCTAssertEqual(self.viewsVisibleToAutofill.count, - [textInputPlugin.activeView isVisibleToAutofill] ? 1 : 0); - XCTAssertNotEqual(textInputPlugin.textInputView, nil); - // The active view should still be installed so it doesn't get - // deallocated. - XCTAssertEqual(self.installedInputViews.count, 1); - XCTAssertEqual(textInputPlugin.autofillContext.count, 0); -} - - (NSMutableDictionary*)mutableTemplateCopy { if (!_template) { _template = @{ @@ -96,22 +80,6 @@ - (NSMutableDictionary*)mutableTemplateCopy { return [_template mutableCopy]; } -- (NSMutableDictionary*)mutablePasswordTemplateCopy { - if (!_passwordTemplate) { - _passwordTemplate = @{ - @"inputType" : @{@"name" : @"TextInuptType.text"}, - @"keyboardAppearance" : @"Brightness.light", - @"obscureText" : @YES, - @"inputAction" : @"TextInputAction.unspecified", - @"smartDashesType" : @"0", - @"smartQuotesType" : @"0", - @"autocorrect" : @YES - }; - } - - return [_passwordTemplate mutableCopy]; -} - - (NSArray*)installedInputViews { UIWindow* keyWindow = [[[UIApplication sharedApplication] windows] @@ -123,11 +91,6 @@ - (NSMutableDictionary*)mutablePasswordTemplateCopy { [FlutterTextInputView class]]]; } -- (NSArray*)viewsVisibleToAutofill { - return [self.installedInputViews - filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]]; -} - #pragma mark - Tests - (void)testSecureInput { @@ -145,6 +108,9 @@ - (void)testSecureInput { // Verify secureTextEntry is set to the correct value. XCTAssertTrue(inputView.secureTextEntry); + // Verify keyboardType is set to the default value. + XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault); + // We should have only ever created one FlutterTextInputView. XCTAssertEqual(inputFields.count, 1); @@ -157,6 +123,86 @@ - (void)testSecureInput { XCTAssert(inputView.autofillId.length > 0); } +- (void)testKeyboardType { + NSDictionary* config = self.mutableTemplateCopy; + [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"]; + [self setClientId:123 configuration:config]; + + // Find all the FlutterTextInputViews we created. + NSArray* inputFields = self.installedInputViews; + + FlutterTextInputView* inputView = inputFields[0]; + + // Verify keyboardType is set to the value specified in config. + XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL); +} + +- (void)testAutocorrectionPromptRectAppears { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; + inputView.textInputDelegate = engine; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + + // Verify behavior. + OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]); +} + +- (void)testTextRangeFromPositionMatchesUITextViewBehavior { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; + FlutterTextPosition* fromPosition = [[FlutterTextPosition alloc] initWithIndex:2]; + FlutterTextPosition* toPosition = [[FlutterTextPosition alloc] initWithIndex:0]; + + FlutterTextRange* flutterRange = (FlutterTextRange*)[inputView textRangeFromPosition:fromPosition + toPosition:toPosition]; + NSRange range = flutterRange.range; + + XCTAssertEqual(range.location, 0); + XCTAssertEqual(range.length, 2); +} + +- (void)testNoZombies { + // Regression test for https://github.com/flutter/flutter/issues/62501. + FlutterSecureTextInputView* passwordView = [[FlutterSecureTextInputView alloc] init]; + + @autoreleasepool { + // Initialize the lazy textField. + [passwordView.textField description]; + } + XCTAssert([[passwordView.textField description] containsString:@"TextField"]); +} + +#pragma mark - EditingState tests + +- (void)testUITextInputCallsUpdateEditingStateOnce { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + __block int updateCount = 0; + OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) + .andDo(^(NSInvocation* invocation) { + updateCount++; + }); + + [inputView insertText:@"text to insert"]; + // Update the framework exactly once. + XCTAssertEqual(updateCount, 1); + + [inputView deleteBackward]; + XCTAssertEqual(updateCount, 2); + + inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]; + XCTAssertEqual(updateCount, 3); + + [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] + withText:@"replace text"]; + XCTAssertEqual(updateCount, 4); + + [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + XCTAssertEqual(updateCount, 5); + + [inputView unmarkText]; + XCTAssertEqual(updateCount, 6); +} + - (void)testTextChangesTriggerUpdateEditingClient { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -166,12 +212,10 @@ - (void)testTextChangesTriggerUpdateEditingClient { inputView.selectedTextRange = nil; // Text changes trigger update. - [inputView setTextInputState:@{@"text" : @"AFTER"}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue([inputView setTextInputState:@{@"text" : @"AFTER"}]); // Don't send anything if there's nothing new. - [inputView setTextInputState:@{@"text" : @"AFTER"}]; - OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); + XCTAssertFalse([inputView setTextInputState:@{@"text" : @"AFTER"}]); } - (void)testSelectionChangeTriggersUpdateEditingClient { @@ -182,22 +226,22 @@ - (void)testSelectionChangeTriggersUpdateEditingClient { inputView.markedTextRange = nil; inputView.selectedTextRange = nil; - [inputView + BOOL shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); // Don't send anything if there's nothing new. - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}]; - OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); + XCTAssertFalse(shouldUpdate); } - (void)testComposingChangeTriggersUpdateEditingClient { @@ -209,22 +253,22 @@ - (void)testComposingChangeTriggersUpdateEditingClient { inputView.markedTextRange = nil; inputView.selectedTextRange = nil; - [inputView + BOOL shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @0, @"composingExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}]; - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]); + XCTAssertTrue(shouldUpdate); // Don't send anything if there's nothing new. - [inputView + shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}]; - OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); + XCTAssertFalse(shouldUpdate); } - (void)testUpdateEditingClientNegativeSelection { @@ -240,13 +284,131 @@ - (void)testUpdateEditingClientNegativeSelection { @"selectionBase" : @-1, @"selectionExtent" : @-1 }]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); + + // Returns (0, 0) when either end goes below 0. + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); + + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); +} + +- (void)testUpdateEditingClientSelectionClamping { + // Regression test for https://github.com/flutter/flutter/issues/62992. + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + [inputView.text setString:@"SELECTION"]; + inputView.markedTextRange = nil; + inputView.selectedTextRange = nil; + + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}]; + [inputView updateEditingState]; OCMVerify([engine updateEditingClient:0 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { return ([state[@"selectionBase"] intValue]) == 0 && ([state[@"selectionExtent"] intValue] == 0); }]]); + + // Needs clamping. + [inputView setTextInputState:@{ + @"text" : @"SELECTION", + @"selectionBase" : @0, + @"selectionExtent" : @9999 + }]; + [inputView updateEditingState]; + + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 9); + }]]); + + // No clamping needed, but in reverse direction. + [inputView + setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 1); + }]]); + + // Both ends need clamping. + [inputView setTextInputState:@{ + @"text" : @"SELECTION", + @"selectionBase" : @9999, + @"selectionExtent" : @9999 + }]; + [inputView updateEditingState]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 9 && + ([state[@"selectionExtent"] intValue] == 9); + }]]); +} + +#pragma mark - Autofill - Utilities + +- (NSMutableDictionary*)mutablePasswordTemplateCopy { + if (!_passwordTemplate) { + _passwordTemplate = @{ + @"inputType" : @{@"name" : @"TextInuptType.text"}, + @"keyboardAppearance" : @"Brightness.light", + @"obscureText" : @YES, + @"inputAction" : @"TextInputAction.unspecified", + @"smartDashesType" : @"0", + @"smartQuotesType" : @"0", + @"autocorrect" : @YES + }; + } + + return [_passwordTemplate mutableCopy]; +} + +- (NSArray*)viewsVisibleToAutofill { + return [self.installedInputViews + filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]]; +} + +- (void)commitAutofillContextAndVerify { + FlutterMethodCall* methodCall = + [FlutterMethodCall methodCallWithMethodName:@"TextInput.finishAutofillContext" + arguments:@YES]; + [textInputPlugin handleMethodCall:methodCall + result:^(id _Nullable result){ + }]; + + XCTAssertEqual(self.viewsVisibleToAutofill.count, + [textInputPlugin.activeView isVisibleToAutofill] ? 1 : 0); + XCTAssertNotEqual(textInputPlugin.textInputView, nil); + // The active view should still be installed so it doesn't get + // deallocated. + XCTAssertEqual(self.installedInputViews.count, 1); + XCTAssertEqual(textInputPlugin.autofillContext.count, 0); } +#pragma mark - Autofill - Tests + - (void)testAutofillContext { NSMutableDictionary* field1 = self.mutableTemplateCopy; @@ -448,67 +610,4 @@ - (void)testPasswordAutofillHack { XCTAssertNotEqual([inputView performSelector:@selector(font)], nil); } -- (void)testAutocorrectionPromptRectAppears { - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; - inputView.textInputDelegate = engine; - [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; - - // Verify behavior. - OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]); -} - -- (void)testTextRangeFromPositionMatchesUITextViewBehavior { - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; - FlutterTextPosition* fromPosition = [[FlutterTextPosition alloc] initWithIndex:2]; - FlutterTextPosition* toPosition = [[FlutterTextPosition alloc] initWithIndex:0]; - - FlutterTextRange* flutterRange = (FlutterTextRange*)[inputView textRangeFromPosition:fromPosition - toPosition:toPosition]; - NSRange range = flutterRange.range; - - XCTAssertEqual(range.location, 0); - XCTAssertEqual(range.length, 2); -} - -- (void)testUITextInputCallsUpdateEditingStateOnce { - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; - inputView.textInputDelegate = engine; - - __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) - .andDo(^(NSInvocation* invocation) { - updateCount++; - }); - - [inputView insertText:@"text to insert"]; - // Update the framework exactly once. - XCTAssertEqual(updateCount, 1); - - [inputView deleteBackward]; - XCTAssertEqual(updateCount, 2); - - inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]; - XCTAssertEqual(updateCount, 3); - - [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] - withText:@"replace text"]; - XCTAssertEqual(updateCount, 4); - - [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; - XCTAssertEqual(updateCount, 5); - - [inputView unmarkText]; - XCTAssertEqual(updateCount, 6); -} - -- (void)testNoZombies { - // Regression test for https://github.com/flutter/flutter/issues/62501. - FlutterSecureTextInputView* passwordView = [[FlutterSecureTextInputView alloc] init]; - - @autoreleasepool { - // Initialize the lazy textField. - [passwordView.textField description]; - } - XCTAssert([[passwordView.textField description] containsString:@"TextField"]); -} @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index d788b461586f6..6114281d8695b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -111,26 +111,24 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine return self; } -- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project { - _viewOpaque = YES; - _weakFactory = std::make_unique>(self); - _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" - project:project - allowHeadlessExecution:self.engineAllowHeadlessExecution]); - _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); - [_engine.get() createShell:nil libraryURI:nil]; - _engineNeedsLaunch = YES; - _ongoingTouches = [[NSMutableSet alloc] init]; - [self loadDefaultSplashScreenView]; - [self performCommonViewControllerInitialization]; +- (instancetype)initWithProject:(FlutterDartProject*)project + nibName:(NSString*)nibName + bundle:(NSBundle*)nibBundle { + self = [super initWithNibName:nibName bundle:nibBundle]; + if (self) { + [self sharedSetupWithProject:project initialRoute:nil]; + } + + return self; } -- (instancetype)initWithProject:(nullable FlutterDartProject*)project - nibName:(nullable NSString*)nibName - bundle:(nullable NSBundle*)nibBundle { +- (instancetype)initWithProject:(FlutterDartProject*)project + initialRoute:(NSString*)initialRoute + nibName:(NSString*)nibName + bundle:(NSBundle*)nibBundle { self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { - [self sharedSetupWithProject:project]; + [self sharedSetupWithProject:project initialRoute:initialRoute]; } return self; @@ -148,7 +146,7 @@ - (instancetype)initWithCoder:(NSCoder*)aDecoder { - (void)awakeFromNib { [super awakeFromNib]; if (!_engine.get()) { - [self sharedSetupWithProject:nil]; + [self sharedSetupWithProject:nil initialRoute:nil]; } } @@ -156,6 +154,21 @@ - (instancetype)init { return [self initWithProject:nil nibName:nil bundle:nil]; } +- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project + initialRoute:(nullable NSString*)initialRoute { + _viewOpaque = YES; + _weakFactory = std::make_unique>(self); + _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" + project:project + allowHeadlessExecution:self.engineAllowHeadlessExecution]); + _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); + [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute]; + _engineNeedsLaunch = YES; + _ongoingTouches = [[NSMutableSet alloc] init]; + [self loadDefaultSplashScreenView]; + [self performCommonViewControllerInitialization]; +} + - (BOOL)isViewOpaque { return _viewOpaque; } @@ -469,7 +482,12 @@ - (UIView*)splashScreenFromStoryboard:(NSString*)name { } - (UIView*)splashScreenFromXib:(NSString*)name { - NSArray* objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil]; + NSArray* objects = nil; + @try { + objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil]; + } @catch (NSException* exception) { + return nil; + } if ([objects count] != 0) { UIView* view = [objects objectAtIndex:0]; return view; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index e508d031eb265..2418ea58bc72b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -13,7 +13,9 @@ FLUTTER_ASSERT_ARC @interface FlutterEngine () -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI; +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute; @end @interface FlutterEngine (TestLowMemory) @@ -513,7 +515,7 @@ - (void)testWillDeallocNotification { - (void)testDoesntLoadViewInInit { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; - [engine createShell:@"" libraryURI:@""]; + [engine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; @@ -523,7 +525,7 @@ - (void)testDoesntLoadViewInInit { - (void)testHideOverlay { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; - [engine createShell:@"" libraryURI:@""]; + [engine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index 2addc2a50932f..1470bca82270a 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -453,6 +453,7 @@ - (BOOL)accessibilityPerformEscape { - (void)accessibilityElementDidBecomeFocused { if (![self isAccessibilityBridgeAlive]) return; + [self bridge]->AccessibilityFocusDidChange([self uid]); if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden) || [self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) { [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen); diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm index 3c42aeacb3efb..8b23f8d9369f0 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -33,6 +33,7 @@ void DispatchSemanticsAction(int32_t id, SemanticsActionObservation observation(id, action); observations.push_back(observation); } + void AccessibilityFocusDidChange(int32_t id) override {} FlutterPlatformViewsController* GetPlatformViewsController() const override { return nil; } std::vector observations; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h index 77e5813792ad1..758b63fdf7545 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h @@ -18,6 +18,7 @@ #include "flutter/lib/ui/semantics/custom_accessibility_action.h" #include "flutter/lib/ui/semantics/semantics_node.h" #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" +#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" #include "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" @@ -42,12 +43,13 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { virtual ~IosDelegate() = default; /// Returns true when the FlutterViewController associated with the `view` /// is presenting a modal view controller. - virtual bool IsFlutterViewControllerPresentingModalViewController(UIView* view) = 0; + virtual bool IsFlutterViewControllerPresentingModalViewController( + FlutterViewController* view_controller) = 0; virtual void PostAccessibilityNotification(UIAccessibilityNotifications notification, id argument) = 0; }; - AccessibilityBridge(UIView* view, + AccessibilityBridge(FlutterViewController* view_controller, PlatformViewIOS* platform_view, FlutterPlatformViewsController* platform_views_controller, std::unique_ptr ios_delegate = nullptr); @@ -59,10 +61,11 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action, std::vector args) override; + void AccessibilityFocusDidChange(int32_t id) override; UIView* textInputView() override; - UIView* view() const override { return view_; } + UIView* view() const override { return view_controller_.view; } fml::WeakPtr GetWeakPtr(); @@ -78,9 +81,10 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { NSMutableArray* doomed_uids); void HandleEvent(NSDictionary* annotatedEvent); - UIView* view_; + FlutterViewController* view_controller_; PlatformViewIOS* platform_view_; FlutterPlatformViewsController* platform_views_controller_; + int32_t last_focused_semantics_object_id_; fml::scoped_nsobject> objects_; fml::scoped_nsprotocol accessibility_channel_; fml::WeakPtrFactory weak_factory_; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index 6fd197e69ceb2..d8f99a3b198b4 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -16,27 +16,12 @@ namespace flutter { namespace { -FlutterViewController* _Nullable GetFlutterViewControllerForView(UIView* view) { - // There is no way to get a view's view controller in UIKit directly, this is - // somewhat of a hacky solution to get that. This could be eliminated if the - // bridge actually kept a reference to a FlutterViewController instead of a - // UIView. - id nextResponder = [view nextResponder]; - if ([nextResponder isKindOfClass:[FlutterViewController class]]) { - return nextResponder; - } else if ([nextResponder isKindOfClass:[UIView class]]) { - return GetFlutterViewControllerForView(nextResponder); - } else { - return nil; - } -} - class DefaultIosDelegate : public AccessibilityBridge::IosDelegate { public: - bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { - FlutterViewController* viewController = GetFlutterViewControllerForView(view); - if (viewController) { - return viewController.isPresentingViewController; + bool IsFlutterViewControllerPresentingModalViewController( + FlutterViewController* view_controller) override { + if (view_controller) { + return view_controller.isPresentingViewController; } else { return false; } @@ -49,13 +34,14 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, }; } // namespace -AccessibilityBridge::AccessibilityBridge(UIView* view, +AccessibilityBridge::AccessibilityBridge(FlutterViewController* view_controller, PlatformViewIOS* platform_view, FlutterPlatformViewsController* platform_views_controller, std::unique_ptr ios_delegate) - : view_(view), + : view_controller_(view_controller), platform_view_(platform_view), platform_views_controller_(platform_views_controller), + last_focused_semantics_object_id_(0), objects_([[NSMutableDictionary alloc] init]), weak_factory_(this), previous_route_id_(0), @@ -74,13 +60,17 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, AccessibilityBridge::~AccessibilityBridge() { [accessibility_channel_.get() setMessageHandler:nil]; clearState(); - view_.accessibilityElements = nil; + view_controller_.view.accessibilityElements = nil; } UIView* AccessibilityBridge::textInputView() { return [[platform_view_->GetOwnerViewController().get().engine textInputPlugin] textInputView]; } +void AccessibilityBridge::AccessibilityFocusDidChange(int32_t id) { + last_focused_semantics_object_id_ = id; +} + void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes, flutter::CustomAccessibilityActionUpdates actions) { BOOL layoutChanged = NO; @@ -164,8 +154,8 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, SemanticsObject* lastAdded = nil; if (root) { - if (!view_.accessibilityElements) { - view_.accessibilityElements = @[ [root accessibilityContainer] ]; + if (!view_controller_.view.accessibilityElements) { + view_controller_.view.accessibilityElements = @[ [root accessibilityContainer] ]; } NSMutableArray* newRoutes = [[[NSMutableArray alloc] init] autorelease]; [root collectRoutes:newRoutes]; @@ -188,7 +178,7 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, previous_routes_.push_back([route uid]); } } else { - view_.accessibilityElements = nil; + view_controller_.view.accessibilityElements = nil; } NSMutableArray* doomed_uids = [NSMutableArray arrayWithArray:[objects_.get() allKeys]]; @@ -198,17 +188,21 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, layoutChanged = layoutChanged || [doomed_uids count] > 0; if (routeChanged) { - if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_)) { + if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) { ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification, [lastAdded routeFocusObject]); } } else if (layoutChanged) { - // TODO(goderbauer): figure out which node to focus next. - ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, nil); + // Tries to refocus the previous focused semantics object to avoid random jumps. + ios_delegate_->PostAccessibilityNotification( + UIAccessibilityLayoutChangedNotification, + [objects_.get() objectForKey:@(last_focused_semantics_object_id_)]); } if (scrollOccured) { - // TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5") - ios_delegate_->PostAccessibilityNotification(UIAccessibilityPageScrolledNotification, @""); + // Tries to refocus the previous focused semantics object to avoid random jumps. + ios_delegate_->PostAccessibilityNotification( + UIAccessibilityPageScrolledNotification, + [objects_.get() objectForKey:@(last_focused_semantics_object_id_)]); } } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h index c2546ac7c3a2c..19b49140edc54 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h @@ -24,6 +24,13 @@ class AccessibilityBridgeIos { virtual void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action, std::vector args) = 0; + /** + * A callback that is called after the accessibility focus has moved to a new + * SemanticObject. + * + * The input id is the uid of the newly focused SemanticObject. + */ + virtual void AccessibilityFocusDidChange(int32_t id) = 0; virtual FlutterPlatformViewsController* GetPlatformViewsController() const = 0; }; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index b6e07133672e1..d5f972aa62ffc 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -96,7 +96,8 @@ void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} class MockIosDelegate : public AccessibilityBridge::IosDelegate { public: - bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { + bool IsFlutterViewControllerPresentingModalViewController( + FlutterViewController* view_controller) override { return result_IsFlutterViewControllerPresentingModalViewController_; }; @@ -157,9 +158,11 @@ - (void)testUpdateSemanticsEmpty { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg isNil]]); auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil); flutter::SemanticsNodeUpdates nodes; @@ -181,10 +184,12 @@ - (void)testUpdateSemanticsOneNode { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; __block auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil); @@ -224,6 +229,8 @@ - (void)testSemanticsDeallocated { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; auto flutterPlatformViewsController = @@ -243,7 +250,7 @@ - (void)testSemanticsDeallocated { result); auto bridge = std::make_unique( - /*view=*/mockFlutterView, + /*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/flutterPlatformViewsController.get()); @@ -274,6 +281,8 @@ - (void)testAnnouncesRouteChanges { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; NSMutableArray*>* accessibility_notifications = @@ -287,7 +296,7 @@ - (void)testAnnouncesRouteChanges { }]; }; __block auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil, /*ios_delegate=*/std::move(ios_delegate)); @@ -297,7 +306,6 @@ - (void)testAnnouncesRouteChanges { flutter::SemanticsNode route_node; route_node.id = 1; - route_node.label = label; route_node.flags = static_cast(flutter::SemanticsFlags::kScopesRoute) | static_cast(flutter::SemanticsFlags::kNamesRoute); route_node.label = "route"; @@ -318,6 +326,213 @@ - (void)testAnnouncesRouteChanges { UIAccessibilityScreenChangedNotification); } +- (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + id mockFlutterView = OCMClassMock([FlutterView class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + ios_delegate->on_PostAccessibilityNotification_ = + [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { + [accessibility_notifications addObject:@{ + @"notification" : @(notification), + @"argument" : argument ? argument : [NSNull null], + }]; + }; + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates first_update; + + flutter::SemanticsNode route_node; + route_node.id = 1; + route_node.label = "route"; + first_update[route_node.id] = route_node; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = "root"; + root_node.childrenInTraversalOrder = {1}; + root_node.childrenInHitTestOrder = {1}; + first_update[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions); + + XCTAssertEqual([accessibility_notifications count], 0ul); + // Simulates the focusing on the node 1. + bridge->AccessibilityFocusDidChange(1); + + flutter::SemanticsNodeUpdates second_update; + // Simulates the removal of the node 1 + flutter::SemanticsNode new_root_node; + new_root_node.id = kRootNodeId; + new_root_node.label = "root"; + second_update[root_node.id] = new_root_node; + bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions); + NSNull* focusObject = accessibility_notifications[0][@"argument"]; + // The node 1 was removed, so the bridge will set the focus object to nil. + XCTAssertEqual(focusObject, [NSNull null]); + XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], + UIAccessibilityLayoutChangedNotification); +} + +- (void)testAnnouncesLayoutChangeWithLastFocused { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + id mockFlutterView = OCMClassMock([FlutterView class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + ios_delegate->on_PostAccessibilityNotification_ = + [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { + [accessibility_notifications addObject:@{ + @"notification" : @(notification), + @"argument" : argument ? argument : [NSNull null], + }]; + }; + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates first_update; + + flutter::SemanticsNode node_one; + node_one.id = 1; + node_one.label = "route1"; + first_update[node_one.id] = node_one; + flutter::SemanticsNode node_two; + node_two.id = 2; + node_two.label = "route2"; + first_update[node_two.id] = node_two; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = "root"; + root_node.childrenInTraversalOrder = {1, 2}; + root_node.childrenInHitTestOrder = {1, 2}; + first_update[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions); + + XCTAssertEqual([accessibility_notifications count], 0ul); + // Simulates the focusing on the node 1. + bridge->AccessibilityFocusDidChange(1); + + flutter::SemanticsNodeUpdates second_update; + // Simulates the removal of the node 2. + flutter::SemanticsNode new_root_node; + new_root_node.id = kRootNodeId; + new_root_node.label = "root"; + new_root_node.childrenInTraversalOrder = {1}; + new_root_node.childrenInHitTestOrder = {1}; + second_update[root_node.id] = new_root_node; + bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions); + SemanticsObject* focusObject = accessibility_notifications[0][@"argument"]; + // Since we have focused on the node 1 right before the layout changed, the bridge should refocus + // the node 1. + XCTAssertEqual([focusObject uid], 1); + XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], + UIAccessibilityLayoutChangedNotification); +} + +- (void)testAnnouncesScrollChangeWithLastFocused { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + id mockFlutterView = OCMClassMock([FlutterView class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + ios_delegate->on_PostAccessibilityNotification_ = + [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { + [accessibility_notifications addObject:@{ + @"notification" : @(notification), + @"argument" : argument ? argument : [NSNull null], + }]; + }; + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates first_update; + + flutter::SemanticsNode node_one; + node_one.id = 1; + node_one.label = "route1"; + node_one.scrollPosition = 0.0; + first_update[node_one.id] = node_one; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = "root"; + root_node.childrenInTraversalOrder = {1}; + root_node.childrenInHitTestOrder = {1}; + first_update[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions); + + // The first update will trigger a scroll announcement, but we are not interested in it. + [accessibility_notifications removeAllObjects]; + + // Simulates the focusing on the node 1. + bridge->AccessibilityFocusDidChange(1); + + flutter::SemanticsNodeUpdates second_update; + // Simulates the scrolling on the node 1. + flutter::SemanticsNode new_node_one; + new_node_one.id = 1; + new_node_one.label = "route1"; + new_node_one.scrollPosition = 1.0; + second_update[new_node_one.id] = new_node_one; + bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions); + SemanticsObject* focusObject = accessibility_notifications[0][@"argument"]; + // Since we have focused on the node 1 right before the scrolling, the bridge should refocus the + // node 1. + XCTAssertEqual([focusObject uid], 1); + XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], + UIAccessibilityPageScrolledNotification); +} + - (void)testAnnouncesIgnoresRouteChangesWhenModal { flutter::MockDelegate mock_delegate; auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); @@ -331,6 +546,8 @@ - (void)testAnnouncesIgnoresRouteChangesWhenModal { /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, /*task_runners=*/runners); id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); std::string label = "some label"; NSMutableArray*>* accessibility_notifications = @@ -345,7 +562,7 @@ - (void)testAnnouncesIgnoresRouteChangesWhenModal { }; ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true; __block auto bridge = - std::make_unique(/*view=*/mockFlutterView, + std::make_unique(/*view_controller=*/mockFlutterViewController, /*platform_view=*/platform_view.get(), /*platform_views_controller=*/nil, /*ios_delegate=*/std::move(ios_delegate)); diff --git a/shell/platform/darwin/ios/ios_external_texture_gl.h b/shell/platform/darwin/ios/ios_external_texture_gl.h index 5c608506bb82f..6a63357a77381 100644 --- a/shell/platform/darwin/ios/ios_external_texture_gl.h +++ b/shell/platform/darwin/ios/ios_external_texture_gl.h @@ -25,6 +25,9 @@ class IOSExternalTextureGL final : public Texture { fml::CFRef cache_ref_; fml::CFRef texture_ref_; fml::CFRef buffer_ref_; + OSType pixel_format_ = 0; + fml::CFRef y_texture_ref_; + fml::CFRef uv_texture_ref_; // |Texture| void Paint(SkCanvas& canvas, @@ -51,6 +54,16 @@ class IOSExternalTextureGL final : public Texture { bool NeedUpdateTexture(bool freeze); + bool IsTexturesAvailable() const; + + void CreateYUVTexturesFromPixelBuffer(); + + void CreateRGBATextureFromPixelBuffer(); + + sk_sp CreateImageFromYUVTextures(GrContext* context, const SkRect& bounds); + + sk_sp CreateImageFromRGBATexture(GrContext* context, const SkRect& bounds); + FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalTextureGL); }; diff --git a/shell/platform/darwin/ios/ios_external_texture_gl.mm b/shell/platform/darwin/ios/ios_external_texture_gl.mm index bb56fc2849bcf..627da33f09793 100644 --- a/shell/platform/darwin/ios/ios_external_texture_gl.mm +++ b/shell/platform/darwin/ios/ios_external_texture_gl.mm @@ -10,8 +10,10 @@ #include "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" #include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/core/SkYUVAIndex.h" #include "third_party/skia/include/gpu/GrBackendSurface.h" #include "third_party/skia/include/gpu/GrDirectContext.h" +#include "third_party/skia/src/gpu/gl/GrGLDefines.h" namespace flutter { @@ -42,10 +44,19 @@ if (buffer_ref_ == nullptr) { return; } + if (pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + CreateYUVTexturesFromPixelBuffer(); + } else { + CreateRGBATextureFromPixelBuffer(); + } +} + +void IOSExternalTextureGL::CreateRGBATextureFromPixelBuffer() { CVOpenGLESTextureRef texture; CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( - kCFAllocatorDefault, cache_ref_, buffer_ref_, nullptr, GL_TEXTURE_2D, GL_RGBA, - static_cast(CVPixelBufferGetWidth(buffer_ref_)), + kCFAllocatorDefault, cache_ref_, buffer_ref_, /*textureAttributes=*/nullptr, GL_TEXTURE_2D, + GL_RGBA, static_cast(CVPixelBufferGetWidth(buffer_ref_)), static_cast(CVPixelBufferGetHeight(buffer_ref_)), GL_BGRA, GL_UNSIGNED_BYTE, 0, &texture); if (err != noErr) { @@ -55,10 +66,83 @@ } } +void IOSExternalTextureGL::CreateYUVTexturesFromPixelBuffer() { + size_t width = CVPixelBufferGetWidth(buffer_ref_); + size_t height = CVPixelBufferGetHeight(buffer_ref_); + { + CVOpenGLESTextureRef yTexture; + CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, cache_ref_, buffer_ref_, /*textureAttributes=*/nullptr, GL_TEXTURE_2D, + GL_LUMINANCE, width, height, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &yTexture); + if (err != noErr) { + FML_DCHECK(yTexture) << "Could not create texture from pixel buffer: " << err; + } else { + y_texture_ref_.Reset(yTexture); + } + } + + { + CVOpenGLESTextureRef uvTexture; + CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, cache_ref_, buffer_ref_, /*textureAttributes=*/nullptr, GL_TEXTURE_2D, + GL_LUMINANCE_ALPHA, width / 2, height / 2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, + &uvTexture); + if (err != noErr) { + FML_DCHECK(uvTexture) << "Could not create texture from pixel buffer: " << err; + } else { + uv_texture_ref_.Reset(uvTexture); + } + } +} + +sk_sp IOSExternalTextureGL::CreateImageFromRGBATexture(GrContext* context, + const SkRect& bounds) { + GrGLTextureInfo textureInfo = {CVOpenGLESTextureGetTarget(texture_ref_), + CVOpenGLESTextureGetName(texture_ref_), GL_RGBA8_OES}; + GrBackendTexture backendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, textureInfo); + sk_sp image = SkImage::MakeFromTexture(context, backendTexture, kTopLeft_GrSurfaceOrigin, + kRGBA_8888_SkColorType, kPremul_SkAlphaType, + /*imageColorSpace=*/nullptr); + return image; +} + +sk_sp IOSExternalTextureGL::CreateImageFromYUVTextures(GrContext* context, + const SkRect& bounds) { + GrGLTextureInfo yTextureInfo = {CVOpenGLESTextureGetTarget(y_texture_ref_), + CVOpenGLESTextureGetName(y_texture_ref_), GR_GL_LUMINANCE8}; + GrBackendTexture yBackendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, yTextureInfo); + GrGLTextureInfo uvTextureInfo = {CVOpenGLESTextureGetTarget(uv_texture_ref_), + CVOpenGLESTextureGetName(uv_texture_ref_), GR_GL_RGBA8}; + GrBackendTexture uvBackendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, + uvTextureInfo); + GrBackendTexture nv12TextureHandles[] = {yBackendTexture, uvBackendTexture}; + SkYUVAIndex yuvaIndices[4] = { + SkYUVAIndex{0, SkColorChannel::kR}, // Read Y data from the red channel of the first texture + SkYUVAIndex{1, SkColorChannel::kR}, // Read U data from the red channel of the second texture + SkYUVAIndex{ + 1, SkColorChannel::kA}, // Read V data from the alpha channel of the second texture, + // normal NV12 data V should be taken from the green channel, but + // currently only the uv texture created by GL_LUMINANCE_ALPHA + // can be used, so the V value is taken from the alpha channel + SkYUVAIndex{-1, SkColorChannel::kA}}; //-1 means to omit the alpha data of YUVA + SkISize size{yBackendTexture.width(), yBackendTexture.height()}; + sk_sp image = SkImage::MakeFromYUVATextures( + context, kRec601_SkYUVColorSpace, nv12TextureHandles, yuvaIndices, size, + kTopLeft_GrSurfaceOrigin, /*imageColorSpace=*/nullptr); + return image; +} + +bool IOSExternalTextureGL::IsTexturesAvailable() const { + return ((pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) && + (y_texture_ref_ && uv_texture_ref_)) || + (pixel_format_ == kCVPixelFormatType_32BGRA && texture_ref_); +} + bool IOSExternalTextureGL::NeedUpdateTexture(bool freeze) { // Update texture if `texture_ref_` is reset to `nullptr` when GrContext // is destroyed or new frame is ready. - return (!freeze && new_frame_ready_) || !texture_ref_; + return (!freeze && new_frame_ready_) || !IsTexturesAvailable(); } void IOSExternalTextureGL::Paint(SkCanvas& canvas, @@ -71,19 +155,23 @@ auto pixelBuffer = [external_texture_.get() copyPixelBuffer]; if (pixelBuffer) { buffer_ref_.Reset(pixelBuffer); + pixel_format_ = CVPixelBufferGetPixelFormatType(buffer_ref_); } CreateTextureFromPixelBuffer(); new_frame_ready_ = false; } - if (!texture_ref_) { + if (!IsTexturesAvailable()) { return; } - GrGLTextureInfo textureInfo = {CVOpenGLESTextureGetTarget(texture_ref_), - CVOpenGLESTextureGetName(texture_ref_), GL_RGBA8_OES}; - GrBackendTexture backendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, textureInfo); - sk_sp image = - SkImage::MakeFromTexture(context, backendTexture, kTopLeft_GrSurfaceOrigin, - kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr); + + sk_sp image = nullptr; + if (pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + image = CreateImageFromYUVTextures(context, bounds); + } else { + image = CreateImageFromRGBATexture(context, bounds); + } + FML_DCHECK(image) << "Failed to create SkImage from Texture."; if (image) { SkPaint paint; diff --git a/shell/platform/darwin/ios/ios_external_texture_metal.h b/shell/platform/darwin/ios/ios_external_texture_metal.h index 9cbf84431eb9d..90e9e95746778 100644 --- a/shell/platform/darwin/ios/ios_external_texture_metal.h +++ b/shell/platform/darwin/ios/ios_external_texture_metal.h @@ -33,6 +33,7 @@ class IOSExternalTextureMetal final : public Texture { std::atomic_bool texture_frame_available_; fml::CFRef last_pixel_buffer_; sk_sp external_image_; + OSType pixel_format_ = 0; // |Texture| void Paint(SkCanvas& canvas, @@ -55,6 +56,10 @@ class IOSExternalTextureMetal final : public Texture { sk_sp WrapExternalPixelBuffer(fml::CFRef pixel_buffer, GrDirectContext* context) const; + sk_sp WrapRGBAExternalPixelBuffer(fml::CFRef pixel_buffer, + GrDirectContext* context) const; + sk_sp WrapNV12ExternalPixelBuffer(fml::CFRef pixel_buffer, + GrDirectContext* context) const; FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalTextureMetal); }; diff --git a/shell/platform/darwin/ios/ios_external_texture_metal.mm b/shell/platform/darwin/ios/ios_external_texture_metal.mm index 46eff3415273b..058a4738ce76c 100644 --- a/shell/platform/darwin/ios/ios_external_texture_metal.mm +++ b/shell/platform/darwin/ios/ios_external_texture_metal.mm @@ -5,6 +5,7 @@ #include "flutter/shell/platform/darwin/ios/ios_external_texture_metal.h" #include "flutter/fml/logging.h" +#include "third_party/skia/include/core/SkYUVAIndex.h" #include "third_party/skia/include/gpu/GrBackendSurface.h" #include "third_party/skia/include/gpu/GrDirectContext.h" #include "third_party/skia/include/gpu/mtl/GrMtlTypes.h" @@ -35,6 +36,8 @@ auto pixel_buffer = fml::CFRef([external_texture_ copyPixelBuffer]); if (!pixel_buffer) { pixel_buffer = std::move(last_pixel_buffer_); + } else { + pixel_format_ = CVPixelBufferGetPixelFormatType(pixel_buffer); } // If the application told us there was a texture frame available but did not provide one when @@ -65,21 +68,130 @@ return nullptr; } + sk_sp image = nullptr; + if (pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixel_format_ == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + image = WrapNV12ExternalPixelBuffer(pixel_buffer, context); + } else { + image = WrapRGBAExternalPixelBuffer(pixel_buffer, context); + } + + if (!image) { + FML_DLOG(ERROR) << "Could not wrap Metal texture as a Skia image."; + } + + return image; +} + +sk_sp IOSExternalTextureMetal::WrapNV12ExternalPixelBuffer( + fml::CFRef pixel_buffer, + GrDirectContext* context) const { auto texture_size = SkISize::Make(CVPixelBufferGetWidth(pixel_buffer), CVPixelBufferGetHeight(pixel_buffer)); + CVMetalTextureRef y_metal_texture_raw = nullptr; + { + auto cv_return = + CVMetalTextureCacheCreateTextureFromImage(/*allocator=*/kCFAllocatorDefault, + /*textureCache=*/texture_cache_, + /*sourceImage=*/pixel_buffer, + /*textureAttributes=*/nullptr, + /*pixelFormat=*/MTLPixelFormatR8Unorm, + /*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*planeIndex=*/0u, + /*texture=*/&y_metal_texture_raw); + + if (cv_return != kCVReturnSuccess) { + FML_DLOG(ERROR) << "Could not create Metal texture from pixel buffer: CVReturn " << cv_return; + return nullptr; + } + } + + CVMetalTextureRef uv_metal_texture_raw = nullptr; + { + auto cv_return = + CVMetalTextureCacheCreateTextureFromImage(/*allocator=*/kCFAllocatorDefault, + /*textureCache=*/texture_cache_, + /*sourceImage=*/pixel_buffer, + /*textureAttributes=*/nullptr, + /*pixelFormat=*/MTLPixelFormatRG8Unorm, + /*width=*/texture_size.width() / 2, + /*height=*/texture_size.height() / 2, + /*planeIndex=*/1u, + /*texture=*/&uv_metal_texture_raw); + + if (cv_return != kCVReturnSuccess) { + FML_DLOG(ERROR) << "Could not create Metal texture from pixel buffer: CVReturn " << cv_return; + return nullptr; + } + } + + fml::CFRef y_metal_texture(y_metal_texture_raw); + + GrMtlTextureInfo y_skia_texture_info; + y_skia_texture_info.fTexture = sk_cf_obj{ + [reinterpret_cast(CVMetalTextureGetTexture(y_metal_texture)) retain]}; - CVMetalTextureRef metal_texture_raw = NULL; + GrBackendTexture y_skia_backend_texture(/*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*mipMapped=*/GrMipMapped ::kNo, + /*textureInfo=*/y_skia_texture_info); + + fml::CFRef uv_metal_texture(uv_metal_texture_raw); + + GrMtlTextureInfo uv_skia_texture_info; + uv_skia_texture_info.fTexture = sk_cf_obj{ + [reinterpret_cast(CVMetalTextureGetTexture(uv_metal_texture)) retain]}; + + GrBackendTexture uv_skia_backend_texture(/*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*mipMapped=*/GrMipMapped ::kNo, + /*textureInfo=*/uv_skia_texture_info); + GrBackendTexture nv12TextureHandles[] = {y_skia_backend_texture, uv_skia_backend_texture}; + SkYUVAIndex yuvaIndices[4] = { + SkYUVAIndex{0, SkColorChannel::kR}, // Read Y data from the red channel of the first texture + SkYUVAIndex{1, SkColorChannel::kR}, // Read U data from the red channel of the second texture + SkYUVAIndex{1, + SkColorChannel::kG}, // Read V data from the green channel of the second texture + SkYUVAIndex{-1, SkColorChannel::kA}}; //-1 means to omit the alpha data of YUVA + + struct ImageCaptures { + fml::CFRef buffer; + fml::CFRef y_texture; + fml::CFRef uv_texture; + }; + + auto captures = std::make_unique(); + captures->buffer = std::move(pixel_buffer); + captures->y_texture = std::move(y_metal_texture); + captures->uv_texture = std::move(uv_metal_texture); + + SkImage::TextureReleaseProc release_proc = [](SkImage::ReleaseContext release_context) { + auto captures = reinterpret_cast(release_context); + delete captures; + }; + sk_sp image = SkImage::MakeFromYUVATextures( + context, kRec601_SkYUVColorSpace, nv12TextureHandles, yuvaIndices, texture_size, + kTopLeft_GrSurfaceOrigin, /*imageColorSpace=*/nullptr, release_proc, captures.release()); + return image; +} + +sk_sp IOSExternalTextureMetal::WrapRGBAExternalPixelBuffer( + fml::CFRef pixel_buffer, + GrDirectContext* context) const { + auto texture_size = + SkISize::Make(CVPixelBufferGetWidth(pixel_buffer), CVPixelBufferGetHeight(pixel_buffer)); + CVMetalTextureRef metal_texture_raw = nullptr; auto cv_return = - CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, // allocator - texture_cache_, // texture cache - pixel_buffer, // source image - NULL, // texture attributes - MTLPixelFormatBGRA8Unorm, // pixel format - texture_size.width(), // width - texture_size.height(), // height - 0u, // plane index - &metal_texture_raw // [out] texture - ); + CVMetalTextureCacheCreateTextureFromImage(/*allocator=*/kCFAllocatorDefault, + /*textureCache=*/texture_cache_, + /*sourceImage=*/pixel_buffer, + /*textureAttributes=*/nullptr, + /*pixelFormat=*/MTLPixelFormatBGRA8Unorm, + /*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*planeIndex=*/0u, + /*texture=*/&metal_texture_raw); if (cv_return != kCVReturnSuccess) { FML_DLOG(ERROR) << "Could not create Metal texture from pixel buffer: CVReturn " << cv_return; @@ -92,11 +204,10 @@ skia_texture_info.fTexture = sk_cf_obj{ [reinterpret_cast(CVMetalTextureGetTexture(metal_texture)) retain]}; - GrBackendTexture skia_backend_texture(texture_size.width(), // width - texture_size.height(), // height - GrMipMapped ::kNo, // mip-mapped - skia_texture_info // texture info - ); + GrBackendTexture skia_backend_texture(/*width=*/texture_size.width(), + /*height=*/texture_size.height(), + /*mipMapped=*/GrMipMapped ::kNo, + /*textureInfo=*/skia_texture_info); struct ImageCaptures { fml::CFRef buffer; @@ -112,21 +223,12 @@ GrBackendTexture skia_backend_texture(texture_size.width(), // width delete captures; }; - auto image = SkImage::MakeFromTexture(context, // context - skia_backend_texture, // backend texture - kTopLeft_GrSurfaceOrigin, // origin - kBGRA_8888_SkColorType, // color type - kPremul_SkAlphaType, // alpha type - nullptr, // color space - release_proc, // release proc - captures.release() // release context - - ); - - if (!image) { - FML_DLOG(ERROR) << "Could not wrap Metal texture as a Skia image."; - } + auto image = + SkImage::MakeFromTexture(context, skia_backend_texture, kTopLeft_GrSurfaceOrigin, + kBGRA_8888_SkColorType, kPremul_SkAlphaType, + /*imageColorSpace=*/nullptr, release_proc, captures.release() + ); return image; } diff --git a/shell/platform/darwin/ios/ios_surface.h b/shell/platform/darwin/ios/ios_surface.h index 041e3bacedb32..a01f9f15caeed 100644 --- a/shell/platform/darwin/ios/ios_surface.h +++ b/shell/platform/darwin/ios/ios_surface.h @@ -34,8 +34,6 @@ class IOSSurface : public ExternalViewEmbedder { std::shared_ptr GetContext() const; - ExternalViewEmbedder* GetExternalViewEmbedderIfEnabled(); - virtual bool IsValid() const = 0; virtual void UpdateStorageSizeIfNecessary() = 0; @@ -88,6 +86,9 @@ class IOSSurface : public ExternalViewEmbedder { void EndFrame(bool should_resubmit_frame, fml::RefPtr raster_thread_merger) override; + // |ExternalViewEmbedder| + bool SupportsDynamicThreadMerging() override; + public: FML_DISALLOW_COPY_AND_ASSIGN(IOSSurface); }; diff --git a/shell/platform/darwin/ios/ios_surface.mm b/shell/platform/darwin/ios/ios_surface.mm index de330fb58fc42..65fccb052e78b 100644 --- a/shell/platform/darwin/ios/ios_surface.mm +++ b/shell/platform/darwin/ios/ios_surface.mm @@ -13,15 +13,6 @@ namespace flutter { -// The name of the Info.plist flag to enable the embedded iOS views preview. -constexpr const char* kEmbeddedViewsPreview = "io.flutter.embedded_views_preview"; - -bool IsIosEmbeddedViewsPreviewEnabled() { - static bool preview_enabled = - [[[NSBundle mainBundle] objectForInfoDictionaryKey:@(kEmbeddedViewsPreview)] boolValue]; - return preview_enabled; -} - std::unique_ptr IOSSurface::Create( std::shared_ptr context, fml::scoped_nsobject layer, @@ -75,14 +66,6 @@ bool IsIosEmbeddedViewsPreviewEnabled() { return nullptr; } -ExternalViewEmbedder* IOSSurface::GetExternalViewEmbedderIfEnabled() { - if (IsIosEmbeddedViewsPreviewEnabled()) { - return this; - } else { - return nullptr; - } -} - // |ExternalViewEmbedder| void IOSSurface::CancelFrame() { TRACE_EVENT0("flutter", "IOSSurface::CancelFrame"); @@ -155,4 +138,9 @@ bool IsIosEmbeddedViewsPreviewEnabled() { return platform_views_controller_->EndFrame(should_resubmit_frame, raster_thread_merger); } +// |ExternalViewEmbedder| +bool IOSSurface::SupportsDynamicThreadMerging() { + return true; +} + } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_gl.h b/shell/platform/darwin/ios/ios_surface_gl.h index e6433eb83ef56..745b5d0070dc4 100644 --- a/shell/platform/darwin/ios/ios_surface_gl.h +++ b/shell/platform/darwin/ios/ios_surface_gl.h @@ -43,7 +43,7 @@ class IOSSurfaceGL final : public IOSSurface, public GPUSurfaceGLDelegate { bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| bool SurfaceSupportsReadback() const override; diff --git a/shell/platform/darwin/ios/ios_surface_gl.mm b/shell/platform/darwin/ios/ios_surface_gl.mm index 3531f61c3c0df..05d2d313566fe 100644 --- a/shell/platform/darwin/ios/ios_surface_gl.mm +++ b/shell/platform/darwin/ios/ios_surface_gl.mm @@ -44,7 +44,7 @@ } // |GPUSurfaceGLDelegate| -intptr_t IOSSurfaceGL::GLContextFBO() const { +intptr_t IOSSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { return IsValid() ? render_target_->GetFramebuffer() : GL_NONE; } @@ -84,7 +84,7 @@ // |GPUSurfaceGLDelegate| ExternalViewEmbedder* IOSSurfaceGL::GetExternalViewEmbedder() { - return GetExternalViewEmbedderIfEnabled(); + return this; } } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_metal.mm b/shell/platform/darwin/ios/ios_surface_metal.mm index 60afa6fd69775..df0d2739cfce7 100644 --- a/shell/platform/darwin/ios/ios_surface_metal.mm +++ b/shell/platform/darwin/ios/ios_surface_metal.mm @@ -55,7 +55,7 @@ // |GPUSurfaceDelegate| ExternalViewEmbedder* IOSSurfaceMetal::GetExternalViewEmbedder() { - return GetExternalViewEmbedderIfEnabled(); + return this; } } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_software.mm b/shell/platform/darwin/ios/ios_surface_software.mm index a68e0509f8ab4..03e85aec611f7 100644 --- a/shell/platform/darwin/ios/ios_surface_software.mm +++ b/shell/platform/darwin/ios/ios_surface_software.mm @@ -124,7 +124,7 @@ // |GPUSurfaceSoftwareDelegate| ExternalViewEmbedder* IOSSurfaceSoftware::GetExternalViewEmbedder() { - return GetExternalViewEmbedderIfEnabled(); + return this; } } // namespace flutter diff --git a/shell/platform/darwin/ios/platform_view_ios.mm b/shell/platform/darwin/ios/platform_view_ios.mm index c00ecb9c0bc05..e36a3bb7ce2a2 100644 --- a/shell/platform/darwin/ios/platform_view_ios.mm +++ b/shell/platform/darwin/ios/platform_view_ios.mm @@ -107,9 +107,8 @@ FML_DCHECK(ios_surface_ != nullptr); if (accessibility_bridge_) { - accessibility_bridge_.reset( - new AccessibilityBridge(static_cast(owner_controller_.get().view), this, - [owner_controller_.get() platformViewsController])); + accessibility_bridge_.reset(new AccessibilityBridge( + owner_controller_.get(), this, [owner_controller_.get() platformViewsController])); } } @@ -150,9 +149,8 @@ new AccessibilityBridge(static_cast(owner_controller_.get().view), return; } if (enabled && !accessibility_bridge_) { - accessibility_bridge_.reset( - new AccessibilityBridge(static_cast(owner_controller_.get().view), this, - [owner_controller_.get() platformViewsController])); + accessibility_bridge_.reset(new AccessibilityBridge( + owner_controller_.get(), this, [owner_controller_.get() platformViewsController])); } else if (!enabled && accessibility_bridge_) { accessibility_bridge_.reset(); } else { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 104733867b317..36b24f3424ddc 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -13,11 +13,30 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/embedder/embedder.h" +/** + * Constructs and returns a FlutterLocale struct corresponding to |locale|, which must outlive + * the returned struct. + */ +static FlutterLocale FlutterLocaleFromNSLocale(NSLocale* locale) { + FlutterLocale flutterLocale = {}; + flutterLocale.struct_size = sizeof(FlutterLocale); + flutterLocale.language_code = [[locale objectForKey:NSLocaleLanguageCode] UTF8String]; + flutterLocale.country_code = [[locale objectForKey:NSLocaleCountryCode] UTF8String]; + flutterLocale.script_code = [[locale objectForKey:NSLocaleScriptCode] UTF8String]; + flutterLocale.variant_code = [[locale objectForKey:NSLocaleVariantCode] UTF8String]; + return flutterLocale; +} + /** * Private interface declaration for FlutterEngine. */ @interface FlutterEngine () +/** + * Sends the list of user-preferred locales to the Flutter engine. + */ +- (void)sendUserLocales; + /** * Called by the engine to make the context the engine should draw into current. */ @@ -181,6 +200,12 @@ - (instancetype)initWithName:(NSString*)labelPrefix _textures = [[NSMutableDictionary alloc] init]; _allowHeadlessExecution = allowHeadlessExecution; + NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self + selector:@selector(sendUserLocales) + name:NSCurrentLocaleDidChangeNotification + object:nil]; + return self; } @@ -254,6 +279,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { return NO; } + [self sendUserLocales]; [self updateWindowMetrics]; return YES; } @@ -314,6 +340,29 @@ - (void)sendPointerEvent:(const FlutterPointerEvent&)event { #pragma mark - Private methods +- (void)sendUserLocales { + if (!self.running) { + return; + } + + // Create a list of FlutterLocales corresponding to the preferred languages. + NSMutableArray* locales = [NSMutableArray array]; + std::vector flutterLocales; + flutterLocales.reserve(locales.count); + for (NSString* localeID in [NSLocale preferredLanguages]) { + NSLocale* locale = [[NSLocale alloc] initWithLocaleIdentifier:localeID]; + [locales addObject:locale]; + flutterLocales.push_back(FlutterLocaleFromNSLocale(locale)); + } + // Convert to a list of pointers, and send to the engine. + std::vector flutterLocaleList; + flutterLocaleList.reserve(flutterLocales.size()); + std::transform( + flutterLocales.begin(), flutterLocales.end(), std::back_inserter(flutterLocaleList), + [](const auto& arg) -> const auto* { return &arg; }); + FlutterEngineUpdateLocales(_engine, flutterLocaleList.data(), flutterLocaleList.size()); +} + - (bool)engineCallbackOnMakeCurrent { if (!_mainOpenGLContext) { return false; @@ -329,6 +378,7 @@ - (bool)engineCallbackOnClearCurrent { - (bool)engineCallbackOnPresent { if (!_mainOpenGLContext) { + return false; } [_mainOpenGLContext flushBuffer]; return true; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index bb928da63a79e..b1a6055e28f70 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -303,6 +303,7 @@ - (BOOL)launchEngine { } // Send the initial user settings such as brightness and text scale factor // to the engine. + // TODO(stuartmorgan): Move this logic to FlutterEngine. [self sendInitialSettings]; return YES; } diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 6b3fa9ceaddbb..8a94ee5eb2dc9 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -93,8 +93,17 @@ static bool IsOpenGLRendererConfigValid(const FlutterRendererConfig* config) { if (SAFE_ACCESS(open_gl_config, make_current, nullptr) == nullptr || SAFE_ACCESS(open_gl_config, clear_current, nullptr) == nullptr || - SAFE_ACCESS(open_gl_config, present, nullptr) == nullptr || - SAFE_ACCESS(open_gl_config, fbo_callback, nullptr) == nullptr) { + SAFE_ACCESS(open_gl_config, present, nullptr) == nullptr) { + return false; + } + + bool fbo_callback_exists = + SAFE_ACCESS(open_gl_config, fbo_callback, nullptr) != nullptr; + bool fbo_with_frame_info_callback_exists = + SAFE_ACCESS(open_gl_config, fbo_with_frame_info_callback, nullptr) != + nullptr; + // only one of these callbacks must exist. + if (fbo_callback_exists == fbo_with_frame_info_callback_exists) { return false; } @@ -168,8 +177,20 @@ InferOpenGLPlatformViewCreationCallback( return ptr(user_data); }; - auto gl_fbo_callback = [ptr = config->open_gl.fbo_callback, - user_data]() -> intptr_t { return ptr(user_data); }; + auto gl_fbo_callback = + [fbo_callback = config->open_gl.fbo_callback, + fbo_with_frame_info_callback = + config->open_gl.fbo_with_frame_info_callback, + user_data](flutter::GLFrameInfo gl_frame_info) -> intptr_t { + if (fbo_callback) { + return fbo_callback(user_data); + } else { + FlutterFrameInfo frame_info = {}; + frame_info.struct_size = sizeof(FlutterFrameInfo); + frame_info.size = {gl_frame_info.width, gl_frame_info.height}; + return fbo_with_frame_info_callback(user_data, &frame_info); + } + }; const FlutterOpenGLRendererConfig* open_gl_config = &config->open_gl; std::function gl_make_resource_current_callback = nullptr; @@ -991,8 +1012,7 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, flutter::Shell::CreateCallback on_create_rasterizer = [](flutter::Shell& shell) { - return std::make_unique( - shell, shell.GetTaskRunners(), shell.GetIsGpuDisabledSyncSwitch()); + return std::make_unique(shell); }; // TODO(chinmaygarde): This is the wrong spot for this. It belongs in the diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 6277ccd4e80c8..9e468e484d312 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -9,6 +9,39 @@ #include #include +// This file defines an Application Binary Interface (ABI), which requires more +// stability than regular code to remain functional for exchanging messages +// between different versions of the embedding and the engine, to allow for both +// forward and backward compatibility. +// +// Specifically, +// - The order, type, and size of the struct members below must remain the same, +// and members should not be removed. +// - New structures that are part of the ABI must be defined with "size_t +// struct_size;" as their first member, which should be initialized using +// "sizeof(Type)". +// - Enum values must not change or be removed. +// - Enum members without explicit values must not be reordered. +// - Function signatures (names, argument counts, argument order, and argument +// type) cannot change. +// - The core behavior of existing functions cannot change. +// +// These changes are allowed: +// - Adding new struct members at the end of a structure. +// - Adding new enum members with a new value. +// - Renaming a struct member as long as its type, size, and intent remain the +// same. +// - Renaming an enum member as long as its value and intent remains the same. +// +// It is expected that struct members and implicitly-valued enums will not +// always be declared in an order that is optimal for the reader, since members +// will be added over time, and they can't be reordered. +// +// Existing functions should continue to appear from the caller's point of view +// to operate as they did when they were first introduced, so introduce a new +// function instead of modifying the core behavior of a function (and continue +// to support the existing function with the previous behavior). + #if defined(__cplusplus) extern "C" { #endif @@ -273,12 +306,69 @@ typedef bool (*TextureFrameCallback)(void* /* user data */, FlutterOpenGLTexture* /* texture out */); typedef void (*VsyncCallback)(void* /* user data */, intptr_t /* baton */); +/// A structure to represent the width and height. +typedef struct { + double width; + double height; +} FlutterSize; + +/// A structure to represent the width and height. +/// +/// See: \ref FlutterSize when the value are not integers. +typedef struct { + uint32_t width; + uint32_t height; +} FlutterUIntSize; + +/// A structure to represent a rectangle. +typedef struct { + double left; + double top; + double right; + double bottom; +} FlutterRect; + +/// A structure to represent a 2D point. +typedef struct { + double x; + double y; +} FlutterPoint; + +/// A structure to represent a rounded rectangle. +typedef struct { + FlutterRect rect; + FlutterSize upper_left_corner_radius; + FlutterSize upper_right_corner_radius; + FlutterSize lower_right_corner_radius; + FlutterSize lower_left_corner_radius; +} FlutterRoundedRect; + +/// This information is passed to the embedder when requesting a frame buffer +/// object. +/// +/// See: \ref FlutterSoftwareRendererConfig.fbo_with_frame_info_callback. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterFrameInfo). + size_t struct_size; + /// The size of the surface that will be backed by the fbo. + FlutterUIntSize size; +} FlutterFrameInfo; + +typedef uint32_t (*UIntFrameInfoCallback)( + void* /* user data */, + const FlutterFrameInfo* /* frame info */); + typedef struct { /// The size of this struct. Must be sizeof(FlutterOpenGLRendererConfig). size_t struct_size; BoolCallback make_current; BoolCallback clear_current; BoolCallback present; + /// Specifying one (and only one) of the `fbo_callback` or + /// `fbo_with_frame_info_callback` is required. Specifying both is an error + /// and engine intialization will be terminated. The return value indicates + /// the id of the frame buffer object that flutter will obtain the gl surface + /// from. UIntCallback fbo_callback; /// This is an optional callback. Flutter will ask the emebdder to create a GL /// context current on a background thread. If the embedder is able to do so, @@ -309,6 +399,14 @@ typedef struct { /// that external texture details can be supplied to the engine for subsequent /// composition. TextureFrameCallback gl_external_texture_frame_callback; + /// Specifying one (and only one) of the `fbo_callback` or + /// `fbo_with_frame_info_callback` is required. Specifying both is an error + /// and engine intialization will be terminated. The return value indicates + /// the id of the frame buffer object (fbo) that flutter will obtain the gl + /// surface from. When using this variant, the embedder is passed a + /// `FlutterFrameInfo` struct that indicates the properties of the surface + /// that flutter will acquire from the returned fbo. + UIntFrameInfoCallback fbo_with_frame_info_callback; } FlutterOpenGLRendererConfig; typedef struct { @@ -457,31 +555,6 @@ typedef void (*FlutterDataCallback)(const uint8_t* /* data */, size_t /* size */, void* /* user data */); -typedef struct { - double left; - double top; - double right; - double bottom; -} FlutterRect; - -typedef struct { - double x; - double y; -} FlutterPoint; - -typedef struct { - double width; - double height; -} FlutterSize; - -typedef struct { - FlutterRect rect; - FlutterSize upper_left_corner_radius; - FlutterSize upper_right_corner_radius; - FlutterSize lower_right_corner_radius; - FlutterSize lower_left_corner_radius; -} FlutterRoundedRect; - /// The identifier of the platform view. This identifier is specified by the /// application when a platform view is added to the scene via the /// `SceneBuilder.addPlatformView` call. diff --git a/shell/platform/embedder/embedder_engine.h b/shell/platform/embedder/embedder_engine.h index 124f8af0cf9aa..151b489bb9465 100644 --- a/shell/platform/embedder/embedder_engine.h +++ b/shell/platform/embedder/embedder_engine.h @@ -12,7 +12,6 @@ #include "flutter/shell/common/shell.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/platform/embedder/embedder.h" -#include "flutter/shell/platform/embedder/embedder_engine.h" #include "flutter/shell/platform/embedder/embedder_external_texture_gl.h" #include "flutter/shell/platform/embedder/embedder_thread_host.h" diff --git a/shell/platform/embedder/embedder_surface_gl.cc b/shell/platform/embedder/embedder_surface_gl.cc index 3deb7b5f3032c..4a28d68ecb295 100644 --- a/shell/platform/embedder/embedder_surface_gl.cc +++ b/shell/platform/embedder/embedder_surface_gl.cc @@ -50,8 +50,8 @@ bool EmbedderSurfaceGL::GLContextPresent() { } // |GPUSurfaceGLDelegate| -intptr_t EmbedderSurfaceGL::GLContextFBO() const { - return gl_dispatch_table_.gl_fbo_callback(); +intptr_t EmbedderSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { + return gl_dispatch_table_.gl_fbo_callback(frame_info); } // |GPUSurfaceGLDelegate| diff --git a/shell/platform/embedder/embedder_surface_gl.h b/shell/platform/embedder/embedder_surface_gl.h index 2a3a76748db17..6a4b8ed94703d 100644 --- a/shell/platform/embedder/embedder_surface_gl.h +++ b/shell/platform/embedder/embedder_surface_gl.h @@ -19,7 +19,7 @@ class EmbedderSurfaceGL final : public EmbedderSurface, std::function gl_make_current_callback; // required std::function gl_clear_current_callback; // required std::function gl_present_callback; // required - std::function gl_fbo_callback; // required + std::function gl_fbo_callback; // required std::function gl_make_resource_current_callback; // optional std::function gl_surface_transformation_callback; // optional @@ -59,7 +59,7 @@ class EmbedderSurfaceGL final : public EmbedderSurface, bool GLContextPresent() override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO() const override; + intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| bool GLContextFBOResetAfterPresent() const override; diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index 1c08b98fde1f9..f69161e6e23cc 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -35,8 +35,10 @@ EmbedderConfigBuilder::EmbedderConfigBuilder( opengl_renderer_config_.present = [](void* context) -> bool { return reinterpret_cast(context)->GLPresent(); }; - opengl_renderer_config_.fbo_callback = [](void* context) -> uint32_t { - return reinterpret_cast(context)->GLGetFramebuffer(); + opengl_renderer_config_.fbo_with_frame_info_callback = + [](void* context, const FlutterFrameInfo* frame_info) -> uint32_t { + return reinterpret_cast(context)->GLGetFramebuffer( + *frame_info); }; opengl_renderer_config_.make_resource_current = [](void* context) -> bool { return reinterpret_cast(context) @@ -110,6 +112,21 @@ void EmbedderConfigBuilder::SetSoftwareRendererConfig(SkISize surface_size) { context_.SetupOpenGLSurface(surface_size); } +void EmbedderConfigBuilder::SetOpenGLFBOCallBack() { + // SetOpenGLRendererConfig must be called before this. + FML_CHECK(renderer_config_.type == FlutterRendererType::kOpenGL); + renderer_config_.open_gl.fbo_callback = [](void* context) -> uint32_t { + FlutterFrameInfo frame_info = {}; + // fbo_callback doesn't use the frame size information, only + // fbo_callback_with_frame_info does. + frame_info.struct_size = sizeof(FlutterFrameInfo); + frame_info.size.width = 0; + frame_info.size.height = 0; + return reinterpret_cast(context)->GLGetFramebuffer( + frame_info); + }; +} + void EmbedderConfigBuilder::SetOpenGLRendererConfig(SkISize surface_size) { renderer_config_.type = FlutterRendererType::kOpenGL; renderer_config_.open_gl = opengl_renderer_config_; diff --git a/shell/platform/embedder/tests/embedder_config_builder.h b/shell/platform/embedder/tests/embedder_config_builder.h index a386cd91d0c6d..e6f0016918776 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.h +++ b/shell/platform/embedder/tests/embedder_config_builder.h @@ -49,6 +49,12 @@ class EmbedderConfigBuilder { void SetOpenGLRendererConfig(SkISize surface_size); + // Used to explicitly set an `open_gl.fbo_callback`. Using this method will + // cause your test to fail since the ctor for this class sets + // `open_gl.fbo_callback_with_frame_info`. This method exists as a utility to + // explicitly test this behavior. + void SetOpenGLFBOCallBack(); + void SetAssetsPath(); void SetSnapshots(); diff --git a/shell/platform/embedder/tests/embedder_test_context.cc b/shell/platform/embedder/tests/embedder_test_context.cc index be7f7641cc586..048d5d9c14111 100644 --- a/shell/platform/embedder/tests/embedder_test_context.cc +++ b/shell/platform/embedder/tests/embedder_test_context.cc @@ -197,11 +197,16 @@ bool EmbedderTestContext::GLPresent() { return true; } -uint32_t EmbedderTestContext::GLGetFramebuffer() { +uint32_t EmbedderTestContext::GLGetFramebuffer(FlutterFrameInfo frame_info) { FML_CHECK(gl_surface_) << "GL surface must be initialized."; + gl_surface_fbo_frame_infos_.push_back(frame_info); return gl_surface_->GetFramebuffer(); } +std::vector EmbedderTestContext::GetGLFBOFrameInfos() { + return gl_surface_fbo_frame_infos_; +} + bool EmbedderTestContext::GLMakeResourceCurrent() { FML_CHECK(gl_surface_) << "GL surface must be initialized."; return gl_surface_->MakeResourceCurrent(); diff --git a/shell/platform/embedder/tests/embedder_test_context.h b/shell/platform/embedder/tests/embedder_test_context.h index 56a44f6b5efe6..f4135c07d7f7c 100644 --- a/shell/platform/embedder/tests/embedder_test_context.h +++ b/shell/platform/embedder/tests/embedder_test_context.h @@ -79,6 +79,9 @@ class EmbedderTestContext { size_t GetSoftwareSurfacePresentCount() const; + // Returns the frame information for all the frames that were rendered. + std::vector GetGLFBOFrameInfos(); + private: // This allows the builder to access the hooks. friend class EmbedderConfigBuilder; @@ -101,6 +104,7 @@ class EmbedderTestContext { std::unique_ptr compositor_; NextSceneCallback next_scene_callback_; SkMatrix root_surface_transformation_; + std::vector gl_surface_fbo_frame_infos_; size_t gl_surface_present_count_ = 0; size_t software_surface_present_count_ = 0; @@ -133,7 +137,7 @@ class EmbedderTestContext { bool GLPresent(); - uint32_t GLGetFramebuffer(); + uint32_t GLGetFramebuffer(FlutterFrameInfo frame_info); bool GLMakeResourceCurrent(); diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index 558669ddb550c..f235613698f6a 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -4352,5 +4352,60 @@ TEST_F(EmbedderTest, CanLaunchAndShutdownWithAValidElfSource) { engine.reset(); } +TEST_F(EmbedderTest, FrameInfoContainsValidWidthAndHeight) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 1024)); + builder.SetDartEntrypoint("push_frames_over_and_over"); + + const auto root_surface_transformation = + SkMatrix().preTranslate(0, 1024).preRotate(-90, 0, 0); + + context.SetRootSurfaceTransformation(root_surface_transformation); + + auto engine = builder.LaunchEngine(); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 1024; + event.height = 600; + event.pixel_ratio = 1.0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + ASSERT_TRUE(engine.is_valid()); + + constexpr size_t frames_expected = 10; + fml::CountDownLatch frame_latch(frames_expected); + size_t frames_seen = 0; + context.AddNativeCallback("SignalNativeTest", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + frames_seen++; + frame_latch.CountDown(); + })); + frame_latch.Wait(); + + ASSERT_EQ(frames_expected, frames_seen); + ASSERT_EQ(context.GetGLFBOFrameInfos().size(), frames_seen); + + for (FlutterFrameInfo frame_info : context.GetGLFBOFrameInfos()) { + // width and height are rotated by 90 deg + ASSERT_EQ(frame_info.size.width, event.height); + ASSERT_EQ(frame_info.size.height, event.width); + } +} + +TEST_F(EmbedderTest, MustNotRunWithBothFBOCallbacksSet) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 1024)); + builder.SetOpenGLFBOCallBack(); + + auto engine = builder.LaunchEngine(); + ASSERT_FALSE(engine.is_valid()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart b/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart index 1b599e793f9e8..e2619c5c39e2a 100644 --- a/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart +++ b/shell/platform/fuchsia/dart-pkg/fuchsia/lib/fuchsia.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. -// @dart = 2.6 library fuchsia; import 'dart:io'; @@ -14,25 +13,25 @@ import 'dart:zircon'; // TODO: refactors this incomingServices instead @pragma('vm:entry-point') -Handle _environment; +Handle? _environment; @pragma('vm:entry-point') -Handle _outgoingServices; +Handle? _outgoingServices; @pragma('vm:entry-point') -Handle _viewRef; +Handle? _viewRef; class MxStartupInfo { // TODO: refactor Handle to a Channel // https://github.com/flutter/flutter/issues/49439 static Handle takeEnvironment() { - if (_outgoingServices == null && Platform.isFuchsia) { + if (_environment == null && Platform.isFuchsia) { throw Exception( 'Attempting to call takeEnvironment more than once per process'); } - Handle handle = _environment; + final handle = _environment; _environment = null; - return handle; + return handle!; } // TODO: refactor Handle to a Channel @@ -42,9 +41,9 @@ class MxStartupInfo { throw Exception( 'Attempting to call takeOutgoingServices more than once per process'); } - Handle handle = _outgoingServices; + final handle = _outgoingServices; _outgoingServices = null; - return handle; + return handle!; } // TODO: refactor Handle to a ViewRef @@ -54,9 +53,9 @@ class MxStartupInfo { throw Exception( 'Attempting to call takeViewRef more than once per process'); } - Handle handle = _viewRef; + final handle = _viewRef; _viewRef = null; - return handle; + return handle!; } } diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.dart index 2e78559bc5de2..da74c9bf658e7 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle.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. -// @dart = 2.6 part of zircon; // ignore_for_file: native_function_body_in_non_sdk_code diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.dart index 169fa41efdca2..e233f42ca4f7b 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/handle_waiter.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. -// @dart = 2.6 part of zircon; // ignore_for_file: native_function_body_in_non_sdk_code diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.dart index 69eba1377d1a1..425e321cff87f 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/src/system.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. -// @dart = 2.6 part of zircon; // ignore_for_file: native_function_body_in_non_sdk_code @@ -16,12 +15,12 @@ class _Namespace { // ignore: unused_element // Library private variable set by the embedder used to cache the // namespace (as an fdio_ns_t*). @pragma('vm:entry-point') - static int _namespace; // ignore: unused_field + static int? _namespace; // ignore: unused_field } /// An exception representing an error returned as an zx_status_t. class ZxStatusException implements Exception { - final String message; + final String? message; final int status; ZxStatusException(this.status, [this.message]); @@ -35,6 +34,9 @@ class ZxStatusException implements Exception { } } +/// Users of the [_Result] subclasses should check the status before +/// trying to read any data. Attempting to use a value stored in a result +/// when the status in not OK will result in an exception. class _Result { final int status; const _Result(this.status); @@ -42,78 +44,107 @@ class _Result { @pragma('vm:entry-point') class HandleResult extends _Result { - final Handle handle; + final Handle? _handle; + Handle get handle => _handle!; + @pragma('vm:entry-point') - const HandleResult(final int status, [this.handle]) : super(status); + const HandleResult(final int status, [this._handle]) : super(status); @override - String toString() => 'HandleResult(status=$status, handle=$handle)'; + String toString() => 'HandleResult(status=$status, handle=$_handle)'; } @pragma('vm:entry-point') class HandlePairResult extends _Result { - final Handle first; - final Handle second; + final Handle? _first; + final Handle? _second; + + Handle get first => _first!; + Handle get second => _second!; + @pragma('vm:entry-point') - const HandlePairResult(final int status, [this.first, this.second]) + const HandlePairResult(final int status, [this._first, this._second]) : super(status); @override String toString() => - 'HandlePairResult(status=$status, first=$first, second=$second)'; + 'HandlePairResult(status=$status, first=$_first, second=$_second)'; } @pragma('vm:entry-point') class ReadResult extends _Result { - final ByteData bytes; - final int numBytes; - final List handles; + final ByteData? _bytes; + final int? _numBytes; + final List? _handles; + + ByteData get bytes => _bytes!; + int get numBytes => _numBytes!; + List get handles => _handles!; + @pragma('vm:entry-point') - const ReadResult(final int status, [this.bytes, this.numBytes, this.handles]) + const ReadResult(final int status, [this._bytes, this._numBytes, this._handles]) : super(status); - Uint8List bytesAsUint8List() => - bytes.buffer.asUint8List(bytes.offsetInBytes, numBytes); + + /// Returns the bytes as a Uint8List. If status != OK this will throw + /// an exception. + Uint8List bytesAsUint8List() { + return _bytes!.buffer.asUint8List(_bytes!.offsetInBytes, _numBytes!); + } + + /// Returns the bytes as a String. If status != OK this will throw + /// an exception. String bytesAsUTF8String() => utf8.decode(bytesAsUint8List()); + @override String toString() => - 'ReadResult(status=$status, bytes=$bytes, numBytes=$numBytes, handles=$handles)'; + 'ReadResult(status=$status, bytes=$_bytes, numBytes=$_numBytes, handles=$_handles)'; } @pragma('vm:entry-point') class WriteResult extends _Result { - final int numBytes; + final int? _numBytes; + int get numBytes => _numBytes!; + @pragma('vm:entry-point') - const WriteResult(final int status, [this.numBytes]) : super(status); + const WriteResult(final int status, [this._numBytes]) : super(status); @override - String toString() => 'WriteResult(status=$status, numBytes=$numBytes)'; + String toString() => 'WriteResult(status=$status, numBytes=$_numBytes)'; } @pragma('vm:entry-point') class GetSizeResult extends _Result { - final int size; + final int? _size; + int get size => _size!; + @pragma('vm:entry-point') - const GetSizeResult(final int status, [this.size]) : super(status); + const GetSizeResult(final int status, [this._size]) : super(status); @override - String toString() => 'GetSizeResult(status=$status, size=$size)'; + String toString() => 'GetSizeResult(status=$status, size=$_size)'; } @pragma('vm:entry-point') class FromFileResult extends _Result { - final Handle handle; - final int numBytes; + final Handle? _handle; + final int? _numBytes; + + Handle get handle => _handle!; + int get numBytes => _numBytes!; + @pragma('vm:entry-point') - const FromFileResult(final int status, [this.handle, this.numBytes]) + const FromFileResult(final int status, [this._handle, this._numBytes]) : super(status); @override String toString() => - 'FromFileResult(status=$status, handle=$handle, numBytes=$numBytes)'; + 'FromFileResult(status=$status, handle=$_handle, numBytes=$_numBytes)'; } @pragma('vm:entry-point') class MapResult extends _Result { - final Uint8List data; + final Uint8List? _data; + Uint8List get data => _data!; + @pragma('vm:entry-point') - const MapResult(final int status, [this.data]) : super(status); + const MapResult(final int status, [this._data]) : super(status); @override - String toString() => 'MapResult(status=$status, data=$data)'; + String toString() => 'MapResult(status=$status, data=$_data)'; } @pragma('vm:entry-point') diff --git a/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.dart b/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.dart index 691e59e7f89fd..5e80671ffb526 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.dart +++ b/shell/platform/fuchsia/dart-pkg/zircon/lib/zircon.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. -// @dart = 2.6 library zircon; import 'dart:convert' show utf8; diff --git a/shell/platform/fuchsia/dart_runner/BUILD.gn b/shell/platform/fuchsia/dart_runner/BUILD.gn index c69df5c1222b6..4e3f514608a3a 100644 --- a/shell/platform/fuchsia/dart_runner/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/BUILD.gn @@ -18,6 +18,9 @@ template("runner") { invoker_output_name = invoker.output_name extra_defines = invoker.extra_defines extra_deps = invoker.extra_deps + if (is_debug) { + extra_defines += [ "DEBUG" ] # Needed due to direct dart dependencies. + } executable(target_name) { output_name = invoker_output_name diff --git a/shell/platform/fuchsia/dart_runner/embedder/builtin.dart b/shell/platform/fuchsia/dart_runner/embedder/builtin.dart index 937a5f21339b2..7972b08e0c932 100644 --- a/shell/platform/fuchsia/dart_runner/embedder/builtin.dart +++ b/shell/platform/fuchsia/dart_runner/embedder/builtin.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. -// @dart = 2.6 library fuchsia_builtin; import 'dart:async'; @@ -25,7 +24,7 @@ class _Logger { } @pragma('vm:entry-point') -String _rawScript; +late String _rawScript; Uri _scriptUri() { if (_rawScript.startsWith('http:') || diff --git a/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn b/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn index faca6e63aab06..7bab46d61592f 100644 --- a/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn @@ -21,7 +21,7 @@ compile_platform("kernel_platform_files") { args = [ "--enable-experiment=non-nullable", - "--nnbd-weak", + "--nnbd-agnostic", # TODO(dartbug.com/36342): enable bytecode for core libraries when performance of bytecode # pipeline is on par with default pipeline and continuously tracked. diff --git a/shell/platform/fuchsia/flutter/BUILD.gn b/shell/platform/fuchsia/flutter/BUILD.gn index ba68d1bac6905..a5da4925a2eff 100644 --- a/shell/platform/fuchsia/flutter/BUILD.gn +++ b/shell/platform/fuchsia/flutter/BUILD.gn @@ -12,29 +12,206 @@ import("//flutter/tools/fuchsia/dart.gni") import("//flutter/tools/fuchsia/fuchsia_archive.gni") import("//flutter/tools/fuchsia/fuchsia_libs.gni") import("//flutter/vulkan/config.gni") -import("engine_flutter_runner.gni") # Fuchsia uses its own custom Surface implementation. -shell_gpu_configuration("fuchsia_legacy_gpu_configuration") { +shell_gpu_configuration("fuchsia_gpu_configuration") { enable_software = false enable_gl = false enable_vulkan = false enable_metal = false } +config("runner_debug_config") { + defines = [ "DEBUG" ] # Needed due to direct dart dependencies. +} + +config("runner_flutter_profile_config") { + defines = [ "FLUTTER_PROFILE" ] +} + +config("runner_product_config") { + defines = [ "DART_PRODUCT" ] +} + +template("runner_sources") { + assert(defined(invoker.product), "runner_sources must define product") + + runner_configs = [] + if (is_debug) { + runner_configs += [ ":runner_debug_config" ] + } + if (flutter_runtime_mode == "profile") { + runner_configs += [ ":runner_flutter_profile_config" ] + } + if (invoker.product) { + runner_configs += [ ":runner_product_config" ] + } + + source_set(target_name) { + sources = [ + "accessibility_bridge.cc", + "accessibility_bridge.h", + "component.cc", + "component.h", + "compositor_context.cc", + "compositor_context.h", + "engine.cc", + "engine.h", + "flutter_runner_product_configuration.cc", + "flutter_runner_product_configuration.h", + "fuchsia_intl.cc", + "fuchsia_intl.h", + "isolate_configurator.cc", + "isolate_configurator.h", + "logging.h", + "loop.cc", + "loop.h", + "platform_view.cc", + "platform_view.h", + "runner.cc", + "runner.h", + "session_connection.cc", + "session_connection.h", + "surface.cc", + "surface.h", + "task_observers.cc", + "task_observers.h", + "task_runner_adapter.cc", + "task_runner_adapter.h", + "thread.cc", + "thread.h", + "unique_fdio_ns.h", + "vsync_recorder.cc", + "vsync_recorder.h", + "vsync_waiter.cc", + "vsync_waiter.h", + "vulkan_surface.cc", + "vulkan_surface.h", + "vulkan_surface_pool.cc", + "vulkan_surface_pool.h", + "vulkan_surface_producer.cc", + "vulkan_surface_producer.h", + ] + + public_configs = runner_configs + + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_public_deps = [ + "//flutter/flow", + "//flutter/lib/ui", + "//flutter/runtime", + "//flutter/shell/common", + ] + flutter_deps = [ + ":fuchsia_gpu_configuration", + "//flutter/assets", + "//flutter/common", + "//flutter/fml", + "//flutter/vulkan", + ] + + public_deps = [ + "$fuchsia_sdk_root/pkg:scenic_cpp", + "$fuchsia_sdk_root/pkg:sys_cpp", + "//flutter/shell/platform/fuchsia/runtime/dart/utils", + ] + flutter_public_deps + + deps = [ + "$fuchsia_sdk_root/fidl:fuchsia.accessibility.semantics", + "$fuchsia_sdk_root/fidl:fuchsia.fonts", + "$fuchsia_sdk_root/fidl:fuchsia.images", + "$fuchsia_sdk_root/fidl:fuchsia.intl", + "$fuchsia_sdk_root/fidl:fuchsia.io", + "$fuchsia_sdk_root/fidl:fuchsia.sys", + "$fuchsia_sdk_root/fidl:fuchsia.ui.app", + "$fuchsia_sdk_root/fidl:fuchsia.ui.scenic", + "$fuchsia_sdk_root/pkg:async-cpp", + "$fuchsia_sdk_root/pkg:async-default", + "$fuchsia_sdk_root/pkg:async-loop", + "$fuchsia_sdk_root/pkg:async-loop-cpp", + "$fuchsia_sdk_root/pkg:fdio", + "$fuchsia_sdk_root/pkg:fidl_cpp", + "$fuchsia_sdk_root/pkg:syslog", + "$fuchsia_sdk_root/pkg:trace", + "$fuchsia_sdk_root/pkg:trace-engine", + "$fuchsia_sdk_root/pkg:trace-provider-so", + "$fuchsia_sdk_root/pkg:vfs_cpp", + "$fuchsia_sdk_root/pkg:zx", + "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", + "//flutter/shell/platform/fuchsia/dart-pkg/zircon", + ] + flutter_deps + } +} + +runner_sources("flutter_runner_sources") { + product = false +} + +runner_sources("flutter_runner_sources_product") { + product = true +} + # Things that explicitly being excluded: # 1. Kernel snapshot framework mode. # 2. Profiler symbols. +# Builds a flutter_runner +# +# Parameters: +# +# output_name (required): +# The name of the resulting binary. +# +# extra_deps (required): +# Any additional dependencies. +# +# product (required): +# Whether to link against a Product mode Dart VM. +# +# extra_defines (optional): +# Any additional preprocessor defines. +template("flutter_runner") { + assert(defined(invoker.output_name), "flutter_runner must define output_name") + assert(defined(invoker.extra_deps), "flutter_runner must define extra_deps") + assert(defined(invoker.product), "flutter_runner must define product") + + invoker_output_name = invoker.output_name + extra_deps = invoker.extra_deps + + product_suffix = "" + if (invoker.product) { + product_suffix = "_product" + } + + executable(target_name) { + output_name = invoker_output_name + + sources = [ "main.cc" ] + + deps = [ + ":flutter_runner_sources${product_suffix}", + "$fuchsia_sdk_root/pkg:async-loop-cpp", + "$fuchsia_sdk_root/pkg:trace", + "$fuchsia_sdk_root/pkg:trace-provider-so", + ] + extra_deps + + # The flags below are needed so that Dart's CPU profiler can walk the + # C++ stack. + cflags = [ "-fno-omit-frame-pointer" ] + + if (!invoker.product) { + # This flag is needed so that the call to dladdr() in Dart's native symbol + # resolver can report good symbol information for the CPU profiler. + ldflags = [ "-rdynamic" ] + } + } +} + flutter_runner("jit") { output_name = "flutter_jit_runner" product = false - extra_defines = [] - if (flutter_runtime_mode == "profile") { - extra_defines += [ "FLUTTER_PROFILE" ] - } - extra_deps = [ "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", @@ -45,8 +222,6 @@ flutter_runner("jit_product") { output_name = "flutter_jit_product_runner" product = true - extra_defines = [ "DART_PRODUCT" ] - extra_deps = [ "//third_party/dart/runtime:libdart_jit_product", "//third_party/dart/runtime/platform:libdart_platform_jit_product", @@ -57,11 +232,6 @@ flutter_runner("aot") { output_name = "flutter_aot_runner" product = false - extra_defines = [] - if (flutter_runtime_mode == "profile") { - extra_defines += [ "FLUTTER_PROFILE" ] - } - extra_deps = [ "//third_party/dart/runtime:libdart_precompiled_runtime", "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime", @@ -72,8 +242,6 @@ flutter_runner("aot_product") { output_name = "flutter_aot_product_runner" product = true - extra_defines = [ "DART_PRODUCT" ] - extra_deps = [ "//third_party/dart/runtime:libdart_precompiled_runtime_product", "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime_product", @@ -267,66 +435,36 @@ executable("flutter_runner_unittests") { output_name = "flutter_runner_tests" sources = [ - "accessibility_bridge.cc", - "accessibility_bridge.h", "accessibility_bridge_unittest.cc", - "component.cc", - "component.h", "component_unittest.cc", "flutter_runner_fakes.h", - "flutter_runner_product_configuration.cc", - "flutter_runner_product_configuration.h", - "fuchsia_intl.cc", - "fuchsia_intl.h", "fuchsia_intl_unittest.cc", - "logging.h", - "loop.cc", - "loop.h", - "platform_view.cc", - "platform_view.h", "platform_view_unittest.cc", - "runner.cc", - "runner.h", "runner_unittest.cc", - "surface.cc", - "surface.h", - "task_observers.cc", - "task_observers.h", - "task_runner_adapter.cc", - "task_runner_adapter.h", "tests/flutter_runner_product_configuration_unittests.cc", "tests/vsync_recorder_unittests.cc", - "thread.cc", - "thread.h", - "vsync_recorder.cc", - "vsync_recorder.h", - "vsync_waiter.cc", - "vsync_waiter.h", "vsync_waiter_unittests.cc", ] # This is needed for //third_party/googletest for linking zircon symbols. libs = [ "//fuchsia/sdk/$host_os/arch/$target_cpu/sysroot/lib/libzircon.so" ] - deps = [ - ":aot", - ":flutter_runner_fixtures", - "//build/fuchsia/fidl:fuchsia.accessibility.semantics", - "//build/fuchsia/pkg:async-default", - "//build/fuchsia/pkg:async-loop-cpp", - "//build/fuchsia/pkg:async-loop-default", - "//build/fuchsia/pkg:scenic_cpp", - "//build/fuchsia/pkg:sys_cpp_testing", - "//flutter/common", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/shell/platform/fuchsia/runtime/dart/utils", - "//flutter/testing", + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/flow", + "//flutter/lib/ui", + "//flutter/shell/common", "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", ] + + deps = [ + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "//build/fuchsia/pkg:sys_cpp_testing", + "//flutter/testing", + ] + flutter_deps } executable("flutter_runner_tzdata_unittests") { @@ -334,34 +472,24 @@ executable("flutter_runner_tzdata_unittests") { output_name = "flutter_runner_tzdata_tests" - sources = [ - "runner.cc", - "runner.h", - "runner_tzdata_unittest.cc", - ] + sources = [ "runner_tzdata_unittest.cc" ] # This is needed for //third_party/googletest for linking zircon symbols. libs = [ "//fuchsia/sdk/$host_os/arch/$target_cpu/sysroot/lib/libzircon.so" ] - deps = [ - ":aot", - ":flutter_runner_fixtures", - "//build/fuchsia/fidl:fuchsia.accessibility.semantics", - "//build/fuchsia/pkg:async-loop-cpp", - "//build/fuchsia/pkg:async-loop-default", - "//build/fuchsia/pkg:scenic_cpp", - "//build/fuchsia/pkg:sys_cpp_testing", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/shell/platform/fuchsia/runtime/dart/utils", - "//flutter/testing", + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/lib/ui", "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", - "//third_party/icu", - "//third_party/skia", ] + + deps = [ + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "//flutter/testing", + ] + flutter_deps } executable("flutter_runner_scenic_unittests") { @@ -369,84 +497,27 @@ executable("flutter_runner_scenic_unittests") { output_name = "flutter_runner_scenic_tests" - sources = [ - "component.cc", - "component.h", - "compositor_context.cc", - "compositor_context.h", - "engine.cc", - "engine.h", - "fuchsia_intl.cc", - "fuchsia_intl.h", - "isolate_configurator.cc", - "isolate_configurator.h", - "logging.h", - "loop.cc", - "loop.h", - "platform_view.cc", - "platform_view.h", - "runner.cc", - "runner.h", - "session_connection.cc", - "session_connection.h", - "surface.cc", - "surface.h", - "task_observers.cc", - "task_observers.h", - "task_runner_adapter.cc", - "task_runner_adapter.h", - "tests/session_connection_unittests.cc", - "thread.cc", - "thread.h", - "unique_fdio_ns.h", - "vsync_recorder.cc", - "vsync_recorder.h", - "vsync_waiter.cc", - "vsync_waiter.h", - "vsync_waiter_unittests.cc", - "vulkan_surface.cc", - "vulkan_surface.h", - "vulkan_surface_pool.cc", - "vulkan_surface_pool.h", - "vulkan_surface_producer.cc", - "vulkan_surface_producer.h", - ] + sources = [ "tests/session_connection_unittests.cc" ] # This is needed for //third_party/googletest for linking zircon symbols. libs = [ "//fuchsia/sdk/$host_os/arch/$target_cpu/sysroot/lib/libzircon.so" ] - deps = [ - ":flutter_runner_fixtures", - ":jit", - "$fuchsia_sdk_root/fidl:fuchsia.ui.policy", - "$fuchsia_sdk_root/pkg:trace-provider-so", - "//build/fuchsia/fidl:fuchsia.accessibility.semantics", - "//build/fuchsia/pkg:async-default", - "//build/fuchsia/pkg:async-loop-cpp", - "//build/fuchsia/pkg:async-loop-default", - "//build/fuchsia/pkg:scenic_cpp", - "//build/fuchsia/pkg:sys_cpp_testing", - "//flutter/common", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", - "//flutter/shell/platform/fuchsia/dart-pkg/zircon", - "//flutter/shell/platform/fuchsia/runtime/dart/utils", - "//flutter/testing", - "//flutter/vulkan", + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/lib/ui", "//third_party/dart/runtime:libdart_jit", "//third_party/dart/runtime/platform:libdart_platform_jit", - "//third_party/icu", - "//third_party/skia", ] - public_deps = [ "//third_party/googletest:gtest" ] + deps = [ + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "$fuchsia_sdk_root/fidl:fuchsia.ui.policy", + "//flutter/testing", + ] + flutter_deps } -# When adding a new dep here, please also ensure the dep is added to -# testing/fuchsia/run_tests.sh and testing/fuchsia/test_fars fuchsia_archive("flutter_runner_tests") { testonly = true @@ -570,30 +641,6 @@ fuchsia_test_archive("flow_tests") { ] } -fuchsia_test_archive("flow_tests_next") { - deps = [ "//flutter/flow:flow_unittests_next" ] - - binary = "flow_unittests_next" - - resources = [ - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_60fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_60fps.png" - }, - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_90fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_90fps.png" - }, - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_120fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_120fps.png" - }, - ] -} - fuchsia_test_archive("runtime_tests") { deps = [ "//flutter/runtime:runtime_fixtures", @@ -613,25 +660,6 @@ fuchsia_test_archive("runtime_tests") { ] } -fuchsia_test_archive("runtime_tests_next") { - deps = [ - "//flutter/runtime:runtime_fixtures", - "//flutter/runtime:runtime_unittests_next", - ] - - binary = "runtime_unittests_next" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/runtime/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - ] -} - fuchsia_test_archive("shell_tests") { deps = [ "//flutter/shell/common:shell_unittests", @@ -659,33 +687,6 @@ fuchsia_test_archive("shell_tests") { resources += vulkan_icds } -fuchsia_test_archive("shell_tests_next") { - deps = [ - "//flutter/shell/common:shell_unittests_fixtures", - "//flutter/shell/common:shell_unittests_next", - ] - - binary = "shell_unittests_next" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/shell/common/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - { - path = - "$root_gen_dir/flutter/shell/common/assets/shelltest_screenshot.png" - dest = "assets/shelltest_screenshot.png" - }, - ] - - libraries = vulkan_validation_libs - resources += vulkan_icds -} - fuchsia_test_archive("testing_tests") { deps = [ "//flutter/testing:testing_unittests" ] @@ -751,65 +752,21 @@ fuchsia_test_archive("ui_tests") { resources += vulkan_icds } -fuchsia_test_archive("ui_tests_next") { - deps = [ - "//flutter/lib/ui:ui_unittests_fixtures", - "//flutter/lib/ui:ui_unittests_next", - ] - - binary = "ui_unittests_next" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/lib/ui/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/DashInNooglerHat.jpg" - dest = "assets/DashInNooglerHat.jpg" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.jpg" - dest = "assets/Horizontal.jpg" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.png" - dest = "assets/Horizontal.png" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.gif" - dest = "assets/hello_loop_2.gif" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.webp" - dest = "assets/hello_loop_2.webp" - }, - ] - - libraries = vulkan_validation_libs - resources += vulkan_icds -} - +# When adding a new dep here, please also ensure the dep is added to +# testing/fuchsia/run_tests.sh and testing/fuchsia/test_fars group("tests") { testonly = true deps = [ ":flow_tests", - ":flow_tests_next", ":flutter_runner_scenic_tests", ":flutter_runner_tests", ":flutter_runner_tzdata_tests", ":fml_tests", ":runtime_tests", - ":runtime_tests_next", ":shell_tests", - ":shell_tests_next", ":testing_tests", ":txt_tests", ":ui_tests", - ":ui_tests_next", ] } diff --git a/shell/platform/fuchsia/flutter/component.cc b/shell/platform/fuchsia/flutter/component.cc index 00cd9d318ae83..0106931820887 100644 --- a/shell/platform/fuchsia/flutter/component.cc +++ b/shell/platform/fuchsia/flutter/component.cc @@ -365,6 +365,12 @@ Application::Application( // Controls whether category "skia" trace events are enabled. settings_.trace_skia = true; + settings_.verbose_logging = true; + + settings_.advisory_script_uri = debug_label_; + + settings_.advisory_script_entrypoint = debug_label_; + settings_.icu_data_path = ""; settings_.assets_dir = application_assets_directory_.get(); diff --git a/shell/platform/fuchsia/flutter/compositor_context.cc b/shell/platform/fuchsia/flutter/compositor_context.cc index b0bbfc7ecbc28..6911ed8ddc2d5 100644 --- a/shell/platform/fuchsia/flutter/compositor_context.cc +++ b/shell/platform/fuchsia/flutter/compositor_context.cc @@ -4,6 +4,8 @@ #include "compositor_context.h" +#include + #include "flutter/flow/layers/layer_tree.h" #include "third_party/skia/include/gpu/GrDirectContext.h" @@ -11,30 +13,38 @@ namespace flutter_runner { class ScopedFrame final : public flutter::CompositorContext::ScopedFrame { public: - ScopedFrame(flutter::CompositorContext& context, - const SkMatrix& root_surface_transformation, + ScopedFrame(CompositorContext& context, + GrContext* gr_context, + SkCanvas* canvas, flutter::ExternalViewEmbedder* view_embedder, + const SkMatrix& root_surface_transformation, bool instrumentation_enabled, - SessionConnection& session_connection) - : flutter::CompositorContext::ScopedFrame( - context, - session_connection.vulkan_surface_producer()->gr_context(), - nullptr, - view_embedder, - root_surface_transformation, - instrumentation_enabled, - true, - nullptr), - session_connection_(session_connection) {} + bool surface_supports_readback, + fml::RefPtr raster_thread_merger, + SessionConnection& session_connection, + VulkanSurfaceProducer& surface_producer, + flutter::SceneUpdateContext& scene_update_context) + : flutter::CompositorContext::ScopedFrame(context, + surface_producer.gr_context(), + canvas, + view_embedder, + root_surface_transformation, + instrumentation_enabled, + surface_supports_readback, + raster_thread_merger), + session_connection_(session_connection), + surface_producer_(surface_producer), + scene_update_context_(scene_update_context) {} private: SessionConnection& session_connection_; + VulkanSurfaceProducer& surface_producer_; + flutter::SceneUpdateContext& scene_update_context_; flutter::RasterStatus Raster(flutter::LayerTree& layer_tree, bool ignore_raster_cache) override { - if (!session_connection_.has_metrics()) { - return flutter::RasterStatus::kSuccess; - } + std::vector frame_paint_tasks; + std::vector> frame_surfaces; { // Preroll the Flutter layer tree. This allows Flutter to perform @@ -47,15 +57,80 @@ class ScopedFrame final : public flutter::CompositorContext::ScopedFrame { // Traverse the Flutter layer tree so that the necessary session ops to // represent the frame are enqueued in the underlying session. TRACE_EVENT0("flutter", "UpdateScene"); - layer_tree.UpdateScene(session_connection_.scene_update_context(), - session_connection_.root_node()); + layer_tree.UpdateScene(scene_update_context_); } { - // Flush all pending session ops. + // Flush all pending session ops: create surfaces and enqueue session + // Image ops for the frame's paint tasks, then Present. TRACE_EVENT0("flutter", "SessionPresent"); + frame_paint_tasks = scene_update_context_.GetPaintTasks(); + for (auto& task : frame_paint_tasks) { + SkISize physical_size = + SkISize::Make(layer_tree.device_pixel_ratio() * task.scale_x * + task.paint_bounds.width(), + layer_tree.device_pixel_ratio() * task.scale_y * + task.paint_bounds.height()); + if (physical_size.width() == 0 || physical_size.height() == 0) { + frame_surfaces.emplace_back(nullptr); + continue; + } + + std::unique_ptr surface = + surface_producer_.ProduceSurface(physical_size); + if (!surface) { + FML_LOG(ERROR) + << "Could not acquire a surface from the surface producer " + "of size: " + << physical_size.width() << "x" << physical_size.height(); + } else { + task.material.SetTexture(*(surface->GetImage())); + } + + frame_surfaces.emplace_back(std::move(surface)); + } + + session_connection_.Present(); + } - session_connection_.Present(this); + { + // Execute paint tasks in parallel with Scenic's side of the Present, then + // signal fences. + TRACE_EVENT0("flutter", "ExecutePaintTasks"); + size_t surface_index = 0; + for (auto& task : frame_paint_tasks) { + std::unique_ptr& task_surface = + frame_surfaces[surface_index++]; + if (!task_surface) { + continue; + } + + SkCanvas* canvas = task_surface->GetSkiaSurface()->getCanvas(); + flutter::Layer::PaintContext paint_context = { + canvas, + canvas, + gr_context(), + nullptr, + context().raster_time(), + context().ui_time(), + context().texture_registry(), + &context().raster_cache(), + false, + layer_tree.device_pixel_ratio()}; + canvas->restoreToCount(1); + canvas->save(); + canvas->clear(task.background_color); + canvas->scale(layer_tree.device_pixel_ratio() * task.scale_x, + layer_tree.device_pixel_ratio() * task.scale_y); + canvas->translate(-task.paint_bounds.left(), -task.paint_bounds.top()); + for (flutter::Layer* layer : task.layers) { + layer->Paint(paint_context); + } + } + + // Tell the surface producer that a present has occurred so it can perform + // book-keeping on buffer caches. + surface_producer_.OnSurfacesPresented(std::move(frame_surfaces)); } return flutter::RasterStatus::kSuccess; @@ -65,51 +140,16 @@ class ScopedFrame final : public flutter::CompositorContext::ScopedFrame { }; CompositorContext::CompositorContext( - std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, - fidl::InterfaceHandle session, - fml::closure session_error_callback, - zx_handle_t vsync_event_handle) - : debug_label_(std::move(debug_label)), - session_connection_( - debug_label_, - std::move(view_token), - std::move(view_ref_pair), - std::move(session), - session_error_callback, - [](auto) {}, - vsync_event_handle) {} - -void CompositorContext::OnSessionMetricsDidChange( - const fuchsia::ui::gfx::Metrics& metrics) { - session_connection_.set_metrics(metrics); -} + flutter::CompositorContext::Delegate& delegate, + SessionConnection& session_connection, + VulkanSurfaceProducer& surface_producer, + flutter::SceneUpdateContext& scene_update_context) + : flutter::CompositorContext(delegate), + session_connection_(session_connection), + surface_producer_(surface_producer), + scene_update_context_(scene_update_context) {} -void CompositorContext::OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - session_connection_.OnSessionSizeChangeHint(width_change_factor, - height_change_factor); -} - -void CompositorContext::OnWireframeEnabled(bool enabled) { - session_connection_.set_enable_wireframe(enabled); -} - -void CompositorContext::OnCreateView(int64_t view_id, - bool hit_testable, - bool focusable) { - session_connection_.scene_update_context().CreateView(view_id, hit_testable, - focusable); -} - -void CompositorContext::OnDestroyView(int64_t view_id) { - session_connection_.scene_update_context().DestroyView(view_id); -} - -CompositorContext::~CompositorContext() { - OnGrContextDestroyed(); -} +CompositorContext::~CompositorContext() = default; std::unique_ptr CompositorContext::AcquireFrame( @@ -120,16 +160,10 @@ CompositorContext::AcquireFrame( bool instrumentation_enabled, bool surface_supports_readback, fml::RefPtr raster_thread_merger) { - // TODO: The AcquireFrame interface is too broad and must be refactored to get - // rid of the context and canvas arguments as those seem to be only used for - // colorspace correctness purposes on the mobile shells. return std::make_unique( - *this, // - root_surface_transformation, // - view_embedder, - instrumentation_enabled, // - session_connection_ // - ); + *this, gr_context, canvas, view_embedder, root_surface_transformation, + instrumentation_enabled, surface_supports_readback, raster_thread_merger, + session_connection_, surface_producer_, scene_update_context_); } } // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/compositor_context.h b/shell/platform/fuchsia/flutter/compositor_context.h index 6ad28785b119c..eb57321c1215c 100644 --- a/shell/platform/fuchsia/flutter/compositor_context.h +++ b/shell/platform/fuchsia/flutter/compositor_context.h @@ -5,15 +5,15 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPOSITOR_CONTEXT_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPOSITOR_CONTEXT_H_ -#include -#include -#include -#include +#include #include "flutter/flow/compositor_context.h" #include "flutter/flow/embedded_views.h" +#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" + #include "session_connection.h" +#include "vulkan_surface_producer.h" namespace flutter_runner { @@ -21,31 +21,17 @@ namespace flutter_runner { // Fuchsia. class CompositorContext final : public flutter::CompositorContext { public: - CompositorContext(std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, - fidl::InterfaceHandle session, - fml::closure session_error_callback, - zx_handle_t vsync_event_handle); + CompositorContext(CompositorContext::Delegate& delegate, + SessionConnection& session_connection, + VulkanSurfaceProducer& surface_producer, + flutter::SceneUpdateContext& scene_update_context); ~CompositorContext() override; - void OnSessionMetricsDidChange(const fuchsia::ui::gfx::Metrics& metrics); - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor); - - void OnWireframeEnabled(bool enabled); - void OnCreateView(int64_t view_id, bool hit_testable, bool focusable); - void OnDestroyView(int64_t view_id); - - flutter::ExternalViewEmbedder* GetViewEmbedder() { - return &session_connection_.scene_update_context(); - } - private: - const std::string debug_label_; - scenic::ViewRefPair view_ref_pair_; - SessionConnection session_connection_; + SessionConnection& session_connection_; + VulkanSurfaceProducer& surface_producer_; + flutter::SceneUpdateContext& scene_update_context_; // |flutter::CompositorContext| std::unique_ptr AcquireFrame( diff --git a/shell/platform/fuchsia/flutter/engine.cc b/shell/platform/fuchsia/flutter/engine.cc index 1d5a2eca8c67f..9470f3aae8b38 100644 --- a/shell/platform/fuchsia/flutter/engine.cc +++ b/shell/platform/fuchsia/flutter/engine.cc @@ -7,8 +7,7 @@ #include #include -#include - +#include "../runtime/dart/utils/files.h" #include "compositor_context.h" #include "flutter/common/task_runners.h" #include "flutter/fml/make_copyable.h" @@ -20,15 +19,15 @@ #include "flutter_runner_product_configuration.h" #include "fuchsia_intl.h" #include "platform_view.h" -#include "runtime/dart/utils/files.h" #include "task_runner_adapter.h" #include "third_party/skia/include/ports/SkFontMgr_fuchsia.h" #include "thread.h" namespace flutter_runner { +namespace { -static void UpdateNativeThreadLabelNames(const std::string& label, - const flutter::TaskRunners& runners) { +void UpdateNativeThreadLabelNames(const std::string& label, + const flutter::TaskRunners& runners) { auto set_thread_name = [](fml::RefPtr runner, std::string prefix, std::string suffix) { if (!runner) { @@ -44,13 +43,15 @@ static void UpdateNativeThreadLabelNames(const std::string& label, set_thread_name(runners.GetIOTaskRunner(), label, ".io"); } -static fml::RefPtr MakeLocalizationPlatformMessage( +fml::RefPtr MakeLocalizationPlatformMessage( const fuchsia::intl::Profile& intl_profile) { return fml::MakeRefCounted( "flutter/localization", MakeLocalizationPlatformMessageData(intl_profile), nullptr); } +} // namespace + Engine::Engine(Delegate& delegate, std::string thread_label, std::shared_ptr svc, @@ -64,28 +65,72 @@ Engine::Engine(Delegate& delegate, FlutterRunnerProductConfiguration product_config) : delegate_(delegate), thread_label_(std::move(thread_label)), - settings_(std::move(settings)), weak_factory_(this) { if (zx::event::create(0, &vsync_event_) != ZX_OK) { FML_DLOG(ERROR) << "Could not create the vsync event."; return; } - // Launch the threads that will be used to run the shell. These threads will - // be joined in the destructor. - for (auto& thread : threads_) { - thread.reset(new Thread()); - } + // Get the task runners from the managed threads. The current thread will be + // used as the "platform" thread. + const flutter::TaskRunners task_runners( + thread_label_, // Dart thread labels + CreateFMLTaskRunner(async_get_default_dispatcher()), // platform + CreateFMLTaskRunner(threads_[0].dispatcher()), // raster + CreateFMLTaskRunner(threads_[1].dispatcher()), // ui + CreateFMLTaskRunner(threads_[2].dispatcher()) // io + ); + UpdateNativeThreadLabelNames(thread_label_, task_runners); - // Set up the session connection. + // Connect to Scenic. auto scenic = svc->Connect(); fidl::InterfaceHandle session; fidl::InterfaceHandle session_listener; auto session_listener_request = session_listener.NewRequest(); - scenic->CreateSession(session.NewRequest(), session_listener.Bind()); + fidl::InterfaceHandle focuser; + scenic->CreateSession2(session.NewRequest(), session_listener.Bind(), + focuser.NewRequest()); + + // Make clones of the `ViewRef` before sending it down to Scenic. + fuchsia::ui::views::ViewRef platform_view_ref, isolate_view_ref; + view_ref_pair.view_ref.Clone(&platform_view_ref); + view_ref_pair.view_ref.Clone(&isolate_view_ref); + + // Session is terminated on the raster thread, but we must terminate ourselves + // on the platform thread. + // + // This handles the fidl error callback when the Session connection is + // broken. The SessionListener interface also has an OnError method, which is + // invoked on the platform thread (in PlatformView). + fml::closure session_error_callback = [dispatcher = + async_get_default_dispatcher(), + weak = weak_factory_.GetWeakPtr()]() { + async::PostTask(dispatcher, [weak]() { + if (weak) { + weak->Terminate(); + } + }); + }; + + // Set up the session connection and other Scenic helpers on the raster + // thread. + task_runners.GetRasterTaskRunner()->PostTask(fml::MakeCopyable( + [this, session = std::move(session), + session_error_callback = std::move(session_error_callback), + view_token = std::move(view_token), + view_ref_pair = std::move(view_ref_pair), + vsync_handle = vsync_event_.get()]() mutable { + session_connection_.emplace( + thread_label_, std::move(session), + std::move(session_error_callback), [](auto) {}, vsync_handle); + surface_producer_.emplace(session_connection_->get()); + scene_update_context_.emplace(thread_label_, std::move(view_token), + std::move(view_ref_pair), + session_connection_.value()); + })); - // Grab the parent environment services. The platform view may want to access - // some of these services. + // Grab the parent environment services. The platform view may want to + // access some of these services. fuchsia::sys::EnvironmentPtr environment; svc->Connect(environment.NewRequest()); fidl::InterfaceHandle @@ -93,27 +138,26 @@ Engine::Engine(Delegate& delegate, environment->GetServices(parent_environment_service_provider.NewRequest()); environment.Unbind(); - // We need to manually schedule a frame when the session metrics change. - OnMetricsUpdate on_session_metrics_change_callback = std::bind( - &Engine::OnSessionMetricsDidChange, this, std::placeholders::_1); - - OnSizeChangeHint on_session_size_change_hint_callback = - std::bind(&Engine::OnSessionSizeChangeHint, this, std::placeholders::_1, - std::placeholders::_2); - OnEnableWireframe on_enable_wireframe_callback = std::bind( - &Engine::OnDebugWireframeSettingsChanged, this, std::placeholders::_1); + &Engine::DebugWireframeSettingsChanged, this, std::placeholders::_1); - flutter_runner::OnCreateView on_create_view_callback = - std::bind(&Engine::OnCreateView, this, std::placeholders::_1, + OnCreateView on_create_view_callback = + std::bind(&Engine::CreateView, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); - flutter_runner::OnDestroyView on_destroy_view_callback = - std::bind(&Engine::OnDestroyView, this, std::placeholders::_1); + OnUpdateView on_update_view_callback = + std::bind(&Engine::UpdateView, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + + OnDestroyView on_destroy_view_callback = + std::bind(&Engine::DestroyView, this, std::placeholders::_1); OnGetViewEmbedder on_get_view_embedder_callback = std::bind(&Engine::GetViewEmbedder, this); + OnGetGrContext on_get_gr_context_callback = + std::bind(&Engine::GetGrContext, this); + // SessionListener has a OnScenicError method; invoke this callback on the // platform thread when that happens. The Session itself should also be // disconnected when this happens, and it will also attempt to terminate. @@ -127,10 +171,6 @@ Engine::Engine(Delegate& delegate, }); }; - fuchsia::ui::views::ViewRef platform_view_ref, isolate_view_ref; - view_ref_pair.view_ref.Clone(&platform_view_ref); - view_ref_pair.view_ref.Clone(&isolate_view_ref); - // Setup the callback that will instantiate the platform view. flutter::Shell::CreateCallback on_create_platform_view = fml::MakeCopyable( @@ -139,18 +179,17 @@ Engine::Engine(Delegate& delegate, parent_environment_service_provider = std::move(parent_environment_service_provider), session_listener_request = std::move(session_listener_request), + focuser = std::move(focuser), on_session_listener_error_callback = std::move(on_session_listener_error_callback), - on_session_metrics_change_callback = - std::move(on_session_metrics_change_callback), - on_session_size_change_hint_callback = - std::move(on_session_size_change_hint_callback), on_enable_wireframe_callback = std::move(on_enable_wireframe_callback), on_create_view_callback = std::move(on_create_view_callback), + on_update_view_callback = std::move(on_update_view_callback), on_destroy_view_callback = std::move(on_destroy_view_callback), on_get_view_embedder_callback = std::move(on_get_view_embedder_callback), + on_get_gr_context_callback = std::move(on_get_gr_context_callback), vsync_handle = vsync_event_.get(), product_config = product_config](flutter::Shell& shell) mutable { return std::make_unique( @@ -161,83 +200,37 @@ Engine::Engine(Delegate& delegate, std::move(runner_services), std::move(parent_environment_service_provider), // services std::move(session_listener_request), // session listener + std::move(focuser), std::move(on_session_listener_error_callback), - std::move(on_session_metrics_change_callback), - std::move(on_session_size_change_hint_callback), std::move(on_enable_wireframe_callback), std::move(on_create_view_callback), + std::move(on_update_view_callback), std::move(on_destroy_view_callback), std::move(on_get_view_embedder_callback), + std::move(on_get_gr_context_callback), vsync_handle, // vsync handle product_config); }); - // Session can be terminated on the raster thread, but we must terminate - // ourselves on the platform thread. - // - // This handles the fidl error callback when the Session connection is - // broken. The SessionListener interface also has an OnError method, which is - // invoked on the platform thread (in PlatformView). - fml::closure on_session_error_callback = - [dispatcher = async_get_default_dispatcher(), - weak = weak_factory_.GetWeakPtr()]() { - async::PostTask(dispatcher, [weak]() { - if (weak) { - weak->Terminate(); - } - }); - }; - - // Get the task runners from the managed threads. The current thread will be - // used as the "platform" thread. - const flutter::TaskRunners task_runners( - thread_label_, // Dart thread labels - CreateFMLTaskRunner(async_get_default_dispatcher()), // platform - CreateFMLTaskRunner(threads_[0]->dispatcher()), // raster - CreateFMLTaskRunner(threads_[1]->dispatcher()), // ui - CreateFMLTaskRunner(threads_[2]->dispatcher()) // io - ); - // Setup the callback that will instantiate the rasterizer. flutter::Shell::CreateCallback on_create_rasterizer = - fml::MakeCopyable([thread_label = thread_label_, // - view_token = std::move(view_token), // - view_ref_pair = std::move(view_ref_pair), // - session = std::move(session), // - on_session_error_callback, // - vsync_event = vsync_event_.get() // - ](flutter::Shell& shell) mutable { - std::unique_ptr compositor_context; - { - TRACE_DURATION("flutter", "CreateCompositorContext"); - compositor_context = - std::make_unique( - thread_label, // debug label - std::move(view_token), // scenic view we attach our tree to - std::move(view_ref_pair), // scenic view ref/view ref control - std::move(session), // scenic session - on_session_error_callback, // session did encounter error - vsync_event); // vsync event handle - } + fml::MakeCopyable([this](flutter::Shell& shell) mutable { + FML_DCHECK(session_connection_); + FML_DCHECK(surface_producer_); + FML_DCHECK(scene_update_context_); + + std::unique_ptr compositor_context = + std::make_unique( + shell, session_connection_.value(), surface_producer_.value(), + scene_update_context_.value()); return std::make_unique( - /*task_runners=*/shell.GetTaskRunners(), - /*compositor_context=*/std::move(compositor_context), - /*is_gpu_disabled_sync_switch=*/shell.GetIsGpuDisabledSyncSwitch()); + shell, std::move(compositor_context)); }); - UpdateNativeThreadLabelNames(thread_label_, task_runners); - - settings_.verbose_logging = true; - - settings_.advisory_script_uri = thread_label_; - - settings_.advisory_script_entrypoint = thread_label_; - - settings_.root_isolate_create_callback = + settings.root_isolate_create_callback = std::bind(&Engine::OnMainIsolateStart, this); - - settings_.root_isolate_shutdown_callback = + settings.root_isolate_shutdown_callback = std::bind([weak = weak_factory_.GetWeakPtr(), runner = task_runners.GetPlatformTaskRunner()]() { runner->PostTask([weak = std::move(weak)] { @@ -247,7 +240,7 @@ Engine::Engine(Delegate& delegate, }); }); - auto vm = flutter::DartVMRef::Create(settings_); + auto vm = flutter::DartVMRef::Create(settings); if (!isolate_snapshot) { isolate_snapshot = vm->GetVMData()->GetIsolateSnapshot(); @@ -256,13 +249,13 @@ Engine::Engine(Delegate& delegate, { TRACE_EVENT0("flutter", "CreateShell"); shell_ = flutter::Shell::Create( - task_runners, // host task runners - flutter::WindowData(), // default window data - settings_, // shell launch settings - std::move(isolate_snapshot), // isolate snapshot - on_create_platform_view, // platform view create callback - on_create_rasterizer, // rasterizer create callback - std::move(vm) // vm reference + std::move(task_runners), // host task runners + flutter::PlatformData(), // default window data + std::move(settings), // shell launch settings + std::move(isolate_snapshot), // isolate snapshot + std::move(on_create_platform_view), // platform view create callback + std::move(on_create_rasterizer), // rasterizer create callback + std::move(vm) // vm reference ); } @@ -339,7 +332,7 @@ Engine::Engine(Delegate& delegate, // Launch the engine in the appropriate configuration. auto run_configuration = flutter::RunConfiguration::InferFromSettings( - settings_, task_runners.GetIOTaskRunner()); + shell_->GetSettings(), shell_->GetTaskRunners().GetIOTaskRunner()); auto on_run_failure = [weak = weak_factory_.GetWeakPtr()]() { // The engine could have been killed by the caller right after the @@ -376,11 +369,11 @@ Engine::Engine(Delegate& delegate, Engine::~Engine() { shell_.reset(); - for (const auto& thread : threads_) { - thread->Quit(); + for (auto& thread : threads_) { + thread.Quit(); } - for (const auto& thread : threads_) { - thread->Join(); + for (auto& thread : threads_) { + thread.Join(); } } @@ -485,105 +478,60 @@ void Engine::Terminate() { // collected this object. } -void Engine::OnSessionMetricsDidChange( - const fuchsia::ui::gfx::Metrics& metrics) { - if (!shell_) { +void Engine::DebugWireframeSettingsChanged(bool enabled) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), metrics]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - - compositor_context->OnSessionMetricsDidChange(metrics); - } - }); + [this, enabled]() { scene_update_context_->EnableWireframe(enabled); }); } -void Engine::OnDebugWireframeSettingsChanged(bool enabled) { - if (!shell_) { +void Engine::CreateView(int64_t view_id, bool hit_testable, bool focusable) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), enabled]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - - compositor_context->OnWireframeEnabled(enabled); - } + [this, view_id, hit_testable, focusable]() { + scene_update_context_->CreateView(view_id, hit_testable, focusable); }); } -void Engine::OnCreateView(int64_t view_id, bool hit_testable, bool focusable) { - if (!shell_) { +void Engine::UpdateView(int64_t view_id, bool hit_testable, bool focusable) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), view_id, hit_testable, - focusable]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - compositor_context->OnCreateView(view_id, hit_testable, focusable); - } + [this, view_id, hit_testable, focusable]() { + scene_update_context_->UpdateView(view_id, hit_testable, focusable); }); } -void Engine::OnDestroyView(int64_t view_id) { - if (!shell_) { +void Engine::DestroyView(int64_t view_id) { + if (!shell_ || !scene_update_context_) { return; } shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), view_id]() { - if (rasterizer) { - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - compositor_context->OnDestroyView(view_id); - } - }); + [this, view_id]() { scene_update_context_->DestroyView(view_id); }); } flutter::ExternalViewEmbedder* Engine::GetViewEmbedder() { - // GetEmbedder should be called only after rasterizer is created. - FML_DCHECK(shell_); - FML_DCHECK(shell_->GetRasterizer()); + if (!scene_update_context_) { + return nullptr; + } - auto rasterizer = shell_->GetRasterizer(); - auto compositor_context = - reinterpret_cast( - rasterizer->compositor_context()); - flutter::ExternalViewEmbedder* view_embedder = - compositor_context->GetViewEmbedder(); - return view_embedder; + return &scene_update_context_.value(); } -void Engine::OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - if (!shell_) { - return; - } +GrDirectContext* Engine::GetGrContext() { + // GetGrContext should be called only after rasterizer is created. + FML_DCHECK(shell_); + FML_DCHECK(shell_->GetRasterizer()); - shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( - [rasterizer = shell_->GetRasterizer(), width_change_factor, - height_change_factor]() { - if (rasterizer) { - auto compositor_context = reinterpret_cast( - rasterizer->compositor_context()); - - compositor_context->OnSessionSizeChangeHint(width_change_factor, - height_change_factor); - } - }); + return surface_producer_->gr_context(); } #if !defined(DART_PRODUCT) diff --git a/shell/platform/fuchsia/flutter/engine.h b/shell/platform/fuchsia/flutter/engine.h index 5ed41394fd599..410ff63151b7a 100644 --- a/shell/platform/fuchsia/flutter/engine.h +++ b/shell/platform/fuchsia/flutter/engine.h @@ -15,11 +15,15 @@ #include #include "flutter/flow/embedded_views.h" +#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" #include "flutter/shell/common/shell.h" + #include "flutter_runner_product_configuration.h" #include "isolate_configurator.h" +#include "session_connection.h" #include "thread.h" +#include "vulkan_surface_producer.h" namespace flutter_runner { @@ -55,15 +59,22 @@ class Engine final { private: Delegate& delegate_; + const std::string thread_label_; - flutter::Settings settings_; - std::array, 3> threads_; + std::array threads_; + + std::optional session_connection_; + std::optional surface_producer_; + std::optional scene_update_context_; + std::unique_ptr isolate_configurator_; std::unique_ptr shell_; + + fuchsia::intl::PropertyProviderPtr intl_property_provider_; + zx::event vsync_event_; + fml::WeakPtrFactory weak_factory_; - // A stub for the FIDL protocol fuchsia.intl.PropertyProvider. - fuchsia::intl::PropertyProviderPtr intl_property_provider_; void OnMainIsolateStart(); @@ -71,18 +82,15 @@ class Engine final { void Terminate(); - void OnSessionMetricsDidChange(const fuchsia::ui::gfx::Metrics& metrics); - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor); - - void OnDebugWireframeSettingsChanged(bool enabled); - - void OnCreateView(int64_t view_id, bool hit_testable, bool focusable); - - void OnDestroyView(int64_t view_id); + void DebugWireframeSettingsChanged(bool enabled); + void CreateView(int64_t view_id, bool hit_testable, bool focusable); + void UpdateView(int64_t view_id, bool hit_testable, bool focusable); + void DestroyView(int64_t view_id); flutter::ExternalViewEmbedder* GetViewEmbedder(); + GrDirectContext* GetGrContext(); + FML_DISALLOW_COPY_AND_ASSIGN(Engine); }; diff --git a/shell/platform/fuchsia/flutter/engine_flutter_runner.gni b/shell/platform/fuchsia/flutter/engine_flutter_runner.gni deleted file mode 100644 index 630575b0980c4..0000000000000 --- a/shell/platform/fuchsia/flutter/engine_flutter_runner.gni +++ /dev/null @@ -1,150 +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. - -assert(is_fuchsia) - -import("//build/fuchsia/sdk.gni") - -# Builds a flutter_runner -# -# Parameters: -# -# output_name (required): -# The name of the resulting binary. -# -# extra_deps (required): -# Any additional dependencies. -# -# product (required): -# Whether to link against a Product mode Dart VM. -# -# extra_defines (optional): -# Any additional preprocessor defines. -template("flutter_runner") { - assert(defined(invoker.output_name), "flutter_runner must define output_name") - assert(defined(invoker.extra_deps), "flutter_runner must define extra_deps") - assert(defined(invoker.product), "flutter_runner must define product") - - invoker_output_name = invoker.output_name - extra_deps = invoker.extra_deps - - extra_defines = [] - if (defined(invoker.extra_defines)) { - extra_defines += invoker.extra_defines - } - - executable(target_name) { - output_name = invoker_output_name - - defines = extra_defines - - libs = [] - - sources = [ - "accessibility_bridge.cc", - "accessibility_bridge.h", - "component.cc", - "component.h", - "compositor_context.cc", - "compositor_context.h", - "engine.cc", - "engine.h", - "flutter_runner_product_configuration.cc", - "flutter_runner_product_configuration.h", - "fuchsia_intl.cc", - "fuchsia_intl.h", - "isolate_configurator.cc", - "isolate_configurator.h", - "logging.h", - "loop.cc", - "loop.h", - "main.cc", - "platform_view.cc", - "platform_view.h", - "runner.cc", - "runner.h", - "session_connection.cc", - "session_connection.h", - "surface.cc", - "surface.h", - "task_observers.cc", - "task_observers.h", - "task_runner_adapter.cc", - "task_runner_adapter.h", - "thread.cc", - "thread.h", - "unique_fdio_ns.h", - "vsync_recorder.cc", - "vsync_recorder.h", - "vsync_waiter.cc", - "vsync_waiter.h", - "vulkan_surface.cc", - "vulkan_surface.h", - "vulkan_surface_pool.cc", - "vulkan_surface_pool.h", - "vulkan_surface_producer.cc", - "vulkan_surface_producer.h", - ] - - # The use of these dependencies is temporary and will be moved behind the - # embedder API. - flutter_deps = [ - "../flutter:fuchsia_legacy_gpu_configuration", - "//flutter/assets", - "//flutter/common", - "//flutter/flow:flow_fuchsia_legacy", - "//flutter/fml", - "//flutter/lib/ui:ui_fuchsia_legacy", - "//flutter/runtime:runtime_fuchsia_legacy", - "//flutter/shell/common:common_fuchsia_legacy", - "//flutter/vulkan", - ] - - _fuchsia_platform = "//flutter/shell/platform/fuchsia" - - # TODO(kaushikiska) evaluate if all of these are needed. - fuchsia_deps = [ - "${_fuchsia_platform}/dart-pkg/fuchsia", - "${_fuchsia_platform}/dart-pkg/zircon", - "${_fuchsia_platform}/runtime/dart/utils", - ] - - deps = [ - "$fuchsia_sdk_root/fidl:fuchsia.accessibility.semantics", - "$fuchsia_sdk_root/fidl:fuchsia.fonts", - "$fuchsia_sdk_root/fidl:fuchsia.images", - "$fuchsia_sdk_root/fidl:fuchsia.intl", - "$fuchsia_sdk_root/fidl:fuchsia.io", - "$fuchsia_sdk_root/fidl:fuchsia.sys", - "$fuchsia_sdk_root/fidl:fuchsia.ui.app", - "$fuchsia_sdk_root/fidl:fuchsia.ui.scenic", - "$fuchsia_sdk_root/pkg:async-cpp", - "$fuchsia_sdk_root/pkg:async-default", - "$fuchsia_sdk_root/pkg:async-loop", - "$fuchsia_sdk_root/pkg:async-loop-cpp", - "$fuchsia_sdk_root/pkg:fdio", - "$fuchsia_sdk_root/pkg:fidl_cpp", - "$fuchsia_sdk_root/pkg:scenic_cpp", - "$fuchsia_sdk_root/pkg:sys_cpp", - "$fuchsia_sdk_root/pkg:syslog", - "$fuchsia_sdk_root/pkg:trace", - "$fuchsia_sdk_root/pkg:trace-engine", - "$fuchsia_sdk_root/pkg:trace-provider-so", - "$fuchsia_sdk_root/pkg:vfs_cpp", - "$fuchsia_sdk_root/pkg:zx", - "//third_party/skia", - "//flutter/third_party/tonic", - ] + fuchsia_deps + flutter_deps + extra_deps - - # The flags below are needed so that Dart's CPU profiler can walk the - # C++ stack. - cflags = [ "-fno-omit-frame-pointer" ] - - if (!invoker.product) { - # This flag is needed so that the call to dladdr() in Dart's native symbol - # resolver can report good symbol information for the CPU profiler. - ldflags = [ "-rdynamic" ] - } - } -} diff --git a/shell/platform/fuchsia/flutter/kernel/BUILD.gn b/shell/platform/fuchsia/flutter/kernel/BUILD.gn index d9dc3a69af98d..f221f4af1977c 100644 --- a/shell/platform/fuchsia/flutter/kernel/BUILD.gn +++ b/shell/platform/fuchsia/flutter/kernel/BUILD.gn @@ -21,7 +21,7 @@ compile_platform("kernel_platform_files") { args = [ "--enable-experiment=non-nullable", - "--nnbd-weak", + "--nnbd-agnostic", # TODO(dartbug.com/36342): enable bytecode for core libraries when performance of bytecode # pipeline is on par with default pipeline and continuously tracked. diff --git a/shell/platform/fuchsia/flutter/platform_view.cc b/shell/platform/fuchsia/flutter/platform_view.cc index df2b454336b1a..5359c1e809ceb 100644 --- a/shell/platform/fuchsia/flutter/platform_view.cc +++ b/shell/platform/fuchsia/flutter/platform_view.cc @@ -6,6 +6,7 @@ #include "platform_view.h" +#include #include #include "flutter/fml/logging.h" @@ -22,41 +23,6 @@ namespace flutter_runner { -namespace { - -inline fuchsia::ui::gfx::vec3 Add(const fuchsia::ui::gfx::vec3& a, - const fuchsia::ui::gfx::vec3& b) { - return {.x = a.x + b.x, .y = a.y + b.y, .z = a.z + b.z}; -} - -inline fuchsia::ui::gfx::vec3 Subtract(const fuchsia::ui::gfx::vec3& a, - const fuchsia::ui::gfx::vec3& b) { - return {.x = a.x - b.x, .y = a.y - b.y, .z = a.z - b.z}; -} - -inline fuchsia::ui::gfx::BoundingBox InsetBy( - const fuchsia::ui::gfx::BoundingBox& box, - const fuchsia::ui::gfx::vec3& inset_from_min, - const fuchsia::ui::gfx::vec3& inset_from_max) { - return {.min = Add(box.min, inset_from_min), - .max = Subtract(box.max, inset_from_max)}; -} - -inline fuchsia::ui::gfx::BoundingBox ViewPropertiesLayoutBox( - const fuchsia::ui::gfx::ViewProperties& view_properties) { - return InsetBy(view_properties.bounding_box, view_properties.inset_from_min, - view_properties.inset_from_max); -} - -inline fuchsia::ui::gfx::vec3 Max(const fuchsia::ui::gfx::vec3& v, - float min_val) { - return {.x = std::max(v.x, min_val), - .y = std::max(v.y, min_val), - .z = std::max(v.z, min_val)}; -} - -} // end namespace - static constexpr char kFlutterPlatformChannel[] = "flutter/platform"; static constexpr char kTextInputChannel[] = "flutter/textinput"; static constexpr char kKeyEventChannel[] = "flutter/keyevent"; @@ -88,27 +54,29 @@ PlatformView::PlatformView( parent_environment_service_provider_handle, fidl::InterfaceRequest session_listener_request, + fidl::InterfaceHandle focuser, fit::closure session_listener_error_callback, - OnMetricsUpdate session_metrics_did_change_callback, - OnSizeChangeHint session_size_change_hint_callback, OnEnableWireframe wireframe_enabled_callback, OnCreateView on_create_view_callback, + OnUpdateView on_update_view_callback, OnDestroyView on_destroy_view_callback, OnGetViewEmbedder on_get_view_embedder_callback, + OnGetGrContext on_get_gr_context_callback, zx_handle_t vsync_event_handle, FlutterRunnerProductConfiguration product_config) : flutter::PlatformView(delegate, std::move(task_runners)), debug_label_(std::move(debug_label)), view_ref_(std::move(view_ref)), + focuser_(focuser.Bind()), session_listener_binding_(this, std::move(session_listener_request)), session_listener_error_callback_( std::move(session_listener_error_callback)), - metrics_changed_callback_(std::move(session_metrics_did_change_callback)), - size_change_hint_callback_(std::move(session_size_change_hint_callback)), wireframe_enabled_callback_(std::move(wireframe_enabled_callback)), on_create_view_callback_(std::move(on_create_view_callback)), + on_update_view_callback_(std::move(on_update_view_callback)), on_destroy_view_callback_(std::move(on_destroy_view_callback)), on_get_view_embedder_callback_(std::move(on_get_view_embedder_callback)), + on_get_gr_context_callback_(std::move(on_get_gr_context_callback)), ime_client_(this), vsync_event_handle_(vsync_event_handle), product_config_(product_config) { @@ -152,64 +120,6 @@ void PlatformView::RegisterPlatformMessageHandlers() { this, std::placeholders::_1); } -void PlatformView::OnPropertiesChanged( - const fuchsia::ui::gfx::ViewProperties& view_properties) { - fuchsia::ui::gfx::BoundingBox layout_box = - ViewPropertiesLayoutBox(view_properties); - - fuchsia::ui::gfx::vec3 logical_size = - Max(Subtract(layout_box.max, layout_box.min), 0.f); - - metrics_.size.width = logical_size.x; - metrics_.size.height = logical_size.y; - metrics_.size.depth = logical_size.z; - metrics_.padding.left = view_properties.inset_from_min.x; - metrics_.padding.top = view_properties.inset_from_min.y; - metrics_.padding.front = view_properties.inset_from_min.z; - metrics_.padding.right = view_properties.inset_from_max.x; - metrics_.padding.bottom = view_properties.inset_from_max.y; - metrics_.padding.back = view_properties.inset_from_max.z; - - FlushViewportMetrics(); -} - -// TODO(SCN-975): Re-enable. -// void PlatformView::ConnectSemanticsProvider( -// fuchsia::ui::viewsv1token::ViewToken token) { -// semantics_bridge_.SetupEnvironment( -// token.value, parent_environment_service_provider_.get()); -// } - -void PlatformView::UpdateViewportMetrics( - const fuchsia::ui::gfx::Metrics& metrics) { - metrics_.scale = metrics.scale_x; - metrics_.scale_z = metrics.scale_z; - - FlushViewportMetrics(); -} - -void PlatformView::FlushViewportMetrics() { - const auto scale = metrics_.scale; - const auto scale_z = metrics_.scale_z; - - SetViewportMetrics({ - scale, // device_pixel_ratio - metrics_.size.width * scale, // physical_width - metrics_.size.height * scale, // physical_height - metrics_.size.depth * scale_z, // physical_depth - metrics_.padding.top * scale, // physical_padding_top - metrics_.padding.right * scale, // physical_padding_right - metrics_.padding.bottom * scale, // physical_padding_bottom - metrics_.padding.left * scale, // physical_padding_left - metrics_.view_inset.front * scale_z, // physical_view_inset_front - metrics_.view_inset.back * scale_z, // physical_view_inset_back - metrics_.view_inset.top * scale, // physical_view_inset_top - metrics_.view_inset.right * scale, // physical_view_inset_right - metrics_.view_inset.bottom * scale, // physical_view_inset_bottom - metrics_.view_inset.left * scale // physical_view_inset_left - }); -} - // |fuchsia::ui::input::InputMethodEditorClient| void PlatformView::DidUpdateState( fuchsia::ui::input::TextInputState state, @@ -302,27 +212,40 @@ void PlatformView::OnScenicError(std::string error) { void PlatformView::OnScenicEvent( std::vector events) { TRACE_EVENT0("flutter", "PlatformView::OnScenicEvent"); + bool should_update_metrics = false; for (const auto& event : events) { switch (event.Which()) { case fuchsia::ui::scenic::Event::Tag::kGfx: switch (event.gfx().Which()) { case fuchsia::ui::gfx::Event::Tag::kMetrics: { - if (!fidl::Equals(event.gfx().metrics().metrics, scenic_metrics_)) { - scenic_metrics_ = std::move(event.gfx().metrics().metrics); - metrics_changed_callback_(scenic_metrics_); - UpdateViewportMetrics(scenic_metrics_); + const fuchsia::ui::gfx::Metrics& metrics = + event.gfx().metrics().metrics; + const float new_view_pixel_ratio = metrics.scale_x; + + // Avoid metrics update when possible -- it is computationally + // expensive. + if (view_pixel_ratio_ != new_view_pixel_ratio) { + view_pixel_ratio_ = new_view_pixel_ratio; + should_update_metrics = true; } break; } - case fuchsia::ui::gfx::Event::Tag::kSizeChangeHint: { - size_change_hint_callback_( - event.gfx().size_change_hint().width_change_factor, - event.gfx().size_change_hint().height_change_factor); - break; - } case fuchsia::ui::gfx::Event::Tag::kViewPropertiesChanged: { - OnPropertiesChanged( - std::move(event.gfx().view_properties_changed().properties)); + const fuchsia::ui::gfx::BoundingBox& bounding_box = + event.gfx().view_properties_changed().properties.bounding_box; + const float new_view_width = + std::max(bounding_box.max.x - bounding_box.min.x, 0.0f); + const float new_view_height = + std::max(bounding_box.max.y - bounding_box.min.y, 0.0f); + + // Avoid metrics update when possible -- it is computationally + // expensive. + if (view_width_ != new_view_width || + view_height_ != new_view_width) { + view_width_ = new_view_width; + view_height_ = new_view_height; + should_update_metrics = true; + } break; } case fuchsia::ui::gfx::Event::Tag::kViewConnected: @@ -372,6 +295,26 @@ void PlatformView::OnScenicEvent( } } } + + if (should_update_metrics) { + SetViewportMetrics({ + view_pixel_ratio_, // device_pixel_ratio + view_width_ * view_pixel_ratio_, // physical_width + view_height_ * view_pixel_ratio_, // physical_height + 0.0f, // physical_padding_top + 0.0f, // physical_padding_right + 0.0f, // physical_padding_bottom + 0.0f, // physical_padding_left + 0.0f, // physical_view_inset_top + 0.0f, // physical_view_inset_right + 0.0f, // physical_view_inset_bottom + 0.0f, // physical_view_inset_left + 0.0f, // p_physical_system_gesture_inset_top + 0.0f, // p_physical_system_gesture_inset_right + 0.0f, // p_physical_system_gesture_inset_bottom + 0.0f, // p_physical_system_gesture_inset_left + }); + } } void PlatformView::OnChildViewConnected(scenic::ResourceId view_holder_id) { @@ -451,8 +394,9 @@ bool PlatformView::OnHandlePointerEvent( pointer_data.change = GetChangeFromPointerEventPhase(pointer.phase); pointer_data.kind = GetKindFromPointerType(pointer.type); pointer_data.device = pointer.pointer_id; - pointer_data.physical_x = pointer.x * metrics_.scale; - pointer_data.physical_y = pointer.y * metrics_.scale; + // Pointer events are in logical pixels, so scale to physical. + pointer_data.physical_x = pointer.x * view_pixel_ratio_; + pointer_data.physical_y = pointer.y * view_pixel_ratio_; // Buttons are single bit values starting with kMousePrimaryButton = 1. pointer_data.buttons = static_cast(pointer.buttons); @@ -578,8 +522,12 @@ std::unique_ptr PlatformView::CreateRenderingSurface() { // This platform does not repeatly lose and gain a surface connection. So the // surface is setup once during platform view setup and returned to the // shell on the initial (and only) |NotifyCreated| call. - auto view_embedder = on_get_view_embedder_callback_(); - return std::make_unique(debug_label_, view_embedder); + auto view_embedder = on_get_view_embedder_callback_ + ? on_get_view_embedder_callback_() + : nullptr; + auto gr_context = + on_get_gr_context_callback_ ? on_get_gr_context_callback_() : nullptr; + return std::make_unique(debug_label_, view_embedder, gr_context); } // |flutter::PlatformView| @@ -816,6 +764,35 @@ void PlatformView::HandleFlutterPlatformViewsChannelPlatformMessage( message->response()->Complete( std::make_unique((const uint8_t*)"[0]", 3u)); } + } else if (method->value == "View.update") { + auto args_it = root.FindMember("args"); + if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { + FML_LOG(ERROR) << "No arguments found."; + return; + } + const auto& args = args_it->value; + + auto view_id = args.FindMember("viewId"); + if (!view_id->value.IsUint64()) { + FML_LOG(ERROR) << "Argument 'viewId' is not a int64"; + return; + } + + auto hit_testable = args.FindMember("hitTestable"); + if (!hit_testable->value.IsBool()) { + FML_LOG(ERROR) << "Argument 'hitTestable' is not a bool"; + return; + } + + auto focusable = args.FindMember("focusable"); + if (!focusable->value.IsBool()) { + FML_LOG(ERROR) << "Argument 'focusable' is not a bool"; + return; + } + + on_update_view_callback_(view_id->value.GetUint64(), + hit_testable->value.GetBool(), + focusable->value.GetBool()); } else if (method->value == "View.dispose") { auto args_it = root.FindMember("args"); if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { @@ -830,6 +807,39 @@ void PlatformView::HandleFlutterPlatformViewsChannelPlatformMessage( return; } on_destroy_view_callback_(view_id->value.GetUint64()); + } else if (method->value == "View.requestFocus") { + auto args_it = root.FindMember("args"); + if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { + FML_LOG(ERROR) << "No arguments found."; + return; + } + const auto& args = args_it->value; + + auto view_ref = args.FindMember("viewRef"); + if (!view_ref->value.IsUint64()) { + FML_LOG(ERROR) << "Argument 'viewRef' is not a int64"; + return; + } + + zx_handle_t handle = view_ref->value.GetUint64(); + zx_handle_t out_handle; + zx_status_t status = + zx_handle_duplicate(handle, ZX_RIGHT_SAME_RIGHTS, &out_handle); + if (status != ZX_OK) { + FML_LOG(ERROR) << "Argument 'viewRef' is not valid"; + return; + } + auto ref = fuchsia::ui::views::ViewRef({ + .reference = zx::eventpair(out_handle), + }); + focuser_->RequestFocus( + std::move(ref), + [view_ref = view_ref->value.GetUint64()]( + fuchsia::ui::views::Focuser_RequestFocus_Result result) { + if (result.is_err()) { + FML_LOG(ERROR) << "Failed to request focus for view: " << view_ref; + } + }); } else { FML_DLOG(ERROR) << "Unknown " << message->channel() << " method " << method->value.GetString(); diff --git a/shell/platform/fuchsia/flutter/platform_view.h b/shell/platform/fuchsia/flutter/platform_view.h index 45e1e1fb97732..35275b3b84c03 100644 --- a/shell/platform/fuchsia/flutter/platform_view.h +++ b/shell/platform/fuchsia/flutter/platform_view.h @@ -5,7 +5,6 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_PLATFORM_VIEW_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_PLATFORM_VIEW_H_ -#include #include #include #include @@ -14,7 +13,6 @@ #include #include "flutter/fml/macros.h" -#include "flutter/lib/ui/window/viewport_metrics.h" #include "flutter/shell/common/platform_view.h" #include "flutter/shell/platform/fuchsia/flutter/accessibility_bridge.h" #include "flutter_runner_product_configuration.h" @@ -24,13 +22,12 @@ namespace flutter_runner { -using OnMetricsUpdate = fit::function; -using OnSizeChangeHint = - fit::function; using OnEnableWireframe = fit::function; using OnCreateView = fit::function; +using OnUpdateView = fit::function; using OnDestroyView = fit::function; using OnGetViewEmbedder = fit::function; +using OnGetGrContext = fit::function; // The per engine component residing on the platform thread is responsible for // all platform specific integrations. @@ -52,26 +49,19 @@ class PlatformView final : public flutter::PlatformView, parent_environment_service_provider, fidl::InterfaceRequest session_listener_request, + fidl::InterfaceHandle focuser, fit::closure on_session_listener_error_callback, - OnMetricsUpdate session_metrics_did_change_callback, - OnSizeChangeHint session_size_change_hint_callback, OnEnableWireframe wireframe_enabled_callback, OnCreateView on_create_view_callback, + OnUpdateView on_update_view_callback, OnDestroyView on_destroy_view_callback, OnGetViewEmbedder on_get_view_embedder_callback, + OnGetGrContext on_get_gr_context_callback, zx_handle_t vsync_event_handle, FlutterRunnerProductConfiguration product_config); - PlatformView(flutter::PlatformView::Delegate& delegate, - std::string debug_label, - flutter::TaskRunners task_runners, - fidl::InterfaceHandle - parent_environment_service_provider, - zx_handle_t vsync_event_handle); ~PlatformView(); - void UpdateViewportMetrics(const fuchsia::ui::gfx::Metrics& metrics); - // |flutter::PlatformView| // |flutter_runner::AccessibilityBridge::Delegate| void SetSemanticsEnabled(bool enabled) override; @@ -88,16 +78,17 @@ class PlatformView final : public flutter::PlatformView, // TODO(MI4-2490): remove once ViewRefControl is passed to Scenic and kept // alive there const fuchsia::ui::views::ViewRef view_ref_; + fuchsia::ui::views::FocuserPtr focuser_; std::unique_ptr accessibility_bridge_; fidl::Binding session_listener_binding_; fit::closure session_listener_error_callback_; - OnMetricsUpdate metrics_changed_callback_; - OnSizeChangeHint size_change_hint_callback_; OnEnableWireframe wireframe_enabled_callback_; OnCreateView on_create_view_callback_; + OnUpdateView on_update_view_callback_; OnDestroyView on_destroy_view_callback_; OnGetViewEmbedder on_get_view_embedder_callback_; + OnGetGrContext on_get_gr_context_callback_; int current_text_input_client_ = 0; fidl::Binding ime_client_; @@ -105,8 +96,7 @@ class PlatformView final : public flutter::PlatformView, fuchsia::ui::input::ImeServicePtr text_sync_service_; fuchsia::sys::ServiceProviderPtr parent_environment_service_provider_; - flutter::LogicalMetrics metrics_; - fuchsia::ui::gfx::Metrics scenic_metrics_; + // last_text_state_ is the last state of the text input as reported by the IME // or initialized by Flutter. We set it to null if Flutter doesn't want any // input, since then there is no text input state at all. @@ -124,16 +114,14 @@ class PlatformView final : public flutter::PlatformView, std::set unregistered_channels_; zx_handle_t vsync_event_handle_ = 0; + float view_width_ = 0.0f; // Width in logical pixels. + float view_height_ = 0.0f; // Height in logical pixels. + float view_pixel_ratio_ = 0.0f; // Logical / physical pixel ratio. + FlutterRunnerProductConfiguration product_config_; void RegisterPlatformMessageHandlers(); - void FlushViewportMetrics(); - - // Called when the view's properties have changed. - void OnPropertiesChanged( - const fuchsia::ui::gfx::ViewProperties& view_properties); - // |fuchsia::ui::input::InputMethodEditorClient| void DidUpdateState( fuchsia::ui::input::TextInputState state, diff --git a/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/shell/platform/fuchsia/flutter/platform_view_unittest.cc index 89a5658d8dd63..6d236e8c8a371 100644 --- a/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -4,7 +4,7 @@ #include "flutter/shell/platform/fuchsia/flutter/platform_view.h" -#include +#include #include #include #include @@ -15,11 +15,11 @@ #include #include -#include "flutter/flow/scene_update_context.h" +#include "flutter/flow/embedded_views.h" #include "flutter/lib/ui/window/platform_message.h" #include "flutter/lib/ui/window/window.h" -#include "fuchsia/ui/views/cpp/fidl.h" #include "gtest/gtest.h" + #include "task_runner_adapter.h" namespace flutter_runner_test::flutter_runner_a11y_test { @@ -41,6 +41,33 @@ class PlatformViewTests : public testing::Test { FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewTests); }; +class MockExternalViewEmbedder : public flutter::ExternalViewEmbedder { + public: + MockExternalViewEmbedder() = default; + ~MockExternalViewEmbedder() override = default; + + SkCanvas* GetRootCanvas() override { return nullptr; } + std::vector GetCurrentCanvases() override { + return std::vector(); + } + + void CancelFrame() override {} + void BeginFrame( + SkISize frame_size, + GrDirectContext* context, + double device_pixel_ratio, + fml::RefPtr raster_thread_merger) override {} + void SubmitFrame(GrDirectContext* context, + std::unique_ptr frame) override { + return; + } + + void PrerollCompositeEmbeddedView( + int view_id, + std::unique_ptr params) override {} + SkCanvas* CompositeEmbeddedView(int view_id) override { return nullptr; } +}; + class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { public: // |flutter::PlatformView::Delegate| @@ -92,6 +119,7 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { flutter::ExternalViewEmbedder* get_view_embedder() { return surface_->GetExternalViewEmbedder(); } + GrDirectContext* get_gr_context() { return surface_->GetContext(); } private: std::unique_ptr surface_; @@ -99,28 +127,18 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { int32_t semantics_features_ = 0; }; -class MockSurfaceProducer - : public flutter::SceneUpdateContext::SurfaceProducer { +class MockFocuser : public fuchsia::ui::views::Focuser { public: - std::unique_ptr - ProduceSurface(const SkISize& size, - const flutter::LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) override { - return nullptr; - } + MockFocuser() = default; + ~MockFocuser() override = default; - bool HasRetainedNode(const flutter::LayerRasterCacheKey& key) const override { - return false; - } + bool request_focus_called = false; - scenic::EntityNode* GetRetainedNode( - const flutter::LayerRasterCacheKey& key) override { - return nullptr; + private: + void RequestFocus(fuchsia::ui::views::ViewRef view_ref, + RequestFocusCallback callback) override { + request_focus_called = true; } - - void SubmitSurface( - std::unique_ptr - surface) override {} }; TEST_F(PlatformViewTests, ChangesAccessibilitySettings) { @@ -146,13 +164,14 @@ TEST_F(PlatformViewTests, ChangesAccessibilitySettings) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -202,13 +221,14 @@ TEST_F(PlatformViewTests, EnableWireframeTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback EnableWireframeCallback, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -269,13 +289,14 @@ TEST_F(PlatformViewTests, CreateViewTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, CreateViewCallback, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -308,6 +329,76 @@ TEST_F(PlatformViewTests, CreateViewTest) { EXPECT_TRUE(create_view_called); } +// Test to make sure that PlatformView correctly registers messages sent on +// the "flutter/platform_views" channel, correctly parses the JSON it receives +// and calls the UdpateViewCallback with the appropriate args. +TEST_F(PlatformViewTests, UpdateViewTest) { + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + zx::eventpair a, b; + zx::eventpair::create(/* flags */ 0u, &a, &b); + auto view_ref = fuchsia::ui::views::ViewRef({ + .reference = std::move(a), + }); + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + // Test wireframe callback function. If the message sent to the platform + // view was properly handled and parsed, this function should be called, + // setting |wireframe_enabled| to true. + int64_t update_view_called = false; + auto UpdateViewCallback = [&update_view_called]( + int64_t view_id, bool hit_testable, + bool focusable) { update_view_called = true; }; + + auto platform_view = flutter_runner::PlatformView( + delegate, // delegate + "test_platform_view", // label + std::move(view_ref), // view_refs + std::move(task_runners), // task_runners + services_provider.service_directory(), // runner_services + nullptr, // parent_environment_service_provider_handle + nullptr, // session_listener_request + nullptr, // focuser, + nullptr, // on_session_listener_error_callback + nullptr, // on_enable_wireframe_callback, + nullptr, // on_create_view_callback, + UpdateViewCallback, // on_update_view_callback, + nullptr, // on_destroy_view_callback, + nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, + 0u, // vsync_event_handle + {} // product_config + ); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = dynamic_cast(&platform_view); + EXPECT_TRUE(base_view); + + // JSON for the message to be passed into the PlatformView. + const uint8_t txt[] = + "{" + " \"method\":\"View.update\"," + " \"args\": {" + " \"viewId\":42," + " \"hitTestable\":true," + " \"focusable\":true" + " }" + "}"; + + fml::RefPtr message = + fml::MakeRefCounted( + "flutter/platform_views", + std::vector(txt, txt + sizeof(txt)), + fml::RefPtr()); + base_view->HandlePlatformMessage(message); + + RunLoopUntilIdle(); + + EXPECT_TRUE(update_view_called); +} + // Test to make sure that PlatformView correctly registers messages sent on // the "flutter/platform_views" channel, correctly parses the JSON it receives // and calls the DestroyViewCallback with the appropriate args. @@ -338,13 +429,14 @@ TEST_F(PlatformViewTests, DestroyViewTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, DestroyViewCallback, // on_destroy_view_callback, nullptr, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -375,6 +467,72 @@ TEST_F(PlatformViewTests, DestroyViewTest) { EXPECT_TRUE(destroy_view_called); } +// Test to make sure that PlatformView correctly registers messages sent on +// the "flutter/platform_views" channel, correctly parses the JSON it receives +// and calls the focuser's RequestFocus with the appropriate args. +TEST_F(PlatformViewTests, RequestFocusTest) { + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + zx::eventpair a, b; + zx::eventpair::create(/* flags */ 0u, &a, &b); + auto view_ref = fuchsia::ui::views::ViewRef({ + .reference = std::move(a), + }); + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + MockFocuser mock_focuser; + fidl::BindingSet focuser_bindings; + auto focuser_handle = focuser_bindings.AddBinding(&mock_focuser); + + auto platform_view = flutter_runner::PlatformView( + delegate, // delegate + "test_platform_view", // label + std::move(view_ref), // view_refs + std::move(task_runners), // task_runners + services_provider.service_directory(), // runner_services + nullptr, // parent_environment_service_provider_handle + nullptr, // session_listener_request + std::move(focuser_handle), // focuser, + nullptr, // on_session_listener_error_callback + nullptr, // on_enable_wireframe_callback, + nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, + nullptr, // on_destroy_view_callback, + nullptr, // on_get_gr_context_callback, + nullptr, // on_get_view_embedder_callback, + 0u, // vsync_event_handle + {} // product_config + ); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = dynamic_cast(&platform_view); + EXPECT_TRUE(base_view); + + // JSON for the message to be passed into the PlatformView. + char buff[254]; + snprintf(buff, sizeof(buff), + "{" + " \"method\":\"View.requestFocus\"," + " \"args\": {" + " \"viewRef\":%u" + " }" + "}", + b.get()); + + fml::RefPtr message = + fml::MakeRefCounted( + "flutter/platform_views", + std::vector(buff, buff + sizeof(buff)), + fml::RefPtr()); + base_view->HandlePlatformMessage(message); + + RunLoopUntilIdle(); + + EXPECT_TRUE(mock_focuser.request_focus_called); +} + // Test to make sure that PlatformView correctly returns a Surface instance // that can surface the view_embedder provided from GetViewEmbedderCallback. TEST_F(PlatformViewTests, GetViewEmbedderTest) { @@ -395,11 +553,8 @@ TEST_F(PlatformViewTests, GetViewEmbedderTest) { ); // Test get view embedder callback function. - MockSurfaceProducer surfaceProducer; - flutter::SceneUpdateContext scene_update_context(nullptr, &surfaceProducer); - flutter::ExternalViewEmbedder* view_embedder = - reinterpret_cast(&scene_update_context); - auto GetViewEmbedderCallback = [view_embedder]() { return view_embedder; }; + MockExternalViewEmbedder view_embedder; + auto GetViewEmbedderCallback = [&view_embedder]() { return &view_embedder; }; auto platform_view = flutter_runner::PlatformView( delegate, // delegate @@ -409,13 +564,14 @@ TEST_F(PlatformViewTests, GetViewEmbedderTest) { services_provider.service_directory(), // runner_services nullptr, // parent_environment_service_provider_handle nullptr, // session_listener_request + nullptr, // focuser, nullptr, // on_session_listener_error_callback - nullptr, // session_metrics_did_change_callback - nullptr, // session_size_change_hint_callback nullptr, // on_enable_wireframe_callback, nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, nullptr, // on_destroy_view_callback, GetViewEmbedderCallback, // on_get_view_embedder_callback, + nullptr, // on_get_gr_context_callback, 0u, // vsync_event_handle {} // product_config ); @@ -426,7 +582,62 @@ TEST_F(PlatformViewTests, GetViewEmbedderTest) { RunLoopUntilIdle(); - EXPECT_EQ(view_embedder, delegate.get_view_embedder()); + EXPECT_EQ(&view_embedder, delegate.get_view_embedder()); +} + +// Test to make sure that PlatformView correctly returns a Surface instance +// that can surface the GrContext provided from GetGrContextCallback. +TEST_F(PlatformViewTests, GetGrContextTest) { + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + zx::eventpair a, b; + zx::eventpair::create(/* flags */ 0u, &a, &b); + auto view_ref = fuchsia::ui::views::ViewRef({ + .reference = std::move(a), + }); + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + nullptr, // platform + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // raster + nullptr, // ui + nullptr // io + ); + + // Test get GrContext callback function. + sk_sp gr_context = + GrDirectContext::MakeMock(nullptr, GrContextOptions()); + auto GetGrContextCallback = [gr_context = gr_context.get()]() { + return gr_context; + }; + + auto platform_view = flutter_runner::PlatformView( + delegate, // delegate + "test_platform_view", // label + std::move(view_ref), // view_refs + std::move(task_runners), // task_runners + services_provider.service_directory(), // runner_services + nullptr, // parent_environment_service_provider_handle + nullptr, // session_listener_request + nullptr, // focuser + nullptr, // on_session_listener_error_callback + nullptr, // on_enable_wireframe_callback, + nullptr, // on_create_view_callback, + nullptr, // on_update_view_callback, + nullptr, // on_destroy_view_callback, + nullptr, // on_get_view_embedder_callback, + GetGrContextCallback, // on_get_gr_context_callback, + 0u, // vsync_event_handle + {} // product_config + ); + + RunLoopUntilIdle(); + + platform_view.NotifyCreated(); + + RunLoopUntilIdle(); + + EXPECT_EQ(gr_context.get(), delegate.get_gr_context()); } } // namespace flutter_runner_test::flutter_runner_a11y_test diff --git a/shell/platform/fuchsia/flutter/session_connection.cc b/shell/platform/fuchsia/flutter/session_connection.cc index 133dc9a43b8be..c87368a8993b9 100644 --- a/shell/platform/fuchsia/flutter/session_connection.cc +++ b/shell/platform/fuchsia/flutter/session_connection.cc @@ -5,8 +5,8 @@ #include "session_connection.h" #include "flutter/fml/make_copyable.h" -#include "lib/fidl/cpp/optional.h" -#include "lib/ui/scenic/cpp/commands.h" +#include "flutter/fml/trace_event.h" + #include "vsync_recorder.h" #include "vsync_waiter.h" @@ -14,23 +14,11 @@ namespace flutter_runner { SessionConnection::SessionConnection( std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, fidl::InterfaceHandle session, fml::closure session_error_callback, on_frame_presented_event on_frame_presented_callback, zx_handle_t vsync_event_handle) - : debug_label_(std::move(debug_label)), - session_wrapper_(session.Bind(), nullptr), - root_view_(&session_wrapper_, - std::move(view_token), - std::move(view_ref_pair.control_ref), - std::move(view_ref_pair.view_ref), - debug_label), - root_node_(&session_wrapper_), - surface_producer_( - std::make_unique(&session_wrapper_)), - scene_update_context_(&session_wrapper_, surface_producer_.get()), + : session_wrapper_(session.Bind(), nullptr), on_frame_presented_callback_(std::move(on_frame_presented_callback)), vsync_event_handle_(vsync_event_handle) { session_wrapper_.set_error_handler( @@ -63,11 +51,7 @@ SessionConnection::SessionConnection( } // callback ); - session_wrapper_.SetDebugName(debug_label_); - - root_view_.AddChild(root_node_); - root_node_.SetEventMask(fuchsia::ui::gfx::kMetricsEventMask | - fuchsia::ui::gfx::kSizeChangeHintEventMask); + session_wrapper_.SetDebugName(debug_label); // Get information to finish initialization and only then allow Present()s. session_wrapper_.RequestPresentationTimes( @@ -91,8 +75,7 @@ SessionConnection::SessionConnection( SessionConnection::~SessionConnection() = default; -void SessionConnection::Present( - flutter::CompositorContext::ScopedFrame* frame) { +void SessionConnection::Present() { TRACE_EVENT0("gfx", "SessionConnection::Present"); TRACE_FLOW_BEGIN("gfx", "SessionConnection::PresentSession", @@ -114,21 +97,6 @@ void SessionConnection::Present( present_session_pending_ = true; ToggleSignal(vsync_event_handle_, false); } - - if (frame) { - // Execute paint tasks and signal fences. - auto surfaces_to_submit = scene_update_context_.ExecutePaintTasks(*frame); - - // Tell the surface producer that a present has occurred so it can perform - // book-keeping on buffer caches. - surface_producer_->OnSurfacesPresented(std::move(surfaces_to_submit)); - } -} - -void SessionConnection::OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - surface_producer_->OnSessionSizeChangeHint(width_change_factor, - height_change_factor); } fml::TimePoint SessionConnection::CalculateNextLatchPoint( @@ -160,17 +128,6 @@ fml::TimePoint SessionConnection::CalculateNextLatchPoint( return minimum_latch_point_to_target; } -void SessionConnection::set_enable_wireframe(bool enable) { - session_wrapper_.Enqueue( - scenic::NewSetEnableDebugViewBoundsCmd(root_view_.id(), enable)); -} - -void SessionConnection::EnqueueClearOps() { - // We are going to be sending down a fresh node hierarchy every frame. So just - // enqueue a detach op on the imported root node. - session_wrapper_.Enqueue(scenic::NewDetachChildrenCmd(root_node_.id())); -} - void SessionConnection::PresentSession() { TRACE_EVENT0("gfx", "SessionConnection::PresentSession"); @@ -232,10 +189,6 @@ void SessionConnection::PresentSession() { VsyncRecorder::GetInstance().UpdateNextPresentationInfo( std::move(info)); }); - - // Prepare for the next frame. These ops won't be processed till the next - // present. - EnqueueClearOps(); } void SessionConnection::ToggleSignal(zx_handle_t handle, bool set) { diff --git a/shell/platform/fuchsia/flutter/session_connection.h b/shell/platform/fuchsia/flutter/session_connection.h index dcd55f19afbf4..7630d15e77837 100644 --- a/shell/platform/fuchsia/flutter/session_connection.h +++ b/shell/platform/fuchsia/flutter/session_connection.h @@ -5,22 +5,14 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_SESSION_CONNECTION_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_SESSION_CONNECTION_H_ -#include +#include #include -#include #include -#include -#include -#include #include -#include -#include "flutter/flow/compositor_context.h" #include "flutter/flow/scene_update_context.h" #include "flutter/fml/closure.h" #include "flutter/fml/macros.h" -#include "flutter/fml/trace_event.h" -#include "vulkan_surface_producer.h" namespace flutter_runner { @@ -29,11 +21,9 @@ using on_frame_presented_event = // The component residing on the raster thread that is responsible for // maintaining the Scenic session connection and presenting node updates. -class SessionConnection final { +class SessionConnection final : public flutter::SessionWrapper { public: SessionConnection(std::string debug_label, - fuchsia::ui::views::ViewToken view_token, - scenic::ViewRefPair view_ref_pair, fidl::InterfaceHandle session, fml::closure session_error_callback, on_frame_presented_event on_frame_presented_callback, @@ -41,36 +31,8 @@ class SessionConnection final { ~SessionConnection(); - bool has_metrics() const { return scene_update_context_.has_metrics(); } - - const fuchsia::ui::gfx::MetricsPtr& metrics() const { - return scene_update_context_.metrics(); - } - - void set_metrics(const fuchsia::ui::gfx::Metrics& metrics) { - fuchsia::ui::gfx::Metrics metrics_copy; - metrics.Clone(&metrics_copy); - scene_update_context_.set_metrics( - fidl::MakeOptional(std::move(metrics_copy))); - } - - void set_enable_wireframe(bool enable); - - flutter::SceneUpdateContext& scene_update_context() { - return scene_update_context_; - } - - scenic::ContainerNode& root_node() { return root_node_; } - scenic::View* root_view() { return &root_view_; } - - void Present(flutter::CompositorContext::ScopedFrame* frame); - - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor); - - VulkanSurfaceProducer* vulkan_surface_producer() { - return surface_producer_.get(); - } + scenic::Session* get() override { return &session_wrapper_; } + void Present() override; static fml::TimePoint CalculateNextLatchPoint( fml::TimePoint present_requested_time, @@ -82,14 +44,8 @@ class SessionConnection final { future_presentation_infos); private: - const std::string debug_label_; scenic::Session session_wrapper_; - scenic::View root_view_; - scenic::EntityNode root_node_; - - std::unique_ptr surface_producer_; - flutter::SceneUpdateContext scene_update_context_; on_frame_presented_event on_frame_presented_callback_; zx_handle_t vsync_event_handle_; @@ -122,8 +78,6 @@ class SessionConnection final { bool present_session_pending_ = false; - void EnqueueClearOps(); - void PresentSession(); static void ToggleSignal(zx_handle_t handle, bool raise); diff --git a/shell/platform/fuchsia/flutter/surface.cc b/shell/platform/fuchsia/flutter/surface.cc index ba916fce2a41e..fdcb42a3c7dd7 100644 --- a/shell/platform/fuchsia/flutter/surface.cc +++ b/shell/platform/fuchsia/flutter/surface.cc @@ -14,8 +14,11 @@ namespace flutter_runner { Surface::Surface(std::string debug_label, - flutter::ExternalViewEmbedder* view_embedder) - : debug_label_(std::move(debug_label)), view_embedder_(view_embedder) {} + flutter::ExternalViewEmbedder* view_embedder, + GrDirectContext* gr_context) + : debug_label_(std::move(debug_label)), + view_embedder_(view_embedder), + gr_context_(gr_context) {} Surface::~Surface() = default; @@ -36,7 +39,7 @@ std::unique_ptr Surface::AcquireFrame( // |flutter::Surface| GrDirectContext* Surface::GetContext() { - return nullptr; + return gr_context_; } static zx_status_t DriverWatcher(int dirfd, diff --git a/shell/platform/fuchsia/flutter/surface.h b/shell/platform/fuchsia/flutter/surface.h index 7040f0b742663..8ebb89de0da24 100644 --- a/shell/platform/fuchsia/flutter/surface.h +++ b/shell/platform/fuchsia/flutter/surface.h @@ -16,7 +16,8 @@ namespace flutter_runner { class Surface final : public flutter::Surface { public: Surface(std::string debug_label, - flutter::ExternalViewEmbedder* view_embedder); + flutter::ExternalViewEmbedder* view_embedder, + GrDirectContext* gr_context); ~Surface() override; @@ -24,6 +25,7 @@ class Surface final : public flutter::Surface { const bool valid_ = CanConnectToDisplay(); const std::string debug_label_; flutter::ExternalViewEmbedder* view_embedder_; + GrDirectContext* gr_context_; // |flutter::Surface| bool IsValid() override; diff --git a/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc b/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc index 87efa11e95f2a..5a5463266faa8 100644 --- a/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/session_connection_unittests.cc @@ -2,18 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "gtest/gtest.h" - +#include +#include +#include #include #include -#include - -#include -#include #include "flutter/shell/platform/fuchsia/flutter/logging.h" #include "flutter/shell/platform/fuchsia/flutter/runner.h" #include "flutter/shell/platform/fuchsia/flutter/session_connection.h" +#include "gtest/gtest.h" using namespace flutter_runner; @@ -30,12 +28,8 @@ class SessionConnectionTest : public ::testing::Test { loop_.StartThread("SessionConnectionTestThread", &fidl_thread_)); auto session_listener_request = session_listener_.NewRequest(); - auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); - view_token_ = std::move(view_token); scenic_->CreateSession(session_.NewRequest(), session_listener_.Bind()); - presenter_->PresentOrReplaceView(std::move(view_holder_token), nullptr); - FML_CHECK(zx::event::create(0, &vsync_event_) == ZX_OK); // Ensure Scenic has had time to wake up before the test logic begins. @@ -60,7 +54,6 @@ class SessionConnectionTest : public ::testing::Test { fidl::InterfaceHandle session_; fidl::InterfaceHandle session_listener_; - fuchsia::ui::views::ViewToken view_token_; zx::event vsync_event_; thrd_t fidl_thread_; }; @@ -76,12 +69,11 @@ TEST_F(SessionConnectionTest, SimplePresentTest) { }; flutter_runner::SessionConnection session_connection( - "debug label", std::move(view_token_), scenic::ViewRefPair::New(), - std::move(session_), on_session_error_callback, + "debug label", std::move(session_), on_session_error_callback, on_frame_presented_callback, vsync_event_.get()); for (int i = 0; i < 200; ++i) { - session_connection.Present(nullptr); + session_connection.Present(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } @@ -99,12 +91,11 @@ TEST_F(SessionConnectionTest, BatchedPresentTest) { }; flutter_runner::SessionConnection session_connection( - "debug label", std::move(view_token_), scenic::ViewRefPair::New(), - std::move(session_), on_session_error_callback, + "debug label", std::move(session_), on_session_error_callback, on_frame_presented_callback, vsync_event_.get()); for (int i = 0; i < 200; ++i) { - session_connection.Present(nullptr); + session_connection.Present(); if (i % 10 == 9) { std::this_thread::sleep_for(std::chrono::milliseconds(20)); } diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.cc b/shell/platform/fuchsia/flutter/vulkan_surface.cc index 0d117047e7ea2..1b9961ceca0e7 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface.cc @@ -332,14 +332,19 @@ bool VulkanSurface::SetupSkiaSurface(sk_sp context, return false; } - const GrVkImageInfo image_info = { - vulkan_image_.vk_image, // image - {vk_memory_, 0, memory_reqs.size, 0}, // alloc - image_create_info.tiling, // tiling - image_create_info.initialLayout, // layout - image_create_info.format, // format - image_create_info.mipLevels, // level count - }; + GrVkAlloc alloc; + alloc.fMemory = vk_memory_; + alloc.fOffset = 0; + alloc.fSize = memory_reqs.size; + alloc.fFlags = 0; + + GrVkImageInfo image_info; + image_info.fImage = vulkan_image_.vk_image; + image_info.fAlloc = alloc; + image_info.fImageTiling = image_create_info.tiling; + image_info.fImageLayout = image_create_info.initialLayout; + image_info.fFormat = image_create_info.format; + image_info.fLevelCount = image_create_info.mipLevels; GrBackendRenderTarget sk_render_target(size.width(), size.height(), 0, image_info); diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.h b/shell/platform/fuchsia/flutter/vulkan_surface.h index e6b7eb4e8943a..bff2713a011c9 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -12,17 +13,46 @@ #include #include "flutter/flow/raster_cache_key.h" -#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" #include "flutter/vulkan/vulkan_command_buffer.h" #include "flutter/vulkan/vulkan_handle.h" #include "flutter/vulkan/vulkan_proc_table.h" #include "flutter/vulkan/vulkan_provider.h" -#include "lib/ui/scenic/cpp/resources.h" #include "third_party/skia/include/core/SkSurface.h" namespace flutter_runner { +class SurfaceProducerSurface { + public: + virtual ~SurfaceProducerSurface() = default; + + virtual size_t AdvanceAndGetAge() = 0; + + virtual bool FlushSessionAcquireAndReleaseEvents() = 0; + + virtual bool IsValid() const = 0; + + virtual SkISize GetSize() const = 0; + + virtual void SignalWritesFinished( + const std::function& on_writes_committed) = 0; + + virtual scenic::Image* GetImage() = 0; + + virtual sk_sp GetSkiaSurface() const = 0; +}; + +class SurfaceProducer { + public: + virtual ~SurfaceProducer() = default; + + virtual std::unique_ptr ProduceSurface( + const SkISize& size) = 0; + + virtual void SubmitSurface( + std::unique_ptr surface) = 0; +}; + // A |VkImage| and its relevant metadata. struct VulkanImage { VulkanImage() = default; @@ -44,8 +74,7 @@ bool CreateVulkanImage(vulkan::VulkanProvider& vulkan_provider, const SkISize& size, VulkanImage* out_vulkan_image); -class VulkanSurface final - : public flutter::SceneUpdateContext::SurfaceProducerSurface { +class VulkanSurface final : public SurfaceProducerSurface { public: VulkanSurface(vulkan::VulkanProvider& vulkan_provider, sk_sp context, @@ -54,16 +83,16 @@ class VulkanSurface final ~VulkanSurface() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| size_t AdvanceAndGetAge() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| bool FlushSessionAcquireAndReleaseEvents() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| bool IsValid() const override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| SkISize GetSize() const override; // Note: It is safe for the caller to collect the surface in the @@ -71,10 +100,10 @@ class VulkanSurface final void SignalWritesFinished( const std::function& on_writes_committed) override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| scenic::Image* GetImage() override; - // |flutter::SceneUpdateContext::SurfaceProducerSurface| + // |SurfaceProducerSurface| sk_sp GetSkiaSurface() const override; const vulkan::VulkanHandle& GetVkImage() { @@ -119,41 +148,6 @@ class VulkanSurface final // if the swap was not successful. bool BindToImage(sk_sp context, VulkanImage vulkan_image); - // Flutter may retain a |VulkanSurface| for a |flutter::Layer| subtree to - // improve the performance. The |retained_key_| identifies which layer subtree - // this |VulkanSurface| is retained for. The key has two parts. One is the - // pointer to the root of that layer subtree: |retained_key_.id()|. Another is - // the transformation matrix: |retained_key_.matrix()|. We need the matrix - // part because a different matrix would invalidate the pixels (raster cache) - // in this |VulkanSurface|. - const flutter::LayerRasterCacheKey& GetRetainedKey() const { - return retained_key_; - } - - // For better safety in retained rendering, Flutter uses a retained - // |EntityNode| associated with the retained surface instead of using the - // retained surface directly. Hence Flutter can't modify the surface during - // retained rendering. However, the node itself is modifiable to be able - // to adjust its position. - scenic::EntityNode* GetRetainedNode() { - used_in_retained_rendering_ = true; - return retained_node_.get(); - } - - // Check whether the retained surface (and its associated |EntityNode|) is - // used in the current frame or not. If unused, the |VulkanSurfacePool| will - // try to recycle the surface. This flag is reset after each frame. - bool IsUsedInRetainedRendering() const { return used_in_retained_rendering_; } - void ResetIsUsedInRetainedRendering() { used_in_retained_rendering_ = false; } - - // Let this surface own the retained EntityNode associated with it (see - // |GetRetainedNode|), and set the retained key (see |GetRetainedKey|). - void SetRetainedInfo(const flutter::LayerRasterCacheKey& key, - std::unique_ptr node) { - retained_key_ = key; - retained_node_ = std::move(node); - } - private: static constexpr int kSizeHistorySize = 4; @@ -202,11 +196,6 @@ class VulkanSurface final size_t age_ = 0; bool valid_ = false; - flutter::LayerRasterCacheKey retained_key_ = {0, SkMatrix::Scale(1, 1)}; - std::unique_ptr retained_node_ = nullptr; - - std::atomic used_in_retained_rendering_ = {false}; - FML_DISALLOW_COPY_AND_ASSIGN(VulkanSurface); }; diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc b/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc index cb40dd25ddbb3..1b85a957898bd 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc @@ -112,8 +112,7 @@ std::unique_ptr VulkanSurfacePool::GetCachedOrCreateSurface( } void VulkanSurfacePool::SubmitSurface( - std::unique_ptr - p_surface) { + std::unique_ptr p_surface) { TRACE_EVENT0("flutter", "VulkanSurfacePool::SubmitSurface"); // This cast is safe because |VulkanSurface| is the only implementation of @@ -126,43 +125,14 @@ void VulkanSurfacePool::SubmitSurface( return; } - const flutter::LayerRasterCacheKey& retained_key = - vulkan_surface->GetRetainedKey(); - - // TODO(https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=44141): Re-enable - // retained surfaces after we find out why textures are being prematurely - // recycled. - const bool kUseRetainedSurfaces = false; - if (kUseRetainedSurfaces && retained_key.id() != 0) { - // Add the surface to |retained_surfaces_| if its retained key has a valid - // layer id (|retained_key.id()|). - // - // We have to add the entry to |retained_surfaces_| map early when it's - // still pending (|is_pending| = true). Otherwise (if we add the surface - // later when |SignalRetainedReady| is called), Flutter would fail to find - // the retained node before the painting is done (which could take multiple - // frames). Flutter would then create a new |VulkanSurface| for the layer - // upon the failed lookup. The new |VulkanSurface| would invalidate this - // surface, and before the new |VulkanSurface| is done painting, another - // newer |VulkanSurface| is likely to be created to replace the new - // |VulkanSurface|. That would make the retained rendering much less useful - // in improving the performance. - auto insert_iterator = retained_surfaces_.insert(std::make_pair( - retained_key, RetainedSurface({true, std::move(vulkan_surface)}))); - if (insert_iterator.second) { - insert_iterator.first->second.vk_surface->SignalWritesFinished(std::bind( - &VulkanSurfacePool::SignalRetainedReady, this, retained_key)); - } - } else { - uintptr_t surface_key = reinterpret_cast(vulkan_surface.get()); - auto insert_iterator = pending_surfaces_.insert(std::make_pair( - surface_key, // key - std::move(vulkan_surface) // value - )); - if (insert_iterator.second) { - insert_iterator.first->second->SignalWritesFinished(std::bind( - &VulkanSurfacePool::RecyclePendingSurface, this, surface_key)); - } + uintptr_t surface_key = reinterpret_cast(vulkan_surface.get()); + auto insert_iterator = pending_surfaces_.insert(std::make_pair( + surface_key, // key + std::move(vulkan_surface) // value + )); + if (insert_iterator.second) { + insert_iterator.first->second->SignalWritesFinished(std::bind( + &VulkanSurfacePool::RecyclePendingSurface, this, surface_key)); } } @@ -213,25 +183,6 @@ void VulkanSurfacePool::RecycleSurface(std::unique_ptr surface) { TraceStats(); } -void VulkanSurfacePool::RecycleRetainedSurface( - const flutter::LayerRasterCacheKey& key) { - auto it = retained_surfaces_.find(key); - if (it == retained_surfaces_.end()) { - return; - } - - // The surface should not be pending. - FML_DCHECK(!it->second.is_pending); - - auto surface_to_recycle = std::move(it->second.vk_surface); - retained_surfaces_.erase(it); - RecycleSurface(std::move(surface_to_recycle)); -} - -void VulkanSurfacePool::SignalRetainedReady(flutter::LayerRasterCacheKey key) { - retained_surfaces_[key].is_pending = false; -} - void VulkanSurfacePool::AgeAndCollectOldBuffers() { TRACE_EVENT0("flutter", "VulkanSurfacePool::AgeAndCollectOldBuffers"); @@ -268,25 +219,6 @@ void VulkanSurfacePool::AgeAndCollectOldBuffers() { } } - // Recycle retained surfaces that are not used and not pending in this frame. - // - // It's safe to recycle any retained surfaces that are not pending no matter - // whether they're used or not. Hence if there's memory pressure, feel free to - // recycle all retained surfaces that are not pending. - std::vector recycle_keys; - for (auto& [key, retained_surface] : retained_surfaces_) { - if (retained_surface.is_pending || - retained_surface.vk_surface->IsUsedInRetainedRendering()) { - // Reset the flag for the next frame - retained_surface.vk_surface->ResetIsUsedInRetainedRendering(); - } else { - recycle_keys.push_back(key); - } - } - for (auto& key : recycle_keys) { - RecycleRetainedSurface(key); - } - TraceStats(); } @@ -320,15 +252,9 @@ void VulkanSurfacePool::ShrinkToFit() { void VulkanSurfacePool::TraceStats() { // Resources held in cached buffers. size_t cached_surfaces_bytes = 0; - size_t retained_surfaces_bytes = 0; - for (const auto& surface : available_surfaces_) { cached_surfaces_bytes += surface->GetAllocationSize(); } - for (const auto& retained_entry : retained_surfaces_) { - retained_surfaces_bytes += - retained_entry.second.vk_surface->GetAllocationSize(); - } // Resources held by Skia. int skia_resources = 0; @@ -342,13 +268,13 @@ void VulkanSurfacePool::TraceStats() { "Created", trace_surfaces_created_, // "Reused", trace_surfaces_reused_, // "PendingInCompositor", pending_surfaces_.size(), // - "Retained", retained_surfaces_.size(), // + "Retained", 0, // "SkiaCacheResources", skia_resources // ); TRACE_COUNTER("flutter", "SurfacePoolBytes", 0u, // "CachedBytes", cached_surfaces_bytes, // - "RetainedBytes", retained_surfaces_bytes, // + "RetainedBytes", 0, // "SkiaCacheBytes", skia_bytes, // "SkiaCachePurgeable", skia_cache_purgeable // ); diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_pool.h b/shell/platform/fuchsia/flutter/vulkan_surface_pool.h index 302be9bac7b8a..0667db88d6c0c 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_pool.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface_pool.h @@ -28,9 +28,7 @@ class VulkanSurfacePool final { std::unique_ptr AcquireSurface(const SkISize& size); - void SubmitSurface( - std::unique_ptr - surface); + void SubmitSurface(std::unique_ptr surface); void AgeAndCollectOldBuffers(); @@ -38,26 +36,7 @@ class VulkanSurfacePool final { // small as they can be. void ShrinkToFit(); - // For |VulkanSurfaceProducer::HasRetainedNode|. - bool HasRetainedNode(const flutter::LayerRasterCacheKey& key) const { - return retained_surfaces_.find(key) != retained_surfaces_.end(); - } - // For |VulkanSurfaceProducer::GetRetainedNode|. - scenic::EntityNode* GetRetainedNode(const flutter::LayerRasterCacheKey& key) { - FML_DCHECK(HasRetainedNode(key)); - return retained_surfaces_[key].vk_surface->GetRetainedNode(); - } - private: - // Struct for retained_surfaces_ map. - struct RetainedSurface { - // If |is_pending| is true, the |vk_surface| is still under painting - // (similar to those in |pending_surfaces_|) so we can't recycle the - // |vk_surface| yet. - bool is_pending; - std::unique_ptr vk_surface; - }; - vulkan::VulkanProvider& vulkan_provider_; sk_sp context_; scenic::Session* scenic_session_; @@ -65,9 +44,6 @@ class VulkanSurfacePool final { std::unordered_map> pending_surfaces_; - // Retained surfaces keyed by the layer that created and used the surface. - flutter::LayerRasterCacheKey::Map retained_surfaces_; - size_t trace_surfaces_created_ = 0; size_t trace_surfaces_reused_ = 0; @@ -79,13 +55,6 @@ class VulkanSurfacePool final { void RecyclePendingSurface(uintptr_t surface_key); - // Clear the |is_pending| flag of the retained surface. - void SignalRetainedReady(flutter::LayerRasterCacheKey key); - - // Remove the corresponding surface from |retained_surfaces| and recycle it. - // The surface must not be pending. - void RecycleRetainedSurface(const flutter::LayerRasterCacheKey& key); - void TraceStats(); FML_DISALLOW_COPY_AND_ASSIGN(VulkanSurfacePool); diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc b/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc index 1e0d03df847a8..36eeadd85afa3 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc @@ -155,9 +155,7 @@ bool VulkanSurfaceProducer::Initialize(scenic::Session* scenic_session) { } void VulkanSurfaceProducer::OnSurfacesPresented( - std::vector< - std::unique_ptr> - surfaces) { + std::vector> surfaces) { TRACE_EVENT0("flutter", "VulkanSurfaceProducer::OnSurfacesPresented"); // Do a single flush for all canvases derived from the context. @@ -197,11 +195,12 @@ void VulkanSurfaceProducer::OnSurfacesPresented( } bool VulkanSurfaceProducer::TransitionSurfacesToExternal( - const std::vector< - std::unique_ptr>& - surfaces) { + const std::vector>& surfaces) { for (auto& surface : surfaces) { auto vk_surface = static_cast(surface.get()); + if (!vk_surface) { + continue; + } vulkan::VulkanCommandBuffer* command_buffer = vk_surface->GetCommandBuffer(logical_device_->GetCommandPool()); @@ -259,21 +258,15 @@ bool VulkanSurfaceProducer::TransitionSurfacesToExternal( return true; } -std::unique_ptr -VulkanSurfaceProducer::ProduceSurface( - const SkISize& size, - const flutter::LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) { +std::unique_ptr VulkanSurfaceProducer::ProduceSurface( + const SkISize& size) { FML_DCHECK(valid_); last_produce_time_ = async::Now(async_get_default_dispatcher()); - auto surface = surface_pool_->AcquireSurface(size); - surface->SetRetainedInfo(layer_key, std::move(entity_node)); - return surface; + return surface_pool_->AcquireSurface(size); } void VulkanSurfaceProducer::SubmitSurface( - std::unique_ptr - surface) { + std::unique_ptr surface) { FML_DCHECK(valid_ && surface != nullptr); surface_pool_->SubmitSurface(std::move(surface)); } diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h index 403ecd19bebbe..2caaf64a36d7f 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h @@ -8,8 +8,8 @@ #include #include -#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" +#include "flutter/fml/memory/weak_ptr.h" #include "flutter/vulkan/vulkan_application.h" #include "flutter/vulkan/vulkan_device.h" #include "flutter/vulkan/vulkan_proc_table.h" @@ -22,9 +22,8 @@ namespace flutter_runner { -class VulkanSurfaceProducer final - : public flutter::SceneUpdateContext::SurfaceProducer, - public vulkan::VulkanProvider { +class VulkanSurfaceProducer final : public SurfaceProducer, + public vulkan::VulkanProvider { public: VulkanSurfaceProducer(scenic::Session* scenic_session); @@ -32,39 +31,15 @@ class VulkanSurfaceProducer final bool IsValid() const { return valid_; } - // |flutter::SceneUpdateContext::SurfaceProducer| - std::unique_ptr - ProduceSurface(const SkISize& size, - const flutter::LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) override; + // |SurfaceProducer| + std::unique_ptr ProduceSurface( + const SkISize& size) override; - // |flutter::SceneUpdateContext::SurfaceProducer| - void SubmitSurface( - std::unique_ptr - surface) override; - - // |flutter::SceneUpdateContext::HasRetainedNode| - bool HasRetainedNode(const flutter::LayerRasterCacheKey& key) const override { - return surface_pool_->HasRetainedNode(key); - } - - // |flutter::SceneUpdateContext::GetRetainedNode| - scenic::EntityNode* GetRetainedNode( - const flutter::LayerRasterCacheKey& key) override { - return surface_pool_->GetRetainedNode(key); - } + // |SurfaceProducer| + void SubmitSurface(std::unique_ptr surface) override; void OnSurfacesPresented( - std::vector< - std::unique_ptr> - surfaces); - - void OnSessionSizeChangeHint(float width_change_factor, - float height_change_factor) { - FX_LOGF(INFO, LOG_TAG, - "VulkanSurfaceProducer:OnSessionSizeChangeHint %f, %f", - width_change_factor, height_change_factor); - } + std::vector> surfaces); GrDirectContext* gr_context() { return context_.get(); } @@ -76,9 +51,7 @@ class VulkanSurfaceProducer final } bool TransitionSurfacesToExternal( - const std::vector< - std::unique_ptr>& - surfaces); + const std::vector>& surfaces); // Note: the order here is very important. The proctable must be destroyed // last because it contains the function pointers for VkDestroyDevice and diff --git a/shell/platform/glfw/client_wrapper/BUILD.gn b/shell/platform/glfw/client_wrapper/BUILD.gn index d5115de50d84c..3a4f57263ef6f 100644 --- a/shell/platform/glfw/client_wrapper/BUILD.gn +++ b/shell/platform/glfw/client_wrapper/BUILD.gn @@ -78,6 +78,8 @@ executable("client_wrapper_glfw_unittests") { "flutter_window_unittests.cc", ] + defines = [ "FLUTTER_DESKTOP_LIBRARY" ] + deps = [ ":client_wrapper_glfw", ":client_wrapper_glfw_fixtures", diff --git a/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h b/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h index a2a9da9ef3f8b..aa0f03deed16a 100644 --- a/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h +++ b/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h @@ -5,10 +5,10 @@ #ifndef FLUTTER_SHELL_PLATFORM_GLFW_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_GLFW_H_ #define FLUTTER_SHELL_PLATFORM_GLFW_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_GLFW_H_ -#include - #include +#include + #include "flutter_window.h" #include "plugin_registrar.h" @@ -34,6 +34,14 @@ class PluginRegistrarGlfw : public PluginRegistrar { FlutterWindow* window() { return window_.get(); } + // Enables input blocking on the given channel name. + // + // If set, then the parent window should disable input callbacks + // while waiting for the handler for messages on that channel to run. + void EnableInputBlockingForChannel(const std::string& channel) { + FlutterDesktopRegistrarEnableInputBlocking(registrar(), channel.c_str()); + } + private: // The owned FlutterWindow, if any. std::unique_ptr window_; diff --git a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc index 38614fc15c554..8875a1b51d23d 100644 --- a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc +++ b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc @@ -182,3 +182,11 @@ FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(2); } + +void FlutterDesktopRegistrarEnableInputBlocking( + FlutterDesktopPluginRegistrarRef registrar, + const char* channel) { + if (s_stub_implementation) { + s_stub_implementation->RegistrarEnableInputBlocking(channel); + } +} diff --git a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h index 25792e8c14be1..765a02e45f7f5 100644 --- a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h +++ b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h @@ -88,6 +88,9 @@ class StubFlutterGlfwApi { // Called for FlutterDesktopShutDownEngine. virtual bool ShutDownEngine() { return true; } + + // Called for FlutterDesktopRegistrarEnableInputBlocking. + virtual void RegistrarEnableInputBlocking(const char* channel) {} }; // A test helper that owns a stub implementation, making it the test stub for diff --git a/shell/platform/glfw/flutter_glfw.cc b/shell/platform/glfw/flutter_glfw.cc index 802a1d9a73126..6ed101449e9ca 100644 --- a/shell/platform/glfw/flutter_glfw.cc +++ b/shell/platform/glfw/flutter_glfw.cc @@ -184,6 +184,9 @@ static FlutterDesktopMessage ConvertToDesktopMessage( // that a screen coordinate is one dp. static double GetScreenCoordinatesPerInch() { auto* primary_monitor = glfwGetPrimaryMonitor(); + if (primary_monitor == nullptr) { + return kDpPerInch; + } auto* primary_monitor_mode = glfwGetVideoMode(primary_monitor); int primary_monitor_width_mm; glfwGetMonitorPhysicalSize(primary_monitor, &primary_monitor_width_mm, diff --git a/shell/platform/glfw/public/flutter_glfw.h b/shell/platform/glfw/public/flutter_glfw.h index 45a6420f93c87..e8b4513877852 100644 --- a/shell/platform/glfw/public/flutter_glfw.h +++ b/shell/platform/glfw/public/flutter_glfw.h @@ -219,10 +219,26 @@ FLUTTER_EXPORT bool FlutterDesktopShutDownEngine( FlutterDesktopEngineRef engine); // Returns the window associated with this registrar's engine instance. +// // This is a GLFW shell-specific extension to flutter_plugin_registrar.h FLUTTER_EXPORT FlutterDesktopWindowRef FlutterDesktopRegistrarGetWindow(FlutterDesktopPluginRegistrarRef registrar); +// Enables input blocking on the given channel. +// +// If set, then the Flutter window will disable input callbacks +// while waiting for the handler for messages on that channel to run. This is +// useful if handling the message involves showing a modal window, for instance. +// +// This must be called after FlutterDesktopSetMessageHandler, as setting a +// handler on a channel will reset the input blocking state back to the +// default of disabled. +// +// This is a GLFW shell-specific extension to flutter_plugin_registrar.h +FLUTTER_EXPORT void FlutterDesktopRegistrarEnableInputBlocking( + FlutterDesktopPluginRegistrarRef registrar, + const char* channel); + #if defined(__cplusplus) } // extern "C" #endif diff --git a/shell/platform/linux/fl_basic_message_channel.cc b/shell/platform/linux/fl_basic_message_channel.cc index 405ae0e0427f1..5411c4a6d139f 100644 --- a/shell/platform/linux/fl_basic_message_channel.cc +++ b/shell/platform/linux/fl_basic_message_channel.cc @@ -117,8 +117,9 @@ static void channel_closed_cb(gpointer user_data) { self->channel_closed = TRUE; // Disconnect handler. - if (self->message_handler_destroy_notify != nullptr) + if (self->message_handler_destroy_notify != nullptr) { self->message_handler_destroy_notify(self->message_handler_data); + } self->message_handler = nullptr; self->message_handler_data = nullptr; self->message_handler_destroy_notify = nullptr; @@ -136,8 +137,9 @@ static void fl_basic_message_channel_dispose(GObject* object) { g_clear_pointer(&self->name, g_free); g_clear_object(&self->codec); - if (self->message_handler_destroy_notify != nullptr) + if (self->message_handler_destroy_notify != nullptr) { self->message_handler_destroy_notify(self->message_handler_data); + } self->message_handler = nullptr; self->message_handler_data = nullptr; self->message_handler_destroy_notify = nullptr; @@ -187,13 +189,15 @@ G_MODULE_EXPORT void fl_basic_message_channel_set_message_handler( g_warning( "Attempted to set message handler on a closed FlBasicMessageChannel"); } - if (destroy_notify != nullptr) + if (destroy_notify != nullptr) { destroy_notify(user_data); + } return; } - if (self->message_handler_destroy_notify != nullptr) + if (self->message_handler_destroy_notify != nullptr) { self->message_handler_destroy_notify(self->message_handler_data); + } self->message_handler = handler; self->message_handler_data = user_data; @@ -211,8 +215,9 @@ G_MODULE_EXPORT gboolean fl_basic_message_channel_respond( g_autoptr(GBytes) data = fl_message_codec_encode_message(self->codec, message, error); - if (data == nullptr) + if (data == nullptr) { return FALSE; + } gboolean result = fl_binary_messenger_send_response( self->messenger, response_handle->response_handle, data, error); @@ -237,8 +242,9 @@ G_MODULE_EXPORT void fl_basic_message_channel_send(FlBasicMessageChannel* self, g_autoptr(GBytes) data = fl_message_codec_encode_message(self->codec, message, &error); if (data == nullptr) { - if (task != nullptr) + if (task != nullptr) { g_task_return_error(task, error); + } return; } @@ -260,8 +266,9 @@ G_MODULE_EXPORT FlValue* fl_basic_message_channel_send_finish( g_autoptr(GBytes) message = fl_binary_messenger_send_on_channel_finish(self->messenger, r, error); - if (message == nullptr) + if (message == nullptr) { return nullptr; + } return fl_message_codec_decode_message(self->codec, message, error); } diff --git a/shell/platform/linux/fl_binary_messenger.cc b/shell/platform/linux/fl_binary_messenger.cc index 5a743fa2b9267..e817fd36225ba 100644 --- a/shell/platform/linux/fl_binary_messenger.cc +++ b/shell/platform/linux/fl_binary_messenger.cc @@ -43,8 +43,9 @@ static void fl_binary_messenger_response_handle_dispose(GObject* object) { FlBinaryMessengerResponseHandle* self = FL_BINARY_MESSENGER_RESPONSE_HANDLE(object); - if (self->response_handle != nullptr && self->messenger->engine != nullptr) + if (self->response_handle != nullptr && self->messenger->engine != nullptr) { g_critical("FlBinaryMessengerResponseHandle was not responded to"); + } g_clear_object(&self->messenger); self->response_handle = nullptr; @@ -93,8 +94,9 @@ static PlatformMessageHandler* platform_message_handler_new( static void platform_message_handler_free(gpointer data) { PlatformMessageHandler* self = static_cast(data); - if (self->message_handler_destroy_notify) + if (self->message_handler_destroy_notify) { self->message_handler_destroy_notify(self->message_handler_data); + } g_free(self); } @@ -116,8 +118,9 @@ static gboolean fl_binary_messenger_platform_message_cb( PlatformMessageHandler* handler = static_cast( g_hash_table_lookup(self->platform_message_handlers, channel)); - if (handler == nullptr) + if (handler == nullptr) { return FALSE; + } g_autoptr(FlBinaryMessengerResponseHandle) handle = fl_binary_messenger_response_handle_new(self, response_handle); @@ -180,8 +183,9 @@ G_MODULE_EXPORT void fl_binary_messenger_set_message_handler_on_channel( "Attempted to set message handler on an FlBinaryMessenger without an " "engine"); } - if (destroy_notify != nullptr) + if (destroy_notify != nullptr) { destroy_notify(user_data); + } return; } @@ -204,8 +208,9 @@ G_MODULE_EXPORT gboolean fl_binary_messenger_send_response( g_return_val_if_fail(response_handle->messenger == self, FALSE); g_return_val_if_fail(response_handle->response_handle != nullptr, FALSE); - if (self->engine == nullptr) + if (self->engine == nullptr) { return TRUE; + } if (response_handle->response_handle == nullptr) { g_set_error( @@ -239,8 +244,9 @@ G_MODULE_EXPORT void fl_binary_messenger_send_on_channel( g_return_if_fail(FL_IS_BINARY_MESSENGER(self)); g_return_if_fail(channel != nullptr); - if (self->engine == nullptr) + if (self->engine == nullptr) { return; + } fl_engine_send_platform_message( self->engine, channel, message, cancellable, @@ -259,8 +265,9 @@ G_MODULE_EXPORT GBytes* fl_binary_messenger_send_on_channel_finish( g_autoptr(GTask) task = G_TASK(result); GAsyncResult* r = G_ASYNC_RESULT(g_task_propagate_pointer(task, nullptr)); - if (self->engine == nullptr) + if (self->engine == nullptr) { return nullptr; + } return fl_engine_send_platform_message_finish(self->engine, r, error); } diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc index 13e5c22088d7c..7308bb792c64a 100644 --- a/shell/platform/linux/fl_engine.cc +++ b/shell/platform/linux/fl_engine.cc @@ -94,34 +94,42 @@ static void parse_locale(const gchar* locale, // Passes locale information to the Flutter engine. static void setup_locales(FlEngine* self) { const gchar* const* languages = g_get_language_names(); - g_autoptr(GPtrArray) locales = g_ptr_array_new_with_free_func(g_free); + g_autoptr(GPtrArray) locales_array = g_ptr_array_new_with_free_func(g_free); // Helper array to take ownership of the strings passed to Flutter. g_autoptr(GPtrArray) locale_strings = g_ptr_array_new_with_free_func(g_free); for (int i = 0; languages[i] != nullptr; i++) { gchar *language, *territory, *codeset, *modifier; parse_locale(languages[i], &language, &territory, &codeset, &modifier); - if (language != nullptr) + if (language != nullptr) { g_ptr_array_add(locale_strings, language); - if (territory != nullptr) + } + if (territory != nullptr) { g_ptr_array_add(locale_strings, territory); - if (codeset != nullptr) + } + if (codeset != nullptr) { g_ptr_array_add(locale_strings, codeset); - if (modifier != nullptr) + } + if (modifier != nullptr) { g_ptr_array_add(locale_strings, modifier); + } FlutterLocale* locale = static_cast(g_malloc0(sizeof(FlutterLocale))); - g_ptr_array_add(locales, locale); + g_ptr_array_add(locales_array, locale); locale->struct_size = sizeof(FlutterLocale); locale->language_code = language; locale->country_code = territory; locale->script_code = codeset; locale->variant_code = modifier; } + FlutterLocale** locales = + reinterpret_cast(locales_array->pdata); FlutterEngineResult result = FlutterEngineUpdateLocales( - self->engine, (const FlutterLocale**)locales->pdata, locales->len); - if (result != kSuccess) + self->engine, const_cast(locales), + locales_array->len); + if (result != kSuccess) { g_warning("Failed to set up Flutter locales"); + } } // Callback to run a Flutter task in the GLib main loop. @@ -133,8 +141,9 @@ static gboolean flutter_source_dispatch(GSource* source, FlutterEngineResult result = FlutterEngineRunTask(self->engine, &fl_source->task); - if (result != kSuccess) + if (result != kSuccess) { g_warning("Failed to run Flutter task\n"); + } return G_SOURCE_REMOVE; } @@ -160,8 +169,9 @@ static bool fl_engine_gl_make_current(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_make_current(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -169,8 +179,9 @@ static bool fl_engine_gl_clear_current(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_clear_current(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -183,8 +194,9 @@ static bool fl_engine_gl_present(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_present(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -192,8 +204,9 @@ static bool fl_engine_gl_make_resource_current(void* user_data) { FlEngine* self = static_cast(user_data); g_autoptr(GError) error = nullptr; gboolean result = fl_renderer_make_resource_current(self->renderer, &error); - if (!result) + if (!result) { g_warning("%s", error->message); + } return result; } @@ -246,7 +259,7 @@ static void fl_engine_platform_message_response_cb(const uint8_t* data, void* user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_task_return_pointer(task, g_bytes_new(data, data_length), - (GDestroyNotify)g_bytes_unref); + reinterpret_cast(g_bytes_unref)); } // Implements FlPluginRegistry::get_registrar_for_plugin. @@ -318,8 +331,9 @@ G_MODULE_EXPORT FlEngine* fl_engine_new_headless(FlDartProject* project) { gboolean fl_engine_start(FlEngine* self, GError** error) { g_return_val_if_fail(FL_IS_ENGINE(self), FALSE); - if (!fl_renderer_start(self->renderer, error)) + if (!fl_renderer_start(self->renderer, error)) { return FALSE; + } FlutterRendererConfig config = {}; config.type = kOpenGL; @@ -499,8 +513,9 @@ void fl_engine_send_platform_message(FlEngine* self, g_object_unref(task); } - if (response_handle != nullptr) + if (response_handle != nullptr) { FlutterPlatformMessageReleaseResponseHandle(self->engine, response_handle); + } } GBytes* fl_engine_send_platform_message_finish(FlEngine* self, @@ -518,8 +533,9 @@ void fl_engine_send_window_metrics_event(FlEngine* self, double pixel_ratio) { g_return_if_fail(FL_IS_ENGINE(self)); - if (self->engine == nullptr) + if (self->engine == nullptr) { return; + } FlutterWindowMetricsEvent event = {}; event.struct_size = sizeof(FlutterWindowMetricsEvent); @@ -539,8 +555,9 @@ void fl_engine_send_mouse_pointer_event(FlEngine* self, int64_t buttons) { g_return_if_fail(FL_IS_ENGINE(self)); - if (self->engine == nullptr) + if (self->engine == nullptr) { return; + } FlutterPointerEvent fl_event = {}; fl_event.struct_size = sizeof(fl_event); @@ -548,8 +565,9 @@ void fl_engine_send_mouse_pointer_event(FlEngine* self, fl_event.timestamp = timestamp; fl_event.x = x; fl_event.y = y; - if (scroll_delta_x != 0 || scroll_delta_y != 0) + if (scroll_delta_x != 0 || scroll_delta_y != 0) { fl_event.signal_kind = kFlutterPointerSignalKindScroll; + } fl_event.scroll_delta_x = scroll_delta_x; fl_event.scroll_delta_y = scroll_delta_y; fl_event.device_kind = kFlutterPointerDeviceKindMouse; diff --git a/shell/platform/linux/fl_json_method_codec.cc b/shell/platform/linux/fl_json_method_codec.cc index 63e04582a3993..7ea1b2c74e992 100644 --- a/shell/platform/linux/fl_json_method_codec.cc +++ b/shell/platform/linux/fl_json_method_codec.cc @@ -56,8 +56,9 @@ static gboolean fl_json_method_codec_decode_method_call(FlMethodCodec* codec, g_autoptr(FlValue) value = fl_message_codec_decode_message( FL_MESSAGE_CODEC(self->codec), message, error); - if (value == nullptr) + if (value == nullptr) { return FALSE; + } if (fl_value_get_type(value) != FL_VALUE_TYPE_MAP) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -131,8 +132,9 @@ static FlMethodResponse* fl_json_method_codec_decode_response( g_autoptr(FlValue) value = fl_message_codec_decode_message( FL_MESSAGE_CODEC(self->codec), message, error); - if (value == nullptr) + if (value == nullptr) { return nullptr; + } if (fl_value_get_type(value) != FL_VALUE_TYPE_LIST) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -167,8 +169,9 @@ static FlMethodResponse* fl_json_method_codec_decode_response( : nullptr; FlValue* args = fl_value_get_list_value(value, 2); - if (fl_value_get_type(args) == FL_VALUE_TYPE_NULL) + if (fl_value_get_type(args) == FL_VALUE_TYPE_NULL) { args = nullptr; + } return FL_METHOD_RESPONSE( fl_method_error_response_new(code, message, args)); diff --git a/shell/platform/linux/fl_method_channel.cc b/shell/platform/linux/fl_method_channel.cc index 70ab771d3aa00..c276c233765a6 100644 --- a/shell/platform/linux/fl_method_channel.cc +++ b/shell/platform/linux/fl_method_channel.cc @@ -44,8 +44,9 @@ static void message_cb(FlBinaryMessenger* messenger, gpointer user_data) { FlMethodChannel* self = FL_METHOD_CHANNEL(user_data); - if (self->method_call_handler == nullptr) + if (self->method_call_handler == nullptr) { return; + } g_autofree gchar* method = nullptr; g_autoptr(FlValue) args = nullptr; @@ -76,8 +77,9 @@ static void channel_closed_cb(gpointer user_data) { self->channel_closed = TRUE; // Disconnect handler. - if (self->method_call_handler_destroy_notify != nullptr) + if (self->method_call_handler_destroy_notify != nullptr) { self->method_call_handler_destroy_notify(self->method_call_handler_data); + } self->method_call_handler = nullptr; self->method_call_handler_data = nullptr; self->method_call_handler_destroy_notify = nullptr; @@ -95,8 +97,9 @@ static void fl_method_channel_dispose(GObject* object) { g_clear_pointer(&self->name, g_free); g_clear_object(&self->codec); - if (self->method_call_handler_destroy_notify != nullptr) + if (self->method_call_handler_destroy_notify != nullptr) { self->method_call_handler_destroy_notify(self->method_call_handler_data); + } self->method_call_handler = nullptr; self->method_call_handler_data = nullptr; self->method_call_handler_destroy_notify = nullptr; @@ -145,13 +148,15 @@ G_MODULE_EXPORT void fl_method_channel_set_method_call_handler( g_warning( "Attempted to set method call handler on a closed FlMethodChannel"); } - if (destroy_notify != nullptr) + if (destroy_notify != nullptr) { destroy_notify(user_data); + } return; } - if (self->method_call_handler_destroy_notify != nullptr) + if (self->method_call_handler_destroy_notify != nullptr) { self->method_call_handler_destroy_notify(self->method_call_handler_data); + } self->method_call_handler = handler; self->method_call_handler_data = user_data; @@ -176,8 +181,9 @@ G_MODULE_EXPORT void fl_method_channel_invoke_method( g_autoptr(GBytes) message = fl_method_codec_encode_method_call(self->codec, method, args, &error); if (message == nullptr) { - if (task != nullptr) + if (task != nullptr) { g_task_return_error(task, error); + } return; } @@ -199,8 +205,9 @@ G_MODULE_EXPORT FlMethodResponse* fl_method_channel_invoke_method_finish( g_autoptr(GBytes) response = fl_binary_messenger_send_on_channel_finish(self->messenger, r, error); - if (response == nullptr) + if (response == nullptr) { return nullptr; + } return fl_method_codec_decode_response(self->codec, response, error); } diff --git a/shell/platform/linux/fl_method_codec.cc b/shell/platform/linux/fl_method_codec.cc index fd9d667b15316..b51979f88eb1a 100644 --- a/shell/platform/linux/fl_method_codec.cc +++ b/shell/platform/linux/fl_method_codec.cc @@ -68,8 +68,9 @@ FlMethodResponse* fl_method_codec_decode_response(FlMethodCodec* self, g_return_val_if_fail(FL_IS_METHOD_CODEC(self), nullptr); g_return_val_if_fail(message != nullptr, nullptr); - if (g_bytes_get_size(message) == 0) + if (g_bytes_get_size(message) == 0) { return FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } return FL_METHOD_CODEC_GET_CLASS(self)->decode_response(self, message, error); } diff --git a/shell/platform/linux/fl_renderer.cc b/shell/platform/linux/fl_renderer.cc index d74395fa0bde3..4baf50df92424 100644 --- a/shell/platform/linux/fl_renderer.cc +++ b/shell/platform/linux/fl_renderer.cc @@ -21,29 +21,6 @@ typedef struct { G_DEFINE_TYPE_WITH_PRIVATE(FlRenderer, fl_renderer, G_TYPE_OBJECT) -// Creates a resource surface. -static void create_resource_surface(FlRenderer* self, EGLConfig config) { - FlRendererPrivate* priv = - static_cast(fl_renderer_get_instance_private(self)); - - EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; - const EGLint resource_context_attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, - EGL_NONE}; - priv->resource_surface = eglCreatePbufferSurface(priv->egl_display, config, - resource_context_attribs); - if (priv->resource_surface == EGL_NO_SURFACE) { - g_warning("Failed to create EGL resource surface: %s", - egl_error_to_string(eglGetError())); - return; - } - - priv->resource_context = eglCreateContext( - priv->egl_display, config, priv->egl_context, context_attributes); - if (priv->resource_context == EGL_NO_CONTEXT) - g_warning("Failed to create EGL resource context: %s", - egl_error_to_string(eglGetError())); -} - static void fl_renderer_class_init(FlRendererClass* klass) {} static void fl_renderer_init(FlRenderer* self) {} @@ -52,12 +29,7 @@ gboolean fl_renderer_setup(FlRenderer* self, GError** error) { FlRendererPrivate* priv = static_cast(fl_renderer_get_instance_private(self)); - // Note the use of EGL_DEFAULT_DISPLAY rather than sharing an existing - // display connection (e.g. an X11 connection from GTK). This is because - // this EGL display is going to be accessed by a thread from Flutter. In the - // case of GTK/X11 the display connection is not thread safe and this would - // cause a crash. - priv->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + priv->egl_display = FL_RENDERER_GET_CLASS(self)->create_display(self); if (!eglInitialize(priv->egl_display, nullptr, nullptr)) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, @@ -93,7 +65,7 @@ gboolean fl_renderer_setup(FlRenderer* self, GError** error) { } if (!eglBindAPI(EGL_OPENGL_ES_API)) { - EGLint egl_error = eglGetError(); // must be before egl_config_to_string(). + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). g_autofree gchar* config_string = egl_config_to_string(priv->egl_display, priv->egl_config); g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, @@ -130,40 +102,63 @@ GdkVisual* fl_renderer_get_visual(FlRenderer* self, return visual; } +void fl_renderer_set_window(FlRenderer* self, GdkWindow* window) { + g_return_if_fail(FL_IS_RENDERER(self)); + g_return_if_fail(GDK_IS_WINDOW(window)); + + if (FL_RENDERER_GET_CLASS(self)->set_window) { + FL_RENDERER_GET_CLASS(self)->set_window(self, window); + } +} + gboolean fl_renderer_start(FlRenderer* self, GError** error) { FlRendererPrivate* priv = static_cast(fl_renderer_get_instance_private(self)); - priv->egl_surface = FL_RENDERER_GET_CLASS(self)->create_surface( - self, priv->egl_display, priv->egl_config); - if (priv->egl_surface == EGL_NO_SURFACE) { - EGLint egl_error = eglGetError(); // must be before egl_config_to_string(). - g_autofree gchar* config_string = - egl_config_to_string(priv->egl_display, priv->egl_config); + if (priv->egl_surface != EGL_NO_SURFACE || + priv->resource_surface != EGL_NO_SURFACE) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, - "Failed to create EGL surface using configuration (%s): %s", - config_string, egl_error_to_string(egl_error)); + "fl_renderer_start() called after surfaces already created"); + return FALSE; + } + + if (!FL_RENDERER_GET_CLASS(self)->create_surfaces( + self, priv->egl_display, priv->egl_config, &priv->egl_surface, + &priv->resource_surface, error)) { return FALSE; } EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; + priv->egl_context = eglCreateContext(priv->egl_display, priv->egl_config, EGL_NO_CONTEXT, context_attributes); - if (priv->egl_context == EGL_NO_CONTEXT) { - EGLint egl_error = eglGetError(); // must be before egl_config_to_string(). + priv->resource_context = + eglCreateContext(priv->egl_display, priv->egl_config, priv->egl_context, + context_attributes); + if (priv->egl_context == EGL_NO_CONTEXT || + priv->resource_context == EGL_NO_CONTEXT) { + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). g_autofree gchar* config_string = egl_config_to_string(priv->egl_display, priv->egl_config); g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, - "Failed to create EGL context using configuration (%s): %s", + "Failed to create EGL contexts using configuration (%s): %s", config_string, egl_error_to_string(egl_error)); return FALSE; } - create_resource_surface(self, priv->egl_config); - return TRUE; } +void fl_renderer_set_geometry(FlRenderer* self, + GdkRectangle* geometry, + gint scale) { + g_return_if_fail(FL_IS_RENDERER(self)); + + if (FL_RENDERER_GET_CLASS(self)->set_geometry) { + FL_RENDERER_GET_CLASS(self)->set_geometry(self, geometry, scale); + } +} + void* fl_renderer_get_proc_address(FlRenderer* self, const char* name) { return reinterpret_cast(eglGetProcAddress(name)); } @@ -172,6 +167,13 @@ gboolean fl_renderer_make_current(FlRenderer* self, GError** error) { FlRendererPrivate* priv = static_cast(fl_renderer_get_instance_private(self)); + if (priv->egl_surface == EGL_NO_SURFACE || + priv->egl_context == EGL_NO_CONTEXT) { + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to make EGL context current: No surface created"); + return FALSE; + } + if (!eglMakeCurrent(priv->egl_display, priv->egl_surface, priv->egl_surface, priv->egl_context)) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, @@ -188,13 +190,17 @@ gboolean fl_renderer_make_resource_current(FlRenderer* self, GError** error) { static_cast(fl_renderer_get_instance_private(self)); if (priv->resource_surface == EGL_NO_SURFACE || - priv->resource_context == EGL_NO_CONTEXT) + priv->resource_context == EGL_NO_CONTEXT) { + g_set_error( + error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to make EGL resource context current: No surface created"); return FALSE; + } if (!eglMakeCurrent(priv->egl_display, priv->resource_surface, priv->resource_surface, priv->resource_context)) { g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, - "Failed to make EGL context current: %s", + "Failed to make EGL resource context current: %s", egl_error_to_string(eglGetError())); return FALSE; } diff --git a/shell/platform/linux/fl_renderer.h b/shell/platform/linux/fl_renderer.h index 25042f9666986..d355bfd565996 100644 --- a/shell/platform/linux/fl_renderer.h +++ b/shell/platform/linux/fl_renderer.h @@ -40,10 +40,42 @@ struct _FlRendererClass { GdkScreen* screen, EGLint visual_id); - // Virtual method called when Flutter needs a surface to render to. - EGLSurface (*create_surface)(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config); + /** + * Virtual method called after a GDK window has been created. + * This is called once. Does not need to be implemented. + */ + void (*set_window)(FlRenderer* renderer, GdkWindow* window); + + /** + * Virtual method to create a new EGL display. + */ + EGLDisplay (*create_display)(FlRenderer* renderer); + + /** + * Virtual method called when Flutter needs surfaces to render to. + * @renderer: an #FlRenderer. + * @display: display to create surfaces on. + * @visible: (out): the visible surface that is created. + * @resource: (out): the resource surface that is created. + * @error: (allow-none): #GError location to store the error occurring, or + * %NULL to ignore. + * + * Returns: %TRUE if both surfaces were created, %FALSE if there was an error. + */ + gboolean (*create_surfaces)(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error); + + /** + * Virtual method called when the EGL window needs to be resized. + * Does not need to be implemented. + */ + void (*set_geometry)(FlRenderer* renderer, + GdkRectangle* geometry, + gint scale); }; /** @@ -56,7 +88,7 @@ struct _FlRendererClass { * * Returns: %TRUE if successfully setup. */ -gboolean fl_renderer_setup(FlRenderer* self, GError** error); +gboolean fl_renderer_setup(FlRenderer* renderer, GError** error); /** * fl_renderer_get_visual: @@ -69,10 +101,19 @@ gboolean fl_renderer_setup(FlRenderer* self, GError** error); * * Returns: a #GdkVisual. */ -GdkVisual* fl_renderer_get_visual(FlRenderer* self, +GdkVisual* fl_renderer_get_visual(FlRenderer* renderer, GdkScreen* screen, GError** error); +/** + * fl_renderer_set_window: + * @renderer: an #FlRenderer. + * @window: the GDK Window this renderer will render to. + * + * Set the window this renderer will use. + */ +void fl_renderer_set_window(FlRenderer* renderer, GdkWindow* window); + /** * fl_renderer_start: * @renderer: an #FlRenderer. @@ -83,7 +124,17 @@ GdkVisual* fl_renderer_get_visual(FlRenderer* self, * * Returns: %TRUE if successfully started. */ -gboolean fl_renderer_start(FlRenderer* self, GError** error); +gboolean fl_renderer_start(FlRenderer* renderer, GError** error); + +/** + * fl_renderer_set_geometry: + * @renderer: an #FlRenderer. + * @geometry: New size and position (unscaled) of the EGL window. + * @scale: Scale of the window. + */ +void fl_renderer_set_geometry(FlRenderer* renderer, + GdkRectangle* geometry, + gint scale); /** * fl_renderer_get_proc_address: diff --git a/shell/platform/linux/fl_renderer_headless.cc b/shell/platform/linux/fl_renderer_headless.cc index 90f917a2b316f..e7880fb7b8b9d 100644 --- a/shell/platform/linux/fl_renderer_headless.cc +++ b/shell/platform/linux/fl_renderer_headless.cc @@ -10,15 +10,18 @@ struct _FlRendererHeadless { G_DEFINE_TYPE(FlRendererHeadless, fl_renderer_headless, fl_renderer_get_type()) -static EGLSurface fl_renderer_headless_create_surface(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config) { - return EGL_NO_SURFACE; +static gboolean fl_renderer_headless_create_surfaces(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error) { + return FALSE; } static void fl_renderer_headless_class_init(FlRendererHeadlessClass* klass) { - FL_RENDERER_CLASS(klass)->create_surface = - fl_renderer_headless_create_surface; + FL_RENDERER_CLASS(klass)->create_surfaces = + fl_renderer_headless_create_surfaces; } static void fl_renderer_headless_init(FlRendererHeadless* self) {} diff --git a/shell/platform/linux/fl_renderer_x11.cc b/shell/platform/linux/fl_renderer_x11.cc index f4e8dfdfc3ca2..16bd1dca6aa7f 100644 --- a/shell/platform/linux/fl_renderer_x11.cc +++ b/shell/platform/linux/fl_renderer_x11.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "fl_renderer_x11.h" +#include "flutter/shell/platform/linux/egl_utils.h" struct _FlRendererX11 { FlRenderer parent_instance; @@ -20,26 +21,77 @@ static void fl_renderer_x11_dispose(GObject* object) { G_OBJECT_CLASS(fl_renderer_x11_parent_class)->dispose(object); } -// Implments FlRenderer::get_visual. +// Implements FlRenderer::get_visual. static GdkVisual* fl_renderer_x11_get_visual(FlRenderer* renderer, GdkScreen* screen, EGLint visual_id) { return gdk_x11_screen_lookup_visual(GDK_X11_SCREEN(screen), visual_id); } -// Implments FlRenderer::create_surface. -static EGLSurface fl_renderer_x11_create_surface(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config) { +// Implements FlRenderer::set_window. +static void fl_renderer_x11_set_window(FlRenderer* renderer, + GdkWindow* window) { FlRendererX11* self = FL_RENDERER_X11(renderer); - return eglCreateWindowSurface(display, config, - gdk_x11_window_get_xid(self->window), nullptr); + g_return_if_fail(GDK_IS_X11_WINDOW(window)); + g_assert_null(self->window); + self->window = GDK_X11_WINDOW(g_object_ref(window)); +} + +// Implements FlRenderer::create_display. +static EGLDisplay fl_renderer_x11_create_display(FlRenderer* renderer) { + // Note the use of EGL_DEFAULT_DISPLAY rather than sharing the existing + // display connection from GTK. This is because this EGL display is going to + // be accessed by a thread from Flutter. The GTK/X11 display connection is not + // thread safe and would cause a crash. + return eglGetDisplay(EGL_DEFAULT_DISPLAY); +} + +// Implements FlRenderer::create_surfaces. +static gboolean fl_renderer_x11_create_surfaces(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error) { + FlRendererX11* self = FL_RENDERER_X11(renderer); + + if (!self->window) { + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Can not create EGL surface: FlRendererX11::window not set"); + return FALSE; + } + + *visible = eglCreateWindowSurface( + display, config, gdk_x11_window_get_xid(self->window), nullptr); + if (*visible == EGL_NO_SURFACE) { + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). + g_autofree gchar* config_string = egl_config_to_string(display, config); + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to create EGL surface using configuration (%s): %s", + config_string, egl_error_to_string(egl_error)); + return FALSE; + } + + const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + *resource = eglCreatePbufferSurface(display, config, attribs); + if (*resource == EGL_NO_SURFACE) { + EGLint egl_error = eglGetError(); // Must be before egl_config_to_string(). + g_autofree gchar* config_string = egl_config_to_string(display, config); + g_set_error(error, fl_renderer_error_quark(), FL_RENDERER_ERROR_FAILED, + "Failed to create EGL resource using configuration (%s): %s", + config_string, egl_error_to_string(egl_error)); + return FALSE; + } + + return TRUE; } static void fl_renderer_x11_class_init(FlRendererX11Class* klass) { G_OBJECT_CLASS(klass)->dispose = fl_renderer_x11_dispose; FL_RENDERER_CLASS(klass)->get_visual = fl_renderer_x11_get_visual; - FL_RENDERER_CLASS(klass)->create_surface = fl_renderer_x11_create_surface; + FL_RENDERER_CLASS(klass)->set_window = fl_renderer_x11_set_window; + FL_RENDERER_CLASS(klass)->create_display = fl_renderer_x11_create_display; + FL_RENDERER_CLASS(klass)->create_surfaces = fl_renderer_x11_create_surfaces; } static void fl_renderer_x11_init(FlRendererX11* self) {} @@ -47,8 +99,3 @@ static void fl_renderer_x11_init(FlRendererX11* self) {} FlRendererX11* fl_renderer_x11_new() { return FL_RENDERER_X11(g_object_new(fl_renderer_x11_get_type(), nullptr)); } - -void fl_renderer_x11_set_window(FlRendererX11* self, GdkX11Window* window) { - g_return_if_fail(FL_IS_RENDERER_X11(self)); - self->window = GDK_X11_WINDOW(g_object_ref(window)); -} diff --git a/shell/platform/linux/fl_renderer_x11.h b/shell/platform/linux/fl_renderer_x11.h index 92d808591da70..8c32dcd0bc31b 100644 --- a/shell/platform/linux/fl_renderer_x11.h +++ b/shell/platform/linux/fl_renderer_x11.h @@ -33,15 +33,6 @@ G_DECLARE_FINAL_TYPE(FlRendererX11, */ FlRendererX11* fl_renderer_x11_new(); -/** - * fl_renderer_x11_set_window: - * @renderer: an #FlRendererX11. - * @window: the X window being rendered to. - * - * Sets the X11 window that is being rendered to. - */ -void fl_renderer_x11_set_window(FlRendererX11* renderer, GdkX11Window* window); - G_END_DECLS #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_RENDERER_X11_H_ diff --git a/shell/platform/linux/fl_standard_message_codec.cc b/shell/platform/linux/fl_standard_message_codec.cc index d06f3ad9454d7..61d7193dc0edc 100644 --- a/shell/platform/linux/fl_standard_message_codec.cc +++ b/shell/platform/linux/fl_standard_message_codec.cc @@ -66,8 +66,9 @@ static void write_float64(GByteArray* buffer, double value) { // Write padding bytes to align to @align multiple of bytes. static void write_align(GByteArray* buffer, guint align) { - while (buffer->len % align != 0) + while (buffer->len % align != 0) { write_uint8(buffer, 0); + } } // Checks there is enough data in @buffer to be read. @@ -88,12 +89,14 @@ static gboolean read_align(GBytes* buffer, size_t* offset, size_t align, GError** error) { - if ((*offset) % align == 0) + if ((*offset) % align == 0) { return TRUE; + } size_t required = align - (*offset) % align; - if (!check_size(buffer, *offset, required, error)) + if (!check_size(buffer, *offset, required, error)) { return FALSE; + } (*offset) += required; return TRUE; @@ -111,8 +114,9 @@ static gboolean read_uint8(GBytes* buffer, size_t* offset, uint8_t* value, GError** error) { - if (!check_size(buffer, *offset, sizeof(uint8_t), error)) + if (!check_size(buffer, *offset, sizeof(uint8_t), error)) { return FALSE; + } *value = get_data(buffer, offset)[0]; (*offset)++; @@ -125,8 +129,9 @@ static gboolean read_uint16(GBytes* buffer, size_t* offset, uint16_t* value, GError** error) { - if (!check_size(buffer, *offset, sizeof(uint16_t), error)) + if (!check_size(buffer, *offset, sizeof(uint16_t), error)) { return FALSE; + } *value = reinterpret_cast(get_data(buffer, offset))[0]; *offset += sizeof(uint16_t); @@ -139,8 +144,9 @@ static gboolean read_uint32(GBytes* buffer, size_t* offset, uint32_t* value, GError** error) { - if (!check_size(buffer, *offset, sizeof(uint32_t), error)) + if (!check_size(buffer, *offset, sizeof(uint32_t), error)) { return FALSE; + } *value = reinterpret_cast(get_data(buffer, offset))[0]; *offset += sizeof(uint32_t); @@ -153,8 +159,9 @@ static gboolean read_uint32(GBytes* buffer, static FlValue* read_int32_value(GBytes* buffer, size_t* offset, GError** error) { - if (!check_size(buffer, *offset, sizeof(int32_t), error)) + if (!check_size(buffer, *offset, sizeof(int32_t), error)) { return nullptr; + } FlValue* value = fl_value_new_int( reinterpret_cast(get_data(buffer, offset))[0]); @@ -168,8 +175,9 @@ static FlValue* read_int32_value(GBytes* buffer, static FlValue* read_int64_value(GBytes* buffer, size_t* offset, GError** error) { - if (!check_size(buffer, *offset, sizeof(int64_t), error)) + if (!check_size(buffer, *offset, sizeof(int64_t), error)) { return nullptr; + } FlValue* value = fl_value_new_int( reinterpret_cast(get_data(buffer, offset))[0]); @@ -183,10 +191,12 @@ static FlValue* read_int64_value(GBytes* buffer, static FlValue* read_float64_value(GBytes* buffer, size_t* offset, GError** error) { - if (!read_align(buffer, offset, 8, error)) + if (!read_align(buffer, offset, 8, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(double), error)) + } + if (!check_size(buffer, *offset, sizeof(double), error)) { return nullptr; + } FlValue* value = fl_value_new_float( reinterpret_cast(get_data(buffer, offset))[0]); @@ -203,10 +213,12 @@ static FlValue* read_string_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!check_size(buffer, *offset, length, error)) + } + if (!check_size(buffer, *offset, length, error)) { return nullptr; + } FlValue* value = fl_value_new_string_sized( reinterpret_cast(get_data(buffer, offset)), length); *offset += length; @@ -222,10 +234,12 @@ static FlValue* read_uint8_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(uint8_t) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(uint8_t) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_uint8_list(get_data(buffer, offset), length); *offset += length; return value; @@ -240,12 +254,15 @@ static FlValue* read_int32_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!read_align(buffer, offset, 4, error)) + } + if (!read_align(buffer, offset, 4, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(int32_t) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(int32_t) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_int32_list( reinterpret_cast(get_data(buffer, offset)), length); *offset += sizeof(int32_t) * length; @@ -261,12 +278,15 @@ static FlValue* read_int64_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!read_align(buffer, offset, 8, error)) + } + if (!read_align(buffer, offset, 8, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(int64_t) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(int64_t) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_int64_list( reinterpret_cast(get_data(buffer, offset)), length); *offset += sizeof(int64_t) * length; @@ -282,12 +302,15 @@ static FlValue* read_float64_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; - if (!read_align(buffer, offset, 8, error)) + } + if (!read_align(buffer, offset, 8, error)) { return nullptr; - if (!check_size(buffer, *offset, sizeof(double) * length, error)) + } + if (!check_size(buffer, *offset, sizeof(double) * length, error)) { return nullptr; + } FlValue* value = fl_value_new_float_list( reinterpret_cast(get_data(buffer, offset)), length); *offset += sizeof(double) * length; @@ -303,15 +326,17 @@ static FlValue* read_list_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; + } g_autoptr(FlValue) list = fl_value_new_list(); for (size_t i = 0; i < length; i++) { g_autoptr(FlValue) child = fl_standard_message_codec_read_value(self, buffer, offset, error); - if (child == nullptr) + if (child == nullptr) { return nullptr; + } fl_value_append(list, child); } @@ -327,19 +352,22 @@ static FlValue* read_map_value(FlStandardMessageCodec* self, GError** error) { uint32_t length; if (!fl_standard_message_codec_read_size(self, buffer, offset, &length, - error)) + error)) { return nullptr; + } g_autoptr(FlValue) map = fl_value_new_map(); for (size_t i = 0; i < length; i++) { g_autoptr(FlValue) key = fl_standard_message_codec_read_value(self, buffer, offset, error); - if (key == nullptr) + if (key == nullptr) { return nullptr; + } g_autoptr(FlValue) value = fl_standard_message_codec_read_value(self, buffer, offset, error); - if (value == nullptr) + if (value == nullptr) { return nullptr; + } fl_value_set(map, key, value); } @@ -354,8 +382,9 @@ static GBytes* fl_standard_message_codec_encode_message(FlMessageCodec* codec, reinterpret_cast(codec); g_autoptr(GByteArray) buffer = g_byte_array_new(); - if (!fl_standard_message_codec_write_value(self, buffer, message, error)) + if (!fl_standard_message_codec_write_value(self, buffer, message, error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); } @@ -370,8 +399,9 @@ static FlValue* fl_standard_message_codec_decode_message(FlMessageCodec* codec, size_t offset = 0; g_autoptr(FlValue) value = fl_standard_message_codec_read_value(self, message, &offset, error); - if (value == nullptr) + if (value == nullptr) { return nullptr; + } if (offset != g_bytes_get_size(message)) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, @@ -419,16 +449,19 @@ gboolean fl_standard_message_codec_read_size(FlStandardMessageCodec* codec, uint32_t* value, GError** error) { uint8_t value8; - if (!read_uint8(buffer, offset, &value8, error)) + if (!read_uint8(buffer, offset, &value8, error)) { return FALSE; + } if (value8 == 255) { - if (!read_uint32(buffer, offset, value, error)) + if (!read_uint32(buffer, offset, value, error)) { return FALSE; + } } else if (value8 == 254) { uint16_t value16; - if (!read_uint16(buffer, offset, &value16, error)) + if (!read_uint16(buffer, offset, &value16, error)) { return FALSE; + } *value = value16; } else { *value = value8; @@ -451,10 +484,11 @@ gboolean fl_standard_message_codec_write_value(FlStandardMessageCodec* self, write_uint8(buffer, kValueNull); return TRUE; case FL_VALUE_TYPE_BOOL: - if (fl_value_get_bool(value)) + if (fl_value_get_bool(value)) { write_uint8(buffer, kValueTrue); - else + } else { write_uint8(buffer, kValueFalse); + } return TRUE; case FL_VALUE_TYPE_INT: { int64_t v = fl_value_get_int(value); @@ -528,8 +562,9 @@ gboolean fl_standard_message_codec_write_value(FlStandardMessageCodec* self, fl_value_get_length(value)); for (size_t i = 0; i < fl_value_get_length(value); i++) { if (!fl_standard_message_codec_write_value( - self, buffer, fl_value_get_list_value(value, i), error)) + self, buffer, fl_value_get_list_value(value, i), error)) { return FALSE; + } } return TRUE; case FL_VALUE_TYPE_MAP: @@ -540,8 +575,9 @@ gboolean fl_standard_message_codec_write_value(FlStandardMessageCodec* self, if (!fl_standard_message_codec_write_value( self, buffer, fl_value_get_map_key(value, i), error) || !fl_standard_message_codec_write_value( - self, buffer, fl_value_get_map_value(value, i), error)) + self, buffer, fl_value_get_map_value(value, i), error)) { return FALSE; + } } return TRUE; } @@ -557,8 +593,9 @@ FlValue* fl_standard_message_codec_read_value(FlStandardMessageCodec* self, size_t* offset, GError** error) { uint8_t type; - if (!read_uint8(buffer, offset, &type, error)) + if (!read_uint8(buffer, offset, &type, error)) { return nullptr; + } g_autoptr(FlValue) value = nullptr; if (type == kValueNull) { diff --git a/shell/platform/linux/fl_standard_method_codec.cc b/shell/platform/linux/fl_standard_method_codec.cc index be191d029ce85..32eff323fed0e 100644 --- a/shell/platform/linux/fl_standard_method_codec.cc +++ b/shell/platform/linux/fl_standard_method_codec.cc @@ -44,10 +44,13 @@ static GBytes* fl_standard_method_codec_encode_method_call(FlMethodCodec* codec, g_autoptr(GByteArray) buffer = g_byte_array_new(); g_autoptr(FlValue) name_value = fl_value_new_string(name); if (!fl_standard_message_codec_write_value(self->codec, buffer, name_value, - error)) + error)) { return nullptr; - if (!fl_standard_message_codec_write_value(self->codec, buffer, args, error)) + } + if (!fl_standard_message_codec_write_value(self->codec, buffer, args, + error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); @@ -65,8 +68,9 @@ static gboolean fl_standard_method_codec_decode_method_call( size_t offset = 0; g_autoptr(FlValue) name_value = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (name_value == nullptr) + if (name_value == nullptr) { return FALSE; + } if (fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, "Method call name wrong type"); @@ -75,8 +79,9 @@ static gboolean fl_standard_method_codec_decode_method_call( g_autoptr(FlValue) args_value = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (args_value == nullptr) + if (args_value == nullptr) { return FALSE; + } if (offset != g_bytes_get_size(message)) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -101,8 +106,9 @@ static GBytes* fl_standard_method_codec_encode_success_envelope( guint8 type = kEnvelopeTypeSuccess; g_byte_array_append(buffer, &type, 1); if (!fl_standard_message_codec_write_value(self->codec, buffer, result, - error)) + error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); @@ -122,16 +128,19 @@ static GBytes* fl_standard_method_codec_encode_error_envelope( g_byte_array_append(buffer, &type, 1); g_autoptr(FlValue) code_value = fl_value_new_string(code); if (!fl_standard_message_codec_write_value(self->codec, buffer, code_value, - error)) + error)) { return nullptr; + } g_autoptr(FlValue) message_value = message != nullptr ? fl_value_new_string(message) : nullptr; if (!fl_standard_message_codec_write_value(self->codec, buffer, message_value, - error)) + error)) { return nullptr; + } if (!fl_standard_message_codec_write_value(self->codec, buffer, details, - error)) + error)) { return nullptr; + } return g_byte_array_free_to_bytes( static_cast(g_steal_pointer(&buffer))); @@ -160,8 +169,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( if (type == kEnvelopeTypeError) { g_autoptr(FlValue) code = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (code == nullptr) + if (code == nullptr) { return nullptr; + } if (fl_value_get_type(code) != FL_VALUE_TYPE_STRING) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, "Error code wrong type"); @@ -170,8 +180,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( g_autoptr(FlValue) error_message = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (error_message == nullptr) + if (error_message == nullptr) { return nullptr; + } if (fl_value_get_type(error_message) != FL_VALUE_TYPE_STRING && fl_value_get_type(error_message) != FL_VALUE_TYPE_NULL) { g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, @@ -181,8 +192,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( g_autoptr(FlValue) details = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (details == nullptr) + if (details == nullptr) { return nullptr; + } response = FL_METHOD_RESPONSE(fl_method_error_response_new( fl_value_get_string(code), @@ -194,8 +206,9 @@ static FlMethodResponse* fl_standard_method_codec_decode_response( g_autoptr(FlValue) result = fl_standard_message_codec_read_value( self->codec, message, &offset, error); - if (result == nullptr) + if (result == nullptr) { return nullptr; + } response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } else { diff --git a/shell/platform/linux/fl_string_codec.cc b/shell/platform/linux/fl_string_codec.cc index 68ea4fbdb52cc..75ddbe9215542 100644 --- a/shell/platform/linux/fl_string_codec.cc +++ b/shell/platform/linux/fl_string_codec.cc @@ -14,7 +14,7 @@ struct _FlStringCodec { G_DEFINE_TYPE(FlStringCodec, fl_string_codec, fl_message_codec_get_type()) -// Implments FlMessageCodec::encode_message. +// Implements FlMessageCodec::encode_message. static GBytes* fl_string_codec_encode_message(FlMessageCodec* codec, FlValue* value, GError** error) { @@ -29,7 +29,7 @@ static GBytes* fl_string_codec_encode_message(FlMessageCodec* codec, return g_bytes_new(text, strlen(text)); } -// Implments FlMessageCodec::decode_message. +// Implements FlMessageCodec::decode_message. static FlValue* fl_string_codec_decode_message(FlMessageCodec* codec, GBytes* message, GError** error) { diff --git a/shell/platform/linux/fl_text_input_plugin.cc b/shell/platform/linux/fl_text_input_plugin.cc index f62203f71bfc1..89580fa798b10 100644 --- a/shell/platform/linux/fl_text_input_plugin.cc +++ b/shell/platform/linux/fl_text_input_plugin.cc @@ -61,8 +61,9 @@ static gboolean finish_method(GObject* object, GError** error) { g_autoptr(FlMethodResponse) response = fl_method_channel_invoke_method_finish( FL_METHOD_CHANNEL(object), result, error); - if (response == nullptr) + if (response == nullptr) { return FALSE; + } return fl_method_response_get_result(response, error) != nullptr; } @@ -113,8 +114,9 @@ static void perform_action_response_cb(GObject* object, GAsyncResult* result, gpointer user_data) { g_autoptr(GError) error = nullptr; - if (!finish_method(object, result, &error)) + if (!finish_method(object, result, &error)) { g_warning("Failed to call %s: %s", kPerformActionMethod, error->message); + } } // Inform Flutter that the input has been activated. @@ -150,8 +152,9 @@ static gboolean im_retrieve_surrounding_cb(FlTextInputPlugin* self) { static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self, gint offset, gint n_chars) { - if (self->text_model->DeleteSurrounding(offset, n_chars)) + if (self->text_model->DeleteSurrounding(offset, n_chars)) { update_editing_state(self); + } return TRUE; } @@ -168,8 +171,9 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { g_free(self->input_action); FlValue* input_action_value = fl_value_lookup_string(config_value, kInputActionKey); - if (fl_value_get_type(input_action_value) == FL_VALUE_TYPE_STRING) + if (fl_value_get_type(input_action_value) == FL_VALUE_TYPE_STRING) { self->input_action = g_strdup(fl_value_get_string(input_action_value)); + } return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } @@ -224,22 +228,24 @@ static void method_call_cb(FlMethodChannel* channel, FlValue* args = fl_method_call_get_args(method_call); g_autoptr(FlMethodResponse) response = nullptr; - if (strcmp(method, kSetClientMethod) == 0) + if (strcmp(method, kSetClientMethod) == 0) { response = set_client(self, args); - else if (strcmp(method, kShowMethod) == 0) + } else if (strcmp(method, kShowMethod) == 0) { response = show(self); - else if (strcmp(method, kSetEditingStateMethod) == 0) + } else if (strcmp(method, kSetEditingStateMethod) == 0) { response = set_editing_state(self, args); - else if (strcmp(method, kClearClientMethod) == 0) + } else if (strcmp(method, kClearClientMethod) == 0) { response = clear_client(self); - else if (strcmp(method, kHideMethod) == 0) + } else if (strcmp(method, kHideMethod) == 0) { response = hide(self); - else + } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } g_autoptr(GError) error = nullptr; - if (!fl_method_call_respond(method_call, response, &error)) + if (!fl_method_call_respond(method_call, response, &error)) { g_warning("Failed to send method call response: %s", error->message); + } } static void fl_text_input_plugin_dispose(GObject* object) { @@ -293,11 +299,13 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, GdkEventKey* event) { g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), FALSE); - if (self->client_id == kClientIdUnset) + if (self->client_id == kClientIdUnset) { return FALSE; + } - if (gtk_im_context_filter_keypress(self->im_context, event)) + if (gtk_im_context_filter_keypress(self->im_context, event)) { return TRUE; + } // Handle navigation keys. gboolean changed = FALSE; @@ -334,8 +342,9 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, } } - if (changed) + if (changed) { update_editing_state(self); + } return FALSE; } diff --git a/shell/platform/linux/fl_text_input_plugin.h b/shell/platform/linux/fl_text_input_plugin.h index f44dbe3a17c6b..14ab5cfc04417 100644 --- a/shell/platform/linux/fl_text_input_plugin.h +++ b/shell/platform/linux/fl_text_input_plugin.h @@ -37,14 +37,14 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger); /** * fl_text_input_plugin_filter_keypress - * @self: an #FlTextInputPlugin. + * @plugin: an #FlTextInputPlugin. * @event: a #GdkEventKey * * Process a Gdk key event. * * Returns: %TRUE if the event was used. */ -gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, +gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* plugin, GdkEventKey* event); G_END_DECLS diff --git a/shell/platform/linux/fl_value.cc b/shell/platform/linux/fl_value.cc index 5b0a4bf8bf66a..d65f02ced05c0 100644 --- a/shell/platform/linux/fl_value.cc +++ b/shell/platform/linux/fl_value.cc @@ -86,8 +86,9 @@ static ssize_t fl_value_lookup_index(FlValue* self, FlValue* key) { for (size_t i = 0; i < fl_value_get_length(self); i++) { FlValue* k = fl_value_get_map_key(self, i); - if (fl_value_equal(k, key)) + if (fl_value_equal(k, key)) { return i; + } } return -1; } @@ -109,8 +110,9 @@ static void float_to_string(double value, GString* buffer) { zero_count = zero_count == 0 ? 0 : zero_count - 1; break; } - if (buffer->str[i] != '0') + if (buffer->str[i] != '0') { break; + } zero_count++; } g_string_truncate(buffer, buffer->len - zero_count); @@ -122,10 +124,11 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "null"); return; case FL_VALUE_TYPE_BOOL: - if (fl_value_get_bool(value)) + if (fl_value_get_bool(value)) { g_string_append(buffer, "true"); - else + } else { g_string_append(buffer, "false"); + } return; case FL_VALUE_TYPE_INT: int_to_string(fl_value_get_int(value), buffer); @@ -141,8 +144,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const uint8_t* values = fl_value_get_uint8_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } int_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -152,8 +156,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const int32_t* values = fl_value_get_int32_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } int_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -163,8 +168,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const int64_t* values = fl_value_get_int64_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } int_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -174,8 +180,9 @@ static void value_to_string(FlValue* value, GString* buffer) { g_string_append(buffer, "["); const double* values = fl_value_get_float_list(value); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } float_to_string(values[i], buffer); } g_string_append(buffer, "]"); @@ -184,8 +191,9 @@ static void value_to_string(FlValue* value, GString* buffer) { case FL_VALUE_TYPE_LIST: { g_string_append(buffer, "["); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } value_to_string(fl_value_get_list_value(value, i), buffer); } g_string_append(buffer, "]"); @@ -194,8 +202,9 @@ static void value_to_string(FlValue* value, GString* buffer) { case FL_VALUE_TYPE_MAP: { g_string_append(buffer, "{"); for (size_t i = 0; i < fl_value_get_length(value); i++) { - if (i != 0) + if (i != 0) { g_string_append(buffer, ", "); + } value_to_string(fl_value_get_map_key(value, i), buffer); g_string_append(buffer, ": "); value_to_string(fl_value_get_map_value(value, i), buffer); @@ -307,8 +316,9 @@ G_MODULE_EXPORT FlValue* fl_value_new_list_from_strv( const gchar* const* str_array) { g_return_val_if_fail(str_array != nullptr, nullptr); g_autoptr(FlValue) value = fl_value_new_list(); - for (int i = 0; str_array[i] != nullptr; i++) + for (int i = 0; str_array[i] != nullptr; i++) { fl_value_append_take(value, fl_value_new_string(str_array[i])); + } return fl_value_ref(value); } @@ -330,8 +340,9 @@ G_MODULE_EXPORT void fl_value_unref(FlValue* self) { g_return_if_fail(self != nullptr); g_return_if_fail(self->ref_count > 0); self->ref_count--; - if (self->ref_count != 0) + if (self->ref_count != 0) { return; + } switch (self->type) { case FL_VALUE_TYPE_STRING: { @@ -388,8 +399,9 @@ G_MODULE_EXPORT bool fl_value_equal(FlValue* a, FlValue* b) { g_return_val_if_fail(a != nullptr, false); g_return_val_if_fail(b != nullptr, false); - if (a->type != b->type) + if (a->type != b->type) { return false; + } switch (a->type) { case FL_VALUE_TYPE_NULL: @@ -406,70 +418,83 @@ G_MODULE_EXPORT bool fl_value_equal(FlValue* a, FlValue* b) { return g_strcmp0(a_->value, b_->value) == 0; } case FL_VALUE_TYPE_UINT8_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const uint8_t* values_a = fl_value_get_uint8_list(a); const uint8_t* values_b = fl_value_get_uint8_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_INT32_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const int32_t* values_a = fl_value_get_int32_list(a); const int32_t* values_b = fl_value_get_int32_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_INT64_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const int64_t* values_a = fl_value_get_int64_list(a); const int64_t* values_b = fl_value_get_int64_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_FLOAT_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } const double* values_a = fl_value_get_float_list(a); const double* values_b = fl_value_get_float_list(b); for (size_t i = 0; i < fl_value_get_length(a); i++) { - if (values_a[i] != values_b[i]) + if (values_a[i] != values_b[i]) { return false; + } } return true; } case FL_VALUE_TYPE_LIST: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } for (size_t i = 0; i < fl_value_get_length(a); i++) { if (!fl_value_equal(fl_value_get_list_value(a, i), - fl_value_get_list_value(b, i))) + fl_value_get_list_value(b, i))) { return false; + } } return true; } case FL_VALUE_TYPE_MAP: { - if (fl_value_get_length(a) != fl_value_get_length(b)) + if (fl_value_get_length(a) != fl_value_get_length(b)) { return false; + } for (size_t i = 0; i < fl_value_get_length(a); i++) { FlValue* key = fl_value_get_map_key(a, i); FlValue* value_b = fl_value_lookup(b, key); - if (value_b == nullptr) + if (value_b == nullptr) { return false; + } FlValue* value_a = fl_value_get_map_value(a, i); - if (!fl_value_equal(value_a, value_b)) + if (!fl_value_equal(value_a, value_b)) { return false; + } } return true; } @@ -676,16 +701,21 @@ G_MODULE_EXPORT FlValue* fl_value_lookup(FlValue* self, FlValue* key) { g_return_val_if_fail(self->type == FL_VALUE_TYPE_MAP, nullptr); ssize_t index = fl_value_lookup_index(self, key); - if (index < 0) + if (index < 0) { return nullptr; + } return fl_value_get_map_value(self, index); } G_MODULE_EXPORT FlValue* fl_value_lookup_string(FlValue* self, const gchar* key) { g_return_val_if_fail(self != nullptr, nullptr); - g_autoptr(FlValue) string_key = fl_value_new_string(key); - return fl_value_lookup(self, string_key); + FlValue* string_key = fl_value_new_string(key); + FlValue* value = fl_value_lookup(self, string_key); + // Explicit unref used because the g_autoptr is triggering a false positive + // with clang-tidy. + fl_value_unref(string_key); + return value; } G_MODULE_EXPORT gchar* fl_value_to_string(FlValue* value) { diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 2abfc39e2dfac..fa9bea13f00a6 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -70,25 +70,28 @@ static gboolean fl_view_send_pointer_button_event(FlView* self, return FALSE; } int old_button_state = self->button_state; - FlutterPointerPhase phase; + FlutterPointerPhase phase = kMove; if (event->type == GDK_BUTTON_PRESS) { // Drop the event if Flutter already thinks the button is down. - if ((self->button_state & button) != 0) + if ((self->button_state & button) != 0) { return FALSE; + } self->button_state ^= button; phase = old_button_state == 0 ? kDown : kMove; } else if (event->type == GDK_BUTTON_RELEASE) { // Drop the event if Flutter already thinks the button is up. - if ((self->button_state & button) == 0) + if ((self->button_state & button) == 0) { return FALSE; + } self->button_state ^= button; phase = self->button_state == 0 ? kUp : kMove; } - if (self->engine == nullptr) + if (self->engine == nullptr) { return FALSE; + } gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_engine_send_mouse_pointer_event( @@ -100,13 +103,14 @@ static gboolean fl_view_send_pointer_button_event(FlView* self, } // Updates the engine with the current window metrics. -static void fl_view_send_window_metrics(FlView* self) { +static void fl_view_geometry_changed(FlView* self) { GtkAllocation allocation; gtk_widget_get_allocation(GTK_WIDGET(self), &allocation); gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_engine_send_window_metrics_event( self->engine, allocation.width * scale_factor, allocation.height * scale_factor, scale_factor); + fl_renderer_set_geometry(self->renderer, &allocation, scale_factor); } // Implements FlPluginRegistry::get_registrar_for_plugin. @@ -175,11 +179,12 @@ static void fl_view_notify(GObject* object, GParamSpec* pspec) { FlView* self = FL_VIEW(object); if (strcmp(pspec->name, "scale-factor") == 0) { - fl_view_send_window_metrics(self); + fl_view_geometry_changed(self); } - if (G_OBJECT_CLASS(fl_view_parent_class)->notify != nullptr) + if (G_OBJECT_CLASS(fl_view_parent_class)->notify != nullptr) { G_OBJECT_CLASS(fl_view_parent_class)->notify(object, pspec); + } } static void fl_view_dispose(GObject* object) { @@ -203,8 +208,9 @@ static void fl_view_realize(GtkWidget* widget) { gtk_widget_set_realized(widget, TRUE); g_autoptr(GError) error = nullptr; - if (!fl_renderer_setup(self->renderer, &error)) + if (!fl_renderer_setup(self->renderer, &error)) { g_warning("Failed to setup renderer: %s", error->message); + } GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); @@ -232,12 +238,11 @@ static void fl_view_realize(GtkWidget* widget) { gtk_widget_register_window(widget, window); gtk_widget_set_window(widget, window); - fl_renderer_x11_set_window( - FL_RENDERER_X11(self->renderer), - GDK_X11_WINDOW(gtk_widget_get_window(GTK_WIDGET(self)))); + fl_renderer_set_window(self->renderer, window); - if (!fl_engine_start(self->engine, &error)) + if (!fl_engine_start(self->engine, &error)) { g_warning("Failed to start Flutter engine: %s", error->message); + } } // Implements GtkWidget::size-allocate. @@ -253,7 +258,7 @@ static void fl_view_size_allocate(GtkWidget* widget, allocation->height); } - fl_view_send_window_metrics(self); + fl_view_geometry_changed(self); } // Implements GtkWidget::button_press_event. @@ -263,8 +268,9 @@ static gboolean fl_view_button_press_event(GtkWidget* widget, // Flutter doesn't handle double and triple click events. if (event->type == GDK_DOUBLE_BUTTON_PRESS || - event->type == GDK_TRIPLE_BUTTON_PRESS) + event->type == GDK_TRIPLE_BUTTON_PRESS) { return FALSE; + } return fl_view_send_pointer_button_event(self, event); } @@ -298,8 +304,8 @@ static gboolean fl_view_scroll_event(GtkWidget* widget, GdkEventScroll* event) { scroll_delta_x = 1; } - // TODO: See if this can be queried from the OS; this value is chosen - // arbitrarily to get something that feels reasonable. + // TODO(robert-ancell): See if this can be queried from the OS; this value is + // chosen arbitrarily to get something that feels reasonable. const int kScrollOffsetMultiplier = 20; scroll_delta_x *= kScrollOffsetMultiplier; scroll_delta_y *= kScrollOffsetMultiplier; @@ -319,8 +325,9 @@ static gboolean fl_view_motion_notify_event(GtkWidget* widget, GdkEventMotion* event) { FlView* self = FL_VIEW(widget); - if (self->engine == nullptr) + if (self->engine == nullptr) { return FALSE; + } gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_engine_send_mouse_pointer_event( diff --git a/shell/platform/linux/testing/mock_renderer.cc b/shell/platform/linux/testing/mock_renderer.cc index 0112c38faa9bc..8793491778951 100644 --- a/shell/platform/linux/testing/mock_renderer.cc +++ b/shell/platform/linux/testing/mock_renderer.cc @@ -17,16 +17,28 @@ static GdkVisual* fl_mock_renderer_get_visual(FlRenderer* renderer, return static_cast(g_object_new(GDK_TYPE_VISUAL, nullptr)); } -// Implements FlRenderer::create_surface. -static EGLSurface fl_mock_renderer_create_surface(FlRenderer* renderer, - EGLDisplay display, - EGLConfig config) { - return eglCreateWindowSurface(display, config, 0, nullptr); +// Implements FlRenderer::create_display. +static EGLDisplay fl_mock_renderer_create_display(FlRenderer* renderer) { + return eglGetDisplay(EGL_DEFAULT_DISPLAY); +} + +// Implements FlRenderer::create_surfaces. +static gboolean fl_mock_renderer_create_surfaces(FlRenderer* renderer, + EGLDisplay display, + EGLConfig config, + EGLSurface* visible, + EGLSurface* resource, + GError** error) { + *visible = eglCreateWindowSurface(display, config, 0, nullptr); + const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + *resource = eglCreatePbufferSurface(display, config, attribs); + return TRUE; } static void fl_mock_renderer_class_init(FlMockRendererClass* klass) { FL_RENDERER_CLASS(klass)->get_visual = fl_mock_renderer_get_visual; - FL_RENDERER_CLASS(klass)->create_surface = fl_mock_renderer_create_surface; + FL_RENDERER_CLASS(klass)->create_display = fl_mock_renderer_create_display; + FL_RENDERER_CLASS(klass)->create_surfaces = fl_mock_renderer_create_surfaces; } static void fl_mock_renderer_init(FlMockRenderer* self) {} diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 14adddc19c51f..9ecb0d2f5e863 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -42,7 +42,11 @@ source_set("flutter_windows_source") { "angle_surface_manager.h", "cursor_handler.cc", "cursor_handler.h", + "flutter_project_bundle.cc", + "flutter_project_bundle.h", "flutter_windows.cc", + "flutter_windows_engine.cc", + "flutter_windows_engine.h", "flutter_windows_view.cc", "flutter_windows_view.h", "key_event_handler.cc", @@ -50,6 +54,8 @@ source_set("flutter_windows_source") { "keyboard_hook_handler.h", "string_conversion.cc", "string_conversion.h", + "system_utils.h", + "system_utils_win32.cc", "text_input_plugin.cc", "text_input_plugin.h", "win32_dpi_utils.cc", @@ -62,6 +68,8 @@ source_set("flutter_windows_source") { "win32_task_runner.h", "win32_window.cc", "win32_window.h", + "win32_window_proc_delegate_manager.cc", + "win32_window_proc_delegate_manager.h", "window_binding_handler.h", "window_binding_handler_delegate.h", "window_state.h", @@ -114,12 +122,14 @@ executable("flutter_windows_unittests") { sources = [ "string_conversion_unittests.cc", + "system_utils_unittests.cc", "testing/win32_flutter_window_test.cc", "testing/win32_flutter_window_test.h", "testing/win32_window_test.cc", "testing/win32_window_test.h", "win32_dpi_utils_unittests.cc", "win32_flutter_window_unittests.cc", + "win32_window_proc_delegate_manager_unittests.cc", "win32_window_unittests.cc", ] diff --git a/shell/platform/windows/angle_surface_manager.cc b/shell/platform/windows/angle_surface_manager.cc index cc046281e8114..eda082c7c868d 100644 --- a/shell/platform/windows/angle_surface_manager.cc +++ b/shell/platform/windows/angle_surface_manager.cc @@ -169,14 +169,22 @@ void AngleSurfaceManager::CleanUp() { } } -bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target) { +bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height) { if (!render_target || !initialize_succeeded_) { return false; } EGLSurface surface = EGL_NO_SURFACE; - const EGLint surfaceAttributes[] = {EGL_NONE}; + // Disable Angle's automatic surface sizing logic and provide and exlicit + // size. AngleSurfaceManager is responsible for initiating Angle surface size + // changes to avoid race conditions with rendering when automatic mode is + // used. + const EGLint surfaceAttributes[] = { + EGL_FIXED_SIZE_ANGLE, EGL_TRUE, EGL_WIDTH, width, + EGL_HEIGHT, height, EGL_NONE}; surface = eglCreateWindowSurface( egl_display_, egl_config_, @@ -190,6 +198,26 @@ bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target) { return true; } +void AngleSurfaceManager::ResizeSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height) { + EGLint existing_width, existing_height; + GetSurfaceDimensions(&existing_width, &existing_height); + if (width != existing_width || height != existing_height) { + // Destroy existing surface with previous stale dimensions and create new + // surface at new size. Since the Windows compositor retains the front + // buffer until the new surface has been presented, no need to manually + // preserve the previous surface contents. This resize approach could be + // further optimized if Angle exposed a public entrypoint for + // SwapChain11::reset or SwapChain11::resize. + DestroySurface(); + if (!CreateSurface(render_target, width, height)) { + std::cerr << "AngleSurfaceManager::ResizeSurface failed to create surface" + << std::endl; + } + } +} + void AngleSurfaceManager::GetSurfaceDimensions(EGLint* width, EGLint* height) { if (render_surface_ == EGL_NO_SURFACE || !initialize_succeeded_) { width = 0; diff --git a/shell/platform/windows/angle_surface_manager.h b/shell/platform/windows/angle_surface_manager.h index a6a5fc213c3d4..60754c37cc5cf 100644 --- a/shell/platform/windows/angle_surface_manager.h +++ b/shell/platform/windows/angle_surface_manager.h @@ -23,6 +23,8 @@ namespace flutter { // destroy surfaces class AngleSurfaceManager { public: + // Creates a new surface manager retaining reference to the passed-in target + // for the lifetime of the manager. AngleSurfaceManager(); ~AngleSurfaceManager(); @@ -32,8 +34,19 @@ class AngleSurfaceManager { // Creates an EGLSurface wrapper and backing DirectX 11 SwapChain // asociated with window, in the appropriate format for display. - // Target represents the visual entity to bind to. - bool CreateSurface(WindowsRenderTarget* render_target); + // Target represents the visual entity to bind to. Width and + // height represent dimensions surface is created at. + bool CreateSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height); + + // Resizes backing surface from current size to newly requested size + // based on width and height for the specific case when width and height do + // not match current surface dimensions. Target represents the visual entity + // to bind to. + void ResizeSurface(WindowsRenderTarget* render_target, + EGLint width, + EGLint height); // queries EGL for the dimensions of surface in physical // pixels returning width and height as out params. diff --git a/shell/platform/windows/client_wrapper/BUILD.gn b/shell/platform/windows/client_wrapper/BUILD.gn index 6f89a81a2dd8a..fe54c2ba2a484 100644 --- a/shell/platform/windows/client_wrapper/BUILD.gn +++ b/shell/platform/windows/client_wrapper/BUILD.gn @@ -7,19 +7,25 @@ import("//flutter/testing/testing.gni") _wrapper_includes = [ "include/flutter/dart_project.h", + "include/flutter/flutter_engine.h", "include/flutter/flutter_view_controller.h", "include/flutter/flutter_view.h", "include/flutter/plugin_registrar_windows.h", ] -_wrapper_sources = [ "flutter_view_controller.cc" ] +_wrapper_sources = [ + "flutter_engine.cc", + "flutter_view_controller.cc", +] # This code will be merged into .../common/cpp/client_wrapper for client use, # so uses header paths that assume the merged state. Include the header -# header directory of the core wrapper files so these includes will work. +# directories of the core wrapper files so these includes will work. config("relative_core_wrapper_headers") { - include_dirs = - [ "//flutter/shell/platform/common/cpp/client_wrapper/include/flutter" ] + include_dirs = [ + "//flutter/shell/platform/common/cpp/client_wrapper", + "//flutter/shell/platform/common/cpp/client_wrapper/include/flutter", + ] } # Windows client wrapper build for internal use by the shell implementation. @@ -69,11 +75,15 @@ executable("client_wrapper_windows_unittests") { sources = [ "dart_project_unittests.cc", + "flutter_engine_unittests.cc", "flutter_view_controller_unittests.cc", "flutter_view_unittests.cc", "plugin_registrar_windows_unittests.cc", ] + # Set embedder.h to export, not import, since the stubs are locally defined. + defines = [ "FLUTTER_DESKTOP_LIBRARY" ] + deps = [ ":client_wrapper_library_stubs_windows", ":client_wrapper_windows", diff --git a/shell/platform/windows/client_wrapper/flutter_engine.cc b/shell/platform/windows/client_wrapper/flutter_engine.cc new file mode 100644 index 0000000000000..8bf94420646c7 --- /dev/null +++ b/shell/platform/windows/client_wrapper/flutter_engine.cc @@ -0,0 +1,83 @@ +// 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. + +#include "include/flutter/flutter_engine.h" + +#include +#include + +#include "binary_messenger_impl.h" + +namespace flutter { + +FlutterEngine::FlutterEngine(const DartProject& project) { + FlutterDesktopEngineProperties c_engine_properties = {}; + c_engine_properties.assets_path = project.assets_path().c_str(); + c_engine_properties.icu_data_path = project.icu_data_path().c_str(); + c_engine_properties.aot_library_path = project.aot_library_path().c_str(); + std::vector engine_switches; + std::transform( + project.engine_switches().begin(), project.engine_switches().end(), + std::back_inserter(engine_switches), + [](const std::string& arg) -> const char* { return arg.c_str(); }); + if (engine_switches.size() > 0) { + c_engine_properties.switches = &engine_switches[0]; + c_engine_properties.switches_count = engine_switches.size(); + } + + engine_ = FlutterDesktopEngineCreate(c_engine_properties); + + auto core_messenger = FlutterDesktopEngineGetMessenger(engine_); + messenger_ = std::make_unique(core_messenger); +} + +FlutterEngine::~FlutterEngine() { + ShutDown(); +} + +bool FlutterEngine::Run(const char* entry_point) { + if (!engine_) { + std::cerr << "Cannot run an engine that failed creation." << std::endl; + return false; + } + if (has_been_run_) { + std::cerr << "Cannot run an engine more than once." << std::endl; + return false; + } + bool run_succeeded = FlutterDesktopEngineRun(engine_, entry_point); + if (!run_succeeded) { + std::cerr << "Failed to start engine." << std::endl; + } + has_been_run_ = true; + return run_succeeded; +} + +void FlutterEngine::ShutDown() { + if (engine_ && owns_engine_) { + FlutterDesktopEngineDestroy(engine_); + } + engine_ = nullptr; +} + +std::chrono::nanoseconds FlutterEngine::ProcessMessages() { + return std::chrono::nanoseconds(FlutterDesktopEngineProcessMessages(engine_)); +} + +FlutterDesktopPluginRegistrarRef FlutterEngine::GetRegistrarForPlugin( + const std::string& plugin_name) { + if (!engine_) { + std::cerr << "Cannot get plugin registrar on an engine that isn't running; " + "call Run first." + << std::endl; + return nullptr; + } + return FlutterDesktopEngineGetPluginRegistrar(engine_, plugin_name.c_str()); +} + +FlutterDesktopEngineRef FlutterEngine::RelinquishEngine() { + owns_engine_ = false; + return engine_; +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc new file mode 100644 index 0000000000000..8fe83225b114f --- /dev/null +++ b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc @@ -0,0 +1,106 @@ +// 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. + +#include +#include + +#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h" +#include "flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h" +#include "gtest/gtest.h" + +namespace flutter { + +namespace { + +// Stub implementation to validate calls to the API. +class TestFlutterWindowsApi : public testing::StubFlutterWindowsApi { + public: + // |flutter::testing::StubFlutterWindowsApi| + FlutterDesktopEngineRef EngineCreate( + const FlutterDesktopEngineProperties& engine_properties) { + create_called_ = true; + return reinterpret_cast(1); + } + + // |flutter::testing::StubFlutterWindowsApi| + bool EngineRun(const char* entry_point) override { + run_called_ = true; + return true; + } + + // |flutter::testing::StubFlutterWindowsApi| + bool EngineDestroy() override { + destroy_called_ = true; + return true; + } + + // |flutter::testing::StubFlutterWindowsApi| + uint64_t EngineProcessMessages() override { return 99; } + + bool create_called() { return create_called_; } + + bool run_called() { return run_called_; } + + bool destroy_called() { return destroy_called_; } + + private: + bool create_called_ = false; + bool run_called_ = false; + bool destroy_called_ = false; +}; + +} // namespace + +TEST(FlutterEngineTest, CreateDestroy) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + { + FlutterEngine engine(DartProject(L"fake/project/path")); + engine.Run(); + EXPECT_EQ(test_api->create_called(), true); + EXPECT_EQ(test_api->run_called(), true); + EXPECT_EQ(test_api->destroy_called(), false); + } + // Destroying should implicitly shut down if it hasn't been done manually. + EXPECT_EQ(test_api->destroy_called(), true); +} + +TEST(FlutterEngineTest, ExplicitShutDown) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + FlutterEngine engine(DartProject(L"fake/project/path")); + engine.Run(); + EXPECT_EQ(test_api->create_called(), true); + EXPECT_EQ(test_api->run_called(), true); + EXPECT_EQ(test_api->destroy_called(), false); + engine.ShutDown(); + EXPECT_EQ(test_api->destroy_called(), true); +} + +TEST(FlutterEngineTest, ProcessMessages) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + FlutterEngine engine(DartProject(L"fake/project/path")); + engine.Run(); + + std::chrono::nanoseconds next_event_time = engine.ProcessMessages(); + EXPECT_EQ(next_event_time.count(), 99); +} + +TEST(FlutterEngineTest, GetMessenger) { + DartProject project(L"data"); + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + FlutterEngine engine(DartProject(L"fake/project/path")); + EXPECT_NE(engine.messenger(), nullptr); +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller.cc b/shell/platform/windows/client_wrapper/flutter_view_controller.cc index 16dd49f8a24d4..e32c4e59ea6cb 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller.cc @@ -12,73 +12,41 @@ namespace flutter { FlutterViewController::FlutterViewController(int width, int height, const DartProject& project) { - std::vector switches; - std::transform( - project.engine_switches().begin(), project.engine_switches().end(), - std::back_inserter(switches), - [](const std::string& arg) -> const char* { return arg.c_str(); }); - size_t switch_count = switches.size(); - - FlutterDesktopEngineProperties properties = {}; - properties.assets_path = project.assets_path().c_str(); - properties.icu_data_path = project.icu_data_path().c_str(); - // It is harmless to pass this in non-AOT mode. - properties.aot_library_path = project.aot_library_path().c_str(); - properties.switches = switch_count > 0 ? switches.data() : nullptr; - properties.switches_count = switch_count; - controller_ = FlutterDesktopCreateViewController(width, height, properties); + engine_ = std::make_unique(project); + controller_ = FlutterDesktopViewControllerCreate(width, height, + engine_->RelinquishEngine()); if (!controller_) { std::cerr << "Failed to create view controller." << std::endl; return; } - view_ = std::make_unique(FlutterDesktopGetView(controller_)); + view_ = std::make_unique( + FlutterDesktopViewControllerGetView(controller_)); } -FlutterViewController::FlutterViewController( - const std::string& icu_data_path, - int width, - int height, - const std::string& assets_path, - const std::vector& arguments) { +FlutterViewController::~FlutterViewController() { if (controller_) { - std::cerr << "Only one Flutter view can exist at a time." << std::endl; + FlutterDesktopViewControllerDestroy(controller_); } - - std::vector engine_arguments; - std::transform( - arguments.begin(), arguments.end(), std::back_inserter(engine_arguments), - [](const std::string& arg) -> const char* { return arg.c_str(); }); - size_t arg_count = engine_arguments.size(); - - controller_ = FlutterDesktopCreateViewControllerLegacy( - width, height, assets_path.c_str(), icu_data_path.c_str(), - arg_count > 0 ? &engine_arguments[0] : nullptr, arg_count); - if (!controller_) { - std::cerr << "Failed to create view controller." << std::endl; - return; - } - view_ = std::make_unique(FlutterDesktopGetView(controller_)); } -FlutterViewController::~FlutterViewController() { - if (controller_) { - FlutterDesktopDestroyViewController(controller_); - } +std::optional FlutterViewController::HandleTopLevelWindowProc( + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + LRESULT result; + bool handled = FlutterDesktopViewControllerHandleTopLevelWindowProc( + controller_, hwnd, message, wparam, lparam, &result); + return handled ? result : std::optional(std::nullopt); } std::chrono::nanoseconds FlutterViewController::ProcessMessages() { - return std::chrono::nanoseconds(FlutterDesktopProcessMessages(controller_)); + return engine_->ProcessMessages(); } FlutterDesktopPluginRegistrarRef FlutterViewController::GetRegistrarForPlugin( const std::string& plugin_name) { - if (!controller_) { - std::cerr << "Cannot get plugin registrar without a window; call " - "CreateWindow first." - << std::endl; - return nullptr; - } - return FlutterDesktopGetPluginRegistrar(controller_, plugin_name.c_str()); + return engine_->GetRegistrarForPlugin(plugin_name); } } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc index ec1b7fb561eae..264a998c29138 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc @@ -15,32 +15,59 @@ namespace { // Stub implementation to validate calls to the API. class TestWindowsApi : public testing::StubFlutterWindowsApi { - FlutterDesktopViewControllerRef CreateViewController( + public: + // |flutter::testing::StubFlutterWindowsApi| + FlutterDesktopViewControllerRef ViewControllerCreate( int width, int height, + FlutterDesktopEngineRef engine) override { + return reinterpret_cast(2); + } + + // |flutter::testing::StubFlutterWindowsApi| + void ViewControllerDestroy() override { view_controller_destroyed_ = true; } + + // |flutter::testing::StubFlutterWindowsApi| + FlutterDesktopEngineRef EngineCreate( const FlutterDesktopEngineProperties& engine_properties) override { - return reinterpret_cast(1); + return reinterpret_cast(1); } + + // |flutter::testing::StubFlutterWindowsApi| + bool EngineDestroy() override { + engine_destroyed_ = true; + return true; + } + + bool engine_destroyed() { return engine_destroyed_; } + bool view_controller_destroyed() { return view_controller_destroyed_; } + + private: + bool engine_destroyed_ = false; + bool view_controller_destroyed_ = false; }; } // namespace -TEST(FlutterViewControllerTest, CreateDestroyLegacy) { +TEST(FlutterViewControllerTest, CreateDestroy) { + DartProject project(L"data"); testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); - { - FlutterViewController controller("", 100, 100, "", - std::vector{}); - } + { FlutterViewController controller(100, 100, project); } + EXPECT_TRUE(test_api->view_controller_destroyed()); + // Per the C API, once a view controller has taken ownership of an engine + // the engine destruction method should not be called. + EXPECT_FALSE(test_api->engine_destroyed()); } -TEST(FlutterViewControllerTest, CreateDestroy) { +TEST(FlutterViewControllerTest, GetEngine) { DartProject project(L"data"); testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); - { FlutterViewController controller(100, 100, project); } + FlutterViewController controller(100, 100, project); + EXPECT_NE(controller.engine(), nullptr); } TEST(FlutterViewControllerTest, GetView) { diff --git a/shell/platform/windows/client_wrapper/include/flutter/dart_project.h b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h index ae542aebf1d68..6c94ffa757215 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/dart_project.h +++ b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h @@ -45,6 +45,7 @@ class DartProject { // flexible options for project structures are needed later without it // being a breaking change. Provide access to internal classes that need // them. + friend class FlutterEngine; friend class FlutterViewController; friend class DartProjectTest; diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h new file mode 100644 index 0000000000000..94edb4064dd2c --- /dev/null +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h @@ -0,0 +1,94 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_ + +#define NOMINMAX +#include + +#include +#include +#include + +#include "binary_messenger.h" +#include "dart_project.h" +#include "plugin_registrar.h" +#include "plugin_registry.h" + +namespace flutter { + +// An instance of a Flutter engine. +// +// In the future, this will be the API surface used for all interactions with +// the engine, rather than having them duplicated on FlutterViewController. +// For now it is only used in the rare where you need a headless Flutter engine. +class FlutterEngine : public PluginRegistry { + public: + // Creates a new engine for running the given project. + explicit FlutterEngine(const DartProject& project); + + virtual ~FlutterEngine(); + + // Prevent copying. + FlutterEngine(FlutterEngine const&) = delete; + FlutterEngine& operator=(FlutterEngine const&) = delete; + + // Starts running the engine, with an optional entry point. + // + // If provided, entry_point must be the name of a top-level function from the + // same Dart library that contains the app's main() function, and must be + // decorated with `@pragma(vm:entry-point)` to ensure the method is not + // tree-shaken by the Dart compiler. If not provided, defaults to main(). + bool Run(const char* entry_point = nullptr); + + // Terminates the running engine. + void ShutDown(); + + // Processes any pending events in the Flutter engine, and returns the + // nanosecond delay until the next scheduled event (or max, if none). + // + // This should be called on every run of the application-level runloop, and + // a wait for native events in the runloop should never be longer than the + // last return value from this function. + std::chrono::nanoseconds ProcessMessages(); + + // flutter::PluginRegistry: + FlutterDesktopPluginRegistrarRef GetRegistrarForPlugin( + const std::string& plugin_name) override; + + // Returns the messenger to use for creating channels to communicate with the + // Flutter engine. + // + // This pointer will remain valid for the lifetime of this instance. + BinaryMessenger* messenger() { return messenger_.get(); } + + private: + // For access to RelinquishEngine. + friend class FlutterViewController; + + // Gives up ownership of |engine_|, but keeps a weak reference to it. + // + // This is intended to be used by FlutterViewController, since the underlying + // C API for view controllers takes over engine ownership. + FlutterDesktopEngineRef RelinquishEngine(); + + // Handle for interacting with the C API's engine reference. + FlutterDesktopEngineRef engine_ = nullptr; + + // Messenger for communicating with the engine. + std::unique_ptr messenger_; + + // Whether or not this wrapper owns |engine_|. + bool owns_engine_ = true; + + // Whether the engine has been run. This will be true if Run has been called, + // or if RelinquishEngine has been called (since the view controller will + // run the engine if it hasn't already been run). + bool has_been_run_ = false; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_ENGINE_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h index 607e01a72f247..32a1f2f8fd30c 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h @@ -5,13 +5,14 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_VIEW_CONTROLLER_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_VIEW_CONTROLLER_H_ +#include #include -#include -#include -#include +#include +#include #include "dart_project.h" +#include "flutter_engine.h" #include "flutter_view.h" #include "plugin_registrar.h" #include "plugin_registry.h" @@ -33,30 +34,32 @@ class FlutterViewController : public PluginRegistry { int height, const DartProject& project); - // DEPRECATED. Will be removed soon; use the version above. - explicit FlutterViewController(const std::string& icu_data_path, - int width, - int height, - const std::string& assets_path, - const std::vector& arguments); - virtual ~FlutterViewController(); // Prevent copying. FlutterViewController(FlutterViewController const&) = delete; FlutterViewController& operator=(FlutterViewController const&) = delete; + // Returns the engine running Flutter content in this view. + FlutterEngine* engine() { return engine_.get(); } + + // Returns the view managed by this controller. FlutterView* view() { return view_.get(); } - // Processes any pending events in the Flutter engine, and returns the - // nanosecond delay until the next scheduled event (or max, if none). + // Allows the Flutter engine and any interested plugins an opportunity to + // handle the given message. // - // This should be called on every run of the application-level runloop, and - // a wait for native events in the runloop should never be longer than the - // last return value from this function. + // If a result is returned, then the message was handled in such a way that + // further handling should not be done. + std::optional HandleTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam); + + // DEPRECATED. Call engine()->ProcessMessages() instead. std::chrono::nanoseconds ProcessMessages(); - // flutter::PluginRegistry: + // DEPRECATED. Call engine()->GetRegistrarForPlugin() instead. FlutterDesktopPluginRegistrarRef GetRegistrarForPlugin( const std::string& plugin_name) override; @@ -64,6 +67,9 @@ class FlutterViewController : public PluginRegistry { // Handle for interacting with the C API's view controller, if any. FlutterDesktopViewControllerRef controller_ = nullptr; + // The backing engine + std::unique_ptr engine_; + // The owned FlutterView. std::unique_ptr view_; }; diff --git a/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h b/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h index 052f22bbc8a80..16bab4891ef28 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h +++ b/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h @@ -5,15 +5,24 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_WINDOWS_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_WINDOWS_H_ +#include #include #include +#include #include "flutter_view.h" #include "plugin_registrar.h" namespace flutter { +// A delegate callback for WindowProc delegation. +// +// Implementations should return a value only if they have handled the message +// and want to stop all further handling. +using WindowProcDelegate = std::function(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)>; + // An extension to PluginRegistrar providing access to Windows-specific // functionality. class PluginRegistrarWindows : public PluginRegistrar { @@ -24,7 +33,7 @@ class PluginRegistrarWindows : public PluginRegistrar { FlutterDesktopPluginRegistrarRef core_registrar) : PluginRegistrar(core_registrar) { view_ = std::make_unique( - FlutterDesktopRegistrarGetView(core_registrar)); + FlutterDesktopPluginRegistrarGetView(core_registrar)); } virtual ~PluginRegistrarWindows() = default; @@ -35,9 +44,78 @@ class PluginRegistrarWindows : public PluginRegistrar { FlutterView* GetView() { return view_.get(); } + // Registers |delegate| to recieve WindowProc callbacks for the top-level + // window containing this Flutter instance. Returns an ID that can be used to + // unregister the handler. + // + // Delegates are not guaranteed to be called: + // - The application may choose not to delegate WindowProc calls. + // - If multiple plugins are registered, the first one that returns a value + // from the delegate message will "win", and others will not be called. + // The order of delegate calls is not defined. + // + // Delegates should be implemented as narrowly as possible, only returning + // a value in cases where it's important that other delegates not run, to + // minimize the chances of conflicts between plugins. + int RegisterTopLevelWindowProcDelegate(WindowProcDelegate delegate) { + if (window_proc_delegates_.empty()) { + FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + registrar(), PluginRegistrarWindows::OnTopLevelWindowProc, this); + } + int delegate_id = next_window_proc_delegate_id_++; + window_proc_delegates_.emplace(delegate_id, std::move(delegate)); + return delegate_id; + } + + // Unregisters a previously registered delegate. + void UnregisterTopLevelWindowProcDelegate(int proc_id) { + window_proc_delegates_.erase(proc_id); + if (window_proc_delegates_.empty()) { + FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + registrar(), PluginRegistrarWindows::OnTopLevelWindowProc); + } + } + private: + // A FlutterDesktopWindowProcCallback implementation that forwards back to + // a PluginRegistarWindows instance provided as |user_data|. + static bool OnTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + void* user_data, + LRESULT* result) { + const auto* registrar = static_cast(user_data); + std::optional optional_result = registrar->CallTopLevelWindowProcDelegates( + hwnd, message, wparam, lparam); + if (optional_result) { + *result = *optional_result; + } + return optional_result.has_value(); + } + + std::optional CallTopLevelWindowProcDelegates(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) const { + std::optional result; + for (const auto& pair : window_proc_delegates_) { + result = pair.second(hwnd, message, wparam, lparam); + // Stop as soon as any delegate indicates that it has handled the message. + if (result) { + break; + } + } + return result; + } + // The associated FlutterView, if any. std::unique_ptr view_; + + // The next ID to return from RegisterWindowProcDelegate. + int next_window_proc_delegate_id_ = 1; + + std::map window_proc_delegates_; }; } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc b/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc index 07eaff0c1e2be..63049a5eb82b9 100644 --- a/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc +++ b/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc @@ -14,7 +14,34 @@ namespace flutter { namespace { // Stub implementation to validate calls to the API. -class TestWindowsApi : public testing::StubFlutterWindowsApi {}; +class TestWindowsApi : public testing::StubFlutterWindowsApi { + public: + void PluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data) override { + ++registered_delegate_count_; + last_registered_delegate_ = delegate; + last_registered_user_data_ = user_data; + } + + void PluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate) override { + --registered_delegate_count_; + } + + int registered_delegate_count() { return registered_delegate_count_; } + + FlutterDesktopWindowProcCallback last_registered_delegate() { + return last_registered_delegate_; + } + + void* last_registered_user_data() { return last_registered_user_data_; } + + private: + int registered_delegate_count_ = 0; + FlutterDesktopWindowProcCallback last_registered_delegate_ = nullptr; + void* last_registered_user_data_ = nullptr; +}; } // namespace @@ -27,4 +54,104 @@ TEST(PluginRegistrarWindowsTest, GetView) { EXPECT_NE(registrar.GetView(), nullptr); } +TEST(PluginRegistrarWindowsTest, RegisterUnregister) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + PluginRegistrarWindows registrar( + reinterpret_cast(1)); + + WindowProcDelegate delegate = [](HWND hwnd, UINT message, WPARAM wparam, + LPARAM lparam) { + return std::optional(); + }; + int id_a = registrar.RegisterTopLevelWindowProcDelegate(delegate); + EXPECT_EQ(test_api->registered_delegate_count(), 1); + int id_b = registrar.RegisterTopLevelWindowProcDelegate(delegate); + // All the C++-level delegates are driven by a since C callback, so the + // registration count should stay the same. + EXPECT_EQ(test_api->registered_delegate_count(), 1); + + // Unregistering one of the two delegates shouldn't cause the underlying C + // callback to be unregistered. + registrar.UnregisterTopLevelWindowProcDelegate(id_a); + EXPECT_EQ(test_api->registered_delegate_count(), 1); + // Unregistering both should unregister it. + registrar.UnregisterTopLevelWindowProcDelegate(id_b); + EXPECT_EQ(test_api->registered_delegate_count(), 0); + + EXPECT_NE(id_a, id_b); +} + +TEST(PluginRegistrarWindowsTest, CallsRegisteredDelegates) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + PluginRegistrarWindows registrar( + reinterpret_cast(1)); + + HWND dummy_hwnd; + bool called_a = false; + WindowProcDelegate delegate_a = [&called_a, &dummy_hwnd]( + HWND hwnd, UINT message, WPARAM wparam, + LPARAM lparam) { + called_a = true; + EXPECT_EQ(hwnd, dummy_hwnd); + EXPECT_EQ(message, 2); + EXPECT_EQ(wparam, 3); + EXPECT_EQ(lparam, 4); + return std::optional(); + }; + bool called_b = false; + WindowProcDelegate delegate_b = [&called_b](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(); + }; + int id_a = registrar.RegisterTopLevelWindowProcDelegate(delegate_a); + int id_b = registrar.RegisterTopLevelWindowProcDelegate(delegate_b); + + LRESULT result = 0; + bool handled = test_api->last_registered_delegate()( + dummy_hwnd, 2, 3, 4, test_api->last_registered_user_data(), &result); + EXPECT_TRUE(called_a); + EXPECT_TRUE(called_b); + EXPECT_FALSE(handled); +} + +TEST(PluginRegistrarWindowsTest, StopsOnceHandled) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + PluginRegistrarWindows registrar( + reinterpret_cast(1)); + + bool called_a = false; + WindowProcDelegate delegate_a = [&called_a](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(7); + }; + bool called_b = false; + WindowProcDelegate delegate_b = [&called_b](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(7); + }; + int id_a = registrar.RegisterTopLevelWindowProcDelegate(delegate_a); + int id_b = registrar.RegisterTopLevelWindowProcDelegate(delegate_b); + + HWND dummy_hwnd; + LRESULT result = 0; + bool handled = test_api->last_registered_delegate()( + dummy_hwnd, 2, 3, 4, test_api->last_registered_user_data(), &result); + // Only one of the delegates should have been called, since each claims to + // have fully handled the message. + EXPECT_TRUE(called_a || called_b); + EXPECT_NE(called_a, called_b); + // The return value should propagate through. + EXPECT_TRUE(handled); + EXPECT_EQ(result, 7); +} + } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index b84d9195daf26..20613734f9005 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -35,86 +35,121 @@ ScopedStubFlutterWindowsApi::~ScopedStubFlutterWindowsApi() { // Forwarding dummy implementations of the C API. -FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( +FlutterDesktopViewControllerRef FlutterDesktopViewControllerCreate( int width, int height, - const FlutterDesktopEngineProperties& engine_properties) { + FlutterDesktopEngineRef engine) { if (s_stub_implementation) { - return s_stub_implementation->CreateViewController(width, height, - engine_properties); + return s_stub_implementation->ViewControllerCreate(width, height, engine); } return nullptr; } -FlutterDesktopViewControllerRef FlutterDesktopCreateViewControllerLegacy( - int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { +void FlutterDesktopViewControllerDestroy( + FlutterDesktopViewControllerRef controller) { if (s_stub_implementation) { - // This stub will be removed shortly, and the current tests don't need the - // arguments, so there's no need to translate them to engine_properties. - FlutterDesktopEngineProperties engine_properties; - return s_stub_implementation->CreateViewController( - initial_width, initial_height, engine_properties); + s_stub_implementation->ViewControllerDestroy(); } - return nullptr; } -void FlutterDesktopDestroyViewController( +FlutterDesktopEngineRef FlutterDesktopViewControllerGetEngine( FlutterDesktopViewControllerRef controller) { - if (s_stub_implementation) { - s_stub_implementation->DestroyViewController(); - } + // The stub ignores this, so just return an arbitrary non-zero value. + return reinterpret_cast(1); } -FlutterDesktopViewRef FlutterDesktopGetView( +FlutterDesktopViewRef FlutterDesktopViewControllerGetView( FlutterDesktopViewControllerRef controller) { // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(1); } -uint64_t FlutterDesktopProcessMessages( - FlutterDesktopViewControllerRef controller) { +bool FlutterDesktopViewControllerHandleTopLevelWindowProc( + FlutterDesktopViewControllerRef controller, + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { if (s_stub_implementation) { - return s_stub_implementation->ProcessMessages(); + return s_stub_implementation->ViewControllerHandleTopLevelWindowProc( + hwnd, message, wparam, lparam, result); } - return 0; + return false; } -HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef controller) { +FlutterDesktopEngineRef FlutterDesktopEngineCreate( + const FlutterDesktopEngineProperties& engine_properties) { if (s_stub_implementation) { - return s_stub_implementation->ViewGetHWND(); + return s_stub_implementation->EngineCreate(engine_properties); } - return reinterpret_cast(-1); + return nullptr; } -FlutterDesktopEngineRef FlutterDesktopRunEngine( - const FlutterDesktopEngineProperties& engine_properties) { +bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine_ref) { if (s_stub_implementation) { - return s_stub_implementation->RunEngine(engine_properties); + return s_stub_implementation->EngineDestroy(); } - return nullptr; + return true; } -bool FlutterDesktopShutDownEngine(FlutterDesktopEngineRef engine_ref) { +bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, + const char* entry_point) { if (s_stub_implementation) { - return s_stub_implementation->ShutDownEngine(); + return s_stub_implementation->EngineRun(entry_point); } return true; } -FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( - FlutterDesktopViewControllerRef controller, +uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { + if (s_stub_implementation) { + return s_stub_implementation->EngineProcessMessages(); + } + return 0; +} + +FlutterDesktopPluginRegistrarRef FlutterDesktopEngineGetPluginRegistrar( + FlutterDesktopEngineRef engine, const char* plugin_name) { // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(1); } -FlutterDesktopViewRef FlutterDesktopRegistrarGetView( +FlutterDesktopMessengerRef FlutterDesktopEngineGetMessenger( + FlutterDesktopEngineRef engine) { + // The stub ignores this, so just return an arbitrary non-zero value. + return reinterpret_cast(2); +} + +HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef controller) { + if (s_stub_implementation) { + return s_stub_implementation->ViewGetHWND(); + } + return reinterpret_cast(-1); +} + +FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView( FlutterDesktopPluginRegistrarRef controller) { // The stub ignores this, so just return an arbitrary non-zero value. return reinterpret_cast(1); } + +void FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate, + void* user_data) { + if (s_stub_implementation) { + return s_stub_implementation + ->PluginRegistrarRegisterTopLevelWindowProcDelegate(delegate, + user_data); + } +} + +void FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate) { + if (s_stub_implementation) { + return s_stub_implementation + ->PluginRegistrarUnregisterTopLevelWindowProcDelegate(delegate); + } +} diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index 631d445802f28..532abff196776 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -29,31 +29,51 @@ class StubFlutterWindowsApi { virtual ~StubFlutterWindowsApi() {} - // Called for FlutterDesktopCreateViewController. - virtual FlutterDesktopViewControllerRef CreateViewController( - int width, - int height, + // Called for FlutterDesktopViewControllerCreate. + virtual FlutterDesktopViewControllerRef + ViewControllerCreate(int width, int height, FlutterDesktopEngineRef engine) { + return nullptr; + } + + // Called for FlutterDesktopViewControllerDestroy. + virtual void ViewControllerDestroy() {} + + // Called for FlutterDesktopViewControllerHandleTopLevelWindowProc. + virtual bool ViewControllerHandleTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { + return false; + } + + // Called for FlutterDesktopEngineCreate. + virtual FlutterDesktopEngineRef EngineCreate( const FlutterDesktopEngineProperties& engine_properties) { return nullptr; } - // Called for FlutterDesktopDestroyView. - virtual void DestroyViewController() {} + // Called for FlutterDesktopEngineDestroy. + virtual bool EngineDestroy() { return true; } + + // Called for FlutterDesktopEngineRun. + virtual bool EngineRun(const char* entry_point) { return true; } - // Called for FlutterDesktopProcessMessages. - virtual uint64_t ProcessMessages() { return 0; } + // Called for FlutterDesktopEngineProcessMessages. + virtual uint64_t EngineProcessMessages() { return 0; } // Called for FlutterDesktopViewGetHWND. virtual HWND ViewGetHWND() { return reinterpret_cast(1); } - // Called for FlutterDesktopRunEngine. - virtual FlutterDesktopEngineRef RunEngine( - const FlutterDesktopEngineProperties& engine_properties) { - return nullptr; - } + // Called for FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate. + virtual void PluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data) {} - // Called for FlutterDesktopShutDownEngine. - virtual bool ShutDownEngine() { return true; } + // Called for + // FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate. + virtual void PluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate) {} }; // A test helper that owns a stub implementation, making it the test stub for diff --git a/shell/platform/windows/cursor_handler.cc b/shell/platform/windows/cursor_handler.cc index cb696e5f63425..c740b4a1ee1c7 100644 --- a/shell/platform/windows/cursor_handler.cc +++ b/shell/platform/windows/cursor_handler.cc @@ -16,33 +16,32 @@ static constexpr char kKindKey[] = "kind"; namespace flutter { -CursorHandler::CursorHandler(flutter::BinaryMessenger* messenger, +CursorHandler::CursorHandler(BinaryMessenger* messenger, WindowBindingHandler* delegate) - : channel_(std::make_unique>( + : channel_(std::make_unique>( messenger, kChannelName, - &flutter::StandardMethodCodec::GetInstance())), + &StandardMethodCodec::GetInstance())), delegate_(delegate) { channel_->SetMethodCallHandler( - [this](const flutter::MethodCall& call, - std::unique_ptr> result) { + [this](const MethodCall& call, + std::unique_ptr> result) { HandleMethodCall(call, std::move(result)); }); } void CursorHandler::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { + const MethodCall& method_call, + std::unique_ptr> result) { const std::string& method = method_call.method_name(); if (method.compare(kActivateSystemCursorMethod) == 0) { - const flutter::EncodableMap& arguments = - method_call.arguments()->MapValue(); - auto kind_iter = arguments.find(EncodableValue(kKindKey)); + const auto& arguments = std::get(*method_call.arguments()); + auto kind_iter = arguments.find(EncodableValue(std::string(kKindKey))); if (kind_iter == arguments.end()) { result->Error("Argument error", "Missing argument while trying to activate system cursor"); } - const std::string& kind = kind_iter->second.StringValue(); + const auto& kind = std::get(kind_iter->second); delegate_->UpdateFlutterCursor(kind); result->Success(); } else { diff --git a/shell/platform/windows/flutter_project_bundle.cc b/shell/platform/windows/flutter_project_bundle.cc new file mode 100644 index 0000000000000..2459311524d40 --- /dev/null +++ b/shell/platform/windows/flutter_project_bundle.cc @@ -0,0 +1,81 @@ +// 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. + +#include "flutter/shell/platform/windows/flutter_project_bundle.h" + +#include +#include + +#include "flutter/shell/platform/common/cpp/path_utils.h" + +namespace flutter { + +FlutterProjectBundle::FlutterProjectBundle( + const FlutterDesktopEngineProperties& properties) + : assets_path_(properties.assets_path), + icu_path_(properties.icu_data_path) { + if (properties.aot_library_path != nullptr) { + aot_library_path_ = std::filesystem::path(properties.aot_library_path); + } + + // Resolve any relative paths. + if (assets_path_.is_relative() || icu_path_.is_relative() || + (!aot_library_path_.empty() && aot_library_path_.is_relative())) { + std::filesystem::path executable_location = GetExecutableDirectory(); + if (executable_location.empty()) { + std::cerr + << "Unable to find executable location to resolve resource paths." + << std::endl; + assets_path_ = std::filesystem::path(); + icu_path_ = std::filesystem::path(); + } else { + assets_path_ = std::filesystem::path(executable_location) / assets_path_; + icu_path_ = std::filesystem::path(executable_location) / icu_path_; + if (!aot_library_path_.empty()) { + aot_library_path_ = + std::filesystem::path(executable_location) / aot_library_path_; + } + } + } + + if (properties.switches_count > 0) { + switches_.insert(switches_.end(), &properties.switches[0], + &properties.switches[properties.switches_count]); + } +} + +bool FlutterProjectBundle::HasValidPaths() { + return !assets_path_.empty() && !icu_path_.empty(); +} + +// Attempts to load AOT data from the given path, which must be absolute and +// non-empty. Logs and returns nullptr on failure. +UniqueAotDataPtr FlutterProjectBundle::LoadAotData() { + if (aot_library_path_.empty()) { + std::cerr + << "Attempted to load AOT data, but no aot_library_path was provided." + << std::endl; + return nullptr; + } + if (!std::filesystem::exists(aot_library_path_)) { + std::cerr << "Can't load AOT data from " << aot_library_path_.u8string() + << "; no such file." << std::endl; + return nullptr; + } + std::string path_string = aot_library_path_.u8string(); + FlutterEngineAOTDataSource source = {}; + source.type = kFlutterEngineAOTDataSourceTypeElfPath; + source.elf_path = path_string.c_str(); + FlutterEngineAOTData data = nullptr; + auto result = FlutterEngineCreateAOTData(&source, &data); + if (result != kSuccess) { + std::cerr << "Failed to load AOT data from: " << path_string << std::endl; + return nullptr; + } + return UniqueAotDataPtr(data); +} + +FlutterProjectBundle::~FlutterProjectBundle() {} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_project_bundle.h b/shell/platform/windows/flutter_project_bundle.h new file mode 100644 index 0000000000000..f8ab57be1ee3c --- /dev/null +++ b/shell/platform/windows/flutter_project_bundle.h @@ -0,0 +1,65 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PROJECT_BUNDLE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PROJECT_BUNDLE_H_ + +#include +#include +#include + +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/public/flutter_windows.h" + +namespace flutter { + +struct AotDataDeleter { + void operator()(FlutterEngineAOTData aot_data) { + FlutterEngineCollectAOTData(aot_data); + } +}; +using UniqueAotDataPtr = std::unique_ptr<_FlutterEngineAOTData, AotDataDeleter>; + +// The data associated with a Flutter project needed to run it in an engine. +class FlutterProjectBundle { + public: + // Creates a new project bundle from the given properties. + // + // Treats any relative paths as relative to the directory of this executable. + explicit FlutterProjectBundle( + const FlutterDesktopEngineProperties& properties); + + ~FlutterProjectBundle(); + + // Whether or not the bundle is valid. This does not check that the paths + // exist, or contain valid data, just that paths were able to be constructed. + bool HasValidPaths(); + + // Returns the path to the assets directory. + const std::filesystem::path& assets_path() { return assets_path_; } + + // Returns the path to the ICU data file. + const std::filesystem::path& icu_path() { return icu_path_; } + + // Returns any switches that should be passed to the engine. + const std::vector& switches() { return switches_; } + + // Attempts to load AOT data for this bundle. The returned data must be + // retained until any engine instance it is passed to has been shut down. + // + // Logs and returns nullptr on failure. + UniqueAotDataPtr LoadAotData(); + + private: + std::filesystem::path assets_path_; + std::filesystem::path icu_path_; + std::vector switches_; + + // Path to the AOT library file, if any. + std::filesystem::path aot_library_path_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PROJECT_BUNDLE_H_ diff --git a/shell/platform/windows/flutter_windows.cc b/shell/platform/windows/flutter_windows.cc index 19bf9c97c77ec..1871021e40e9e 100644 --- a/shell/platform/windows/flutter_windows.cc +++ b/shell/platform/windows/flutter_windows.cc @@ -19,253 +19,160 @@ #include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" #include "flutter/shell/platform/common/cpp/path_utils.h" #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/flutter_project_bundle.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/win32_dpi_utils.h" #include "flutter/shell/platform/windows/win32_flutter_window.h" -#include "flutter/shell/platform/windows/win32_platform_handler.h" #include "flutter/shell/platform/windows/win32_task_runner.h" #include "flutter/shell/platform/windows/window_binding_handler.h" #include "flutter/shell/platform/windows/window_state.h" static_assert(FLUTTER_ENGINE_VERSION == 1, ""); -// Attempts to load AOT data from the given path, which must be absolute and -// non-empty. Logs and returns nullptr on failure. -UniqueAotDataPtr LoadAotData(std::filesystem::path aot_data_path) { - if (aot_data_path.empty()) { - std::cerr - << "Attempted to load AOT data, but no aot_library_path was provided." - << std::endl; - return nullptr; - } - if (!std::filesystem::exists(aot_data_path)) { - std::cerr << "Can't load AOT data from " << aot_data_path.u8string() - << "; no such file." << std::endl; - return nullptr; - } - std::string path_string = aot_data_path.u8string(); - FlutterEngineAOTDataSource source = {}; - source.type = kFlutterEngineAOTDataSourceTypeElfPath; - source.elf_path = path_string.c_str(); - FlutterEngineAOTData data = nullptr; - auto result = FlutterEngineCreateAOTData(&source, &data); - if (result != kSuccess) { - std::cerr << "Failed to load AOT data from: " << path_string << std::endl; - return nullptr; - } - return UniqueAotDataPtr(data); +// Returns the engine corresponding to the given opaque API handle. +static flutter::FlutterWindowsEngine* EngineFromHandle( + FlutterDesktopEngineRef ref) { + return reinterpret_cast(ref); } -// Spins up an instance of the Flutter Engine. -// -// This function launches the Flutter Engine in a background thread, supplying -// the necessary callbacks for rendering within a win32window (if one is -// provided). -// -// Returns the state object for the engine, or null on failure to start the -// engine. -static std::unique_ptr RunFlutterEngine( - flutter::FlutterWindowsView* view, - const FlutterDesktopEngineProperties& engine_properties) { - auto state = std::make_unique(); - - // FlutterProjectArgs is expecting a full argv, so when processing it for - // flags the first item is treated as the executable and ignored. Add a dummy - // value so that all provided arguments are used. - std::vector argv = {"placeholder"}; - if (engine_properties.switches_count > 0) { - argv.insert(argv.end(), &engine_properties.switches[0], - &engine_properties.switches[engine_properties.switches_count]); - } +// Returns the opaque API handle for the given engine instance. +static FlutterDesktopEngineRef HandleForEngine( + flutter::FlutterWindowsEngine* engine) { + return reinterpret_cast(engine); +} - view->CreateRenderSurface(); +// Returns the view corresponding to the given opaque API handle. +static flutter::FlutterWindowsView* ViewFromHandle(FlutterDesktopViewRef ref) { + return reinterpret_cast(ref); +} - // Provide the necessary callbacks for rendering within a win32 child window. - FlutterRendererConfig config = {}; - config.type = kOpenGL; - config.open_gl.struct_size = sizeof(config.open_gl); - config.open_gl.make_current = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->MakeCurrent(); - }; - config.open_gl.clear_current = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->ClearContext(); - }; - config.open_gl.present = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->SwapBuffers(); - }; - config.open_gl.fbo_callback = [](void* user_data) -> uint32_t { return 0; }; - config.open_gl.gl_proc_resolver = [](void* user_data, - const char* what) -> void* { - return reinterpret_cast(eglGetProcAddress(what)); - }; - config.open_gl.make_resource_current = [](void* user_data) -> bool { - auto host = static_cast(user_data); - return host->MakeResourceCurrent(); - }; +// Returns the opaque API handle for the given view instance. +static FlutterDesktopViewRef HandleForView(flutter::FlutterWindowsView* view) { + return reinterpret_cast(view); +} - // Configure task runner interop. - auto state_ptr = state.get(); - state->task_runner = std::make_unique( - GetCurrentThreadId(), [state_ptr](const auto* task) { - if (FlutterEngineRunTask(state_ptr->engine, task) != kSuccess) { - std::cerr << "Could not post an engine task." << std::endl; - } - }); - FlutterTaskRunnerDescription platform_task_runner = {}; - platform_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription); - platform_task_runner.user_data = state->task_runner.get(); - platform_task_runner.runs_task_on_current_thread_callback = - [](void* user_data) -> bool { - return reinterpret_cast(user_data) - ->RunsTasksOnCurrentThread(); - }; - platform_task_runner.post_task_callback = [](FlutterTask task, - uint64_t target_time_nanos, - void* user_data) -> void { - reinterpret_cast(user_data)->PostTask( - task, target_time_nanos); - }; +FlutterDesktopViewControllerRef FlutterDesktopViewControllerCreate( + int width, + int height, + FlutterDesktopEngineRef engine) { + std::unique_ptr window_wrapper = + std::make_unique(width, height); - FlutterCustomTaskRunners custom_task_runners = {}; - custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); - custom_task_runners.platform_task_runner = &platform_task_runner; - - std::filesystem::path assets_path(engine_properties.assets_path); - std::filesystem::path icu_path(engine_properties.icu_data_path); - std::filesystem::path aot_library_path = - engine_properties.aot_library_path == nullptr - ? std::filesystem::path() - : std::filesystem::path(engine_properties.aot_library_path); - if (assets_path.is_relative() || icu_path.is_relative() || - (!aot_library_path.empty() && aot_library_path.is_relative())) { - // Treat relative paths as relative to the directory of this executable. - std::filesystem::path executable_location = - flutter::GetExecutableDirectory(); - if (executable_location.empty()) { - std::cerr - << "Unable to find executable location to resolve resource paths." - << std::endl; - return nullptr; - } - assets_path = std::filesystem::path(executable_location) / assets_path; - icu_path = std::filesystem::path(executable_location) / icu_path; - if (!aot_library_path.empty()) { - aot_library_path = - std::filesystem::path(executable_location) / aot_library_path; - } - } - std::string assets_path_string = assets_path.u8string(); - std::string icu_path_string = icu_path.u8string(); + auto state = std::make_unique(); + state->view = + std::make_unique(std::move(window_wrapper)); + state->view->CreateRenderSurface(); - if (FlutterEngineRunsAOTCompiledDartCode()) { - state->aot_data = LoadAotData(aot_library_path); - if (!state->aot_data) { - std::cerr << "Unable to start engine without AOT data." << std::endl; + // Take ownership of the engine, starting it if necessary. + state->view->SetEngine( + std::unique_ptr(EngineFromHandle(engine))); + if (!state->view->GetEngine()->running()) { + if (!state->view->GetEngine()->RunWithEntrypoint(nullptr)) { return nullptr; } } - FlutterProjectArgs args = {}; - args.struct_size = sizeof(FlutterProjectArgs); - args.assets_path = assets_path_string.c_str(); - args.icu_data_path = icu_path_string.c_str(); - args.command_line_argc = static_cast(argv.size()); - args.command_line_argv = &argv[0]; - args.platform_message_callback = - [](const FlutterPlatformMessage* engine_message, - void* user_data) -> void { - auto window = reinterpret_cast(user_data); - return window->HandlePlatformMessage(engine_message); - }; - args.custom_task_runners = &custom_task_runners; - if (state->aot_data) { - args.aot_data = state->aot_data.get(); - } - - FLUTTER_API_SYMBOL(FlutterEngine) engine = nullptr; - auto result = - FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, view, &engine); - if (result != kSuccess || engine == nullptr) { - std::cerr << "Failed to start Flutter engine: error " << result - << std::endl; - return nullptr; - } - state->engine = engine; - return state; + // Must happen after engine is running. + state->view->SendInitialBounds(); + return state.release(); } -FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( - int width, - int height, - const FlutterDesktopEngineProperties& engine_properties) { - std::unique_ptr window_wrapper = - std::make_unique(width, height); +void FlutterDesktopViewControllerDestroy( + FlutterDesktopViewControllerRef controller) { + delete controller; +} - FlutterDesktopViewControllerRef state = - flutter::FlutterWindowsView::CreateFlutterWindowsView( - std::move(window_wrapper)); +FlutterDesktopEngineRef FlutterDesktopViewControllerGetEngine( + FlutterDesktopViewControllerRef controller) { + return HandleForEngine(controller->view->GetEngine()); +} - auto engine_state = RunFlutterEngine(state->view.get(), engine_properties); +FlutterDesktopViewRef FlutterDesktopViewControllerGetView( + FlutterDesktopViewControllerRef controller) { + return HandleForView(controller->view.get()); +} - if (!engine_state) { - return nullptr; +bool FlutterDesktopViewControllerHandleTopLevelWindowProc( + FlutterDesktopViewControllerRef controller, + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { + std::optional delegate_result = + controller->view->GetEngine() + ->window_proc_delegate_manager() + ->OnTopLevelWindowProc(hwnd, message, wparam, lparam); + if (delegate_result) { + *result = *delegate_result; } - state->view->SetState(engine_state->engine); - state->engine_state = std::move(engine_state); - return state; + return delegate_result.has_value(); } -FlutterDesktopViewControllerRef FlutterDesktopCreateViewControllerLegacy( - int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { - std::filesystem::path assets_path_fs = std::filesystem::u8path(assets_path); - std::filesystem::path icu_data_path_fs = - std::filesystem::u8path(icu_data_path); - FlutterDesktopEngineProperties engine_properties = {}; - engine_properties.assets_path = assets_path_fs.c_str(); - engine_properties.icu_data_path = icu_data_path_fs.c_str(); - engine_properties.switches = arguments; - engine_properties.switches_count = argument_count; - - return FlutterDesktopCreateViewController(initial_width, initial_height, - engine_properties); +FlutterDesktopEngineRef FlutterDesktopEngineCreate( + const FlutterDesktopEngineProperties& engine_properties) { + flutter::FlutterProjectBundle project(engine_properties); + auto engine = std::make_unique(project); + return HandleForEngine(engine.release()); } -uint64_t FlutterDesktopProcessMessages( - FlutterDesktopViewControllerRef controller) { - return controller->engine_state->task_runner->ProcessTasks().count(); +bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine_ref) { + flutter::FlutterWindowsEngine* engine = EngineFromHandle(engine_ref); + bool result = true; + if (engine->running()) { + result = engine->Stop(); + } + delete engine; + return result; } -void FlutterDesktopDestroyViewController( - FlutterDesktopViewControllerRef controller) { - FlutterEngineShutdown(controller->engine_state->engine); - delete controller; +bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, + const char* entry_point) { + return EngineFromHandle(engine)->RunWithEntrypoint(entry_point); } -FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( - FlutterDesktopViewControllerRef controller, +uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { + return EngineFromHandle(engine)->task_runner()->ProcessTasks().count(); +} + +FlutterDesktopPluginRegistrarRef FlutterDesktopEngineGetPluginRegistrar( + FlutterDesktopEngineRef engine, const char* plugin_name) { // Currently, one registrar acts as the registrar for all plugins, so the // name is ignored. It is part of the API to reduce churn in the future when // aligning more closely with the Flutter registrar system. - return controller->view->GetRegistrar(); + return EngineFromHandle(engine)->GetRegistrar(); } -FlutterDesktopViewRef FlutterDesktopGetView( - FlutterDesktopViewControllerRef controller) { - return controller->view_wrapper.get(); +FlutterDesktopMessengerRef FlutterDesktopEngineGetMessenger( + FlutterDesktopEngineRef engine) { + return EngineFromHandle(engine)->messenger(); +} + +HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view) { + return std::get(*ViewFromHandle(view)->GetRenderTarget()); } -HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view_ref) { - return std::get(*view_ref->view->GetRenderTarget()); +FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView( + FlutterDesktopPluginRegistrarRef registrar) { + return HandleForView(registrar->engine->view()); +} + +void FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate, + void* user_data) { + registrar->engine->window_proc_delegate_manager() + ->RegisterTopLevelWindowProcDelegate(delegate, user_data); +} + +void FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate) { + registrar->engine->window_proc_delegate_manager() + ->UnregisterTopLevelWindowProcDelegate(delegate); } UINT FlutterDesktopGetDpiForHWND(HWND hwnd) { @@ -287,39 +194,17 @@ void FlutterDesktopResyncOutputStreams() { std::ios::sync_with_stdio(); } -FlutterDesktopEngineRef FlutterDesktopRunEngine( - const FlutterDesktopEngineProperties& engine_properties) { - auto engine = RunFlutterEngine(nullptr, engine_properties); - return engine.release(); -} - -bool FlutterDesktopShutDownEngine(FlutterDesktopEngineRef engine_ref) { - std::cout << "Shutting down flutter engine process." << std::endl; - auto result = FlutterEngineShutdown(engine_ref->engine); - delete engine_ref; - return (result == kSuccess); -} - -void FlutterDesktopRegistrarEnableInputBlocking( - FlutterDesktopPluginRegistrarRef registrar, - const char* channel) { - registrar->messenger->dispatcher->EnableInputBlockingForChannel(channel); -} +// Implementations of common/cpp/ API methods. FlutterDesktopMessengerRef FlutterDesktopRegistrarGetMessenger( FlutterDesktopPluginRegistrarRef registrar) { - return registrar->messenger.get(); + return registrar->engine->messenger(); } void FlutterDesktopRegistrarSetDestructionHandler( FlutterDesktopPluginRegistrarRef registrar, FlutterDesktopOnRegistrarDestroyed callback) { - registrar->destruction_handler = callback; -} - -FlutterDesktopViewRef FlutterDesktopRegistrarGetView( - FlutterDesktopPluginRegistrarRef registrar) { - return registrar->view; + registrar->engine->SetPluginRegistrarDestructionCallback(callback); } bool FlutterDesktopMessengerSendWithReply(FlutterDesktopMessengerRef messenger, @@ -331,7 +216,7 @@ bool FlutterDesktopMessengerSendWithReply(FlutterDesktopMessengerRef messenger, FlutterPlatformMessageResponseHandle* response_handle = nullptr; if (reply != nullptr && user_data != nullptr) { FlutterEngineResult result = FlutterPlatformMessageCreateResponseHandle( - messenger->engine, reply, user_data, &response_handle); + messenger->engine->engine(), reply, user_data, &response_handle); if (result != kSuccess) { std::cout << "Failed to create response handle\n"; return false; @@ -346,11 +231,11 @@ bool FlutterDesktopMessengerSendWithReply(FlutterDesktopMessengerRef messenger, response_handle, }; - FlutterEngineResult message_result = - FlutterEngineSendPlatformMessage(messenger->engine, &platform_message); + FlutterEngineResult message_result = FlutterEngineSendPlatformMessage( + messenger->engine->engine(), &platform_message); if (response_handle != nullptr) { - FlutterPlatformMessageReleaseResponseHandle(messenger->engine, + FlutterPlatformMessageReleaseResponseHandle(messenger->engine->engine(), response_handle); } @@ -370,13 +255,14 @@ void FlutterDesktopMessengerSendResponse( const FlutterDesktopMessageResponseHandle* handle, const uint8_t* data, size_t data_length) { - FlutterEngineSendPlatformMessageResponse(messenger->engine, handle, data, - data_length); + FlutterEngineSendPlatformMessageResponse(messenger->engine->engine(), handle, + data, data_length); } void FlutterDesktopMessengerSetCallback(FlutterDesktopMessengerRef messenger, const char* channel, FlutterDesktopMessageCallback callback, void* user_data) { - messenger->dispatcher->SetMessageCallback(channel, callback, user_data); + messenger->engine->message_dispatcher()->SetMessageCallback(channel, callback, + user_data); } diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc new file mode 100644 index 0000000000000..76af353ab0b52 --- /dev/null +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -0,0 +1,259 @@ +// 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. + +#include "flutter/shell/platform/windows/flutter_windows_engine.h" + +#include +#include +#include + +#include "flutter/shell/platform/common/cpp/path_utils.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" +#include "flutter/shell/platform/windows/string_conversion.h" +#include "flutter/shell/platform/windows/system_utils.h" + +namespace flutter { + +namespace { + +// Creates and returns a FlutterRendererConfig that renders to the view (if any) +// of a FlutterWindowsEngine, which should be the user_data received by the +// render callbacks. +FlutterRendererConfig GetRendererConfig() { + FlutterRendererConfig config = {}; + config.type = kOpenGL; + config.open_gl.struct_size = sizeof(config.open_gl); + config.open_gl.make_current = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->MakeCurrent(); + }; + config.open_gl.clear_current = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->ClearContext(); + }; + config.open_gl.present = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->SwapBuffers(); + }; + config.open_gl.fbo_callback = [](void* user_data) -> uint32_t { return 0; }; + config.open_gl.gl_proc_resolver = [](void* user_data, + const char* what) -> void* { + return reinterpret_cast(eglGetProcAddress(what)); + }; + config.open_gl.make_resource_current = [](void* user_data) -> bool { + auto host = static_cast(user_data); + if (!host->view()) { + return false; + } + return host->view()->MakeResourceCurrent(); + }; + return config; +} + +// Converts a FlutterPlatformMessage to an equivalent FlutterDesktopMessage. +static FlutterDesktopMessage ConvertToDesktopMessage( + const FlutterPlatformMessage& engine_message) { + FlutterDesktopMessage message = {}; + message.struct_size = sizeof(message); + message.channel = engine_message.channel; + message.message = engine_message.message; + message.message_size = engine_message.message_size; + message.response_handle = engine_message.response_handle; + return message; +} + +// Converts a LanguageInfo struct to a FlutterLocale struct. |info| must outlive +// the returned value, since the returned FlutterLocale has pointers into it. +FlutterLocale CovertToFlutterLocale(const LanguageInfo& info) { + FlutterLocale locale = {}; + locale.struct_size = sizeof(FlutterLocale); + locale.language_code = info.language.c_str(); + if (!info.region.empty()) { + locale.country_code = info.region.c_str(); + } + if (!info.script.empty()) { + locale.script_code = info.script.c_str(); + } + return locale; +} + +} // namespace + +FlutterWindowsEngine::FlutterWindowsEngine(const FlutterProjectBundle& project) + : project_(std::make_unique(project)) { + task_runner_ = std::make_unique( + GetCurrentThreadId(), [this](const auto* task) { + if (!engine_) { + std::cerr << "Cannot post an engine task when engine is not running." + << std::endl; + return; + } + if (FlutterEngineRunTask(engine_, task) != kSuccess) { + std::cerr << "Failed to post an engine task." << std::endl; + } + }); + + // Set up the legacy structs backing the API handles. + messenger_ = std::make_unique(); + messenger_->engine = this; + plugin_registrar_ = std::make_unique(); + plugin_registrar_->engine = this; + + message_dispatcher_ = + std::make_unique(messenger_.get()); + window_proc_delegate_manager_ = + std::make_unique(); +} + +FlutterWindowsEngine::~FlutterWindowsEngine() { + Stop(); +} + +bool FlutterWindowsEngine::RunWithEntrypoint(const char* entrypoint) { + if (!project_->HasValidPaths()) { + std::cerr << "Missing or unresolvable paths to assets." << std::endl; + return false; + } + std::string assets_path_string = project_->assets_path().u8string(); + std::string icu_path_string = project_->icu_path().u8string(); + if (FlutterEngineRunsAOTCompiledDartCode()) { + aot_data_ = project_->LoadAotData(); + if (!aot_data_) { + std::cerr << "Unable to start engine without AOT data." << std::endl; + return false; + } + } + + // FlutterProjectArgs is expecting a full argv, so when processing it for + // flags the first item is treated as the executable and ignored. Add a dummy + // value so that all provided arguments are used. + std::vector argv = {"placeholder"}; + std::transform( + project_->switches().begin(), project_->switches().end(), + std::back_inserter(argv), + [](const std::string& arg) -> const char* { return arg.c_str(); }); + + // Configure task runners. + FlutterTaskRunnerDescription platform_task_runner = {}; + platform_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription); + platform_task_runner.user_data = task_runner_.get(); + platform_task_runner.runs_task_on_current_thread_callback = + [](void* user_data) -> bool { + return static_cast(user_data)->RunsTasksOnCurrentThread(); + }; + platform_task_runner.post_task_callback = [](FlutterTask task, + uint64_t target_time_nanos, + void* user_data) -> void { + static_cast(user_data)->PostTask(task, target_time_nanos); + }; + FlutterCustomTaskRunners custom_task_runners = {}; + custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); + custom_task_runners.platform_task_runner = &platform_task_runner; + + FlutterProjectArgs args = {}; + args.struct_size = sizeof(FlutterProjectArgs); + args.assets_path = assets_path_string.c_str(); + args.icu_data_path = icu_path_string.c_str(); + args.command_line_argc = static_cast(argv.size()); + args.command_line_argv = argv.size() > 0 ? argv.data() : nullptr; + args.platform_message_callback = + [](const FlutterPlatformMessage* engine_message, + void* user_data) -> void { + auto host = static_cast(user_data); + return host->HandlePlatformMessage(engine_message); + }; + args.custom_task_runners = &custom_task_runners; + if (aot_data_) { + args.aot_data = aot_data_.get(); + } + if (entrypoint) { + args.custom_dart_entrypoint = entrypoint; + } + + FlutterRendererConfig renderer_config = GetRendererConfig(); + + auto result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &renderer_config, + &args, this, &engine_); + if (result != kSuccess || engine_ == nullptr) { + std::cerr << "Failed to start Flutter engine: error " << result + << std::endl; + return false; + } + + SendSystemSettings(); + + return true; +} + +bool FlutterWindowsEngine::Stop() { + if (engine_) { + if (plugin_registrar_destruction_callback_) { + plugin_registrar_destruction_callback_(plugin_registrar_.get()); + } + FlutterEngineResult result = FlutterEngineShutdown(engine_); + engine_ = nullptr; + return (result == kSuccess); + } + return false; +} + +void FlutterWindowsEngine::SetView(FlutterWindowsView* view) { + view_ = view; +} + +// Returns the currently configured Plugin Registrar. +FlutterDesktopPluginRegistrarRef FlutterWindowsEngine::GetRegistrar() { + return plugin_registrar_.get(); +} + +void FlutterWindowsEngine::SetPluginRegistrarDestructionCallback( + FlutterDesktopOnRegistrarDestroyed callback) { + plugin_registrar_destruction_callback_ = callback; +} + +void FlutterWindowsEngine::HandlePlatformMessage( + const FlutterPlatformMessage* engine_message) { + if (engine_message->struct_size != sizeof(FlutterPlatformMessage)) { + std::cerr << "Invalid message size received. Expected: " + << sizeof(FlutterPlatformMessage) << " but received " + << engine_message->struct_size << std::endl; + return; + } + + auto message = ConvertToDesktopMessage(*engine_message); + + message_dispatcher_->HandleMessage( + message, [this] {}, [this] {}); +} + +void FlutterWindowsEngine::SendSystemSettings() { + std::vector languages = GetPreferredLanguageInfo(); + std::vector flutter_locales; + flutter_locales.reserve(languages.size()); + for (const auto& info : languages) { + flutter_locales.push_back(CovertToFlutterLocale(info)); + } + // Convert the locale list to the locale pointer list that must be provided. + std::vector flutter_locale_list; + flutter_locale_list.reserve(flutter_locales.size()); + std::transform( + flutter_locales.begin(), flutter_locales.end(), + std::back_inserter(flutter_locale_list), + [](const auto& arg) -> const auto* { return &arg; }); + FlutterEngineUpdateLocales(engine_, flutter_locale_list.data(), + flutter_locale_list.size()); + + // TODO: Send 'flutter/settings' channel settings here as well. +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h new file mode 100644 index 0000000000000..d18a414ee5838 --- /dev/null +++ b/shell/platform/windows/flutter_windows_engine.h @@ -0,0 +1,126 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_ENGINE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_ENGINE_H_ + +#include +#include +#include +#include + +#include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" +#include "flutter/shell/platform/windows/flutter_project_bundle.h" +#include "flutter/shell/platform/windows/public/flutter_windows.h" +#include "flutter/shell/platform/windows/win32_task_runner.h" +#include "flutter/shell/platform/windows/win32_window_proc_delegate_manager.h" +#include "flutter/shell/platform/windows/window_state.h" + +namespace flutter { + +class FlutterWindowsView; + +// Manages state associated with the underlying FlutterEngine that isn't +// related to its display. +// +// In most cases this will be associated with a FlutterView, but if not will +// run in headless mode. +class FlutterWindowsEngine { + public: + // Creates a new Flutter engine object configured to run |project|. + explicit FlutterWindowsEngine(const FlutterProjectBundle& project); + + virtual ~FlutterWindowsEngine(); + + // Prevent copying. + FlutterWindowsEngine(FlutterWindowsEngine const&) = delete; + FlutterWindowsEngine& operator=(FlutterWindowsEngine const&) = delete; + + // Starts running the engine with the given entrypoint. If null, defaults to + // main(). + // + // Returns false if the engine couldn't be started. + bool RunWithEntrypoint(const char* entrypoint); + + // Returns true if the engine is currently running. + bool running() { return engine_ != nullptr; } + + // Stops the engine. This invalidates the pointer returned by engine(). + // + // Returns false if stopping the engine fails, or if it was not running. + bool Stop(); + + // Sets the view that is displaying this engine's content. + void SetView(FlutterWindowsView* view); + + // The view displaying this engine's content, if any. This will be null for + // headless engines. + FlutterWindowsView* view() { return view_; } + + // Returns the currently configured Plugin Registrar. + FlutterDesktopPluginRegistrarRef GetRegistrar(); + + // Sets |callback| to be called when the plugin registrar is destroyed. + void SetPluginRegistrarDestructionCallback( + FlutterDesktopOnRegistrarDestroyed callback); + + FLUTTER_API_SYMBOL(FlutterEngine) engine() { return engine_; } + + FlutterDesktopMessengerRef messenger() { return messenger_.get(); } + + IncomingMessageDispatcher* message_dispatcher() { + return message_dispatcher_.get(); + } + + Win32TaskRunner* task_runner() { return task_runner_.get(); } + + Win32WindowProcDelegateManager* window_proc_delegate_manager() { + return window_proc_delegate_manager_.get(); + } + + // Callback passed to Flutter engine for notifying window of platform + // messages. + void HandlePlatformMessage(const FlutterPlatformMessage*); + + private: + // Sends system settings (e.g., locale) to the engine. + // + // Should be called just after the engine is run, and after any relevant + // system changes. + void SendSystemSettings(); + + // The handle to the embedder.h engine instance. + FLUTTER_API_SYMBOL(FlutterEngine) engine_ = nullptr; + + std::unique_ptr project_; + + // AOT data, if any. + UniqueAotDataPtr aot_data_; + + // The view displaying the content running in this engine, if any. + FlutterWindowsView* view_ = nullptr; + + // Task runner for tasks posted from the engine. + std::unique_ptr task_runner_; + + // The plugin messenger handle given to API clients. + std::unique_ptr messenger_; + + // Message dispatch manager for messages from engine_. + std::unique_ptr message_dispatcher_; + + // The plugin registrar handle given to API clients. + std::unique_ptr plugin_registrar_; + + // A callback to be called when the engine (and thus the plugin registrar) + // is being destroyed. + FlutterDesktopOnRegistrarDestroyed plugin_registrar_destruction_callback_; + + // The manager for WindowProc delegate registration and callbacks. + std::unique_ptr window_proc_delegate_manager_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_ENGINE_H_ diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index db650a4e0ae7d..5736ce988f53e 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -1,63 +1,39 @@ +// 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. + #include "flutter/shell/platform/windows/flutter_windows_view.h" #include namespace flutter { -FlutterWindowsView::FlutterWindowsView() { +FlutterWindowsView::FlutterWindowsView( + std::unique_ptr window_binding) { surface_manager_ = std::make_unique(); + + // Take the binding handler, and give it a pointer back to self. + binding_handler_ = std::move(window_binding); + binding_handler_->SetView(this); + + render_target_ = std::make_unique( + binding_handler_->GetRenderTarget()); } FlutterWindowsView::~FlutterWindowsView() { DestroyRenderSurface(); - if (plugin_registrar_ && plugin_registrar_->destruction_handler) { - plugin_registrar_->destruction_handler(plugin_registrar_.get()); - } -} - -FlutterDesktopViewControllerRef FlutterWindowsView::CreateFlutterWindowsView( - std::unique_ptr windowbinding) { - auto state = std::make_unique(); - state->view = std::make_unique(); - - // FlutterWindowsView instance owns windowbinding - state->view->binding_handler_ = std::move(windowbinding); - - // a window wrapper for the state block, distinct from the - // window_wrapper handed to plugin_registrar. - state->view_wrapper = std::make_unique(); - - // Give the binding handler a pointer back to this | FlutterWindowsView | - state->view->binding_handler_->SetView(state->view.get()); - - // opaque pointer to FlutterWindowsView - state->view_wrapper->view = state->view.get(); - - state->view->render_target_ = std::make_unique( - state->view->binding_handler_->GetRenderTarget()); - - return state.release(); } -void FlutterWindowsView::SetState(FLUTTER_API_SYMBOL(FlutterEngine) eng) { - engine_ = eng; +void FlutterWindowsView::SetEngine( + std::unique_ptr engine) { + engine_ = std::move(engine); - auto messenger = std::make_unique(); - message_dispatcher_ = - std::make_unique(messenger.get()); - messenger->engine = engine_; - messenger->dispatcher = message_dispatcher_.get(); - - window_wrapper_ = std::make_unique(); - window_wrapper_->view = this; - plugin_registrar_ = std::make_unique(); - plugin_registrar_->messenger = std::move(messenger); - plugin_registrar_->view = window_wrapper_.get(); + engine_->SetView(this); internal_plugin_registrar_ = - std::make_unique(plugin_registrar_.get()); + std::make_unique(engine_->GetRegistrar()); - // Set up the keyboard handlers. + // Set up the system channel handlers. auto internal_plugin_messenger = internal_plugin_registrar_->messenger(); keyboard_hook_handlers_.push_back( std::make_unique(internal_plugin_messenger)); @@ -74,41 +50,9 @@ void FlutterWindowsView::SetState(FLUTTER_API_SYMBOL(FlutterEngine) eng) { binding_handler_->GetDpiScale()); } -FlutterDesktopPluginRegistrarRef FlutterWindowsView::GetRegistrar() { - return plugin_registrar_.get(); -} - -// Converts a FlutterPlatformMessage to an equivalent FlutterDesktopMessage. -static FlutterDesktopMessage ConvertToDesktopMessage( - const FlutterPlatformMessage& engine_message) { - FlutterDesktopMessage message = {}; - message.struct_size = sizeof(message); - message.channel = engine_message.channel; - message.message = engine_message.message; - message.message_size = engine_message.message_size; - message.response_handle = engine_message.response_handle; - return message; -} - -// The Flutter Engine calls out to this function when new platform messages -// are available. -void FlutterWindowsView::HandlePlatformMessage( - const FlutterPlatformMessage* engine_message) { - if (engine_message->struct_size != sizeof(FlutterPlatformMessage)) { - std::cerr << "Invalid message size received. Expected: " - << sizeof(FlutterPlatformMessage) << " but received " - << engine_message->struct_size << std::endl; - return; - } - - auto message = ConvertToDesktopMessage(*engine_message); - - message_dispatcher_->HandleMessage( - message, [this] {}, [this] {}); -} - void FlutterWindowsView::OnWindowSizeChanged(size_t width, size_t height) const { + surface_manager_->ResizeSurface(GetRenderTarget(), width, height); SendWindowMetrics(width, height, binding_handler_->GetDpiScale()); } @@ -162,17 +106,17 @@ void FlutterWindowsView::OnScroll(double x, } void FlutterWindowsView::OnFontChange() { - if (engine_ == nullptr) { + if (engine_->engine() == nullptr) { return; } - FlutterEngineReloadSystemFonts(engine_); + FlutterEngineReloadSystemFonts(engine_->engine()); } // Sends new size information to FlutterEngine. void FlutterWindowsView::SendWindowMetrics(size_t width, size_t height, double dpiScale) const { - if (engine_ == nullptr) { + if (engine_->engine() == nullptr) { return; } @@ -181,7 +125,14 @@ void FlutterWindowsView::SendWindowMetrics(size_t width, event.width = width; event.height = height; event.pixel_ratio = dpiScale; - auto result = FlutterEngineSendWindowMetricsEvent(engine_, &event); + auto result = FlutterEngineSendWindowMetricsEvent(engine_->engine(), &event); +} + +void FlutterWindowsView::SendInitialBounds() { + PhysicalWindowBounds bounds = binding_handler_->GetPhysicalWindowBounds(); + + SendWindowMetrics(bounds.width, bounds.height, + binding_handler_->GetDpiScale()); } // Set's |event_data|'s phase to either kMove or kHover depending on the current @@ -293,7 +244,7 @@ void FlutterWindowsView::SendPointerEventWithData( std::chrono::high_resolution_clock::now().time_since_epoch()) .count(); - FlutterEngineSendPointerEvent(engine_, &event, 1); + FlutterEngineSendPointerEvent(engine_->engine(), &event, 1); if (event_data.phase == FlutterPointerPhase::kAdd) { SetMouseFlutterStateAdded(true); @@ -320,7 +271,9 @@ bool FlutterWindowsView::SwapBuffers() { } void FlutterWindowsView::CreateRenderSurface() { - surface_manager_->CreateSurface(render_target_.get()); + PhysicalWindowBounds bounds = binding_handler_->GetPhysicalWindowBounds(); + surface_manager_->CreateSurface(GetRenderTarget(), bounds.width, + bounds.height); } void FlutterWindowsView::DestroyRenderSurface() { @@ -329,8 +282,12 @@ void FlutterWindowsView::DestroyRenderSurface() { } } -WindowsRenderTarget* FlutterWindowsView::GetRenderTarget() { +WindowsRenderTarget* FlutterWindowsView::GetRenderTarget() const { return render_target_.get(); } +FlutterWindowsEngine* FlutterWindowsView::GetEngine() { + return engine_.get(); +} + } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index f19e9f97ac02a..5df86e476299d 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -7,14 +7,15 @@ #include +#include #include #include #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h" -#include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/windows/angle_surface_manager.h" #include "flutter/shell/platform/windows/cursor_handler.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/key_event_handler.h" #include "flutter/shell/platform/windows/keyboard_hook_handler.h" #include "flutter/shell/platform/windows/public/flutter_windows.h" @@ -30,26 +31,18 @@ namespace flutter { // view that works with win32 hwnds and Windows::UI::Composition visuals. class FlutterWindowsView : public WindowBindingHandlerDelegate { public: - FlutterWindowsView(); + // Creates a FlutterWindowsView with the given implementator of + // WindowBindingHandler. + // + // In order for object to render Flutter content the SetEngine method must be + // called with a valid FlutterWindowsEngine instance. + FlutterWindowsView(std::unique_ptr window_binding); ~FlutterWindowsView(); - // Factory for creating FlutterWindowsView requiring an implementator of - // WindowBindingHandler. In order for object to render Flutter content - // the SetState method must be called with a valid FlutterEngine instance. - static FlutterDesktopViewControllerRef CreateFlutterWindowsView( - std::unique_ptr window_binding); - // Configures the window instance with an instance of a running Flutter // engine. - void SetState(FLUTTER_API_SYMBOL(FlutterEngine) state); - - // Returns the currently configured Plugin Registrar. - FlutterDesktopPluginRegistrarRef GetRegistrar(); - - // Callback passed to Flutter engine for notifying window of platform - // messages. - void HandlePlatformMessage(const FlutterPlatformMessage*); + void SetEngine(std::unique_ptr engine); // Creates rendering surface for Flutter engine to draw into. // Should be called before calling FlutterEngineRun using this view. @@ -59,7 +52,10 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { void DestroyRenderSurface(); // Return the currently configured WindowsRenderTarget. - WindowsRenderTarget* GetRenderTarget(); + WindowsRenderTarget* GetRenderTarget() const; + + // Returns the engine backing this view. + FlutterWindowsEngine* GetEngine(); // Callbacks for clearing context, settings context and swapping buffers. bool ClearContext(); @@ -67,6 +63,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { bool MakeResourceCurrent(); bool SwapBuffers(); + // Send initial bounds to embedder. Must occur after engine has initialized. + void SendInitialBounds(); + // |WindowBindingHandlerDelegate| void OnWindowSizeChanged(size_t width, size_t height) const override; @@ -189,21 +188,12 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { // surfaces. Surface creation functionality requires a valid render_target. std::unique_ptr surface_manager_; - // The handle to the Flutter engine instance. - FLUTTER_API_SYMBOL(FlutterEngine) engine_ = nullptr; + // The engine associated with this view. + std::unique_ptr engine_; // Keeps track of mouse state in relation to the window. MouseState mouse_state_; - // The window handle given to API clients. - std::unique_ptr window_wrapper_; - - // The plugin registrar handle given to API clients. - std::unique_ptr plugin_registrar_; - - // Message dispatch manager for messages from the Flutter engine. - std::unique_ptr message_dispatcher_; - // The plugin registrar managing internal plugins. std::unique_ptr internal_plugin_registrar_; diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h index 5915da7b32cae..3a636e60cb19f 100644 --- a/shell/platform/windows/public/flutter_windows.h +++ b/shell/platform/windows/public/flutter_windows.h @@ -22,10 +22,12 @@ typedef struct FlutterDesktopViewControllerState* FlutterDesktopViewControllerRef; // Opaque reference to a Flutter window. +struct FlutterDesktopView; typedef struct FlutterDesktopView* FlutterDesktopViewRef; // Opaque reference to a Flutter engine instance. -typedef struct FlutterDesktopEngineState* FlutterDesktopEngineRef; +struct FlutterDesktopEngine; +typedef struct FlutterDesktopEngine* FlutterDesktopEngineRef; // Properties for configuring a Flutter engine instance. typedef struct { @@ -55,64 +57,157 @@ typedef struct { size_t switches_count; } FlutterDesktopEngineProperties; -// Creates a View with the given dimensions running a Flutter Application. +// ========== View Controller ========== + +// Creates a view that hosts and displays the given engine instance. // -// This will set up and run an associated Flutter engine using the settings in -// |engine_properties|. +// This takes ownership of |engine|, so FlutterDesktopEngineDestroy should no +// longer be called on it, as it will be called internally when the view +// controller is destroyed. If creating the view controller fails, the engine +// will be destroyed immediately. // -// Returns a null pointer in the event of an error. -FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopCreateViewController( - int width, - int height, - const FlutterDesktopEngineProperties& engine_properties); - -// DEPRECATED. Will be removed soon; switch to the version above. +// If |engine| is not already running, the view controller will start running +// it automatically before displaying the window. +// +// The caller owns the returned reference, and is responsible for calling +// FlutterDesktopViewControllerDestroy. Returns a null pointer in the event of +// an error. FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopCreateViewControllerLegacy(int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count); +FlutterDesktopViewControllerCreate(int width, + int height, + FlutterDesktopEngineRef engine); // Shuts down the engine instance associated with |controller|, and cleans up // associated state. // // |controller| is no longer valid after this call. -FLUTTER_EXPORT void FlutterDesktopDestroyViewController( +FLUTTER_EXPORT void FlutterDesktopViewControllerDestroy( FlutterDesktopViewControllerRef controller); -// Returns the plugin registrar handle for the plugin with the given name. +// Returns the handle for the engine running in FlutterDesktopViewControllerRef. // -// The name must be unique across the application. -FLUTTER_EXPORT FlutterDesktopPluginRegistrarRef -FlutterDesktopGetPluginRegistrar(FlutterDesktopViewControllerRef controller, - const char* plugin_name); - +// Its lifetime is the same as the |controller|'s. +FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopViewControllerGetEngine( + FlutterDesktopViewControllerRef controller); // Returns the view managed by the given controller. + FLUTTER_EXPORT FlutterDesktopViewRef -FlutterDesktopGetView(FlutterDesktopViewControllerRef controller); +FlutterDesktopViewControllerGetView(FlutterDesktopViewControllerRef controller); + +// Allows the Flutter engine and any interested plugins an opportunity to +// handle the given message. +// +// If the WindowProc was handled and further handling should stop, this returns +// true and |result| will be populated. |result| is not set if returning false. +FLUTTER_EXPORT bool FlutterDesktopViewControllerHandleTopLevelWindowProc( + FlutterDesktopViewControllerRef controller, + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result); + +// ========== Engine ========== + +// Creates a Flutter engine with the given properties. +// +// The caller owns the returned reference, and is responsible for calling +// FlutterDesktopEngineDestroy. +FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopEngineCreate( + const FlutterDesktopEngineProperties& engine_properties); + +// Shuts down and destroys the given engine instance. Returns true if the +// shutdown was successful, or if the engine was not running. +// +// |engine| is no longer valid after this call. +FLUTTER_EXPORT bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine); + +// Starts running the given engine instance and optional entry point in the Dart +// project. If the entry point is null, defaults to main(). +// +// If provided, entry_point must be the name of a top-level function from the +// same Dart library that contains the app's main() function, and must be +// decorated with `@pragma(vm:entry-point)` to ensure the method is not +// tree-shaken by the Dart compiler. +// +// Returns false if running the engine failed. +FLUTTER_EXPORT bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, + const char* entry_point); // Processes any pending events in the Flutter engine, and returns the -// number of nanoseconds until the next scheduled event (or max, if none). +// number of nanoseconds until the next scheduled event (or max, if none). // // This should be called on every run of the application-level runloop, and // a wait for native events in the runloop should never be longer than the // last return value from this function. FLUTTER_EXPORT uint64_t -FlutterDesktopProcessMessages(FlutterDesktopViewControllerRef controller); +FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine); + +// Returns the plugin registrar handle for the plugin with the given name. +// +// The name must be unique across the application. +FLUTTER_EXPORT FlutterDesktopPluginRegistrarRef +FlutterDesktopEngineGetPluginRegistrar(FlutterDesktopEngineRef engine, + const char* plugin_name); + +// Returns the messenger associated with the engine. +FLUTTER_EXPORT FlutterDesktopMessengerRef +FlutterDesktopEngineGetMessenger(FlutterDesktopEngineRef engine); + +// ========== View ========== // Return backing HWND for manipulation in host application. FLUTTER_EXPORT HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view); +// ========== Plugin Registrar (extensions) ========== +// These are Windows-specific extensions to flutter_plugin_registrar.h + +// Function pointer type for top level WindowProc delegate registration. +// +// The user data will be whatever was passed to +// FlutterDesktopRegisterTopLevelWindowProcHandler. +// +// Implementations should populate |result| and return true if the WindowProc +// was handled and further handling should stop. |result| is ignored if the +// function returns false. +typedef bool (*FlutterDesktopWindowProcCallback)(HWND /* hwnd */, + UINT /* uMsg */, + WPARAM /*wParam*/, + LPARAM /* lParam*/, + void* /* user data */, + LRESULT* result); + +// Returns the view associated with this registrar's engine instance. +FLUTTER_EXPORT FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView( + FlutterDesktopPluginRegistrarRef registrar); + +FLUTTER_EXPORT void +FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate, + void* user_data); + +FLUTTER_EXPORT void +FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopWindowProcCallback delegate); + +// ========== Freestanding Utilities ========== + // Gets the DPI for a given |hwnd|, depending on the supported APIs per // windows version and DPI awareness mode. If nullptr is passed, returns the DPI // of the primary monitor. +// +// This uses the same logic and fallback for older Windows versions that is used +// internally by Flutter to determine the DPI to use for displaying Flutter +// content, so should be used by any code (e.g., in plugins) that translates +// between Windows and Dart sizes/offsets. FLUTTER_EXPORT UINT FlutterDesktopGetDpiForHWND(HWND hwnd); // Gets the DPI for a given |monitor|. If the API is not available, a default // DPI of 96 is returned. +// +// See FlutterDesktopGetDpiForHWND for more information. FLUTTER_EXPORT UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor); // Reopens stdout and stderr and resysncs the standard library output streams. @@ -120,22 +215,6 @@ FLUTTER_EXPORT UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor); // (e.g., after an AllocConsole call). FLUTTER_EXPORT void FlutterDesktopResyncOutputStreams(); -// Runs an instance of a headless Flutter engine. -// -// Returns a null pointer in the event of an error. -FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopRunEngine( - const FlutterDesktopEngineProperties& engine_properties); - -// Shuts down the given engine instance. Returns true if the shutdown was -// successful. |engine_ref| is no longer valid after this call. -FLUTTER_EXPORT bool FlutterDesktopShutDownEngine( - FlutterDesktopEngineRef engine_ref); - -// Returns the view associated with this registrar's engine instance -// This is a Windows-specific extension to flutter_plugin_registrar.h. -FLUTTER_EXPORT FlutterDesktopViewRef -FlutterDesktopRegistrarGetView(FlutterDesktopPluginRegistrarRef registrar); - #if defined(__cplusplus) } // extern "C" #endif diff --git a/shell/platform/windows/system_utils.h b/shell/platform/windows/system_utils.h new file mode 100644 index 0000000000000..2585d4e8d5241 --- /dev/null +++ b/shell/platform/windows/system_utils.h @@ -0,0 +1,36 @@ +// 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. + +// This file contains utilities for system-level information/settings. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_SYSTEM_UTILS_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_SYSTEM_UTILS_H_ + +#include +#include + +namespace flutter { + +// Components of a system language/locale. +struct LanguageInfo { + std::string language; + std::string region; + std::string script; +}; + +// Returns the list of user-preferred languages, in preference order, +// parsed into LanguageInfo structures. +std::vector GetPreferredLanguageInfo(); + +// Returns the list of user-preferred languages, in preference order. +// The language names are as described at: +// https://docs.microsoft.com/en-us/windows/win32/intl/language-names +std::vector GetPreferredLanguages(); + +// Parses a Windows language name into its components. +LanguageInfo ParseLanguageName(std::wstring language_name); + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_SYSTEM_UTILS_H_ diff --git a/shell/platform/windows/system_utils_unittests.cc b/shell/platform/windows/system_utils_unittests.cc new file mode 100644 index 0000000000000..d784d5027645f --- /dev/null +++ b/shell/platform/windows/system_utils_unittests.cc @@ -0,0 +1,75 @@ +// 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. + +#include + +#include "flutter/shell/platform/windows/system_utils.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +TEST(SystemUtils, GetPreferredLanguageInfo) { + std::vector languages = GetPreferredLanguageInfo(); + // There should be at least one language. + ASSERT_GE(languages.size(), 1); + // The info should have a valid languge. + EXPECT_GE(languages[0].language.size(), 2); +} + +TEST(SystemUtils, GetPreferredLanguages) { + std::vector languages = GetPreferredLanguages(); + // There should be at least one language. + ASSERT_GE(languages.size(), 1); + // The language should be non-empty. + EXPECT_FALSE(languages[0].empty()); + // There should not be a trailing null from the parsing step. + EXPECT_EQ(languages[0].size(), wcslen(languages[0].c_str())); +} + +TEST(SystemUtils, ParseLanguageNameGeneric) { + LanguageInfo info = ParseLanguageName(L"en"); + EXPECT_EQ(info.language, "en"); + EXPECT_TRUE(info.region.empty()); + EXPECT_TRUE(info.script.empty()); +} + +TEST(SystemUtils, ParseLanguageNameWithRegion) { + LanguageInfo info = ParseLanguageName(L"hu-HU"); + EXPECT_EQ(info.language, "hu"); + EXPECT_EQ(info.region, "HU"); + EXPECT_TRUE(info.script.empty()); +} + +TEST(SystemUtils, ParseLanguageNameWithScript) { + LanguageInfo info = ParseLanguageName(L"us-Latn"); + EXPECT_EQ(info.language, "us"); + EXPECT_TRUE(info.region.empty()); + EXPECT_EQ(info.script, "Latn"); +} + +TEST(SystemUtils, ParseLanguageNameWithRegionAndScript) { + LanguageInfo info = ParseLanguageName(L"uz-Latn-UZ"); + EXPECT_EQ(info.language, "uz"); + EXPECT_EQ(info.region, "UZ"); + EXPECT_EQ(info.script, "Latn"); +} + +TEST(SystemUtils, ParseLanguageNameWithSuplementalLanguage) { + LanguageInfo info = ParseLanguageName(L"en-US-x-fabricam"); + EXPECT_EQ(info.language, "en"); + EXPECT_EQ(info.region, "US"); + EXPECT_TRUE(info.script.empty()); +} + +// Ensure that ISO 639-2/T codes are handled. +TEST(SystemUtils, ParseLanguageNameWithThreeCharacterLanguage) { + LanguageInfo info = ParseLanguageName(L"ale-ZZ"); + EXPECT_EQ(info.language, "ale"); + EXPECT_EQ(info.region, "ZZ"); + EXPECT_TRUE(info.script.empty()); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/system_utils_win32.cc b/shell/platform/windows/system_utils_win32.cc new file mode 100644 index 0000000000000..a198523bf536b --- /dev/null +++ b/shell/platform/windows/system_utils_win32.cc @@ -0,0 +1,93 @@ +// 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. + +#include "flutter/shell/platform/windows/system_utils.h" + +#include + +#include + +#include "flutter/shell/platform/windows/string_conversion.h" + +namespace flutter { + +std::vector GetPreferredLanguageInfo() { + std::vector languages = GetPreferredLanguages(); + std::vector language_info; + language_info.reserve(languages.size()); + + for (auto language : languages) { + language_info.push_back(ParseLanguageName(language)); + } + return language_info; +} + +std::vector GetPreferredLanguages() { + std::vector languages; + DWORD flags = MUI_LANGUAGE_NAME | MUI_UI_FALLBACK; + + // Get buffer length. + ULONG count = 0; + ULONG buffer_size = 0; + if (!::GetThreadPreferredUILanguages(flags, &count, nullptr, &buffer_size)) { + return languages; + } + + // Get the list of null-separated languages. + std::wstring buffer(buffer_size, '\0'); + if (!::GetThreadPreferredUILanguages(flags, &count, buffer.data(), + &buffer_size)) { + return languages; + } + + // Extract the individual languages from the buffer. + size_t start = 0; + while (true) { + // The buffer is terminated by an empty string (i.e., a double null). + if (buffer[start] == L'\0') { + break; + } + // Read the next null-terminated language. + std::wstring language(buffer.c_str() + start); + if (language.size() == 0) { + break; + } + languages.push_back(language); + // Skip past that language and its terminating null in the buffer. + start += language.size() + 1; + } + return languages; +} + +LanguageInfo ParseLanguageName(std::wstring language_name) { + LanguageInfo info; + + // Split by '-', discarding any suplemental language info (-x-foo). + std::vector components; + std::istringstream stream(Utf8FromUtf16(language_name)); + std::string component; + while (getline(stream, component, '-')) { + if (component == "x") { + break; + } + components.push_back(component); + } + + // Determine which components are which. + info.language = components[0]; + if (components.size() == 3) { + info.script = components[1]; + info.region = components[2]; + } else if (components.size() == 2) { + // A script code will always be four characters long. + if (components[1].size() == 4) { + info.script = components[1]; + } else { + info.region = components[1]; + } + } + return info; +} + +} // namespace flutter diff --git a/shell/platform/windows/win32_dpi_utils_unittests.cc b/shell/platform/windows/win32_dpi_utils_unittests.cc index 90a580bb95318..c580a9d55807c 100644 --- a/shell/platform/windows/win32_dpi_utils_unittests.cc +++ b/shell/platform/windows/win32_dpi_utils_unittests.cc @@ -1,3 +1,7 @@ +// 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. + #include #include "flutter/shell/platform/windows/win32_dpi_utils.h" diff --git a/shell/platform/windows/win32_flutter_window_unittests.cc b/shell/platform/windows/win32_flutter_window_unittests.cc index dece5a280f807..ff3a5d81f8cde 100644 --- a/shell/platform/windows/win32_flutter_window_unittests.cc +++ b/shell/platform/windows/win32_flutter_window_unittests.cc @@ -1,3 +1,7 @@ +// 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. + #include "flutter/shell/platform/windows/testing/win32_flutter_window_test.h" #include "gtest/gtest.h" diff --git a/shell/platform/windows/win32_platform_handler.cc b/shell/platform/windows/win32_platform_handler.cc index a9efc31f9a2d7..02a3d9c37faa2 100644 --- a/shell/platform/windows/win32_platform_handler.cc +++ b/shell/platform/windows/win32_platform_handler.cc @@ -228,12 +228,11 @@ void PlatformHandler::HandleMethodCall( if (!clipboard.Open(std::get(*view_->GetRenderTarget()))) { rapidjson::Document error_code; error_code.SetInt(::GetLastError()); - result->Error(kClipboardError, "Unable to open clipboard", &error_code); + result->Error(kClipboardError, "Unable to open clipboard", error_code); return; } if (!clipboard.HasString()) { - rapidjson::Document null; - result->Success(&null); + result->Success(rapidjson::Document()); return; } std::optional clipboard_string = clipboard.GetString(); @@ -241,7 +240,7 @@ void PlatformHandler::HandleMethodCall( rapidjson::Document error_code; error_code.SetInt(::GetLastError()); result->Error(kClipboardError, "Unable to get clipboard data", - &error_code); + error_code); return; } @@ -252,7 +251,7 @@ void PlatformHandler::HandleMethodCall( rapidjson::Value(kTextKey, allocator), rapidjson::Value(Utf8FromUtf16(*clipboard_string), allocator), allocator); - result->Success(&document); + result->Success(document); } else if (method.compare(kSetClipboardDataMethod) == 0) { const rapidjson::Value& document = *method_call.arguments(); rapidjson::Value::ConstMemberIterator itr = document.FindMember(kTextKey); @@ -266,14 +265,14 @@ void PlatformHandler::HandleMethodCall( if (!clipboard.Open(std::get(*view_->GetRenderTarget()))) { rapidjson::Document error_code; error_code.SetInt(::GetLastError()); - result->Error(kClipboardError, "Unable to open clipboard", &error_code); + result->Error(kClipboardError, "Unable to open clipboard", error_code); return; } if (!clipboard.SetString(Utf16FromUtf8(itr->value.GetString()))) { rapidjson::Document error_code; error_code.SetInt(::GetLastError()); result->Error(kClipboardError, "Unable to set clipboard data", - &error_code); + error_code); return; } result->Success(); diff --git a/shell/platform/windows/win32_window_proc_delegate_manager.cc b/shell/platform/windows/win32_window_proc_delegate_manager.cc new file mode 100644 index 0000000000000..3d70feb255658 --- /dev/null +++ b/shell/platform/windows/win32_window_proc_delegate_manager.cc @@ -0,0 +1,42 @@ +// 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. + +#include "flutter/shell/platform/windows/win32_window_proc_delegate_manager.h" + +#include "flutter/shell/platform/embedder/embedder.h" + +namespace flutter { + +Win32WindowProcDelegateManager::Win32WindowProcDelegateManager() = default; +Win32WindowProcDelegateManager::~Win32WindowProcDelegateManager() = default; + +void Win32WindowProcDelegateManager::RegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data) { + top_level_window_proc_handlers_[delegate] = user_data; +} + +void Win32WindowProcDelegateManager::UnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate) { + top_level_window_proc_handlers_.erase(delegate); +} + +std::optional Win32WindowProcDelegateManager::OnTopLevelWindowProc( + HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + std::optional result; + for (const auto& [handler, user_data] : top_level_window_proc_handlers_) { + LPARAM handler_result; + // Stop as soon as any delegate indicates that it has handled the message. + if (handler(hwnd, message, wparam, lparam, user_data, &handler_result)) { + result = handler_result; + break; + } + } + return result; +} + +} // namespace flutter diff --git a/shell/platform/windows/win32_window_proc_delegate_manager.h b/shell/platform/windows/win32_window_proc_delegate_manager.h new file mode 100644 index 0000000000000..a87aad239671c --- /dev/null +++ b/shell/platform/windows/win32_window_proc_delegate_manager.h @@ -0,0 +1,58 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WIN32_WINDOW_PROC_DELEGATE_MANAGER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_WIN32_WINDOW_PROC_DELEGATE_MANAGER_H_ + +#include + +#include +#include + +#include "flutter/shell/platform/windows/public/flutter_windows.h" + +namespace flutter { + +// Handles registration, unregistration, and dispatching for WindowProc +// delegation. +class Win32WindowProcDelegateManager { + public: + explicit Win32WindowProcDelegateManager(); + ~Win32WindowProcDelegateManager(); + + // Prevent copying. + Win32WindowProcDelegateManager(Win32WindowProcDelegateManager const&) = + delete; + Win32WindowProcDelegateManager& operator=( + Win32WindowProcDelegateManager const&) = delete; + + // Adds |delegate| as a delegate to be called for |OnTopLevelWindowProc|. + // + // Multiple calls with the same |delegate| will replace the previous + // registration, even if |user_data| is different. + void RegisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate, + void* user_data); + + // Unregisters |delegate| as a delate for |OnTopLevelWindowProc|. + void UnregisterTopLevelWindowProcDelegate( + FlutterDesktopWindowProcCallback delegate); + + // Calls any registered WindowProc delegates. + // + // If a result is returned, then the message was handled in such a way that + // further handling should not be done. + std::optional OnTopLevelWindowProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam); + + private: + std::map + top_level_window_proc_handlers_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WIN32_WINDOW_PROC_DELEGATE_MANAGER_H_ diff --git a/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc b/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc new file mode 100644 index 0000000000000..27e5c77ae0a1c --- /dev/null +++ b/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc @@ -0,0 +1,168 @@ +// 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. + +#include "flutter/shell/platform/windows/win32_window_proc_delegate_manager.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { + +using TestWindowProcDelegate = std::function(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)>; + +// A FlutterDesktopWindowProcCallback that forwards to a std::function provided +// as user_data. +bool TestWindowProcCallback(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + void* user_data, + LRESULT* result) { + TestWindowProcDelegate& delegate = + *static_cast(user_data); + auto delegate_result = delegate(hwnd, message, wparam, lparam); + if (delegate_result) { + *result = *delegate_result; + } + return delegate_result.has_value(); +} + +// Same as the above, but with a different address, to test multiple +// registration. +bool TestWindowProcCallback2(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam, + void* user_data, + LRESULT* result) { + return TestWindowProcCallback(hwnd, message, wparam, lparam, user_data, + result); +} + +} // namespace + +TEST(Win32WindowProcDelegateManagerTest, CallsCorrectly) { + Win32WindowProcDelegateManager manager; + HWND dummy_hwnd; + + bool called = false; + TestWindowProcDelegate delegate = [&called, &dummy_hwnd]( + HWND hwnd, UINT message, WPARAM wparam, + LPARAM lparam) { + called = true; + EXPECT_EQ(hwnd, dummy_hwnd); + EXPECT_EQ(message, 2); + EXPECT_EQ(wparam, 3); + EXPECT_EQ(lparam, 4); + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, &delegate); + auto result = manager.OnTopLevelWindowProc(dummy_hwnd, 2, 3, 4); + + EXPECT_TRUE(called); + EXPECT_FALSE(result); +} + +TEST(Win32WindowProcDelegateManagerTest, ReplacementRegister) { + Win32WindowProcDelegateManager manager; + + bool called_a = false; + TestWindowProcDelegate delegate_a = + [&called_a](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(); + }; + bool called_b = false; + TestWindowProcDelegate delegate_b = + [&called_b](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_a); + // The function pointer is the same, so this should replace, not add. + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_b); + manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_FALSE(called_a); + EXPECT_TRUE(called_b); +} + +TEST(Win32WindowProcDelegateManagerTest, RegisterMultiple) { + Win32WindowProcDelegateManager manager; + + bool called_a = false; + TestWindowProcDelegate delegate_a = + [&called_a](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(); + }; + bool called_b = false; + TestWindowProcDelegate delegate_b = + [&called_b](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_a); + // Function pointer is different, so both should be called. + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback2, + &delegate_b); + manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_TRUE(called_a); + EXPECT_TRUE(called_b); +} + +TEST(Win32WindowProcDelegateManagerTest, ConflictingDelegates) { + Win32WindowProcDelegateManager manager; + + bool called_a = false; + TestWindowProcDelegate delegate_a = + [&called_a](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_a = true; + return std::optional(1); + }; + bool called_b = false; + TestWindowProcDelegate delegate_b = + [&called_b](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + called_b = true; + return std::optional(1); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, + &delegate_a); + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback2, + &delegate_b); + auto result = manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_TRUE(result); + // Exactly one of the handlers should be called since each will claim to have + // handled the message. Which one is unspecified, since the calling order is + // unspecified. + EXPECT_TRUE(called_a || called_b); + EXPECT_NE(called_a, called_b); +} + +TEST(Win32WindowProcDelegateManagerTest, Unregister) { + Win32WindowProcDelegateManager manager; + + bool called = false; + TestWindowProcDelegate delegate = [&called](HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + called = true; + return std::optional(); + }; + manager.RegisterTopLevelWindowProcDelegate(TestWindowProcCallback, &delegate); + manager.UnregisterTopLevelWindowProcDelegate(TestWindowProcCallback); + auto result = manager.OnTopLevelWindowProc(nullptr, 0, 0, 0); + + EXPECT_FALSE(result); + EXPECT_FALSE(called); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/win32_window_unittests.cc b/shell/platform/windows/win32_window_unittests.cc index 0adc0b0388389..fea9faa4547d7 100644 --- a/shell/platform/windows/win32_window_unittests.cc +++ b/shell/platform/windows/win32_window_unittests.cc @@ -1,3 +1,7 @@ +// 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. + #include "flutter/shell/platform/windows/testing/win32_window_test.h" #include "gtest/gtest.h" diff --git a/shell/platform/windows/window_state.h b/shell/platform/windows/window_state.h index d1eceb866f507..3092a7bc0b90e 100644 --- a/shell/platform/windows/window_state.h +++ b/shell/platform/windows/window_state.h @@ -8,76 +8,36 @@ #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h" #include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" #include "flutter/shell/platform/embedder/embedder.h" -#include "flutter/shell/platform/windows/key_event_handler.h" -#include "flutter/shell/platform/windows/keyboard_hook_handler.h" -#include "flutter/shell/platform/windows/text_input_plugin.h" -#include "flutter/shell/platform/windows/win32_platform_handler.h" -#include "flutter/shell/platform/windows/win32_task_runner.h" + +// Structs backing the opaque references used in the C API. +// +// DO NOT ADD ANY NEW CODE HERE. These are legacy, and are being phased out +// in favor of objects that own and manage the relevant functionality. namespace flutter { +struct FlutterWindowsEngine; struct FlutterWindowsView; -} +} // namespace flutter -// Struct for storing state within an instance of the windows native (HWND or -// CoreWindow) Window. +// Wrapper to distinguish the view controller ref from the view ref given out +// in the C API. struct FlutterDesktopViewControllerState { - // The view that owns this state object. + // The view that backs this state object. std::unique_ptr view; - - // The state associate with the engine backing the view. - std::unique_ptr engine_state; - - // The window handle given to API clients. - std::unique_ptr view_wrapper; -}; - -// Opaque reference for the native windows itself. This is separate from the -// controller so that it can be provided to plugins without giving them access -// to all of the controller-based functionality. -struct FlutterDesktopView { - // The view that (indirectly) owns this state object. - flutter::FlutterWindowsView* view; -}; - -struct AotDataDeleter { - void operator()(FlutterEngineAOTData aot_data) { - FlutterEngineCollectAOTData(aot_data); - } }; -using UniqueAotDataPtr = std::unique_ptr<_FlutterEngineAOTData, AotDataDeleter>; - -// Struct for storing state of a Flutter engine instance. -struct FlutterDesktopEngineState { - // The handle to the Flutter engine instance. - FLUTTER_API_SYMBOL(FlutterEngine) engine; - - // Task runner for tasks posted from the engine. - std::unique_ptr task_runner; - - // AOT data, if any. - UniqueAotDataPtr aot_data; -}; - -// State associated with the plugin registrar. +// Wrapper to distinguish the plugin registrar ref from the engine ref given out +// in the C API. struct FlutterDesktopPluginRegistrar { - // The plugin messenger handle given to API clients. - std::unique_ptr messenger; - - // The handle for the view associated with this registrar. - FlutterDesktopView* view; - - // Callback to be called on registrar destruction. - FlutterDesktopOnRegistrarDestroyed destruction_handler; + // The engine that owns this state object. + flutter::FlutterWindowsEngine* engine = nullptr; }; -// State associated with the messenger used to communicate with the engine. +// Wrapper to distinguish the messenger ref from the engine ref given out +// in the C API. struct FlutterDesktopMessenger { - // The Flutter engine this messenger sends outgoing messages to. - FLUTTER_API_SYMBOL(FlutterEngine) engine; - - // The message dispatcher for handling incoming messages. - flutter::IncomingMessageDispatcher* dispatcher; + // The engine that owns this state object. + flutter::FlutterWindowsEngine* engine = nullptr; }; #endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOW_STATE_H_ diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 8d14e6376e4a8..7029e5f3bc724 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -140,8 +140,7 @@ int RunTester(const flutter::Settings& settings, }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { - return std::make_unique(shell, shell.GetTaskRunners(), - shell.GetIsGpuDisabledSyncSwitch()); + return std::make_unique(shell); }; auto shell = Shell::Create(task_runners, // @@ -235,10 +234,10 @@ int RunTester(const flutter::Settings& settings, } }); - flutter::ViewportMetrics metrics; + flutter::ViewportMetrics metrics{}; metrics.device_pixel_ratio = 3.0; - metrics.physical_width = 2400; // 800 at 3x resolution - metrics.physical_height = 1800; // 600 at 3x resolution + metrics.physical_width = 2400.0; // 800 at 3x resolution. + metrics.physical_height = 1800.0; // 600 at 3x resolution. shell->GetPlatformView()->SetViewportMetrics(metrics); // Run the message loop and wait for the script to do its thing. diff --git a/sky/packages/sky_engine/LICENSE b/sky/packages/sky_engine/LICENSE index 817331779fd1e..1bd8c4561a569 100644 --- a/sky/packages/sky_engine/LICENSE +++ b/sky/packages/sky_engine/LICENSE @@ -6158,6 +6158,30 @@ freely, subject to the following restrictions: -------------------------------------------------------------------------------- harfbuzz +Copyright (C) 2011 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + Copyright (C) 2012 Grigori Goronzy Permission to use, copy, modify, and/or distribute this software for any @@ -6174,6 +6198,30 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- harfbuzz +Copyright (C) 2013 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + Copyright © 1998-2004 David Turner and Werner Lemberg Copyright © 2004,2007,2009 Red Hat, Inc. Copyright © 2011,2012 Google, Inc. @@ -6454,6 +6502,32 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- harfbuzz +Copyright © 2007,2008,2009 Red Hat, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2018 Khaled Hosny + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + Copyright © 2007,2008,2009,2010 Red Hat, Inc. Copyright © 2010,2012 Google, Inc. @@ -7839,7 +7913,7 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. harfbuzz Copyright © 2018 Ebrahim Byagowi -Copyright © 2018 Khaled Hosny +Copyright © 2020 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -8105,12 +8179,85 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- harfbuzz +Copyright © 2019-2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + +Copyright © 2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + +Copyright © 2020 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +-------------------------------------------------------------------------------- +harfbuzz + HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. For parts of HarfBuzz that are licensed under different licenses see individual files names COPYING in subdirectories where applicable. -Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019 Google, Inc. -Copyright © 2019 Facebook, Inc. +Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020 Google, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. Copyright © 2012 Mozilla Foundation Copyright © 2011 Codethink Limited Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) @@ -12893,7 +13040,7 @@ wasmer MIT License -Copyright (c) 2019 Wasmer, Inc. and its affiliates. +Copyright (c) 2019-present Wasmer, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/testing/BUILD.gn b/testing/BUILD.gn index db338fb42d646..71fc74b08b501 100644 --- a/testing/BUILD.gn +++ b/testing/BUILD.gn @@ -39,7 +39,7 @@ source_set("testing") { public_deps = [ ":testing_lib" ] } -source_set_maybe_fuchsia_legacy("dart") { +source_set("dart") { testonly = true sources = [ @@ -54,13 +54,12 @@ source_set_maybe_fuchsia_legacy("dart") { public_deps = [ ":testing_lib", "//flutter/common", + "//flutter/runtime", "//flutter/runtime:libdart", "//flutter/third_party/tonic", "//third_party/dart/runtime/bin:elf_loader", "//third_party/skia", ] - - public_deps_legacy_and_next = [ "//flutter/runtime:runtime" ] } source_set("skia") { @@ -80,7 +79,7 @@ source_set("skia") { ] } -source_set_maybe_fuchsia_legacy("fixture_test") { +source_set("fixture_test") { testonly = true sources = [ @@ -88,11 +87,10 @@ source_set_maybe_fuchsia_legacy("fixture_test") { "fixture_test.h", ] - public_deps = [ "//flutter/common" ] - - public_deps_legacy_and_next = [ + public_deps = [ ":dart", - "//flutter/runtime:runtime", + "//flutter/common", + "//flutter/runtime", ] } diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 7bf507206d0b1..8640cc86bf63d 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -38,6 +38,35 @@ void testCanvas(CanvasCallback callback) { } catch (error) { } // ignore: empty_catches } +void expectAssertion(Function callback) { + bool assertsEnabled = false; + assert(() { + assertsEnabled = true; + return true; + }()); + if (assertsEnabled) { + bool threw = false; + try { + callback(); + } catch (e) { + expect(e is AssertionError, true); + threw = true; + } + expect(threw, true); + } +} + +void expectArgumentError(Function callback) { + bool threw = false; + try { + callback(); + } catch (e) { + expect(e is ArgumentError, true); + threw = true; + } + expect(threw, true); +} + void testNoCrashes() { test('canvas APIs should not crash', () async { final Paint paint = Paint(); @@ -218,32 +247,54 @@ void main() { expect(areEqual, true); }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 - test('Image size reflected in picture size for image*, drawAtlas, and drawPicture methods', () async { + test('Null values allowed for drawAtlas methods', () async { final Image image = await createImage(100, 100); final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); const Rect rect = Rect.fromLTWH(0, 0, 100, 100); - canvas.drawImage(image, Offset.zero, Paint()); - canvas.drawImageRect(image, rect, rect, Paint()); - canvas.drawImageNine(image, rect, rect, Paint()); - canvas.drawAtlas(image, [], [], [], BlendMode.src, rect, Paint()); - final Picture picture = recorder.endRecording(); - - // Some of the numbers here appear to utilize sharing/reuse of common items, - // e.g. of the Paint() or same `Rect` usage, etc. - // The raw utilization of a 100x100 picture here should be 53333: - // 100 * 100 * 4 * (4/3) = 53333.333333.... - // To avoid platform specific idiosyncrasies and brittleness against changes - // to Skia, we just assert this is _at least_ 4x the image size. - const int minimumExpected = 53333 * 4; - expect(picture.approximateBytesUsed, greaterThan(minimumExpected)); - - final PictureRecorder recorder2 = PictureRecorder(); - final Canvas canvas2 = Canvas(recorder2); - canvas2.drawPicture(picture); - final Picture picture2 = recorder2.endRecording(); + final RSTransform transform = RSTransform(1, 0, 0, 0); + const Color color = Color(0); + final Paint paint = Paint(); + canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, rect, paint); + canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, null, paint); + canvas.drawAtlas(image, [transform], [rect], [], null, rect, paint); + canvas.drawAtlas(image, [transform], [rect], null, null, rect, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), BlendMode.src, rect, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), BlendMode.src, null, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), null, null, rect, paint); + + expectAssertion(() => canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, rect, null)); + expectAssertion(() => canvas.drawAtlas(image, [transform], [rect], [color], null, rect, paint)); + expectAssertion(() => canvas.drawAtlas(image, [transform], null, [color], BlendMode.src, rect, paint)); + expectAssertion(() => canvas.drawAtlas(image, null, [rect], [color], BlendMode.src, rect, paint)); + expectAssertion(() => canvas.drawAtlas(null, [transform], [rect], [color], BlendMode.src, rect, paint)); + }); - expect(picture2.approximateBytesUsed, greaterThan(minimumExpected)); + test('Data lengths must match for drawAtlas methods', () async { + final Image image = await createImage(100, 100); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + const Rect rect = Rect.fromLTWH(0, 0, 100, 100); + final RSTransform transform = RSTransform(1, 0, 0, 0); + const Color color = Color(0); + final Paint paint = Paint(); + canvas.drawAtlas(image, [transform], [rect], [color], BlendMode.src, rect, paint); + canvas.drawAtlas(image, [transform, transform], [rect, rect], [color, color], BlendMode.src, rect, paint); + canvas.drawAtlas(image, [transform], [rect], [], null, rect, paint); + canvas.drawAtlas(image, [transform], [rect], null, null, rect, paint); + canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), BlendMode.src, rect, paint); + canvas.drawRawAtlas(image, Float32List(4), Float32List(4), Int32List(1), BlendMode.src, rect, paint); + canvas.drawRawAtlas(image, Float32List(4), Float32List(4), null, null, rect, paint); + + expectArgumentError(() => canvas.drawAtlas(image, [transform], [], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [], [rect], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [transform], [rect], [color, color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [transform], [rect, rect], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawAtlas(image, [transform, transform], [rect], [color], BlendMode.src, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(3), Float32List(3), null, null, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(4), Float32List(0), null, null, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(0), Float32List(4), null, null, rect, paint)); + expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(4), Float32List(4), Int32List(2), BlendMode.src, rect, paint)); }); test('Vertex buffer size reflected in picture size for drawVertices', () async { diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index 186ca928a1c3e..21eda23623664 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -46,7 +46,6 @@ void main() { double? oldDPR; Size? oldSize; - double? oldDepth; WindowPadding? oldPadding; WindowPadding? oldInsets; WindowPadding? oldSystemGestureInsets; @@ -54,7 +53,6 @@ void main() { void setUp() { oldDPR = window.devicePixelRatio; oldSize = window.physicalSize; - oldDepth = window.physicalDepth; oldPadding = window.viewPadding; oldInsets = window.viewInsets; oldSystemGestureInsets = window.systemGestureInsets; @@ -76,7 +74,6 @@ void main() { oldDPR!, // DPR oldSize!.width, // width oldSize!.height, // height - oldDepth!, // depth oldPadding!.top, // padding top oldPadding!.right, // padding right oldPadding!.bottom, // padding bottom @@ -161,7 +158,6 @@ void main() { 0.1234, // DPR 0.0, // width 0.0, // height - 0.0, // depth 0.0, // padding top 0.0, // padding right 0.0, // padding bottom @@ -378,7 +374,6 @@ void main() { 1.0, // DPR 800.0, // width 600.0, // height - 100.0, // depth 50.0, // padding top 0.0, // padding right 40.0, // padding bottom @@ -396,14 +391,12 @@ void main() { expectEquals(window.viewInsets.bottom, 0.0); expectEquals(window.viewPadding.bottom, 40.0); expectEquals(window.padding.bottom, 40.0); - expectEquals(window.physicalDepth, 100.0); expectEquals(window.systemGestureInsets.bottom, 0.0); _updateWindowMetrics( 1.0, // DPR 800.0, // width 600.0, // height - 100.0, // depth 50.0, // padding top 0.0, // padding right 40.0, // padding bottom @@ -421,7 +414,6 @@ void main() { expectEquals(window.viewInsets.bottom, 400.0); expectEquals(window.viewPadding.bottom, 40.0); expectEquals(window.padding.bottom, 0.0); - expectEquals(window.physicalDepth, 100.0); expectEquals(window.systemGestureInsets.bottom, 44.0); }); } diff --git a/testing/dart/window_test.dart b/testing/dart/window_test.dart index a434482024d69..79767f881e7bc 100644 --- a/testing/dart/window_test.dart +++ b/testing/dart/window_test.dart @@ -22,8 +22,14 @@ void main() { }); test('FrameTiming.toString has the correct format', () { - final FrameTiming timing = FrameTiming([1000, 8000, 9000, 19500]); - expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, totalSpan: 18.5ms)'); + final FrameTiming timing = FrameTiming( + vsyncStart: 500, + buildStart: 1000, + buildFinish: 8000, + rasterStart: 9000, + rasterFinish: 19500 + ); + expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, vsyncOverhead: 0.5ms, totalSpan: 19.0ms)'); }); test('computePlatformResolvedLocale basic', () { diff --git a/testing/fuchsia/run_tests.sh b/testing/fuchsia/run_tests.sh index 93ec4bc0b2e63..61c2de7b8dd27 100755 --- a/testing/fuchsia/run_tests.sh +++ b/testing/fuchsia/run_tests.sh @@ -50,11 +50,11 @@ reboot() { --timeout-seconds $ssh_timeout_seconds \ --identity-file $pkey - echo "$(date) START:REBOOT ------------------------------------------" + echo "$(date) START:REBOOT ----------------------------------------" # note: this will set an exit code of 255, which we can ignore. ./fuchsia_ctl -d $device_name ssh -c "dm reboot-recovery" \ --identity-file $pkey || true - echo "$(date) END:REBOOT --------------------------------------------" + echo "$(date) END:REBOOT ------------------------------------------" } trap reboot EXIT @@ -103,7 +103,7 @@ echo "$(date) DONE:txt_tests ----------------------------------------" # once it passes on Fuchsia. # TODO(https://github.com/flutter/flutter/issues/58211): Re-enable MessageLoop # test once it passes on Fuchsia. -echo "$(date) START:fml_tests ----------------------------------------" +echo "$(date) START:fml_tests ---------------------------------------" ./fuchsia_ctl -d $device_name test \ -f fml_tests-0.far \ -t fml_tests \ @@ -121,13 +121,6 @@ echo "$(date) START:flow_tests --------------------------------------" --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f flow_tests_next-0.far \ - -t flow_tests_next \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages echo "$(date) DONE:flow_tests ---------------------------------------" @@ -138,13 +131,6 @@ echo "$(date) START:runtime_tests -----------------------------------" --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f runtime_tests_next-0.far \ - -t runtime_tests_next \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages echo "$(date) DONE:runtime_tests ------------------------------------" echo "$(date) START:ui_tests ----------------------------------------" @@ -154,31 +140,12 @@ echo "$(date) START:ui_tests ----------------------------------------" --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f ui_tests_next-0.far \ - -t ui_tests_next \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages echo "$(date) DONE:ui_tests -----------------------------------------" -# TODO(https://github.com/flutter/flutter/issues/53399): Re-enable -# OnServiceProtocolGetSkSLsWorks, CanLoadSkSLsFromAsset, and -# CanRemoveOldPersistentCache once they pass on Fuchsia. echo "$(date) START:shell_tests -------------------------------------" ./fuchsia_ctl -d $device_name test \ -f shell_tests-0.far \ -t shell_tests \ - -a "--gtest_filter=-ShellTest.CacheSkSLWorks:ShellTest.SetResourceCacheSize*:ShellTest.OnServiceProtocolGetSkSLsWorks:ShellTest.CanLoadSkSLsFromAsset:ShellTest.CanRemoveOldPersistentCache" \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages - -./fuchsia_ctl -d $device_name test \ - -f shell_tests_next-0.far \ - -t shell_tests_next \ - -a "--gtest_filter=-ShellTest.CacheSkSLWorks:ShellTest.SetResourceCacheSize*:ShellTest.OnServiceProtocolGetSkSLsWorks:ShellTest.CanLoadSkSLsFromAsset:ShellTest.CanRemoveOldPersistentCache" \ --identity-file $pkey \ --timeout-seconds $test_timeout_seconds \ --packages-directory packages @@ -194,13 +161,14 @@ echo "$(date) START:flutter_runner_tests ----------------------------" --timeout-seconds $test_timeout_seconds \ --packages-directory packages -./fuchsia_ctl -d $device_name test \ - -f flutter_aot_runner-0.far \ - -f flutter_runner_scenic_tests-0.far \ - -t flutter_runner_scenic_tests \ - --identity-file $pkey \ - --timeout-seconds $test_timeout_seconds \ - --packages-directory packages +# TODO(https://github.com/flutter/flutter/issues/61768): De-flake and re-enable +# ./fuchsia_ctl -d $device_name test \ +# -f flutter_aot_runner-0.far \ +# -f flutter_runner_scenic_tests-0.far \ +# -t flutter_runner_scenic_tests \ +# --identity-file $pkey \ +# --timeout-seconds $test_timeout_seconds \ +# --packages-directory packages ./fuchsia_ctl -d $device_name test \ -f flutter_aot_runner-0.far \ diff --git a/testing/fuchsia/test_fars b/testing/fuchsia/test_fars index b26051c1ea870..e5b84456afbb7 100644 --- a/testing/fuchsia/test_fars +++ b/testing/fuchsia/test_fars @@ -8,7 +8,3 @@ shell_tests-0.far testing_tests-0.far txt_tests-0.far ui_tests-0.far -flow_tests_next-0.far -runtime_tests_next-0.far -shell_tests_next-0.far -ui_tests_next-0.far diff --git a/testing/ios/IosUnitTests/App/Info.plist b/testing/ios/IosUnitTests/App/Info.plist index 16be3b681122d..52b6c2050105b 100644 --- a/testing/ios/IosUnitTests/App/Info.plist +++ b/testing/ios/IosUnitTests/App/Info.plist @@ -24,6 +24,26 @@ LaunchScreen UIMainStoryboardFile Main + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + invalid-site.com + + NSIncludesSubdomains + + NSThirdPartyExceptionAllowsInsecureHTTPLoads + + + sub.invalid-site.com + + NSThirdPartyExceptionAllowsInsecureHTTPLoads + + + + UIRequiredDeviceCapabilities armv7 diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 598624564fe53..eeaef513460a9 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -28,6 +28,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SemanticsObjectTest.mm; sourceTree = ""; }; + 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEngineTest.mm; sourceTree = ""; }; + 0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = accessibility_bridge_test.mm; sourceTree = ""; }; + 0AC2330B24BA71D300A85907 /* FlutterTextInputPluginTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterTextInputPluginTest.m; sourceTree = ""; }; + 0AC2330F24BA71D300A85907 /* FlutterBinaryMessengerRelayTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterBinaryMessengerRelayTest.mm; sourceTree = ""; }; + 0AC2331024BA71D300A85907 /* connection_collection_test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = connection_collection_test.mm; sourceTree = ""; }; + 0AC2331224BA71D300A85907 /* FlutterEnginePlatformViewTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEnginePlatformViewTest.mm; sourceTree = ""; }; + 0AC2331924BA71D300A85907 /* FlutterPluginAppLifeCycleDelegateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterPluginAppLifeCycleDelegateTest.m; sourceTree = ""; }; + 0AC2332124BA71D300A85907 /* FlutterViewControllerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterViewControllerTest.mm; sourceTree = ""; }; 0D1CE5D7233430F400E5D880 /* FlutterChannelsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterChannelsTest.m; sourceTree = ""; }; 0D6AB6B122BB05E100EEE540 /* IosUnitTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IosUnitTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6B422BB05E100EEE540 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -62,6 +71,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0AC232E924BA71D300A85907 /* Source */ = { + isa = PBXGroup; + children = ( + 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */, + 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */, + 0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */, + 0AC2330B24BA71D300A85907 /* FlutterTextInputPluginTest.m */, + 0AC2330F24BA71D300A85907 /* FlutterBinaryMessengerRelayTest.mm */, + 0AC2331024BA71D300A85907 /* connection_collection_test.mm */, + 0AC2331224BA71D300A85907 /* FlutterEnginePlatformViewTest.mm */, + 0AC2331924BA71D300A85907 /* FlutterPluginAppLifeCycleDelegateTest.m */, + 0AC2332124BA71D300A85907 /* FlutterViewControllerTest.mm */, + ); + name = Source; + path = ../../../shell/platform/darwin/ios/framework/Source; + sourceTree = ""; + }; 0D1CE5D62334309900E5D880 /* Source-Common */ = { isa = PBXGroup; children = ( @@ -74,6 +100,7 @@ 0D6AB6A822BB05E100EEE540 = { isa = PBXGroup; children = ( + 0AC232E924BA71D300A85907 /* Source */, 0D6AB6B322BB05E100EEE540 /* App */, 0D6AB6CC22BB05E200EEE540 /* Tests */, 0D6AB6B222BB05E100EEE540 /* Products */, diff --git a/testing/ios/IosUnitTests/run_tests.sh b/testing/ios/IosUnitTests/run_tests.sh index 6c44de0aef00f..a06335283ac17 100755 --- a/testing/ios/IosUnitTests/run_tests.sh +++ b/testing/ios/IosUnitTests/run_tests.sh @@ -8,9 +8,6 @@ if [ $# -eq 1 ]; then FLUTTER_ENGINE=$1 fi -set -o pipefail && xcodebuild -sdk iphonesimulator \ - -scheme IosUnitTests \ - -destination 'platform=iOS Simulator,name=iPhone 8' \ - test \ - FLUTTER_ENGINE=$FLUTTER_ENGINE +../../run_tests.py --variant $FLUTTER_ENGINE --type objc --ios-variant $FLUTTER_ENGINE + popd diff --git a/testing/run_tests.py b/testing/run_tests.py index 7df1757ee80b0..a5c7d833fb279 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -331,10 +331,11 @@ def AssertExpectedJavaVersion(): def AssertExpectedXcodeVersion(): """Checks that the user has a recent version of Xcode installed""" - EXPECTED_MAJOR_VERSION = '11' + EXPECTED_MAJOR_VERSION = ['11', '12'] version_output = subprocess.check_output(['xcodebuild', '-version']) + match = re.match("Xcode (\d+)", version_output) message = "Xcode must be installed to run the iOS embedding unit tests" - assert "Xcode %s." % EXPECTED_MAJOR_VERSION in version_output, message + assert match.group(1) in EXPECTED_MAJOR_VERSION, message def RunJavaTests(filter, android_variant='android_debug_unopt'): """Runs the Java JUnit unit tests for the Android embedding""" @@ -344,7 +345,7 @@ def RunJavaTests(filter, android_variant='android_debug_unopt'): embedding_deps_dir = os.path.join(buildroot_dir, 'third_party', 'android_embedding_dependencies', 'lib') classpath = map(str, [ - os.path.join(buildroot_dir, 'third_party', 'android_tools', 'sdk', 'platforms', 'android-29', 'android.jar'), + os.path.join(buildroot_dir, 'third_party', 'android_tools', 'sdk', 'platforms', 'android-30', 'android.jar'), os.path.join(embedding_deps_dir, '*'), # Wildcard for all jars in the directory os.path.join(android_out_dir, 'flutter.jar'), os.path.join(android_out_dir, 'robolectric_tests.jar') @@ -369,17 +370,19 @@ def RunObjcTests(ios_variant='ios_debug_sim_unopt'): ios_out_dir = os.path.join(out_dir, ios_variant) EnsureIosTestsAreBuilt(ios_out_dir) - pretty = "cat" if subprocess.call(["which", "xcpretty"]) else "xcpretty" ios_unit_test_dir = os.path.join(buildroot_dir, 'flutter', 'testing', 'ios', 'IosUnitTests') + # Avoid using xcpretty unless the following can be addressed: + # - Make sure all relevant failure output is printed on a failure. + # - Make sure that a failing exit code is set for CI. + # See https://github.com/flutter/flutter/issues/63742 command = [ 'xcodebuild ' '-sdk iphonesimulator ' '-scheme IosUnitTests ' "-destination platform='iOS Simulator,name=iPhone 8' " 'test ' - 'FLUTTER_ENGINE=' + ios_variant + - ' | ' + pretty + 'FLUTTER_ENGINE=' + ios_variant ] RunCmd(command, cwd=ios_unit_test_dir, shell=True) diff --git a/testing/scenario_app/.gitignore b/testing/scenario_app/.gitignore index 6dc8f6e23d125..b9de2ce97b175 100644 --- a/testing/scenario_app/.gitignore +++ b/testing/scenario_app/.gitignore @@ -5,3 +5,6 @@ build/ ios/Scenarios/*.framework/ android/app/libs/flutter.jar android/app/src/main/jniLibs/arm64-v8a/libapp.so +android/gradle-home/.cache + +!.vpython diff --git a/testing/scenario_app/README.md b/testing/scenario_app/README.md index 6f78ad9afdaca..131f7186feaf5 100644 --- a/testing/scenario_app/README.md +++ b/testing/scenario_app/README.md @@ -42,6 +42,19 @@ compared against golden reside. ## Running for Android +The test is run on a x86 emulator. To run the test locally, you must create an emulator running API level 28, using an x86_64 ABI, and set the following screen settings in the avd's `config.ini` file: + +``` +hw.lcd.density = 480 +hw.lcd.height = 1920 +hw.lcd.width = 1080 +lcd.depth = 16 +``` + +This file is typically located in your `$HOME/.android/avd/` folder. + +Once the emulator is up, you can run the test by running: + ```bash ./build_and_run_android_tests.sh ``` diff --git a/testing/scenario_app/android/app/build.gradle b/testing/scenario_app/android/app/build.gradle index 4438a32d79ac1..44915f7d7050e 100644 --- a/testing/scenario_app/android/app/build.gradle +++ b/testing/scenario_app/android/app/build.gradle @@ -7,7 +7,7 @@ screenshots { } android { - compileSdkVersion 28 + compileSdkVersion 30 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -26,6 +26,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + sourceSets { + main.assets.srcDirs += "${project.buildDir}/assets" + } } dependencies { diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java index 85dcfa5051e7b..d060bf6ccb907 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java @@ -13,7 +13,6 @@ import android.util.Xml; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnitRunner; -import androidx.test.runner.screenshot.Screenshot; import com.facebook.testing.screenshot.ScreenshotRunner; import com.facebook.testing.screenshot.internal.AlbumImpl; import com.facebook.testing.screenshot.internal.Registry; @@ -164,8 +163,6 @@ public static void capture(TestableFlutterActivity activity) // This method is called from the runner thread, // so block the UI thread while taking the screenshot. - // UiThreadLocker locker = new UiThreadLocker(); - // locker.lock(); // Screenshot.capture(view or activity) does not capture the Flutter UI. // Unfortunately, it doesn't work with Android's `Surface` or `TextureSurface`. @@ -182,7 +179,8 @@ public static void capture(TestableFlutterActivity activity) new Callable() { @Override public Void call() { - Bitmap bitmap = Screenshot.capture().getBitmap(); + Bitmap bitmap = + InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot(); // Remove the status and action bars from the screenshot capture. bitmap = Bitmap.createBitmap( diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java index c00042ade9a5a..457d980c5ecae 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java @@ -4,27 +4,34 @@ package dev.flutter.scenarios; -import android.os.Bundle; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import java.util.concurrent.atomic.AtomicBoolean; public class TestableFlutterActivity extends FlutterActivity { - private Object flutterUiRenderedLock; + private Object flutterUiRenderedLock = new Object(); + private AtomicBoolean isScenarioReady = new AtomicBoolean(false); @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Reset the lock. - flutterUiRenderedLock = new Object(); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + flutterEngine + .getDartExecutor() + .setMessageHandler("take_screenshot", (byteBuffer, binaryReply) -> notifyFlutterRendered()); } protected void notifyFlutterRendered() { synchronized (flutterUiRenderedLock) { + isScenarioReady.set(true); flutterUiRenderedLock.notifyAll(); } } public void waitUntilFlutterRendered() { try { + if (isScenarioReady.get()) { + return; + } synchronized (flutterUiRenderedLock) { flutterUiRenderedLock.wait(); } diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java index 486d848e1697c..1fd0609df3bc1 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java @@ -12,7 +12,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.view.Choreographer; import androidx.annotation.NonNull; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; @@ -71,6 +70,7 @@ public FlutterShellArgs getFlutterShellArgs() { @Override public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); flutterEngine .getPlatformViewsController() .getRegistry() @@ -89,22 +89,6 @@ public void onFlutterUiDisplayed() { test.put("name", launchIntent.getStringExtra("scenario")); test.put("use_android_view", launchIntent.getBooleanExtra("use_android_view", false)); channel.invokeMethod("set_scenario", test); - - notifyFlutterRenderedAfterVsync(); - } - - private void notifyFlutterRenderedAfterVsync() { - // Wait 1s after the next frame, so the Android texture are rendered. - Choreographer.getInstance() - .postFrameCallbackDelayed( - new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - reportFullyDrawn(); - notifyFlutterRendered(); - } - }, - 1000L); } private void writeTimelineData(Uri logFile) { diff --git a/testing/scenario_app/android/build.gradle b/testing/scenario_app/android/build.gradle index 7f9bc60362ffa..11451c5bbf298 100644 --- a/testing/scenario_app/android/build.gradle +++ b/testing/scenario_app/android/build.gradle @@ -4,12 +4,11 @@ buildscript { repositories { google() jcenter() - } dependencies { classpath 'com.android.tools.build:gradle:3.6.0' classpath 'com.facebook.testing.screenshot:plugin:0.12.0' - + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/testing/scenario_app/android/gradle-home/.vpython b/testing/scenario_app/android/gradle-home/.vpython new file mode 100644 index 0000000000000..741711e4e06bc --- /dev/null +++ b/testing/scenario_app/android/gradle-home/.vpython @@ -0,0 +1,11 @@ +# +# "vpython" specification file for build +# +# Pillow is required by com.facebook.testing.screenshot. + +python_version: "2.7" + +wheel: < + name: "infra/python/wheels/pillow/${vpython_platform}" + version: "version:6.0.0" +> diff --git a/testing/scenario_app/android/gradle-home/bin/python b/testing/scenario_app/android/gradle-home/bin/python new file mode 100755 index 0000000000000..367e9b679d413 --- /dev/null +++ b/testing/scenario_app/android/gradle-home/bin/python @@ -0,0 +1,14 @@ +#!/bin/bash + +# This script is added to the PATH on LUCI when executing the junit tests. +# The Python scripts used to compare the screenshots are contained in a jar file. +# As a result, vpython cannot find the .vpython spec file because it's outside +# of the jar file. + +set -e + +SCRIPT_DIR="$( dirname "${BASH_SOURCE[0]}" )" + +echo "Running from $SCRIPT_DIR/bin/python" + +vpython -vpython-spec "$SCRIPT_DIR/../.vpython" "$@" diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformView.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformView.png index f3379e547e5fdfd3001ba9645444876bbb9f5848..731fa2cef4e5bc3d6cc81415571f6fe9d5e5da8b 100644 GIT binary patch literal 29557 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 z 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 KBN_j)zWqOZJ8BL9 literal 30028 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprect.png index 831086188cc152fedddf6690ef1e2b09bb0a8eea..fa335e53e51ccd6b316e2b8b20ba403037897013 100644 GIT binary patch literal 18389 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 literal 21585 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 Q5$`>B=8{hSDf_?v7X!F(^Z)<= diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprrect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewCliprrect.png index d6a5eede8c1981c997c5c59ac7a387b87f502b9b..d827032a9b2392cfb85f4811805d35db71fb0cc0 100644 GIT binary patch literal 20209 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 literal 23520 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 literal 53141 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 QXc!ngUHx3vIVCg!0OlPPqW}N^ diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleBackgroundForeground.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleBackgroundForeground.png index 46f3bc5f784040d64f82b86a340c49a2fe3409a6..03df55e98afda985eddc81de5c64e67d911b57cc 100644 GIT binary patch literal 11452 zcmeHN>r0bS9DbIUnWkL?nOY=CMsz`uN~hB_Q^bRzz83 z8XEW^uf3%UH(Ld+dbfnca%64c2O;OB3uD@*op*i6{~&Yc{dNu0=VDZmr0k<= zwOU=GR3?-M!XSNW15y$b6T^*0BXb;3-7)Y$&=Q2gi00;IZpsx(_I+k(c6L^)({cE+ z6CNmrVM>aNWB)=84-cE_fvIuVUO!@nSD_wbB$L&PeUL*U!V-;&E%RdHjUTZ$u5BPb z21<#==z!XQ#sSR*dLGa-k=6xT4rxn3+oA>A8g{qHTbMM$XtE2I1Gi1InWe${kAt;) zM>)`;Kb?0x!x_dt;;5#Wqp!esLXm-}jDUw*x#`S6S`g=Nm>?FJF zEo3a!LZYcqlCm-(x6M>eO{g57oYb%^ z%i(v$a8Saeq)0xWKM%@mHrtm1*0<6u;*<(+p#Oh_4xM6+{GMnDhThG@*Z)vt7i0j9 zxs?7>34|&?RQaLG4^@6hJzV5nWI1d1vmVR)U9>n zSXV9uF=CEKS%qd*`{R^Gpo4&drSb(7E^f7ZwJESLU`r{R|Iu8<~OB z31Og_3=KA;oY62CO$VcSVYECLEfYuUh0*G8v`H}9EE;VZjy8`;=pyU}b~V0mFg%!R z3+!Md%wT5+>Yboo03;bInMOIIVL2|}9%KY2$0wXJ zK+<8}fluTa(Y!EP9*mZWqxHgQbvU$}1b3^e4!WPSyete%$_bP0l+XkKQQw&% diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleWithoutOverlays.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewMultipleWithoutOverlays.png index a39406e1f5917eb01dd30d413101dae4ff01f529..24c9c9d3278e2500cd127bcfe1660541be1c03f6 100644 GIT binary patch literal 43916 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 literal 62517 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewOpacity.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewOpacity.png index 19269308da7e002c036d2717a358d2effa9b4827..30c42c9ea7d2e8c96414e002f6b7e45988033eb9 100644 GIT binary patch literal 22231 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 !O1S!Ts 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 literal 31570 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewRotate.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewRotate.png index f3379e547e5fdfd3001ba9645444876bbb9f5848..183f24f433da5000607e53e4cc0a33452655f4f5 100644 GIT binary patch literal 22559 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 L+UIl5UJd*oc7~lM literal 33464 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 z z=%O47ikJfzc6Ka#Y0Wrd!u7;=@80n|=453BT)T(y^`8AlmTZ0Do_w?ykR4oY^51&Z zvyw}|r&}(vFZ(V1x-oq9P(T#!iJ+J;XXq z^d&}{VtJ{Z>A~=cZ)H8~p7SaZ`>jgrdvonATH;kRod${wVY43Kk(U8I>kxV+(#F#G z3y|s+uNrJ#6VBJt+IompbnDm!pQZ7)=l{NP;F$O)hg=5CCBEeDfCRuwO=D@GSOLvr zp0@8yo0#uDc<`VreL5eN>M$(=x0tyKm-nxiJHnn9Ib(H9Y5N#ZI@6>sZm+=y^Xpw@ zL=ww;)cF8~$1BtKDb78p$N4R%Vk?Kkl+0ZEk;oc)arCVjrF{D_hgqOewsj?Fk=+xy zptsEfn>09=z1)LnN9J;;aar1Q`99v~N}Chf-lR34((OOjLy(GP2uS`Z4WW-u z<=0RZAL1wre7v9IzR&d6zkdv!=3Tq?{J3O_pkB zx&P6_RB*U1?PEls`@@ZLv(Ezq|0{~cj}n0)AbFX)HxjlF>=nNW3Wul}7YEkYHIifs z{e5F_pHd*aWTjt%Ov}q>*TcSX_NVs^Fc;r3HXeS$=@iK6RB#d%HC}o*$tUOd8EW=> zCI2mr2bCexbgc12+tP)^uozEYDy2VL0@qF#<1`8 zM**l{G6MHswOoF**NV2hl3*MQ^&2380<*MKMBNC}j7F>8*atT)5Zyv}V4JPZc$OK5 zXP!ExPcY+V6@BtM1q5&(c`|y}6*A5_tf~O8=&8uMWtsl&c0f3>h3fH!8gGQ2rDhZf zh!hMy6fp%Rg>7Av%Ld)=4F(22 zR4b=-J$(uduHaX=hL6?MVv~}Rb>j!4_EIRDfNjls|IE~dWdSX&{%nmQh&VyKTXRqO z4jx7_aRD`>IH+i4ok|EpKwYDY8H8^nX^$8rp;NN@lf~aq*K|$jPR`lU`Y4^OdG))n z2jz?F$%c*VcSBh0DySltS6ouXJ4nr>VWyct2ZSmz{(1TA6x#k4ZAsMFc0m4 zz#=%=4=lvstAF*2lL$~%!p84E`3^)hxr{UtBP>JU2amm4S%mB>B$@s!0s+41N1$2)(Agl(68 z5XdjmK%H>vYU_FOwe7QJFgl92jQi&E|3)YO7r)Xogu55FK)#RblZ;TuMyt1i=D8yn zn(3IDa;tHcC_^#D=T3yg%L1NEsHUE$C5&sbaWfV?;DYNvcQ#+>>G_D02ly{G{<KKaz0z8uno zpK(JvE@+tG9?W!3@Uti^-L$4mB*l_PTuu*FHroT!Z=6yB)vj0`#KQx+;F|%)mPI#I z#8eJ&8C=hN_35ysdwSgLy;E}<*!A1DEI@;tt)HS!QV%|+y;z;HQR*8AKXnTCs209= z;g@-{yWju$UA5E))J%RWny^_(LEW&WBXcqI7Y}Njrw;Gxw|(Mt^}?Uy70(XiM*fML zv9qxt%OKQDr8xu2&DL=iUqc?LC=3R<5t zCEaSH4EBMU?44!k$ll;aJz7Mg zCXX&tgQWsiQR2P!=vf~f-sNZfWATHsT5$hJ=hFvoBMX2br@G9#PxT+7^?(M=KoNhT2sESV%h-4J)g$L#C?lQUb5=byMj9cO5eA z%Iq&sk{WQx6165W$cLE}Ke`n;a{0v?f}tcyZnt_d@0Ih_>&adQ0O@iD;hT+5{J0Ak zk-$BD5gY}l^z-APF=>Yca$yr=qwuJ1YlxHJK=-TySc+M(jNPfM5d`1F!A&?LQDsVq zYw&E!%8tIZSjZSZe(|1Vi^VR?w@c@NHiRICG(8&n$ z`$|6g_Lm#f;vDR^GpK{1G*MbPaOT4wZG9P~ zv|eDf`&dLKCIj{#B|&!gaRez!6se2j!LdsO z7H37^rr_AIZdJG4J z269Lr6&_`afW9U;Q$)>>845gY9*d_Q*00V*T!>ZMWYwMf(ZM!I@>mB(y8{Ip~P=lnFI;-FWe zXBmA!baV0Hp8e*~IR|=W7EqT(ChCqI(1AQ7o&k#SnHz6kcptFy41t>A{LK|aCYHc} zVbIP(kffjKKGEv6)S^~r%KRQwO}Qb|4VQ{Qd$x?p0{e~!+B(3%`C$?X`2Yx)U#9}2 z+X@Rv2ta(PQGT%=IZntz$pp1gWFQ~jQU=P^Fe*eXZealQ$8k6TYISem4x$FF*~eo` zT~XM);hp3>NCrwkt!hox%W@ogvJ|5UpFf|OC9J3hAMu#jV)2`MgaDzcva)hH^^K6x zA%sv>RFMDc3W`z{fX5;%NbN7Ke{0zTO$jmRjYR(@S^VERh>%b=XFQRx!7gcYkumfN z=X2WQD-0SbMGJr2aKES`6mk94nGuDk&tN>(S$w!xdk%VrK?Mk*Medu~O^TcxrtU2o z+ls&ykt5?M0=HbD-9J?8T6GtZmb+SSqu4g#u?_enC?+8<&$mZMJJfDu1i}tS3r(~N zT2xIhmVbH;ikOCYpnr7Cy<@grxoALJXpWKc!+o?=jYiD(swd!MzU z@5~jMhs6nxRsCZYdo~0uuIt)Jq$f2dPWfpS5Qv2>=M&>pEDlbkcVF<@rY9^0^1c7{ zo+xnRP?Z2>ag~bQ?wzJHVE+2J7^Kb;?ehJ1keH5xdVin11-@RDF=f5eiI>nD9ipir zOpPNDbPbYXi(%Qyg2A*b8kGDWY_i}#TOTE1v9+<5c$PpB=m-kRm<(Y*jR4Qxw$Fg% zf$bbZADb^dS~^Rl4-x4z!4{zxt5O94vZ?M0J@f@!FlfNGckkBp`6K_L54eR>h*6^+ z+vD$VMZql;b02S3Ogp21K-#KLE11BC(dXxrxPK#sFhd2_9;@>V(pH(+Rb@~*sW+cu zP%3~32|!s~eXNoak{U%Q+ki5}K5NICvBvR&1SrLz_6#X!!3exMW5v`0GVbK2B=NDQ zH}6gQ9@5wVB8oE(!V|vJ_SnU*cP`lwd?R|n!(PY>12yZ@Vtadg#otf4k38kR->u;Y zz}FQ}Qw6*mc8;9gC@ZKXJMjs5Yu7>73{cm3$w1OGFl7fNZnBx!WBtAoC}O{$8#7RI zP(KzjO3qq8szDr-&5GTs_0#noN{_$0rob(1T67_M76G0g>aDsnT?tY?4fbYjR5w%r zuJcE;K_?VgY;g9ISnNex2=I+hDHOhIMyh+cK-jC9qNWFcyaSfLfQ+Z-Yd4nAD?Gk) zml@O;5F;bgwiX7px|Ws+^PsL*Bmr$*1>zLxtqDqCfR`pnIzd+HzQ2cXUCioRDS-^g z%wu&Q5vtw;(1Vv<`R*11Z(PhP1N(hySY4nADI?*b2DXn{t`P1nzk!qqhe@IVmw7~%!nZCIzST6;ack3*UiNwpHu&e=5ljA%->IG|p0DDB-h6TF!>gVyqc4VM z(~3ZS2+p{hkcP?29t0C$Z&A>UH~F)|&fXg3xc$+8qTRgoB_jk$NeHUTul;((cZ^V0yUtvc(meVq;tNPV z+0MX76nM%K6Zx_UVAhPgYPhbqfJnKR$8l#GzJRXnH&#|xz**L`Xiu`YD**KfhYZ1t zf>Uth7ib+_T_n{0CXO0$z6PUPf(JyIczZcmUI{wr2xfES#$7Lv^r+gA)3Uxy#rn9Q z%F2U63}IDNmd}9k@=GCEG|T$kbRON?#kl<2(NMIb_@5wo1#IufQVa61Uy3hOS-*FD zlV1uUC}{o$H;hD$itjs;Y2cR@|K|pQrUbO;J_Wy0zXc6l00exHX^X}$-nnxJL3Mef zU%}W7^T?`oXp^x$o(+;+Y`NhH&5!zlnKX)o%*U*h@Mq70aCT1iY!UD~*%FZ)Ks*m6 zlLkRM^|YqIR^X>!33Wx7C;$TEny?FP9AD-_Lp3*wH;FKMen1KCnSMQ{`~>)Sd`%TEO|&&?V$`) zz>1*q&f6$1M%5FeBy6gn4s|5DqQ;5zQR{6$^A^21cTJDmLx~8Q&Zj75`&-;>=jJeR zxjbsdgi2PY;buc~obrAe1yH4_PsmT;y&=4y2PFy!UXOOH_W{MgbImdW+yxo9c_?VM z_^wf3$PvUp0rK1vX>jd;AU%hVzwccFkWOZ{WRa;6dK{bn%cL(30+Dr<$i)KDq`KsD za`J@LgKuDnn*(J>!8z0;P=|z_g@kb=TRSxja6>jQ2%5E()8Iv$#t&~LY_tyWD$2v2 zI0P$TgYW=Kz8QY~>oQ6{Mb-65djPfZNX-CgPZ4F&9-h1j{SlzXQ3>lC=sb&o#f?%i z@^BI8wQnWu^+AgMG$@Z;tq)#9&jwP`m3+Wi|b zhzWS`d594(r@>XoR9)O7eWbs=>hy6A5fj6u59n?M7tLDm(hZZ!tC0!|h#X1yPGMNv z9{-bwsV8|rc0UcM*5}~lu&a}ZKKVK0m+Gk8Dp_ZC$v6zCuVHK z*xK3#`fm*LB9xWTHyj2E=6FJaw%l}1{b>v7TnITLym6OnvU@Q&X16f=1r@yI%GPUfTZ;TC}w|N3hRhhtiLI3hgA2Y&5P zBI-9EJfGoNDww?u`%$>ZWb>Bu<~p+4_7jhHM!|hG+svi!B%-=vKsx6*7|71Wc@StS z9Bd@)xOV6nScwRWB5MYPc%qWx+`G_0LE`;k0MoNR@|~df(H?SU+$SZ0XMkLm8roCz z88~+#8-E%QAwEP$Rw3S{w{#S#PbFX&2L*mlB7@Z-j2g)i1#KoNCnjI$N;I!DO{K2e zK!N_p;S8TwHw8p;U^k=n9@1w(C_E_aR5Ig1pw1hD@D(f#(KgrGb4jm-`nzob`9IT! zAgn6k9SWQQnhSr-$q4~tDYzzx^yQ9=0k#|lxIpN3u}DCU%6wrHKI4n#Dayi|e0Q(v zhF-7iR01~938FVBait0u-(pb$v~o6LZt&wH<{(IJDqMYjv?aln447}fG?1v@^jL2d zSsk!yk1jp1aM#f>1k#Bl!1}Z-JTqkYurwE0KxtlW@5yZ7b7&Z8Z4^K+8U|cXMGbm7N>}fFpSVdkY1#1%d;RLvt~NPzg~#zs-z&^gm^l{%%Lvt#LUmT99Rt)hCuvt zFrhqcDbUu7*ql(ld_EPsDUgz?kspnuTNEi2!>=#NK=>sA(21o*8;%I7Qx$miMR*F( zM4vbm7k^;&Z6DX#cStg=gTkfQoJf}j5fujycmot!mAG>|T!QDJ-g{ipmry0Iv07 zBpG5JZxk@S@-0N&5Q{+GCVKBvrx4&mVHD(kxT`(n!|HTd?Tn>CpR2eTB?LBxx0 zqZJ@sngcKk^(>{Dj)>TNv$wT9+r14(T?9mlZ02b=3`d;c7Qe0zI3N=P0nN7n_Q=Sx zRLuD71qK?Rn*w581^rEOmmP@*9SY&KT}zycJ7 zd%fyWmUyLo-0nAd4InOZ-X((eOp#$M7Osqj$|PTGYGPud+KToE88M_npgId-w<9Gq zKmgM%CTwE>!0M*@Am2s*ieh9B953zR0*|PvZwn&DD0)Ri8P>)2$PiS2XqsXrzmPY( zGX+qA*)OxO2Lo!^JPL6g=wKTIX{36J4otIef-3a76LwauDSR=ESir7ONO>w95jE?N z+dLe*s&Xvl9JxJN$D;Y;<*3a_lY7^B^&edV((=*&=y?Vb3Ks2zvZ%xAJ~Q@M`W$^~cH@QB6e8PE% z*B10!v7z`7390rXro~`-SHMl)`jV0|Ns|YchcaBlA3N2l0H9?a3^{pc3VdwuTPerh zy53HhEkOBAS(Ep(xk({#H?eHKk4KRD6lo{?3t5L?XE=K^mKgX@jVX5)Hmw)%PflNb z9_U#z%q?nf54!=GU)ham2i*ai_56$hmMa5CRL-Oigb2i+`rX?!qdPYru=t+H1467t z=xhJ4KPFe|1cY&Vjso`H5|M$`rQd5f#Jf)|jAwup@med6ovWdtVb99MxFb?2hfyRt zpO)mcP{R+N{jpnEEN{|>d+Pb4IM5Y?d?fh=*^&WM{^0^L)91dEwV;m`nd%sTKkFa# zl*i|baqK+ObXQ>&xb|_mT;lRu{i%=<2OxIV=C+>Ra@M{L@b)Xvrj8#)$0#zR80li* z9#Izxlz@>S+CT(qd=)xXxqTN*V<65HsYLznNd=W4a0MIz(+s%f<558@GBJMZ6ZayU z$H1_;`6gf(;DZMO>->*L)Gu=OqM$Vt->EzME0sDCFZXx;FR z9XZ1&YAkxoYTKsm#K-J}5Zoj*05uc;&1DE2!&<}ca8mh2$Oe)IDgF=f_PC9C5;Ts= zTWZ|wh=R8a!&WkVHFJi|)EyYjFUQymcW?6=w0eRcxbE8sF5VKjh`NwjAt-Sro%xTc z2t^uBetqER=xBIIA(HM1{{Cp*1Jc-L6A`&F{#$DbOwv-z$UK_?k`?|O8iZx)YJdgg zo4ot@wPX585lhj%y+qTuivY6nxJASsH{EZ??|EAr2MTVvi6a;0CA=5e;MO?a#_q*rR?Iw!)qtAwYWN zQ5K4nYPOtGvWcwzKIbw8H}-Obz|nXHf;(A|Hmu$0YzFg zL&lCE#J=Bqeh9-)S!9C$TfbI3IhjhBaSwY5a#tmiu9zvClO3tUC;5O9RkV_ zpQ$UF;dz!c+fLh$iRyxf2ga^!kYuoiFW$4 zH)7u?h{-3IWd;L)!|)HmJ+UI&gS-Oc6=E@f1xMstxm?K`f_J9(?r z2(d9lJ#bc2JF%OU!X_(P7<4)`dJ-6DSa{FMccqxpP$Gpb*lE^q1|-H{gigT%a+uEp zIh4ag1TUN21+U%}NJv%n%hM8Lz#D@MS_oe(6>N1MZhiH(i`!(8QoMP;1H2`y%Sg>7 z(;@6xs2Q9T!}I-KiRjiC5Eml;CH1rFZ)1?e?<3sb4Wb9r6HY?T+UR={Ho?1Qtl!}sIWzn{A#!d1cXX}-S6P4Bm`yTorj3QJ)T`yK3L!Wq8 zczKeMMZS4@{CfBTf8-^MBgigTcmi__7pg<^7CGtYj7 z9>!nbklVMc=)VJ4H0H>A!S-gDRQU8%mw&RB-deSq;_YzPEW~nDDYyz&HFuu|6n2Rx z!Mm@U!Mhs?9>^p6P~pAZnWpVCQNKV|9lwV>%#oCe&j6Mu!_l+`)DVRSb;K@=q%@t= zCQkQ*MnUu1K;^B}=XMM1j{XF?ClKa`o3U&4Ha-{p#01~MF^x$c*SwmoFZ-#WhO&pl zWVqbsVKJtO zTER2XJph};S0*MV&Qt)^)JaOpgDRG&hoV(6LbJ1>F^c~nO zsru=)Q#i;h-p=x7mL30CuPF*Mi5iDKE!jXMnwlFDC=Kw*>=*|IQ!RCZF*GeiNMjmh zbzgZyJvlrJ>AQ7H5g!U9EnerB!i0|5;=8V;>jzOg97Y;XY`vZcu(rBBGc1-eoQca~ z0n&T(0|2L`4yI7zTvU_qXHwn71Qu12d_3e_h1N`TKHqqT93ETIoPF04A@@9<{tTdj zxnAR|EsQ(p_-ELik@M%j1yO@1GrT(l!%?n;-44>uhqYk-b!OK?e zs>By}Ppd`4Tc2ZfxtW6kEPrjom|e|L7*XZbQx*fb3JLS+CRm|n%7N+tBviqypYDu! zcxQh<7X(VY>8MDeOQ#pa*LDH;bg5Jo1HmwIXAC5~5t>-@d!{nMn{iB=?e8)QCsR_z z>Hlz+A~X#%0}um=!bg`P;NBd2pql^adqdg{XpW=L&hf$_TrYU{U>KC>b8Bxw)ix&6 zLO9Fe>wb!d`jV!w8|FAEqzK(NfM`KnUHDalH@t2I96cHm?8bR%zjo4n&h#V34!0Hx zVL5zs;Na%wuH_%~UY|&!7lhX?K!9S@kryUSc@1tyk(WM8k2pFzI6Ma(Y32GsG}uPB z62^UkfL%2;df*Iv$Yl zf-G~-?IYS-P-q~Q$1XXF+AD;U1oJ-KFyepk$whY#2Kge<=!E2seoWc~GeqaBKX|FC zVd(IQ0D8De}hKDiiP_&cw5R6bN5QIOW8ebG42cs>? zcx4JesVl$<8zMOi#BY?u$9AIOA(`=ykAgC?+tH-hDwrO+=LZX6QISd{;uCu1eJDB! zABPNFR8)K?bz*D#{6Gd>a^;J#f>C6cM<_|K9M%MhYw!^b`E2b4vGo42A`iU(fgfnL z0<|mbf-55GKlj3IXN3rd7z9U4rUwQNyb47F7OUMP#m-?@7>VLW89`v=5bE(J0C_!U zm(cR_$5S;AkM**4u;y32pmCUH&7%uP=P+v71oxOf=iWADlY74&bqmBH-i&}$i9}r_){ZO))mg50mEiJUd}iQq`2e$e-hBk65;ecZm7Acs%F2efZHny@(a1-)g^|Nx)pn z+-nIdFc0;r3MFfqr=Rlrd2zXXV@MR(|K%@x&OY;r-ZzGMd6N1Ua#WPvjIWajyt>>8 zPdci)|CSW9_HsPHD?fN-Knw#<3J5Si*qv}B4@BXkQVxdb)>utLv=S7@>YVsE$r$5~ zL%{~pq<2-FX3?o}g~G@6r7E)pu^TI_kL=BdtRI;VRrem%!?vZmF=8&+?VVx@4Ou7+ z7l)m<3gE8wM^Jz|x=w9^ZK#d@h02tJrJQ!|9oEWr<_lkny6xIOeo0*x*dOtO2~Ax< zT*R=z#m!S3Ax*O`(s*A;JJi-q&$m#m4PSqJzgj>?8Dql zW7xj3^7@<(WLQG0`&J+;wd5*!$^Wa5vc3FV`}%61h5c}iAD5F#$=jPsg(YHJwC-N7 z!Q|!4&#`IeVcd!+HE7`CummcE;;dLEv;$uU7`|3G&UsB)M*}CHS0FYjYA@t@P9bZc zyF7DvwcO0~6Jd_p({?3=NtehUDOq{C$vNF#qU z&1(2S!OXnO98wpUwf>fS)Og-I$@_rJrQ(sx^_xJ;_YqC8v7Y4?eO7-a>(q>( zZjZV+F{_*ot|kBfd`C7Hu)#<$Wo5eH`d|S!9(8d3*Fw%p?|6peRhH0!Gc4ykkKKF$ z(lU$(G9JCzDV>*D8r z$qJ{o<}DvZz<8kUZg_nr?CvW3{>Pk;bJQyE`#Gm-Ac4QIo9mM59ZLI=6;X$|JL()c zIk}|G{ha2?uQsoPf*u*b_~jald@1=!{6KSa|B-D!AADgQG`RTRWded0n^#q}h0rK6s=O z^!**~V36z#K}2rh(Ky@p-}ztXDEc_TSqMeJV}z6u|=}{H%gGX`C)cX#&J2(Ze^Gr_t0m60e1oRU-(= zi3%|$B7@_C+9J;G@sQ!#yi>V{D;9#;FeeLrSuu4d=xQU8Q29QynI0~s zqLG{_K%m^cv7oElIf0+#9Ikh*hkKAc#KAy^D+B>H2XdA+QP|*{RdQytLg($Xfq&dSZ~#5KVu8nKiY^+mEuVif#_Qzv%jTtwY0|+^(=!L5Lg}7Q`T`|4 zrJ8+bl+zj;_e%4lE;Bq(0w(^!u|w!L$1C^MliErf7WmqFFHx!&1t98)x%OR<%tZbY zyln7s?LtvkDL5f069l@#0y?&mA!e`6MoNaHXXf3yc>uI+olFK2+&6JLGVaXYuRxr% zW(xO%liX~-`2A9*dpi5>kx@C=^GYe*kYl3fgoF18-XCRacLDHkz+vr4Znw=4v0cpD zh7pxW+vs=zJ+de_@RM!p@BiTG)>Zy!ABV!|Rf2r-CwM5wpjf%- z%jGo=omuEr3!xqWQ!7F0#OmT1jV#=ZFI?!`j_W`Et}i~bPtEW9Yx z;6tYU*KBhN$xTMHx;)??Fi$INKF?CzCg7*-2bi^y$^=;?PT3qKQ1Dedq?*&oC;G=l z^_^{lU11~+v;Hz@yYFsXmo5?}!y6=k3)B;}eTNRi@Ol!utssD!GD$C>nG49#RH6jR z2o6$aDC*C4SIvzZQPZsw(Akr^E2iZ2#vned2U@EI_A&(cv7F0txUgG6l;!Z!VY1-o4 zd^!s;z^#Ad87r5ovmoRP56gg^sF3a4U*HNMswDLI2(7TnbO^hE7$#3f6UjLEuM7mM zk<~ZjK7|ww4M|JK=0YG=A_Lp%i0qVaAs0k7>*{O=;PC&^`n9ywE{&(g8^WaqwG zf7{K4I271nee;CGo*r%*1# zhUT%7w8{E7Y)#1evC6qOn&>c{zh83_+Dq$EMyN@!9mE)08bvKoaBg0>P+4I^&WgvEz}Aw=5@8VzqVAp(y9K| zs_qSY2|R{NAg|XhHGU?VZ~4OH9C37E=3mLi#!_E&tdI_u0Iw}z98 zC_4w5a-bk!_}?K~i6;5B5!W5zJd3sttzThTmrT1|>hy9sNCRnfsXUxk={ab?N-lZ@ zPJMM>4EsA3m43)=MTc^-qGY~^sX^Km{^n+nN3g#*|Fge1Tu{~k_73=UFj0aUZ=QkP zGNo*(Y?yoYY>(kiBpZ!AvXzM1vxI>&T-J7R5wG-~cx3_7VTH=ufL4NK#idT+ESk+> zSlpVH4sm45MctyJ-Yq)a{^L9x{=#^vZb$)+x;P?xF z%Vmv!uV;!^(BSL#TI}{S)KtCc;1fw0@H@Cu)AOxqC~d2dk}i$%oP3;u)VK}!0dvp8BIC}ci6v{R9Y-hpQj%zy(5Dw%V)LcguP%7kUp6}&$B=9mj zKBj3r3-p2p!$e!jYorPP+-_&#`QI*zfiB&Sj+b{IZlUN>=OY~U4tP&SW3#oHNyyCN z)RKpsNYo0NMN;nL2SJJ1(TMQV(y^on9>!BL4UzV`w8sm_enPAG&&KL5i#y`8`LS7f z9eKd@M3|*b;OC1_5g^E+xRQ5Y@u3$E7a+e_GQFlHR3p1rMo_oMeX*6SUt4-hV4(V> zV(Qj+cp&5@e~FLVPu}HHe!z1sUU_SGK3V`X>Q4AOAAyK2ZZn7mj5%&YiR+1*h88s4 z*^Qpp_%tsKFN0=5OQvBed#3Y)LQ7AK=tgY$qm=2*W|nrAl+beYG`#gFhQgZhtLP=@ zDPi2bHcc@D(_NZK=5A%{P<|{mQ7>!t>2;CMpZ1+8k9`U8I}ev=miqhqKlPa)GIbSz z#9gGTrijo$FfsoqJ~!%aA}A4@2UD45Q?JY(*Asosi21mEteF9t_|!{jNb(c_sgm2h z>#_c!rO2)W5Q})zvw}7{i{OL_#D@ST5!0q41aG2emhcR(Lx?YRup8ANI6Grjjx6$X zmm;+adzQv~ZlBA^y85j}!Qx3*MRsbfY2`O5>(zj*G6Xx4x8`-tt|H9b=Le$$?zZ$A zZNKo9UIRFYeCf&jU_Qblg^H%o16;^7O@jxhQA{)|fg4w^<04g?ziSNInO{$tO>}Sg zvVA;1}zh_LfHy^D;ske757VH{MI7Q%Yi5qhR6%g6|>_S^`14EEQ+@YyU+* z$Xb>yH9@*%yXelCVtS)9$$#5$Mv8CQZfx) zejd#?>up7Dg~3)eOHP(X`p<|++#?92jyPgb9QvSI@!{Wg!hp>*prC9eN%>koyrDrt zq?@U`TI#OE|7!2r|DjIX@TiS+Amp61Ne8rw44rMFvM-^c9HX|^Y0NrxAU?LzOVbfuKODK z=8r}`#(zOQGXW#40__5zh>d03t&`s&zj5n@tsqiZEEbToCs8M~O3*ViBN9ouK8#fL z4GM_X>4?X}a^LY`k=x>uk^)J)0Ml~ejOW($oQKP;XE07y%VrJz%y@zwi0mjr4cK8Hb0k-IQ8p`qiO& z&NEU1x?eAR$uQ9y+j%%Fx*jNc@?YhmM+Mt{B{R?DfzG}$qW?OttjvT;XO>3LyN6E@qVbF((7T=@4dMR)rt`eB3k<6!OE<<;xGq#NbacetgHrf3SWJ}3hK>#zzd0@= z&X7dIkj9JnhO{B+BkqbYEl@UT?Rt@365_Vjv+{4S{Hll4PJQ_$dVZ%{KId7brZafX z_6<%P^`0-7DH}DF;x8vn^B+8jjz;38dsCDuACYx%iXd#LhTdrmfuye9#QTC#! zx!U*;Vf9*4G)!V2H!fXDicPZ*aTZ4GlJUW3LyLtEvg=QF8doZ-D?s3LjsSvQ!#Cnj zuR4e)V};)|FyQysA}3wtmNgu?8}jb8C{!e_$pqE;A~RNXsT8eYO4iK%udMXgm`8{L zv)F_Yf4QSk{1!(~75(@p`nv289AG{K@b2PGH&|^~fzBrar)m7JVq-QGL=xY42k(G@ zSQHhVeT5a?KBQqRWT`J2`tu80Cr-ce=pkvBG6ZzR(zL-jL|b#KX`aR(t>IHdI?29( z+^0#T9hmBiSc=eol6b)(SzD zGA(M|Y_kX1`kIcC9Uy{tC!+IkLt6G-KFG7DAPC_I{B=qug&162jVK$*sRD6unL9Dp zV|W|8ZvBGBM#swMO3~=<#S{)b{odAAinc`E-M*Ue>nveF$!exYjeI78hzJ$_7G!Z^ z9l@x*ZpzLIJSy;vo@PS^oeYQ=d^)xlJ(?CM3qnJi!)J>-BlSky&tD}D9okz;n>rU| zan$#y*0Tf7>H1xJ=4NfFw$;nd-us8uEpYI7D;t%DRZgxjJQHv6W&qJba zN~iN6zHz-I;{UF#&0-n0-SQ+Pd$nNXTck{FTqdp+ua@SwY5T=;BtcG>INdqlI)l4N zuPfdBja6)j2TZt#6|s#Ci?t?viC;%!Qb3ip=iEO{D^J(oHaw=jOBgZOkYLo(J-
z;eAU4O#Zj})4n?T)}^?$qw&9C#$m5(W|>#BGRQu5NW_9*wb z{wg6dG>UZlkKPxQ!Gng4V zY$~ci!m5w~(Tzo<4Ej0i42#FgVnuKif(|$T`}(Mw>ElRUykA%+>jq)VKQOj}79PE) zh{5uzAM@KdPttfjdWK{X6C5oSB`Z0-W2>-a#Tjxde`TKR&^#dwzVF(mvMn=E$G0O? zIKNQjU3xFID-wSNVYngYJ#)0d0&LR@fk2QN0eO&tg~>uAqzp;&27hzy?>8RV&*neh0|W6 zDyI((A7p1>1DZ+S80P?!6vsccwV^Fi2_#|1i&@DVs4&Uz=X?=;D99Y{AJR;_Uu<+G zcpienjTLg|7`*-P!YMDeQ&vzGaYqo9&`*_4fa?#R_}9Ud05Vk#GA12tDc<{kilyhC zkH0lX)7b22;u^DVU#;^$g<)vT1-6P&+YeMoS!$r(1_RrBNLwjj@7V_}ISCIx#tFh| zQ;vJ>+Ow(9=#I@!ZcFJ8nX&Bi7cELb+Lo6enk8d?hV*%>Z`w>u7R3|3gTa$i^tV&E zO}H4{c3RhVvVh{W^+^X_EX*}6x>AyO_N?0ajO84SeX1@jjSu@4>c#xcjihLTad;Y> z>->JCaWyy@<`V!XKd;ACEeS=8bbc zWOOtE{@Q<;rr<7Uxp!naNrT9s*%`p?X`#<>yQ)PrYeznv&IL`zzz3{;W5Tfnb+V4m zwZt;laf^5Oxr7%@Ff&};l$fHsRB$U(^;*r+~T$r8#H44UZ|{GSD`A z&Z15Pbt0(jKqW{DBT)E=!bem#q_QEE4XJEMWkV_(QrVEohX0IgI6if>H;N1^m^Vx{ z_00}B(<=*lns#tE8IYrepMs!Rj{pdtVvzs*lUQyn5u` zSAQdDC?7RHXUc^5TdP@;-Eu+T@ibL-h@f{a4v0zTJG#k@D<=ae!Wpf7mT* zpOc67hKf*seR*$Wg(iCP+)7hlWU=RrN9k74O5e2G>&Q#ZE~EBM+-o-562mecoP4~;}+U=mp`|y#2gNPCObh?q6rd^{j`CuF3 zxw#^rdiFndJbf4bfAOuHX54dbW>HMaPAV#O7CH@Y=E(2u&#%!_Q9bUp_1mX;?IT=f zF3pHhf2Lb~(3_fyDh_zEa#ck|MUp_4=jJ+h8lv|+C!W#rir`wix{fh0JiB7_b9VA+ z0=)m4$5!^)^=CmHU$kqFm9N$q5#;IM(Rlk;8c7v8H(5A0$>rN_CG%k!sSEya{qY8B zn!tF??t(cy3u%2agMGo_Gb9mI%#GI+;-B8&VF+?IwMfwH!;%7pjYFiCk1v;4NVC8b zUoc?rKE2`n>uci$z7yv^tGqkQ;AMcJO#<)&o*3Oh~Zxy96z=gjsA9ijhYIb** zxY)mUnjf~>&&fcGwAV;%;nu{a-M|E#A|&kFN;QBSs6_`%a* zhs0qryre@+61vIuR;haV=@0wP9NA}r52_qJ;5uK=^TqHqTtO15yj&Yq*RJ5?@&|)4v zy)oKbFjF*%ztn{X-m*YeRrOuf2= z0_qnYzIPdk%xb()e1MR5ul^NrLCm^OXU@#fso$Z7pqKYBdDQ`~SAI`R*W0Oclb%y; zUEHkcJuE+gsE|Aw9>B?I)nAnBOn%C!nlvV2B3J{~RFA2+OgQU+ZP2-ue&>YJ+jA#Y zT-rXX@C~r>+XcG2S3cRzlwo#`1DJ;FgA0sGddW^y$C)pfeve=njrH1!Xnx^5B9!sv zR$59A^Bs-dR8)5_di&(HB-KBMeYMbRADfv8K6~?Xb(@-u{=5h$y>IgF&|Pp9);H;; z0Vh2nLd3E&siJjoP$szmx6<#V?JgY>N^y1pSjJC_Wz{VmobgOwp^3Egd{(`(O9T;? zxGLrC^CpT_Y>!pXSFT&BIt=ebt^Cu|)17N22N-{MKklbzQ!0{Pwm#v!wGhkhHFwObb&5+XwNG9#cXW)hp&Xwty6sSZ z5zok?ZRp`3FW-l&W&NSs^wfP^$q%Sb4dT#Rc22 zQog;piWe)>C@~EA@}~WI>1R;|A!P1H65c*U9Yo7-NiKRAJ-hOa2%sU$BV2j8P37Xl151l-YPGMpl>Ap% zq-r!~PJe(W2aLBRiak2-ql|+MiT-COE;;1|xbiVWcig2v6}xK4ZHa0j_Z#186{q6# zy*8I{q#&{2-ZFx9am|Co=vynz>>+i?AW>(37*Itg_hx3fER0}R`s|ggGx8Pm*O>qd z8X||{;!_U7W6fec93S=-mRjp(hCNg6WZFjd0Bqw8HtCmNyWO#R&VwnNR0J#J*Sfy z_j|!Yszz|JR84X^hgR+G8!{b?e!->s*sLk@OX7i| z?JY7%ICf2W&htEHEh$8XEq3EK&llI`S@%)Nx%u$&O8`Y%+-&ZDDv8S;%RM~Oi}Caw z#*qn>nef0&0$f$C0*@VkUN%#)i#`zWt5hrF(+zC{m-~INas~!LudNN80^45^%^9QB z_3D(ppP{C+NbfDcb!chVY;VnO!+wm$xZ$_SUfUTTR?IcA$2DiZW-*Xr=|3`9G6AkX zcI@h(+T9|(XV0FLU=|M#4=$2Iym}JWTK-7l1{w3pHm5m^|j~haAUaI zdTyBOVymKWzhSOLM`mML`kg`{gVm~IM`s7!7k(rRm=N-;Y6vnLzvw438_X!s(cAOJ zl>hO2i>&Mt^VScqW*6S(mv9ANny*V4EN4hBnn)o|1CYELEpl#l8(;+MSU(&n5l=5& z?!^-QIG;;NFQ)LjP3LNF9%X6XC>3W%M=Ds^R=_B?=*sovm9t0oJs>K`SN9d#2jn|- z8~J{124KEDx7;G9b)jhNgHf~P??O3PUhj*F%QY6b!>yM&n}f1oD$n2ZCvSF+_XE{#y8a0oPk0(#>RI*KiK++P7`EcdxMDER!HdVbZ>5LZ5i<#Wqq(b)id||wHkgn+)#QVOJNV2Zwky} z`ZFKLhSP=n_tyruLbcDhOj{bVWVrmWWS;M#m)ZD}i50w+bjE=zEG8yKC(HP1{pd{Q zH#%IC_~-$`))&J~UOMd1W8PaE5RoA+^KfI&@X7W4UuuxM>C)_G@U2t8X{Iki+QStY zk|Tzek>9PSk@&;L(|%6ehvx+AgYloAP(;-rG)qjT`uTNd8hy)5epxB|v%jB}Pdh!; zq90L=$BxENd^Cif2KoJLJH)^sIa^|n@m&3d15_4yBo({fo=A-MWlI?=48ueTFOpe3 z7F*{a4Bzc0^dwTKTFR|cXlO-R1HOXvmI8P)e zHb@&hh$xuOBM=+-GE&z)@SI44oR%Fr$zfL=r1DO*#$_~Gdp@f&dNYVc4FB_!lCRWq zx>G41oz!BBE5A*|jW#Z~sVv3!t`o^j<+WFj>0g}SJ=Pa-hUKO|qPk5&q%2BP{qF#om@A$2$RHME!P^DDUnv1< ziQAi=(=U3|9sBGoqKB>2!E~=rS@(IY^v@MGdsw$Gk6s!TpKA^A%%SB=$*?ysdE(| z6SKLwDQMN3v$|R zt0lh<+9ZuhSr-n!Ef|Ti)XrLg4564af z#IjRSon&FeSmA@mZDrQ#)rfVRA%T8=Kos%#AeMj-5!Y9wS4tON04+K)yWJDJdAsB{ z7k^~Cn+mtC@(4MK;y8z}z4AQ1LGhAg@@%=+d-wVJ`>~Sq`_2aiV&uARPt(d}09+NU zjl5H|l35+{A$@$U%DWbcA`6LrYlxnEQJk_D&*U_FWfSp1Wo2a=uMd=OEoD$v1PA9; zMvvQUsR=xWcc6T!Q*W{eA5SN}Iz*g4$Ret|?tvDC54sXBaL)RGHOt~Kzn4o@Y)7(; z9z;p$or=cM?&(2ubgeptcK6*$EQ4p(x)r78Ly6Nj!XKoCht3&&xKKRx^%`rG5ZJ{p zVk75z;#MHIcHv&kHHUfC(fsnwX$=C5L=Ld#!$pN_R+7-HwEf z*Nv?R^^5-A3RF|Wj@;rb5cy4FVmwf|$*&(`?e|fXXZ#+1SX@-3@GLZbg&lAyD$V}1 zj2GC0@7>R--2&2*Z-04^R_j&5zqudVzXJxiXGDPH~J+)2%bvosq9(P=Z!vq0SH5lm{!}&8-4)ZO)j@>xD zH6FY5pnVywQZoOavqAnWyLRomzzX%0oR5%m9_xQn9h7x2+V1%5ouk9t9Lofoa_sP% zn-)ds^->^OZC3=IwYz;6=rtojOY-Fc9bDaNatM;2&Yhd(_9f_2Tj zurdqGho3vA&Z!Q_s~*>_|HY;zwWRspYikYHa+#*4cyM#Z;;rZ@*xR|R5RY??gB~li zrPcr^bAV7`?hi+!or1HQB)%MNFFl$ngA^wwNR2w9rb zrY*Nt z|DtO1Mt63<;c6A)4#F1X+h);BgnS$Rfs8Jv!zhH^I%#|HWu}JA_8j{*!yup+2M3Wc zc9jyUzmE=rFX4v6w1dfI{2t>D za!Z!dY=QuHKM=b-Hzy54*th=pGWjToF?GPli z!*hdBNT?z7_xCgUL#RUI1mu^K`)4~J>T|BSz)nV88%o+G>6&KJk)p*#f6awJ7`W9c zk8c@uU(SwA8}H>^0uNxa$+%N!SCzqjiAGVr`WwB>o!25^Br||j^S6*l$Vz6}cS(po z>^^ktj_u|8+9(g{Acf*pVn9GXh!Nc*=iz9@1yctzGl=ObB^Uf(7fTs%x_kFzMAEoJF1fvi9D8P|GmOP75GOIo^eokcz(f8Te zH<}>V_xv6qn2@p*#)(c%&`H}sIheE9pu7DYpu-7yGAo76K?pTfbTnKHH;BhfmL@uw zq?}R(?Ej1I`g03kxX{f@BN&XyA+cV=FKSkSi{A9^CpoUia^7okw&Y)lhAp_^;bZQ`{S?sdCl6N2OyBg?4NU3A#E3Ay_KH~45%@Eda`RtD_T zgWMMPFcDI2j&sW@V*Tx7!-1A#uKS-|DVg<{eYfbQZ~sH&TNNH5M@MhNoEt3_Rr?J+ z8oHY-niimY)ez)N6u2{7JHA*5<>DI8=N}*}f8zH@PFcYO464Dt-s%;#?Zz2%h}sO? zDJg|4KHR;@pNY>uzZ}wAI=56Ny5Ut41nq0(%Wbh+kLqVLfzU%9ZY0tONMi1nw;%Ia zG$JtCX`s;A?KkzeIKXS-H2d~y?Kd{JwMIjO5)a}Fo=#_nrG-^eh$4j4OF-ec*dfE! z>Tj<3wE}3J^enH?-#_|v)Oe7MyGh@pHkDRLm3%RF|73^X47F<|w%=3>0=VPk;5f=} zfPo~KPrAHU?rsB43t_#kK0|2P0Y8@ZDzWmlA4FRDMCHiCNPbY1GBZDt^vJ9ni0#1U zj8pAp8dlA|Wq{p-lE1k)h@9+e|4ij}8N9x|{D~aoSg=%Gwu`O*W?Kas3>HQ;Cs z^34JeA)ZUwa4=#RTRS?KAPI_UvBWPx4#N*HT%QOHr5tu`_{acz8JDLd(7DL zxn|fmi6`Def;7k;{-HxEHm*0{+cli6sXfMTa4G9*xZu16KSOhBc`t3?;KqoUR;lMX z&t_P9{Xc@0M!vqLa3^kibES50YkCl$XGI{!`Ox{lR+UKp(Dha_bX4bg%p@_VP4!)i zWuptIl+1>pUj?jKI+)k*puwoWUO8Kz8qNTk(uLr@Y&%QYb zIGfl*6(|Jo6Is#!S*|+zs~z@=Aq{!f?d*_DwJR{AAW% z%e;VGtZ2saT_AuMzQ-(9zttXztMtXy*5^^>+poe0CH@ew<=4HTgEF)(ZrmBQj1x6sh-9yOix-mPcRhe7yrdg@%&wEH;7K0DiAMH|u8e zVH@CVULbGq#JUT?J|9JSc>O}8=5>_0Dx?sQVWQLT^nuhIP-s7TbT;JDqfZ$b8AhQd zUvVVD;FhC13aoi)iHT<4A)U?wKIG@;Je5_;y;KN~7;fhR&wW&M1D9|oCqmFfVXlwO zN7TCSI0&n$g!L4SiDCyQrw}*uhPtjagHj356Gl}m4yL1;PM~I+oaWPmen>xmNXScb z_JZ;TZhbsad$Bj3A+966#qQU$V|WP03RYhIlLlflr|SUQpQ~6+!J*1>o+GAookrl{ zV<8HgQz85uALK(Xja8Kx|LAWh3DN_?0jalIVHOw8lHUz(n6{rfo9hSVFDf}LRtQb2 zr4Z!ki)BKQP5?9GDf{z`UtYc%DDtspmnM6Qyu5myqDK!NLjKgg9OAi{z?3l!D#Au4 zIrqIX4lzH0q?8I6+IF%FFgoUY@CKC9{PVlg^$G128Wt8N>x<(xjL%d@q;#hw4S5(W!;;UVaGH7Feg4d zHG1k4SGc}oPbL{jQm*=70Fz{_EfhRoU<86e=SK(V2m^!Kr8)WUk-~`|x`F~(AbCkz z>*f9sWq<$TWC;tZ^*wG9jhEA$x>V8K1Qi_Ow3<7Sk*U=5-k}Hy2aCnU#r8+~L3T5R zqjbgPP!D_VS+*Cl-Du0I(IG0lsAXp=aXO1ma_u*d%)-zQavRP+sE~!u+kl%X z&QE~yp=c=hYkPjN{G&kLjbFwMZ_aXMs0y{~_}aVA*vAF{K`Fiu2XIjmN`5ltc%^b)? z?G~VE$ofXMQ`eph-Nh>`AaDq!&Ktl7(Mbw+v-XG5*BOLQLA?WQZEcHusRx!~x7T9l znzuKaYsmYLT?kxT7!4s8)v|kCx=^-C>@L8O3YybQnX6GM{fN$cm#*@sn=It1Y&G{W zIrfwV3%ux~G=N+~6s5Rg^HC9^h1v$oT<^<1s+bfU|l>4z*7Z`P$0Lu#4f1w~wLkVM=x?sJrF)9;cnO#Uyq;QpU@3qsDNf=Hq8!yzt^+ z7=UPY$_fKoheyXbTdJ_LOkycPOsG16vHvbO=dWGzA!Q|5lathd3=XAi8z|5mz5%nl zVd3c4aPlTbAPqYaGMqxUnv6BnV22B`EbsYPsKs`Rf$ZJBEdKqesg)(4t4T%W-M* zi-Qz^AwFsXx#9#)l>_(P=&SlpGX_Qbcq5n-iNm z^!4>QXf(&5;2RA5C%D(M;cQ|xaO;#4L7rhrYhTJzQ>Uo(95>ASx z_e`*1Ippb*CA zvivzU-CcKR2GRMAg(GhQ71j5D$bo?`f`VBXG=!|r8I)kb>l43b+|IYwzvKMtnMs)q zALb`S(wxslu|6hHS{2Ac6_HVy?KSLlP8;_o8z;}@D5l-qc5E;0+W0=XFD9@Dq1wP} zgDxFJA7l*<9;9qRBV=Hgcuzw}sDV=MF_dvJmn{r{i1lA>IozYEe50|v8kI&~h5U#T z;X@inHL8fIf#8-|`ro@ic>%Ptu$V6q)-ZG%~5i(v| zNz*c*2$G;?WijVjS{8ulta!)DcMq;^m&d(@kZUyJgD3@g>Gd%q0_2p&Fl3NUZvj(pNyn6SsI|3v2%*pL8 zSJDedq6&uJ0=G7XX!rtz2K&CQPBbY8W znUP5p>N8dO-bKXw)&<^KXpwFEzL(Lc0eVTk3Rdo*g%}!p#-$EV%ky3!{@X?9!Og8W>AfJ*1YY10f1AZ?E?MKNQQakBT3iilMCs;7xUjvz%7r=e*`yENSu z?Yw_J6xI#t-KHRku=-;Gx1EcUt^`!91}Vz+Cr=mPgk(TIw4>bq!qw3SRkMJ68hWk5 zFJk{2kPoIQq#pbf--=CUCDyp2P1-0&;9Vv_@K+E ze_?J}r;Q6$Z045tgD=0bET2gEVvIxwGQwudoa?Z+uhad^legaj9Z`=4hY^OBBSkBl zXPJWST;X>P1E)EWH;vN95=gazptDAEk?ntV-x@L;2QGTUWG)IpiPF7*0;j-<|4*cA zj+V^-)Kn?h4pm?_R8GjW;)k4_BsJ@zb~S^2t(2JwM*&KyoisV#b~_R}K8rTo{+BI7 z3Wazs7SxDZ50z}9!c%mH(vrA#A!~&})xGQxBp;(m)BLWj0nhc*KK5S}wmv`!zPXx? zac+$N(Z3rw&c=59z1%B(e$L!g{wQWh=sG^(tU5xYX>9Ae?J?_Sh_S8Mp<@R=9?wk|O{PQg+$)XX$+>&;Itkwer7PESJRImensc3(s$?*tc{1o` zr{r}h_mhmM&18rbf#!qP$q&VR#uIPSBU%Zp*@+Idivw@19hHAFY2RVUAM|va+e*>; z(y*sx5UNRYYt3HLdEg`u-J}wws?l@H-b&Hbo*X5P-I)}#3dx!M*J4TPJ7wvCba^SI^&wb}$VEB5Vr&WF9d?3Qe^AtE6*AcgFfT*eO+s|NKuPNXdf zJ$Q+_(VWRp*7Jv)Q3P?wRS@dM<5!Z+`fRH-DASdZzyGqLb?er)wNhV}wbH|ZUc-fz zf%~Rvm$rM_Oq}95!$NtOSesGL5l@V>F@-`cE4&ZXM4V>ix*%B5KOs8J%~)@vdGj4{ zPK7bHJE=VZ7929`%Ii3oEJiorq3}Isq$TW@_O(7}&_gOtb>InMe*$WkFlv6#9(Id@ z`SzbsgjbYYAEXhjou;?8?jG&mUhQ=L^TGAlN)4$yFvW>(r{gXiZ(bm~BL zuZ}c}xjnZ)b6zj++-J2|gN#pMT*!y)KceWb=%|->u$ta$;rB;X4qZ#PSqYIX+lcaxLM{G*Mjfh>Oh>ys?ZV(94_ zeXBjqp11Os{4&9lozL=MIi_UqE%`{s`qRXk4;Y;tE*a`+BgOSIdtrBV=uxt}Rq{X3 z119FV-WH2T8VPX|GC(ou)k^x#`}$YT!ML|lUI)?*KTTNMF#hrREh_Hk%M^%@D9;8< zIpK({%BWP0BjKY&EQwGq25}$BMGkuJ7=w3G$(npZF;V zh1rSK-;>XV^iBNJb;n)>noLT4pPdJ^l8J$QA?kuq^gv8dd`BV%Rk^b7VjICN5^m! zWb?!{6mD9td*&PIWrwSWO--5=#QT0${>^(Xmm4KO9y$6$aUu`hi)3PYe-2LF*BlN_ zqz-n>2A%7OjUmX{DtAD{R=2b|0b%;$;JOL{jb_!|$4*$^v~iOZhb9XDd>e3>&uQOK zp4V=mhd6FRPmHp3sbG?R8BMuUwo2HV)rAA(ZuL)$r%YYELN(X~s9!dRZvNvD)4YA5*MswJen6GwZcYo>PQc@ehON z4}?McV_?LzUoC$0$#IP>uyY@d^UBlRPxV1RYffM*i-d%oG(o~{x=!E!@pP`e zkPVHVv^jz|K`&9)D_oJ~sieC~`c63Ibkh0O{8C%dh9ngCa=UQeoK;O%6V7#K<@YEh zHDo&-!6u!j+$6(#0&5;9sIhLG;o{T?esuA>RdehzZ>#XuC%0pv<=`rntIcN28e`bWE z;NW3O^nx9Jx;{lCUhw9einv%MZJ(I8aj!bw?Ym#J>GDOl6Q@lq5QXunnDd&7Yaa|) zjyfTkZ!X*IHq-7rkp14x=f&JCjGRN~?elzm#vf}pdt1=n!g8~FI?u#r<5!IEyV3)P z1#=N4NX%Wn`0hKM(kAQ+D?h>M)_050I&|={7K66|XJ4#3K?t0V{V`Idzhaf7ky(D9w0(2R?qZV-yNkru=$1;>eZ9@??@o~&jJ{LSoFh#!H&&WC zWN@&~`9oOzNw~5TV2#lGBWvABZ(@F{&8?WUwvbqOV;>WPC~z@$Z<7N%6-icb^k!UF z$5*F0)oMr5HnqwJRfS!Nh4zW~vW3sj^l$N=vW7XT?ySYi2ah0}NYyVFLFf5W;iviT ztj6y#&YkZEbTUbAx*guiX(R<^f|&|wF-(WVqihyBy`7C0mYpq>ydXxi>yc@U?;@)5 zAwQANRCzkMr>)2&@q|=so0VeN3kImPos{uT*a`fM#|ReEtLT({?4@_~Npl&`j2%>Q zrP#|mS~Ms-twyI2N#&a&Yk*9XBj%f={FwE0UH$9@cqQYTL}jJa4Vc(u^u*-Jl)z;= zPi?q88%tAr8k!n&xY5HUUenUWSt0$hrBK;cv+@W{&K~r>K&8fRNv#%lPkNB(_G)YF zcy$=e^*A2$;l?gHEpI$B-s2nSm19Ai;6^&Eb{H$a+uQ}sM1YMEBVtT*VB*^>z@IuT zBSuz{Fkz~c;X`2(nb~{77Rca5 z?N8)1C+gyP)nkxN+h4(ie^rB@$8{>(wnv_&Ta`i+PCCYjaWM6zi}~dQS0(YllqasE zhcQ=DEAHy9Ot`t$1Hi~!BaUURTf>eWL1VU$i8oB364Y*gJ5n#jFyw=AD< zWvjg7@%k!CH_XJHtGmyjouK+^>vw%WG)@8`><0N& z7x+v9b?{LoQOx~Ff)PX8?Q;K|r!-vm-iMQElyvtDGWgI6%;X9YtXN^ojeR9p)7pc^ zc>MGIy(5PYfpw_xBZtE7O^rFSd5yv=lD*&c8=#GNp=HPp!_E8AGN`!m)QZNRvArQ^ zcLl_`v4^vT`wTWbVCR)%z9h-MM$4e;5Uvb;A5H4p*o3RvpWEB!(2kPot|Fr*o%cDQq!QKQ9P$@&64EXe3d!of(QxisbdEOn(eSq{Yql=}_ft3Z@XzJfc&E8{N7p-ar^xv^t9L}WxV+*&E&;Q#OYGSK&_ z!!tAHM0A1=$EYb^ZlR_ur4+g}i74lxSN`5-s;Uud8r;a+Lr+rKjg??abqHk{%OOuij@xes34 z<$#7~C4?+g5eFDCfnPLQ|9$`KdmkVFj!AbY=E7$k@Wq~+l>KUdLQBW}@z%|M-_O1G z#D_NdY6Qh(Ng~=B@kYoYsV3lLD>ul~X;Ge>1SlzRHcVkZI<=1#;QLh$l!dfFyG=xn z9fKU2NnES4t-3H}v>&jLlmiL}DVLktF+p{m>ZBASu)n{*R`B&mhmtzYAZBC=DegU+;Ygbj})34lp@_zuMzMRPb literal 24157 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� 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
%)Mv}Hu*8e9a!pya2& zO#gw4$VD?px1d~?`Jh44m4%SU$|T23wiUq(X5rcRM?u1H6;j=&M3jKxGzbEyhyfY1fS%=R$C;;WNy*_)~U$)@m^#4J8h1z zBb2^?!bl%%!SeCmL=>EhjS9H_^|;0s{d$U^%kssSnM%;P9(4mhky3>>O#^rGOFkDp z1b2#|Hj+7-&sw8?V>cuu=aF?x0C5}benc!ud65M2x}un zpowmYh2~z&)Vj7GI?*Fzq}sFMe;)_?)0uDDdl}k0-K_i5ZLp_Dzq*4f& z6~jN;li(*qAN!xL{#fIWZ~O^|Kgr=wKKxT1{uIPN@Zb+3`h$r6Afi8r=no?LgNXhh zqCbe}4LmEd zu*wK}o(vy^>ARxz2?&4;^UP4%`|}7e!XJzMahN~x@_%Z&*oQmLGZSsGe+EYK^=n#c K#aHe<{(k`YgE!*< diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewTwoIntersectingOverlays.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformTextureUiTests__testPlatformViewTwoIntersectingOverlays.png index eec5596f8859c15d22d12366743b0c266628d2f4..d0679ad960c1a09489b771473a1b75305bb83626 100644 GIT binary patch literal 30760 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�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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformView.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformView.png index 57431b17fd0bbfb9a704d2e626e57d7490505e59..1cc75de30df9b81e0a6bd512b7a268ca65bbf438 100644 GIT binary patch literal 32713 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 vh(Zw)BT*-38@NBM5n;gs5?TG&e7Y9wP literal 39235 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

$W&WMwo;d1NvGvh&7hrvIpPlxz}nb>x#d|9|P zAP+ou-W7Y^&nWBG>Ia0NxPE;Jr4aAHuPq@Xqh^e`3A_zbUTC2x{6%0HgcwXLEG)+2 zWabn=`L^85rs6K?m{{T-iYFCNMRMdO96?S%i0G@#6^#iE0mxyZp{YrBR&-ld zbybyd)3)1u+a~>YJFXGqNmt`6-H-l$oi9Z|9K88$o#GAFZ;ie>t~hfjfIrfr<4$gX zOLLhk(Ty0{?Bs=#thp6XXaQ~gBd`5d;4^p=(nO3~#hbT(bM*sniEut-+a^hZ8D@z1 zq@>yxXPzjowUHhI@W-~V54SBm9PjIxJ_CAojcT(4*TIHY$;q4}u8mbfYsD{^f4=nq zz_JxI4k3xr1zTDspjCsC#5O8JlP}j?hqSW~eAe?xwDm%K&X%Hqz9^$dc{LNiLbYb3 z<>%#%!S>MyG^!4E812f-H*X|K_W}y&Ih9!7elagyx5?h#9Rq<%Cpe!r~F#J%TpSr8As`_l%s&ygBt~rLE zO{RbUjO`4q)%7#Ae|LEmd)hh6=LrdCqC96_;YOcJZTT#~+a+<{i?e)bK-$x=PCM}o zr+!VqR7JT!P7)QRhkPd{oH?YgX`{>0gkgb16!iYo_(ghwduf0@_+_263%9zyL?pPh zd(@9LPs4a3i3A7W2y2$OjEU&SA1M!&YV%fw#2=Yy*gwjWLFD_9~B8hOab zHtmH{eS)NQ^qQ|+AhjqHx}|+3$)F($tHpO3fd^{Jv5#KIA#t$*3DAtyE<~y&Y>Vzw zdQ&DBWLG!ZV0N6VXq^Mm&)x+GsYCvO@U>S;3UcKoMxe>m)&qVad(-W?({`73*u8|5 z0S|~*C9W8ZVP@x-1}LUMH=Ho*I`Odv;S1E$f--Sjcea*IRX0-fdi3UO>xhGw45&C#$W zy^4uBiESM;6!EvZnp!mA#Vy7og|olUphTzYlV@f=R=$GWAVrJo+OO-z?TEz;@OcWk zw&2)=goJ9Usst1HTg-V)na<%u1}QGp%7LrPtdvpVRKp)?=oPrtrnaI=L$|xh#x1X; zBw{t(->(KW9!YnnGkElbuC6Y@3T{MB5Gk2xJ9+^WiET^K(wSZQT1c%LZliehg=EDd zN{8#&m%}3q<(#HF- zzIw;ZM2bkb{MBBZ@BE(x2XCozzh#o%NL~xU#jzd6K>d07`EgdtWP!K5*DLODlu+W@ zSpGO#?vtyEfk47ld!+H(%DvY?Qi7jaN8{g2&zLU?<=62kXfeNveRZ(Vz$L;wfs&^* zVwJd$sKWEN&buCh_#ZCi?tF4sgcYP$r8F(kd_nupBf?V&tCm}n!h^ce@Q zpOBsHQeCJZc0`1G_wM&&7dJln{Gz)093_bn{wRVg_DH6nS@fET^Aeg}Ih6va>??IG zE+%HLm{|R<>wU1oG^HBfFpmfbwK6g)9`kFxdGqGJNPiLdcKLKN0XYEiBl05F3SiUqvI~`v_nLsJlFT8em=j2Nuyd;-2SV&S+#;30l<~nsj zIYQzEadlip4;*Gg9htuN?{~vu_vrECun9nelCig@JIn8!IbDrqpcG~S^^WQGBCRT3 zHVC@<@?k`Tbza!}fgNgjTE7I&#k~PkVoT0Sdu}IpSJ~11n6-6MN4$?VTF;9*JrhI- z_xw6Zn*${BGmZj$1))GGlx|tW`=CM7bgNGqJ#oXW=HIg{02%{aQBA2S0RK@?MNbwn zqD!bnHBh3F6`vRf1)`JhoMhaN-ufM<@R6XlHUrY``*?t%k4+)I2Ou~`8dlBm*SEK+ zyjx|XA@`vwR>7nv#{rWgT-C=rPlbQ}Da3e>xSQwa=k5IoV+qUkhm3YB-Fq5%$<{47 zRjDcY`MTJ0u#xbnL_*UDH*QBWAt$*T$)-5NCZI9vN!4O6z*G_Nm27i66~&e8L1Qpg zhD*QH%Xd}+a$GQVjp^40Dc?Ug*)HZG9)*4v79lxyGh|ku1aZ)K<6u()ENgF9b_;tX zQ94!ws#Ih5UdnD}q=BD(iK2Dy$d8_$p0D6+IG?5~1cym|Nrd1quOB^#Uyr-u=R7C$LF_g5QDCuaA7>L;^2vo@2Onp%ue!q15tQ5!*V*1Q3noH`< zejBvNb3`NuJ$@XELclTsH%LG3kc}!BiZ|#6Ny~9=#)&cy;+z*w!~te(;^r)C;XxS~ zw}3+9(3}Oh83Z~0{Iq4RORT60eG#WQ!S?u}SZrEsI9!h*6QBiMK+^ttsTt3}z|aE@ zvu@aPGJgXlX(X8;^b6z&f>fyUU=kPG$aM|>AY$n`?St6|? zYdNa+LOYi=EfWBV)gLnjbu)Ph+s`h-iD6Nv*25?@$oKeFqi<1RU3_Cs@!P z_%NcSMWR7HgDCA%&f13ziudP8bh3HEOXorBt4lFV0l9TJ;)`*8%R<2s`|mf+8+a5F zv{E?Ja(XBPlOW#N>VCuj4t;(;28p8QHc#YPfLHU7o=F-Q#{?`|FQF9b8wQ=xfqT|C z5RhaVAF@t7#@)RS5EHVTty{&nZ+{=u&l~%@;ROgw9i1?Ogtl#qLsA+6{-)}SQxyxU8*2X3o&veML?f|v{7+NByL8Kyb+a!iL7F`jc zp@-wIDro_NH)UCed(xWQ!wjf3H8G$rcRimR1RI|VK4#lS=n4R*?^26k13}oM>nAn_ zRD3c&@Q1)fa~6>E?nO?Hb|sMcOHCaqtIkq?qLG^$)kD4_#CCPTUxh$*sxYo0p07zq z5e(N~m$X64%?hu!@(2(`qbKOlgJh_FU5qnB2?=ki-~mA6P*)vz3!W9%U>(jju|fWdx=Qt@7@Td z4Gf-b?{s&RnA9cXL)c=u351O^_YWJva6kk+J^pJridZ3Bf;6{; zEV|)oqZHAPvTWOvb8=Wl>xr9DV25DQU!BHSmzlRro_}LVW>KGW8PYo$(4eTGPz6s+ z1|rSRdeeuBirPxAc?W=4MG!g6lalGLUn6gBCA4YznO|IlNdsF-;05Legc7$1>HLSW zMz%n|)Pjsbz!*b3$lP_7fD-PUrl{t6eN; zOL`Jg3WbvFg;@W|i6bNiUm7tN5c9O#SWn2EC2XN6Pm%6JhqGRgm@+vOy)=M^JO=_& z*}64PgBUC0Ny{q$c)q@iWhInG%hT-1p9B1{WjT?zU3}sx0s5j_n<>vm@Lqfu$ zn`5G4laA7bnE!0~QCu4)eUOJo4mBKw2nOHNC%Z*Bkm*#sM#a3MA>M(7zoP!^?7xqK zyq3#ShVhnwb6EVCC&FT5_dt#N&^dC_ah_U1SZbsqbx1Bm0fliy)@i>#`g@q+Kqtf^ zt=L<0tp({y97cd@b$B3RCI)m2oT)qotq8p}?6*Sbips3clG?7gCp_kC2G52yozp(E z#+a-?sFT40OqD_A-m-f6X8Kz{uH^o3&WAxI7)m?nW<;XW^a*(g@MdOlM2~K5O+uyH zzkaoR-lX@VkdT*SV~#L1C|_=z(?&Lj<>X~_^a-fWOl?PeDLdk*(?$k>-M_}3gvJ2J zfRn&i)<6?M5+Ig+geR_W$;^xwV?eS+awhgg2Ff730mOffPIJx<{fm1^&ETfU`qeKn zB!Z&++pXaD^fgvy=KB?Xlt(NQ6)y~7C`5NB9xZ<0D0AXBS4HB+88=+efQFBY&rWv2TG|Mppb}wz%O}sFmkAIFk!I22tCz?v5O4bc4 zugMr(bi>Bg%N-&AlFS3I1-Ai)78fS(1O`U^6-pBA(Se=c?$=aTKSx2Z)y{#(lQ;z5 ztLI$*h>4b$7^=|*Js9oj|M_QF2@Cm+;&Oq`WdX3F5|Py)W%9uUpgRN=UBD7!LoWJT zHI0odY11X|-+%0|`!MK?NlRHPJ;g$zEJO!NLyBY{w*&}nj9grv4Fb{20*nM+qI{l2 zWCmvB>m4kZpXulJVXH0}TwOF%o3iJUCuAzZk0>h#Koo+ATB+${-iMVSlQ(785(N@& zkOkTo!lkXsGNm9E2v-)@s)q17j(&v6(hxl* z%%quX0^6IKETAI*KTw@T)|0g?31iu2N4h&3C6oovk`qwSFA#;)#Ld5b_(^5A8e;Ds zoGoR`YNlqGdf^As{4dc|M)kzy!p=t- zq$k`S(*1M12`K8MAjm9CR}!x@wI3Q9@k1zely7fs-gEJ$8iDq7Mivz>y*nYa`V5-1 z%5G$Y;RDhLb*vfB4_3wXbhl9GLqYsl}ETM6&^MhJdnN#9I?-hE0m+`4=c zld?5XBa989GFox5vYy+%USJ@-(Td{951Vp_b|*{w0B0wX%-^W41O}rW1`H;~S&0aw z4=oFecwkuw^_xbTwwuaCQ#zO@&dlb;p zW+=b!B)<&inOS-O0@1f)1YjsSyg|P+-@!HiHkDsYOl#q1(d-ksrbT}xCKGBpxnw-` zFv2P}XjphH7fzm^s)3U^7{J3d^`T$ZV1!n7E)UFXJg8`}K~I03aK)N>FnNvKHM|Uo zEoc{PF4Q=ce)y$Oe|_rRB_oZW>0uWwPQd<}DWc^Ux&Ls9L5U_t2f!z#;gM8$7SInX z10Dh48K(@Q3gIVC;CU&3kJ$qxLT{@DjgMwj`ujME*PYy5%hti6IYrlVB^MLGPxqGZ z6$0u}q{)tYi=F?6r|LXfdlOGTO30DsY}m&AkgZ?mx+3-g}>(5!JtxgD|!xfp{WAf_e4Iss-@ z{R-(pmjB0KEUTJ68&<=%Xik%i#u8W!YyPM^18!gOI%sCtSbk0-G4gAt8kN}E#HHIg za1GwMEf0?kF>3XyzA+I*Y+~;!l9_zyCG4W*p$Z7yLRba--jw0#2=kC(QM$@8p9 z5Mqh-G2eMms8y3>q3uE!<}Kf2PK+|~=FOXZkqQ@YB@7GH(iUhm+W0(eSnkJLnz5Ko z3!!YD=k2_dhp|WuJ=m7Ccm88q)_=;q#5HQO5tE|H=WNV9Kek-L3tV)Lz5C(%* zc}}Yh#eRtGbM`dMGhAM?BYsRR|C1}}*opseB?%8aE&C3G-NV;cItvNU_RGUXoSj%2 z|1|hKAh2U&V>fgo=Z&Mc>&Ng*n)K|i6C$FbtOidaBM(C6hb)a+J3cYdhdzk=%OoVF z{d@O*MSmzexs2cl=5){rz~V~%n_VR%_eCmQyhZD38Zh<}Vh5>ts3jF+qmUe68+--0 zLa_wuB9IJPdk<(mpL#Ox1HlCw_s~E)`g+*4O&cbO;gFcn>I&^Tj+2Q;pA^B@|7EP! zQX(#mFr4t`Su_>>laWZTK=^+)5^Z@9#x8sPuT~pO7T+dp5;_7nj?oF=F>yA9r(&lJ zr%fW7DG=ho8*CGt7?mXEDPRKP0`2{HAls(3nvh_|URfCRB5VyN<#8$jgS!`+1Z*Ob zCm_LvewoIQFih+bkHQvDxws(tO@P<27tPAkr;_i}Zy$*E?W2MEh`qQD*A(I#3PBSi z*5+zbl5~`q0JV zOsy}M=VjLQ(rwZ(>l9!|8!uBuy9yrgCHOCr9pj=rEnM}Xd;x+056aw$BDtej`@JLM zx^!=DF!|MStd34jEXk*H_%U03y2~CzL&VGcEW6FkQqf5ndm_?f;ygHewuyEqMri3e z!{Rh>S^yBJ%0w&Ek9qjf9O^eAGW~HU%u3V zpOlWzy=ZFM2m2c5xCMWBMJ(xq0rO((Ti#rCsBs{jrGRLi$0x2^fc}(;d-v|WijZG^ z>-dgEB0lC30Ex=SSKO$(|8A`*by}W6>G%rht3>PX?=O>#4gT*9HHnZbnI|xu2Y~9P z*C~AvMJ=UZA`GT~ZlShL;{CLrJ3rwn2||+MR}VhgQjfO)9wn2RJoG? zxY4b$e@{Gu2~!y_6(unD%FjpT(FFyqhc1T7?|2Ub;C?Bo*3`-YW0|efbMvXqUMMx3 zU%=;Fnvuq^n$XS_$H};!`3hStnBLyKduLzd2_ls{K<07q4km~E)FH`D?*oF@!mr?# z1lj>kL77XLr-(~sjE+5^acj7IolnS6dB9C47=f~^&Yv7g1BXI_PC%RtBi@v5A4&EM z*^hmP<5_%`__o?`sm(EScVyj0a4w!`VzjZusu%^($c;S)jaks)L*U$z%2}wEu#MoD z5^`7w&dPmaU1}eDUaIxB-RMwUzk0c1paUO2|C8pyfDay^h2carc0GLt zB_FvyHYV~+#=lGhOGp3PG%y|W-9~T9ftLHR2fyWb(6Wfu+dFv`$55cs#w!#t#mYh| z!3{9b$henV3WPVR$>s3yaA@_Lt7u9*rpI)4E!~C5<)&>z!;7c6bc%5=3%KohH9NgckIeKOfOwJHTGu+*uU4C)Zn95x1mq8oOoSf8vZWy zWF%G^A)IMU@iF(GCJb{=%RFGCt=_3T2l}afCV^OFl;Ae12R4}mdLWx=? zqKl{Ggaj;_zW?HG`90Em;K&hO0PK3;*PZi!v+ctS6Ot(*#>tH6V6Qf@2QxNHaaP9M zbRew!k}}2peTy@_GvqS2j>meaW&XuYlDS+pViS{j7>nEoV+>eBc*g*aRl#c^Hev_+ z9zq($c`K)2DOi{Pekr%GZtDfj%?{7YmA42bEEYLBs%rU4cGDHhU;OHj+eP{vV3M#P z_fu*hqKLBsJ;O=&{eo|;#;Y!J@L6oNvuh%eO2@mnR-&8JzdvTq$3Wp6t!N=1vxpP} z3}}!MN8*fyH$_^yEu+B_VlLnwk4YZ$jov&RqR`RZTW@F7^6%DwPEWxaAYF`D+18Ye z!rJB{yNca3A#ji7XL&ELg^;erHV8K>{r0B2_*yOR&CxDI%K!f8KS}u>|0Lz}Se_v% z9An+sR^?eT@?EFc1>KO9C7k*m8MlMI8^?&^%{d06FNp%yh_d6)&X!~ckQ|?d{`usX zH2H0T(C^Qsu7CWF%0^`U?nxSq1Zmw!xpG(5zlzxxRB(=o`P|xcBl9=!{LQwmCy=Gs@nrFJK{AMg4#n6I zj>^rIdnUveMdlBw!;nfhG+zL>NVrK#A-ZoWpTXiIa^R~TJa}N-&{kI``d;`pUq?Wr ze+ebKF)dyABi4zrdDY65j_|nnwM660mc{gkR{8D+De~(^@g=y^tde=CYq7lR1zy{g z`L-2$xF0vEudC~WDR&cVr?Dwdz|G^&{8~+fHp9vlc>$KT2Wz61V}j{lXPSwjf-b}Y zhg-Qya$>g4c<`-}!1p-S@?8g?e9pMvLk0l!OQ5@Jklx;W!S(pQ>}Kl*>DEAcXNd@c z$czo)WPoE>^x23?q@;OX5D_sjmihGS6uGuQEh>;5&SrvkV$9X7zNa)%_GW#5?E`MP z(l^JSLm~5;qHrl@E6Pt=$leP+Ut9%;zjH!`W9t8?erAG`xA!N$Y`Ut=)Fr;VsNS70 zsi;XlY#=UhY9Xm}#Mc9+%Kj-j8jrd0`kB^Y&uyYD_e)*gq%@x11k8$o6v05T;Qy*S zTKs3-QB`jlfLuih@JUKeeq7)GqOMpRSyb=kU7j^CT#~XC9=B2z23lin0K8A8aAX64ao`MJk)@S1L3;t(TOIt zw6xq-c{qMh2Aw4l!|>3YS3fxC>XAunu$lq`B1DPk>A7Fl$wHk4 z2Q-!*HK#}x5i>D6QtH_zW_p?JTEt+boP+BV2f0KL9|PuLR3o%aFcko+3YHnZsf$fD z%%oCy%cm-Y6{=h@AqN3wwZjHZjAQ#SiY@PsCs||!?d^px2!4J9XRs;)4y6m!)OJt^ z=$PPS;FXfn$Nl0!oO`ELKV!?HckIKS2G4?H*&w|V)(9yWZ5E%CdiB{21 zYu9&-Px&~y7|DAS9=m#D2ji6Q49E*Koq5o1E$jeO7Pp_!Gn&Ms#M zT~^ihGJaxX_TY=Uf1H)|(b5|`uI_&2W!E5bJ6Ze%kNA(!OIlqA@=bFOH*Le?3kY*B zTZdWOZ3eE#;DA_2G!mHlyS01N98(u=$gPJ(w|YPOGJmYKcU_;d%F%*lloMcjW_*8) z(`p+$h8I+vn}Z+rsVm>n@4YSjJH-~4EUvldaix!?VrEjFMx5Mas8vQr=CtrK0gkbC4DneNDKpAN$EgS8dN`TYUDS$_4~-{$?bN(p z_Ue&qRw}trS6CXa_^ywp>Af;@aSJgREtYFqwQ3dSu?(P3o-{Y5d;ZGB=xn#mM893F zeyx*Y5ys?pC}a*wbG*t+a1f)_Zn_n`n>?K(M3HsmNWz(_K+hNquF5s#=jRh@DIp<& zS7OsLfuFN)@9$?_<(Si#%$he_75ODZ#BtgwrZ0KF4O6NU=`4l6++z~o1>r=2oOoth z8968Kj#yJwZ@OwwC zxS^zhup*h&y6w&RpbS{o_ExSgzL?{hV7Sd@-172XXHDx67J3EzPyhF%`O7NgVM7iH z&xB|TLpQ^yOD*akA*Kg=>HPloT)6*5WGo?x-&xB#K#{$wwC-qB5uzU^#ZNxrbd zFvl1J2y&v~d`(ok^*bWwZb)dhigDv?x~8F(PKmK95e9u%@D*YVy4E;rE4VtSAJ4B3gUu+gUV4Jivs%6ulx3G+QLrXd(qlZaho>o zv$*_t@sjPanG-9=^drQYs3DJ!@952!&|JK}D6a2!o_xQ@!-pr{t+^FLbuzFZKUzH6 zv?ON9!u`S&6Wvdjm-`-{DQrvdlo?mRPmT1+msU6Wc+%eqxGp9Tlg^mvjD+OvgyT8= z4sT;yWjdys+F9GSt>(S%O?Lc7>K=5Ff-edwDS7108=rUL@Wg9bJk5g6sC>1!w|(B9 z=8j{~{M!zfxf6LcALwu1&U17>>f&KFOfH^@yX|zu{N-Hx0?o?_t~5r*5dm=y^+OYu zeqqlyi5}W~?!EP2JMneeYnQANScm2bJo%$tglZ;E>simo<8o>qzJ`1SHLZS+9=HA7 z*EAi0cjE0tE`3%@3(O$x+ky-Rm8GY?Et|epd>&4gdM-{0 z?T}sDQF<}I>O1vQ?iy}GQWQ?ADz0E}zc%WRIbzF(Y_ZAM7d@ER#G_26^uf~IsV+D5 zF=)TfDzWe8;YL=8x&5JonyK)qk5(#8vBysl@iaM0Ei%rDRyW+-Mg(%9O zL!hh(m-|<03e%)8hQk6}bV4rT1jnx+1AH>QkP;K$gf3@{xPDp1xU@qGy<@TCR)-~W zewt#IHMP=iZM>oG>Euw7Z)dA4c7e?6dWc?9HEqt`H7uEawj7(bB5Q+o!>ey zPeV6|L1QDgcTAJx3Y*a<{Obo8sFJpUv6r0#`pLLKeX!g>l1^`}hBb z2j%~1@5;lWUi-c}ofd_pWGO-mN!B8fqflyWDby%UG)cBP5=ko)k||1KZ5dk;%91sr zEM++;dy7U3p=2$^^SP(k z0SD1=`bmo3^XvsJf+L5qeSvU&d#P2RV@Dx(bmoOk+6Wb5*X9|dNW2^xNX!BUNSN_k zBo+0(WJ&2rF=-@$VJD}ml{SBlwfC1fCZ4KJ84fz4X!Uj&65s`0AyPne`2&}asq z=DYelvFQU{8uw72Gi^F%>pO3Mb&y(w@C11VsW&*>s~*aW{!1=#@S$Hd!oFtA5)?!X z)rU*jIXOAB^30GcJA`*4r#fMzIOTc|bRa~R`Lx%QdGLgKgtAzuri#WFf7Z5_#TSo1 zyx1;C%}gW+gwE`SNgH7zLRk!xyG}|P%rwdcpU=Q-BX^i9YJL7(d>B9qQbX*Y*^`h8 z;@#z@Vz23#=4G7cJ&-_pg6>{zK$T=I5@~B_s9M6x}7ZXYxU z7~SV`E`+y-Bu%?db*;AcV6xqhC+VNu#^1RSveJTi1&jc8O_cEK>epdB07<4N{wTIF z(^H(5a6Wcif8Ubw!3jUS{^GrRkJTE$_}8`i3ib+g6=XQtol@Yqxyf_C=i0)b z7FglC6u>ZHk(%@-2K3B6ydMS`Sah-o{hj*lMGw0?B4ef0FL91lTRp{x3eE3{c1~n9 zH^8R#roENTFj$iWDs4|HlVhAHu#Vss6iroWcQ`gchCECEgarhqC+f2<;)G0!zA7X5 z2w0NeCspCU#WV!ONLeBaL)T|G*hwd2=Um{J=!`gdMRna5#!BY-2rQe+ACa1Z7|sPv zELZ^#YcZ5LBgYC+2Mp(wMOS|Ng)`=>-`T$dkKdCQ`1RBVwO6ugh&8f+N zSPqq7dLbYNcCft%_-~oX&p)dz`SxuiU)l2>pWcIk`V!Ctd&!Os8`yU1 z^~8rX`$)*6^QXSPSo56KA_9W_=DNE+|C}HZs*}o5W+4!T)~p#hjV=e-$Kc>)&^9nL z`q*-eO0Zv>ST9D7R;h;j^!>z;H?$bt*ZkouqIO26VY+8p)7pfcbII5NR1Ih%Q{_NA zAbk+u0taiWsum20MwTPvGjzIES-}48uD>S73@vD138&GFbc{PSH8rYC?~jd24b!$| zD5+Y~FikS)MsP2J4PKLa2mW;Y%omKbkSre5ZWqlwz5PrqdV8?wh;?6T#?(wumG?4|rYL zN~IpmJJvyRGi=*O^@cGFn6To*{%xsp#)gK5j9#{YEKoyugVj2TM0f z0jY$_>5cCNt7Cxr;5FMpB8?IYa8JVpEmw%sJFM6+L`AqNi6?hz3nJ`ptp5t)&%Pq&|aGIfexJw?MBVH2$vV) zCJP1Guiu^6#l$Ktby+Uw5_PHSqZ?(=tN9$*!BS7eDBks{nzB;r_Fwxgp0f$LO>p0H z&-f&B$4>Sn&|KVN=srXyut?0PJ|tV>Qu*0SM>uyQde{!983#Ho^{Y(XRF_MAOGkoP zskXS#Ib)*^W1ws})k;Ek+1a@@yJpAJb*k29{kCW}d2D~G_=t65=HvaO%jzl{RajL9 zD!1yhR}%y}MBD|Si)LqMX=%SxUDj5n;nwUv4Hk0OEL>W|1VC@^QPE2AjhifO@ z#6|reEv@)CBq6m#QQXa}vbx%Lai%JD4Jf1ic`isD-2}{8x-m8;X5p|`(_OWheppRQ zA3nT&y6en8H``fSB)TR#+Sz8}HAz$e2i0atL{vaK1F8-cRCS-(RF4#qbzLLU3a@5HHoc_F-4Md2{+J`Hgn(b&`^YEzb)=2Fpv%^x7SF# zL`kUANsqddsj}ApG2ZLc1xB3qw95W;9LbRnl<^|$BRO6XKX!}VIdeMKVI=sw;=!G7 z{`~#+6aG%wSFrsr=?RnqaxQ^OBzn{pudlgsbD=w{P*X@<(bLIeTe8FPIh*0xgja%* zCAskI$APbuQW5qyNo)qfc7gVykr8)#PTjpcDn5pfuk`1~M=u+niN2<3P^Pg*pBf6< z(7E{m08m0c?sXDTi##RMiX2mc`!}&$mVcfr%C5D?QVu#VIsP#U=6KZDFyb3h03!*M z@6L3zcr^2Ir0+_4VsbI7O+3*ch-$R!rlP6qvTI~RNVqCKHgp%g1UY7y(+eSgM{tHt z&m<^++6J5JAn-7bcyA-FJGfDbo<^XU-ph24AZ2rp>5Z{GKpHpWYZnk>rq zL$&U3i6OQuoY4YlzFv>@GZE$?mOX-~SKalVw_BW7m%2kAFrY66 zQIMm}0PtXkNBtMpg`t7gn{Gx_m!jesZ!$#oiMq3VcdKYVb+K04c-hG0UR&ypf(bm0 z#h@QAwt$~vbaa%~3tEpET|gUDSz(OY+j48$-b{qWn2eRg2#N^MH{N~uO8fBGu*=Zk zpiVtlUdsLZI#rHQn*jl0(JeNHs+)1T*0D^A+2_j9oi}9xCoJqqW9&bhwN;#71fjS= zNS)a53uSo;et&}XlpZ9R#XFnTC7{nV84C_ExR8K4YTeesfd^ydCF>r<8{}F4_Dufb zF& zU(f9XVaz>4dxZjj+3!0!bWgS5#UxXJBva*@VJg86r*T8I%4zokZ4XBqDiqVKY!Ac` zicPg9=n$>0wo*&VDwG@9jcAK%{*W20sJ@Fztzj&E=e!XDaPT$En^b;MrOoLu^siaj z*@6clzGG`w%SnK_q5KjBc8maW=oCcqHkz3+(Uq`C*@<|!*K*5hg8jJ)1@Kf49z3AA zbexftLnXmT-hon0Gv!WFl4E^0%>$PjWt&;)H$;@Y4Y1Uos>q+PS)Z|G<+O<~?iqIe zZuM1HYo_o8YB%GzN@cb>e-p3;}qk^j&15_=jEBJKet%P!rK_mSw3u zg0BtcA$DG4&$BD;Mw06Nqz@)X%x-bl9n#J>6R?73aWM8OMVnzjv zQ0bAEz4G5B^uN|^d6$qYPc$B<4lqw~WKcA-wGHJ~t=f);CF+d0Nfl;aY3vs=++5Er z!XFaR=QNP$YD^OD%`LUDx?rYCQ;jKetB!#1gKz=}C0 zOXr}H5jY=AcW{{PZm_A}d2&c)8CNE&cztjC1{@v8a7jfC$S8D0dQsTnXva86;em~8 z0lf@_K&7Azd>Xuse?WwL1NSzd@FV~v+#%4?l6#H7deAIt#=uyTEbG!UvFr|*(STQw zNUp)ierL1XG>Gm1p{e)U-E#;;F6gq4KF8yq`l5}G%JiwxQ0Vup9?99(=6Zat@>2SP zbI%EFn$GB&xV-K$S&@5-RU7wSt%C&oI#TK^Ne)7nRp1yA=-|X*9{7&Zj$ouByX&^v z+B!?PQ#`UZ0G1#Jqjl^ik(p*jdxvAqnkHviWvR5?q3)XW>(mW(v70G}T3vRsdP7J8 zEZ-@M4#m#$swhom=V}J_Boa1isl^V29=x9FRu~UuzoADvD(@e}3RvM8<8HqKMA!P{ zh0&(_n_ZTN9nndF9Ro6MMi%a5P|(yoy0veOW$uDQJEKLUdk;K{e!QkXI-0bfPhZ=m zG6@a6F=cZ-j#>b@3GPOh$gsh~_oH)}p2dk1<9=n>s7O@hiSHa~X7cC6OWekE{BE?X zeFACReKD)SOR_$XqPlkK9Qew}N~7MdnL~|EN%(_P-{bJIX;VbZn{7AFwQ>hkFm~!J z;IK9uLl&s^L{)JN64xVn1eF=kvP96xz~L%BN$=`C12+&MSbyec3twvk9l~(Agvo~N zw3mM6E9K>pBqb%PMR{i>%vRE5D;x{zr-n?lpX?jw`g?1fO~~TSYF_;K zQP^)(Dj=gh#!&u3SPk;m99zXt=Nm3;fT5L$SMm0|b)@1G6RC%XHfv;NW`1ZY8Xrm@ zCOMQ)bp@3K(?h-q7OiaWW)5qtaPdBIhF{I5@bs;t$XkWIa24nmK0eXM{2}5sxSS0z zhHUcU&+|d)t2^6)|D0Rv@J^w^{Z`xBq&z{>?c&UmS@Syx!nYz&V!>~IVAzNmy0rD_ z)29k6wGE#Ph;$mu7A;gX3*GScYnWvi7Yua9#bJIVP78=XuEas*>)%pkoBVF6d|jp8 zA@-FIK@GQa{kX%ua(qb0z#L>VQVexdd=Y7dbQ#mRbdBGa#z$)3w1S=87UzysHd^zO7QzU}Ak9NH+lc)4z!RPQZ( zKc5H)A%hCcI^37DD&+_2teDI8M^v>l!eWlyR8b`qk#lG)>snljL-!SYfAKf4Gt)mH zK9g%4x*8ttg!0BxPew;}?c-a^-e>H3OAFD8ijBS&*TQ`t0m9%$ra)FnQs;n1c*p6& z0|Jck@{5}KiLO1K_bfl250lqfu28qH>6@$-vB@mT{;ce~=M0hV1M(6$IDu4wB(xDA zrRnt|J+0=^8f@mpp~Y)5Zv;e=!6Lj@@e~qzU$~V{Z|Q8`d6a?Q-rxgmG^R zB~>fRWS@8c@(OoDt}U-PPUbaV8Og|jTBk3481{jK*pDy}!CP?l#4bJuA~Y#pb9QeR zC8(uRQ0b}3A1gGI$t^Fl+X%sh1X1b6VT9=sDNE3Kj(+`SdM;ne#!D;(|?#j3E+S{M^ zSeoQ3w>Csvlx0$9-g|L)&8RPKGAypr2mQ1GcqK9qQ_%r`gztPC6o{FLe$(C8IPVOf zpXM!<=$bitB>Aayufm1Oc8#k+`|~f)shy!fp6=}&y7y+mZth|{C7u9F4xG#IYuCl{ zFEy?$zV6ia{q`r;OZ|;B2Gu`eW&EQIUH(ST|F*mY_e4Vj%+~#tat~)(`1@s!hfE7x zJ*RpkH>Aa$?y`uJLv8fkBk^5)esd7n?gCEz!e`WZxOrRX+pWgoTVh^bkIb9)yj{dR z&CRJaWJjOY+c+)o^X5jfCw)o9A;ht(;8U5}6(|_LOgqydgj)DOKkHTDh+U$lJZMvd zUwM=%=+>nM?-1Nqb5F6w#04Sx$lu1zj=`4q#&&-&|ByQ}AvA^YY^_QhXsU0=&vpy> zY0MT2)f3ygM?UW1jf;$NcDF^e$-VNH&ftv7x9OF=!Iq(lrcfU&$>&_2<94;JvQY%T zbWbMkgj}@#P0XR7qh$(;Yg)ON4br7H_mLNho&4V+KmR}YY5?9_<4(iB-B`+-klYdB z-JU@tW%%=d`C^b*xmJ;Y%b<@P&3?XEgFut5@~vUq7tY6;o}KjbZ@gWWt6u$2k51rz z*#GJlX5OC09U9)A_HVGK{mcAT{(7hF(I1&`HkPyw^GB_xGLey1w(Cbc_xV)^vSrQZy^U(WRV#9CMa7kNV&AFzV{+zwlgp`WHew{8OYtn6q9rW7{Ru+C9gPeiD24 zrBbNym*`*KF^!h@IThb;&&Zw2otwfgk1r_uh}@)S6b-m5>QIZCeRNMB{UXivH9@JU zMLRvdu&^+k7&L2-jgaj$!8!BGTq`IlqL|tw@GgusPBXgRRP952JA2t_Ixo&y2OIB1 zxQr#DtPrl$xw*MmS$iXQPft1D)9^s;e^-A9BYQJw3X^Z23RZC4E3S*+r#v^dIuyY# zMVKw@mF%?go7I$hw5_$Jg~!s;(xq8txpt=e{YaGjKv-Cq%Se6Q7jY8C+^NJmQdCXV z%-lTLKup%V06{ioW_D22Ev?Q5Zme`Uyl=cSp5P_&cr}9rYf!C=N=;4OX2RXQu^hzq zQeGK)z)LYRGoy9kh{Em6Gp6r;y{^ottzp<w|e!&Z z$5&o(s$3l_5?TKqQ#Et)^~LtZ$+q=HWwn-HkJ4n4RbBh)_pU+ zNBM$64J;jZSurS~=D)rIk8d!6%5>K1`T&cija(m2hT z{I=_4IHgVxCnWejZDW1geA(b;Q-XZxV|!9Xp|e9@v5T~0Pky+|v7>jM>c|~RxBmO< zTp8U;!4uArt4A^VjOk4e|L2BAr!nT`-Y&x?CCX=<(q(CV?rl0dN{hY~P_2Xp*{o$w zhd?CQ*Oh~U*xB13P|WoH=Wi|An+VBF1vvrHA~;Khsq|!y4G#g4!mc|J#k6p7F@lGO z2X~L+b+ygK8nT_sFWM*OORM%%@UfG|DX)u&0D9=~# z$Wjd|n=On`+Zf)*+uXUbR8-YZXG(hqt~Ovh56agQgoTpU+SBg5Z41Y?PL0zyU=QD$RCQYjn2Gpmc;bBHb~ef zQmAjrdwP3Eo@h&+#z34g66pRXHKPPOrb~=1Jvw6L9L>JQN+-`Rtk&$2Iykc}_(;Qv zWRKQ1dH*2x`XHyxk@mNpNuD2-a!SVHLU7|rL9E~kCoeCrj<9!-S=?RrGVWjFWV4*J z|CNVp2j!zX+N^}wt4Zu`(&{#ZBizf!qh0?Wh1vWz4r}2>_@1oq&%ZKLqyjRWXI|JO z9?GKz&>o%KO<7wHjmC%sZ7?!Nw3|<5KeYQB*Yxs&ho3M6+J1#82YSV_B*t&HfXabC zQXQPmcOcWTwF_IdQYW>W!lJU*usfDYxrEi&tIc~Tg|N2LN+UNnw@6{7Lg$U|4N|1( z>1{~+gK9mSc&Ad^@X%WMkrdal>Gda^OHntwR&A;#-%f6`KQo0oX#m4Fc%$sop{kll;oo(AhmVlfSHK!#K$riDj`VF*qO<8p~KCbs6cUN2wCqn~w+uf5Ow^|m<+t)4&gowl-^DS|4uO=G6zE&dTAQv2oJ;T;IOH|pV*~LlM^K=J zZBuf)+bwUbw(?;g4N6VAuRorhiv>6-VwEK)AG_NaXDFZ>zVXjVwMSuL$B4@3>WQ$r zShQ%tMM=wrCF6;HG}y^U+;UxqD6G;U?2rKL68 zbwgocJVCN*Wdk?~^tihTZNqCt4~LHYrXo`bZS4z`9HY6mlz;%x{-f*Ps-6v zM&)t^$$4fAQ`d`v){E|&T-OmiG_ws6<5AEIhJZMk3pKB_X?10UNV#${!GL-}rflfX zJ+zpr4QtEs^07F3nwHv{p5=I?_X1Z0JXNPZg*Y(xNE*sa@x+^mOe>=np&$qy+G{w4 z>E(TD8_xa!$fSlo9=z4LZ^q);dB4|D8b)1p80mcSgZ^&AT~Gw|W2h4(KS0VHnewUO z7A?^znI?tgRNQipJ*kHL=5otZJD0a=K_q^xze`f0JlssR>aL1I-Up75Dg+7Le;*gS z{#iG0f3xbk1CjFlYD$q@dA0{V7)ap0E?pTJph0eS1MDMjygm#8DSRZlF~k{QYV;b7 z?`Hgl9pkG^b1g*MP3napsI{6U7KHBiHp{CzcVyKzCw12s%XjQmc;%ce<=0%U@8{jxP&GAuCuPU9(Gcr!2 z1z}}{qGsj&6#`!ft!;ZKSb$7mlD`eB{+yNl?&@%=KT-MnWefKf4PJ_G+27a}h6Zuj z>p)A&9FP%_9j$-;e1JB{-WRK}PUOC#(wmD2MS+lN5iHpRe)${`q89rQS<`=p;D~VVS$vhD}ti52CctT%izlZPAa# z@|`Ogr!7~p13GY>05UZVr*ccDGFKX~teY!dZct+CDQYZ+h`gw3Qu~?G>0FmiQ=2-d zzXpuh1G>s&%FyblR8R@!z2wwnMp~j^J~fl?kbIx>$KT935p@npy+cSxllRK}Lp9}7 z_UWFRYn%YbR_sM`j}Y^IdEf0*@j%IkyCzza{UU|v?(kss#V#YeDKP`qZxTI$&nhf+ z;W&SFiWHO;BLH;8NnMFmaJe(=cuw5ZrH(hNKFSO7wu07{#Bv{A!4XxA7AzWiJgAm? zD6C~RYcL>A=FQsZ>CKV|tqK$hogJWL(WRZvOuu@tH~h-e6TAe#>juLl?CesZO2VPV z>jh4)`g$7-!pCMOZziY2Z#Vuv=T%i^Y>yxUb2|x&WouJeLKS~OK+;36UQM~#A@8FR z7Z*o+nzZr9@QRLnfBWVq(fiVWn zQTwzVA(lhDkc@{cf38lz9}(i@Y`1hnCf~}MuiVXovKNaUA3IJa`M;x$z_U3w$A7pv z>-xB|z9LLs%2=$mZLm?U*Fm4bsM?&xSD0PQDdMgiAt?FE;{UcQ5+bDg+jic3zQo*y z2z-XEHhuTUl@4vbqeodxEZgH-XKP=d1%rB3Jg0r!Co949#;p6=aB5G+WQU~mWRzWe z%hDJ@LZfgna6PbD`f+9eCZ_+V0x6U))sUfVzSnhg%5TvO!Npkl+QOdZ-_sj5Ne;5Z zI_id9jsLV%55QxguoT20LnW_ILl0zjrH#%Y7Cy zMPzQoY&U3)*%Y;twQElgmrhn2thR9jFybdL?+)I#+p9S7swRq8s(9#6t3m6#rX7J- zRSmYX&SqD@lzx&R(E6#2v^%|yMYGHW2%_C=N09MvsSBGkHfrLStnq%OwXGUB+~rg@ z+eu&5B8pftaGs!Wm%V)~fjzAhqyv*dwaSRSv7-2hCEeF}2B3p?1f=#BlhPnAB!X(> z6~!-Vk`d%%W0>j#;KJ{zRiI<{7xjCZzx3!dZRzafb98i6sYgAAOd+1TH{H#@{(Vq^ zW7^@lnfy40u!WgqR^ss+IzdjezFoH;@)DR!Umw3w3eV3!UwNf!smGpIltdk$2I+68 z(Ah?P@%iaVWB>9~XWP%Qsg+9wVd)#=>p*_=+sbv-Hr?we%V1o@CGJDi9R=C_t~(lm zxmRP8p%4Lcpo7Bw=FMIqvQ4O~O9OuN;(wlKixHkTjNTSj&zzOGK~(c<(Op|VK+5dz z{wK~pH)Hi2s3B`mwA0K|j)v<>vj#CfnU-^H-QA`h`cx7~V3*b_Cnc6w0H>)V(|j7~ zDe*DrouYs|q*Htl+S>vTYS_efeDBN3EwwA5L`NC1 zH=5ZN3?G&Ln!<_wDVq3$dyuo3s0kSbj#kOCnVZE~>>H>i0%^46kbqP^2vs1XwB!g6 zdO*oBzwqz?%X{3``|wqBz?_>jP`_|o&{|uPuX010g(QFC2WjwK0!gGQRKuVG*L@nm zfzH^U0{SN0%fg4XI-WJ~I<0jqgA?Oi-5ZFV{F?N>SyicWy`Rq4&B6!Cu5qJ1S}!ffqBXHvyc0fi2^ciG3C4U~k2 z#IdValcGo=nMziGUiBRx_dUkg3R0cG%_u7W&5NIcOVlI_mf3o9p~lcDXH;Jg6u_IcKgJr zLC3&=2nsUhdzbX_lCrn{=1@>u&TxX%i@nB*q-H%X&es@2Q?!?s;dd%8MyD zD3(NnjTI@AyjSxVpc>A(5)F5AVpqQyiw%6ZclhGPmii1Ho@V8RYaj7KTtJjiHzCQ~ z@vbXu9tFy4$AootSRNplf&T+x#@O9tjiQp-xj9L$$0#-G?~F0ciTMD+(6IcUw~%49 zgnMEk4R2KB#Vk|5kBG7r_JZv)N{|Q_6WyH95?8hQ40_c{VF~=4pD_s1koS4wtbD-Q zqE%hg&5VPf7Ftle_r#v?bN3Al$ zpsU5%@QkunK)8I*>ks0hjP#h2zF)njx+PRG zQSV;db5`QfaJ<$=6y861>^*S&%cvGJ8!U5n=*hSB9H1vdl9GpoB%XZC%*r)moSX?- z?;VWZgF$uSurjKHrz;C9#d>=0oU(It%n?8o6&2qU6v!S8m(rb_bdn#T5V1}{QCnd_nkbph;zu?J<3 zZ>RT!5+Q$_EV0wA+ENmjUNAnV*UY#g!AMA@jevDiYtG&TI!&Hm6>`Y?tsARqh za38m`8ypxQUAJe!OURk_U+(w9T8`u9!7sbcD@vc63!IKj0>Yj_lYV+aM=;NW`>)q4 ztLSy106~4s1!FE8r(&7-&YS?}1I7xTU?wj63?xF~{iAw}14);M z``P00^kvM0f{gwfMzi@dzK-f zb5N>S(^W?P{!266E~C$ljWFQ(W(w{XoCELHllb5@O;*TF4Ay(2zClK=P5n{ic7tmXd*NJ}QFmd=qlk#E@Tl%LM| zEzJa~RQ1HqRestyP@PirBKvKRgs(k$hQ&qyXRpF)HmC$%QK^^)wB=!d5&9DIG)h#h za>~16i3ToA;4km*Him17_`Jm~ERIcI%ZW)l!z^hIa4MmMH@#hf4ntp7iTDNRZ0w=% zNm*gM`dqZCu(*u(O^R90U#UlsTj~21LSNgdt!^$PvHPZ1!G_8&C}83=p68J31r4!o zr`Cb2n=5#(j>Z^QEWU~Z+X&d#p4}AUg|S%GM(vSjS5r{?j+7|VE|2f?8VuM&0pqgL zdu=jpU6yT|mcR$6p?T>Ns&G+{hUt?Vu+*KFIZ5ZR}rDc z9M+SlwQ&7>ydn{wmjllif;;)r`FD~o36IBJw)$Cjck?;u5s?4*{3KR2!1pB> zM6wlkE1HW}dIwp2q{=lr%kk`@faU(8DP3!u#yDQojx+aaIMv$UFI>f9l5m~briMf% zh`06p>w=^nN7ca$Qqg{42a2$lMF#`a_!-o{(w`8AD1nky8+k}(H@m_>$?f&Yf*G50 zV9={JR-mHY`z!Fi{DIV7plxA5D3Mi(dKW!sG5J55hmaK z`-O>D4}2@EK##Y!PV72+R<7T@EwjkcAIkU0Uk9#xPcNt3wEbks=L(0dRU7Zc{Q{IE z$SHX=)=TOpVdw&wqEQ*3n^Nbu|HLjtKNd-k4q{m^b; z&znxXD@4fjmg}LQg&#bHHp5VJ*+}K?K)DaTO#C+qwZAdf3d&nR;4ZXOI;fjD;(q(y z1rpnr3lq@I`R4Bv*J6JGs~~8D8nkIlq^Po1Mi^k>Ew9O@UQg(uzvo!$b8F6IFNHgu zRC*>0%nf{Eei@sl?8)h6=sK=1GO$h_BrHVFq7jlbf0r?&DAM;D6pb==Alj{ zqtPB^zrN|IY6i2i3hQQE=0Ap7U{zzesU;;B;86LXxg?TDz31r2D~|-9-$}TQx>nN* z1*6&$n;c#M@aeu5SUj<(7oYghY{Dud(;+?X?5MEu)LTC_hMGjyj27 ztZQwi`sP%D(AU!vHe`-;n0Hj}U6Q|!UKFGQ8}yE>mjrkK^jct>rpup7`#pP^#eC|RX6<>FZ&p&^+w9=Bsoby-5;wgAwJmI*Gxi3%aiYl2oVPPw* z{VL_FIqXJEE`Kblcx^Q2wJHiUuk`e`T<+1e*b_$=I0p za@n9TcC9r=u&@$ky3+W8@}htT1MTex;ql1e>(--9+=E4}kzw9o_|Sl(I{& zwiuIrvdJI@MZ%g@prf~^iiJvte?C9OfyR!~k?VeYKWmendhJE)1k5B{0oq;t_=q!t z1X%Ce3!VMLD&Vvu$YtFj_&?Hm0LTXtytwn<a*V=n3}fd_XdO13i$ zZG3<+ai=|9&m&$GE-~e3!bzCUEt_+M_V^nMNy=nel9l%hbtBY;dR5+(rpnA%8RAqE z(9yZh+J+#vCImX^H&NzTL*64F@6k z!J!25aPA)~FXZu7UteC3VS#&ierxpNY`K)4t(~1%B*q>%-C@#QZ_vQ3gP=+LFwnJ# zvbx%@0zDU(ZtECB%sK$D?58YP#H+a5pzOiyhVBZ;7nDF3p)(sW${qTMlI4)T1~2PDXYfE$W&2t{PjUlR z7zwS=>}Jv~xJH0%_71H}wZ@=Zpq8T2o`*+5;vz2`5+M)6VKk*E3UFQv+MzRm+vLwD;@k5>Ur(W%_Vl`92CWM^mxAcDOVwBv z3|`QnH%o*rCv1(LWXzzPmDuw)fX@TVmCJQDstYevD>R|)0gx!eOA*_wkYa*Q#7lr{ z__nkg40?Lk@fw#iFEE>k7oj4e12jwhUW2jF7O=$fLk>sW7FKX79u7qfZ!tnaK|#lP z%8q@?zo4yrS0X&!7Z>1E<;?Zm++4Ilhniti{?KGa&nd>j^$JSM$lw~*QGw6Ib)IVn zhLY;+0FCy5IJXaf?sDm|P168Ngh+{Q7zNmlgl3XFO3CiQzy*M38=2W@PTlY&LFaCR z#$PqTRLoKtkHrxlL3@Mg+vJtWbjvI&QyObd6192hq6w4~ZM%q|ZdVtb%84MK{(vOA zpFi{B2-?y?4PV^_)TT#WI)Gd6YjNi@tQxd!foio33=E8qLkMmSAQ@u=JpAc?gIObA zI0n(tCX|tql8hx|M)n1*Ttrdb1^6I$^%pqq&=Y>ni!H6%y1;P;#*4{Y@?XSz=zd`H z(-vA?1lj_`LQfHL61_3n_puxka*(Ej(fbw0tET`LT36LoLu#d@%_(L zGnY=}Lq$aaDpt|M4mJnPL)v{#5fdl3PgesMlSMnq)jr*j^PK65BPf9c^i{qKLn;xV zh-JYmx>UYeAHdpJb(u_wsRysmm|0?z6yVuGv;iCC+Dip8%>-6{#-ccnX42wzl$PVM z)Y&LMXt8pkwWmjr4qY!C(5fh4ms@QmC-+hV8yg$={@MB{A!?c|z{wv+oZDOYVX6Y= zPPic}VYs0#CC18(84KbJZSR;jr*MZ&ld3A69f0A^Fjj%4lN4Fdk19L+>{_Dog{8qD zc9{3V-edo=CD;#HTCEbOLTHmKMJl}gAU z&DwaLF=!)3!g&`s59nXNa2%zqRUz9EgzGm+y^IH76KI&5n-kF)rZyeLqWQYf{)11Aj|hw+_w&49{U zJ|An=gn`?9x;`|K2JI7+`gCNC!!e0ST|6`;bV(9;3AUN!xuk`aZ0Ww29CZgefgvMk zx0E#Q@C;003iPJ>v&Q{raH3hhw%1hTY5+3@zU(9fq6q}ULFLWO>@685*B&$#xaFtI z$}%@>4c(XOp(EU)N}7xx+_*d31?|=DaUV{7-~lKK>Ne+ins-5ENF`~*5-0TnvjEzVqtHM&VMC+&SJ z0i30pwE-#iF`>HURzO9{#H}@SMh)6gw80N5As zpy+WRS|I>NOIjKP7&}FvLcE2^vtPWxDnYH&o(XFMSrePEOXVe|6C0}w$rTk9H1C#v zQxx_Gb`p-vt&S(*7x(X;g<4APG_&Xa)CU~|OH`kh7fC2@IA2^@Qpe1$X`Du=BTHpk7T#JHf)Xk*6}V_O^z@2cNFN!a=lyhqR3$ z%4Ud$;Lv&mMF80AtQ{Tk# z_WrF&$0gLbKqr2XYUd-)T-%^N*$;O?0aOYTKZJo%(AgI{LSiTSlY*s>JN{lZ`b^#DFycle!G*vQ8x3gVK-%9eP9{tvLJo123+yDkU@Cp#GL~_B~MmbVTqX=dofKd3I)a~X_(TRp}z~wPNX%WLqYnFQBEp?PN1Omj)I#G zsILp*w}SQ$tY$(qo~!-y|D5m1u41h*q}bo3c!{fYHjC~(|AEu*Wfq`SQ*t1dfG>oMKyrHZfm?$2a{%-AP?rW=D-RQszW5QXAEix2Aa^N{W5(1A4>Q5_A6)A! zno=A`$AeLgJ-h z8tp`H(R#QxzsLe?Yo)H+vYaB?WI!J1!nr;=a8eVh18}FxW;b-q3?10*a53}&rgH-A ziPd$HaqEj4Ag<8a1lmVYmNSuJRA#Rui#|MR5zC57p^}12)z7*8^WRz`hj@6v<6m|4 zJ9iNE@3IEM-QS{cj;c`rkV7!YY^4k$O-0oP{1XC!1RtZIy;EY0D%V7($*d+vjEbT57TmeC2lwv-+csHwJ31gH!l|U zXJpZEWbiZr0fBiZIp%TGJMt}XMV#jCqNxC15Luh0=OJ5z*T|f(CggEqH5?@J_T;~0 z)z;3c`=ZPJkAIXBc1qrthj|wKb`%08(xAlw#yz5bvuOyf_irad|E)FjfBWLi1S_E5 zbLWbd*H`mC-aXOJMxu`QlhfZT@fCS#`Pp1hEdA1ANC4@?!=s1lrh_d#7U6mlDFQN? zr|S#P*ZtL^BOT~|+DM=clWZ<~4?)tOS8w~KwGiW8*C$^}8v26tQQ(iw16VqJ1{$Q?a6T#8Gj!45=S*PqJuZ4XIuCVYI%qxL0L3CDUentVw` zVrGJHVWA8)M$+%odGa zTkpuEagR`8s0NxS?U#lS?}z9IIJX|$l6}-3A?-%XZ{4UdqrQ7PXEnPGF8UB_v0t~c z{B4W2Pdp*lHD4|U{@?;AOY6?P!C~I6PJYTOHMEHtNqOJX4L$*S#@7;u8e|c~U_|Ne ztxNG$Z^cJKZIJ>} zQWKF?suidBJjQl&pg9|yh+cl4HZANzY1@PQY2eostvyQUZcSX#Pv%F?ezS_D@)W5& zmvK7N(em7z0W11x=iXZ;ehem|cylr&%1X1UqB1=TmFmGqbcT@7oEU!hdf|_Uod6Kf z#oV^tTFj(IhLpBxXWy=^+uEVmSeqVP=$8Q2F`;yShk)nYi;7cLrm(Hn?a$5Ywj=5u z@O0VAo34J!n^*mkWAwZtwvO>C+$3sMyOcIC&L{5+yg_}(PnhaFM$vfmr4r?9p1g2H zUk&(D$jR|S%F9*ZD69J57fN534sL_lb6C-0k5U6;Xpf{0y#D*z5Ypofh}QH$K4v&quqvQ)>2l z?T4R7Lv^tIYOd_sJOIncge9KmJL3{>0QWx!Ylp;^)P=8!ZGGUe8>+LqHQc10=83wH z>F6Hbqhq@LoUaK^%aIY}24qa@#Drk?{p5m@b~I0_A0LNXM9x>jWemW_w0+iatjIai zo;eDJPbNcHMt%E;=Bg_!zhi}P=yI~bJ5D&p8xXZ`rJpQ!IUM>N?e%6AWm~$GhRIMP)U#{{33)Nd;ffWVPv0_sl>6B8D4m+ z?o&V!@>b^d;-}3g1vLVGP#M)_k1h9af84ava%|yDs7ucO!j!ktH=FMW$p5)4f?j?# z_t0gWyM0@ja{TRV^wd?pTgp8C5Vzdv;1!UNbc-(Q`PjBA*F%mFRpOZklvv-md!K7S0TfGg&E5%$j1y7g1Z zwK`$+1mr5+I9g2uLORjwMb@fl!Fy70s0q*OWhrl4j*T?LaBFV+*Y|FkxsB>YS=GGC z`)~??OLGXh>AQUOqZ;hA<2#fOw14tyQoovPc9pzzRxRQqO9(a@vhT8e|LKp)&XHa| zyWu;KVXblJQ!jK{xU6UrPH$yw90a`GH0hl3j)$A_umUgtWv;h!w%6RUm3-XMo3p^cpCenz&~Re;9Je!8~=z6|sS3ckfjS4aeaDR4nRm#w3`O8PQ5!eZV%K!)t=&sSRD(n=orzg9D z(7n`#Tz_70k{DiaAZeR)ZUK}Oxc1gees$ja?l>H7lyO0R(x1BfBMwyz0uFGUGma&l zT>`yN=-Pc|;(H*XzwxhDu5+e;DAv;C|7;h z)bD$`iO=tD8?zg*-WhOHSEVWNK>v zlcgB!>5EnJ5%o*iO4bJ1_#T&|bKDEv6h#*fsCzP*DF@?orB4(N zI{%(sLaToGkeNy-EI~u>QT4X@y-(}UQ=;i2LcMr}+v@bfchQZYDqcBRyxD%^R%`57 zmqR0&%qXj`l_3>UW{i7An8*7)Uijra*Q_hn6O@6gL;xdr0Gz=jXlere&)eg+v9dhr z*jV|%As`A&pQhzp#ZrR!PWU^{y}?!{wia&L=W{QxZWn|>g}5^(WXbq0l)&hz$u;TH z>+Ke%$F)zPdz3D7f5$8034#;hQF1)4|1*BBjr=xQX-r}+e3$kYM*WjHllE@mh~Bc~ ziuV{{%h3rjEf|#cCMu!l+z%w$<6J4knXO}ZY}dqdfVWR07+ZkOfA6-;e>-&#A z{CDf-{eKuX6*n(E6RH4z6A5NHb3k4FkBjO5oCN>zE7)sf^8d)j__2pS_V7o-{z#(# zLTB^`r2K%CACU3`Qhq=R+PVAB_LqL3=noYAfucW9^aqOmKv6)#|B{)7A2sDiP5Dt% ze$lOD>iWmt$0g2(XLG1$*n((Q~3Y` z2guCNXnXON?$#uv-=EOtM!F85S@Gj1un9kQ>_@=-$d?~*fdv0wh?e|m|AcD1w)t}y Q1*$6=*DvM%e(TTw0mC}5yZ`_I literal 28517 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 MdQv6vj|+GH2ecTcR{#J2 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprect.png index a5e2a3fa99cd0b889849525b6509612f15bda3d5..b02a10caf6e9ed358d8ff375ec15a4b72232b45e 100644 GIT binary patch literal 18636 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 literal 21011 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 I&syLAFO3r~p#T5? diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprrect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprrect.png index e3ff650bc8372dfc1bd23d66d6b73ef410696a24..e97edb37e4fc3950ae0ffab64b5a5e5d4db30ad4 100644 GIT binary patch literal 25946 zcmeIZcUY5I)IA!zg2%cVpiDOhEJFRndi8e_!sJ znkgaJRdRlj$v#({Ej{1mCD#@<$>Z42?Pz{N*QJyuv#cH%;1ImiakSog=`crpadGkB z^ggWJh19amHQwUas>ulnCvQp)zkDrW+xuWy$U`2(u0z^ViIj1lJCm^=!=5G;LBg=s5}Zrt{e;(2 z&iL%`sx!JpPWKlU7psNxyAL~#e19?;O_1Z^sR`xgwoJ`ZD%o7j2t9D*go3%b`OB+u zCo)(uw=I;s``Oc*=}%mL{fE9S$+?8qGkfgS>iyKs&CQF|A$fUucXSv!*5|^@f(h$G z+$>}%DXGB#ULt$r;%VJae-R}|URP4_#>+(c%@v)@+Z91&Wy`&uu^Ix^3**h4g$fZC z1C@TFo=a0Y1$F}umPvEp1dZot?_4_hT{_IdEo^PM(}#Cp4o0eSNXa0k zWR$K|cP(|<%F7?HR#iUc{`38@(UTDh3SKLpxrwo%az%qW=We=6P3JVzk!ymFs^gZX zvKvbOvN^3T96nTaL?t-Gd)ksgm%Gls`G!Pw24Q3Jc%A)ln08H=w$afca%iY8qnyqd z2BXG=;}{D*S04}%(Bsl2l-K7&JCWgUwh6DR!6g!QHETRRvW6_Tl*LkGQ?{`*pvU^e z$;CyBYpc%I=Lq-OkH<?8RPChv{39v&sL+V%*9eX zQ)D@n`S}pTJ5J2)0^Ft#yMe=3zVwpDzlDpZK2>H>hsb2uF2S0RfB>Pqc9Vivif6gk z`}fh4Z3>59G3k?^^Ic0OW{{RT{Y-<{M1-+|;mXx}a6<2DqD~kQZ?Dy+RFO)n8#SAJY($Vye=C~HY2w?)mf#b zrSOC+C*Nt{w1jLFN;1nTx7HiBG_agwr&#nFRD9-Mxb;8W=fC0pimOi4x-;hvK^XJk z!yh>RH(weJ2yh~FO$(+hQoAc2b zYb)sFH&@8=S~7Y=@>}!rk4xl~>P390K@o#B1@=ROq*-rUE7D@C-?AjEd#KlQHUuKE zl7^xh*2ruOXR|Nc0P}z;OKl(Vga6~z`BCzdlTo<-4?R4}^9HUx&VHsxPF!cN2s^{! zwOVUT+*-@nO2)ag8M-pi=C!*#V_O9`uSd(t-jv^1a9e0pjx`V_zZNycetr3UI=}Cj zYp=UGg?9bTS-wLuzse&<^j!jbheG9#B}94P2ez7{+(+INj7E8yKwzcWR$wr^cjFFW zFNxU?RU5!+=>Nvonb$6^5$tQ=aXiQo3e{7m04|Nn~P!% zLzb#Wva+)F^^&8^#PF+eD>V|OOooPr&JCn`smUq`hoQt+qQE-Mt5w(hz!K&{?WeC6 zz6Tc=`rzvs&vf^TB`qfO{Bq#5%Dgzxj!x;0XD@73GSl!b?v?UPS&1~$)>K_T(>OK$ zc$H|m#Dc7nhfAF+z00K%A6Iu^{DV~78P4DOxl-?1dX8uE_U$;eq>gKUCV4M)B-l-T zczlc_Z**f+eoGf8-olvk)2`2ZS(D4yMd1#wMi5p-hN|#6Y9GYfZsOi9#|Bw?auj^= zng0GWr<1Gy!AG7yL}~C@%C5{JY%LSG^G4&G-@?ZEnMzORs9%asA(3QjLO8W?J{5b| z&M&NDFdP^AEiwVMK6PBf`0M5}zUbT9T;|;L+BH2(ozN~T8fds;=|1=MBt5yavvcs> zte5%c=$*Hib)MRav06=6+cDCc? z1jfasB_&&1Tbb3LFGO8uy2qxbf)`u$MYMFy!p6qNEZRpt*?M+$4So~MNX3a`AJ1-0 z+Y$t^DS6cUf;vmf6RV@qp^}ax3XoE%fFW7m1X2|A_4NUQ#66$zzjgS=?`Iml){>xP z2h}dE_GH_gOaNcD8S?ffj6aOc&zJC_b@gd&{8(_9OfRG*-Cw$Ul{#s=r&V0I^i=F zBGJn((d%X!%qby^knoPQ!@X5T?UQfq{+8KVpVyo9wZ2iF?r=XLtfe0>!D!|Yy>KFJ!EX;eYn3P&dxS~kEvu^a?Bj4tOUsmk<*(aC za1ZfZJuY@|x|UeZks3pw=U)AyOvioBy#8%vR)~0~#ShD*YR}&JsLFD5@&~Y(3)k+G zW{F2x(d&JGE17-#p&5XRrbMy6ZjbxuJNp(@Wd;G8)3%KZub*Pt--uXi7h3$OWlGP?VRB(D*!(q!cO8!=;QY_F{7NJBKY zd9=3+rCDHtN|xUgohg~GXCn}xvTPS$|LjWx=qkA~tTilf65D5FE>kzIV?7cp#-@>ymMv1p$1e-`r_|;`g1lyrCC{MW#^0=` zb#B>P)rqwQ!#!O61OpKpfdFJ#?jsN=jDZmOM_X#%o05r;j8y})vei0UImu*b%vXPVWSziexNTe4{B)hd0 z_i8R?g`F-7z0L>a<=#~BY^igD{?AzM4T}MtTmoB~&mJz!gJ&n~4*J5Tk?f{m*ERxD zKNtvWZ;X#BaawCMON`NG%Loe0v+LYUR46RcMBHG&#k6zT!1$6A?&Rd>lXLDjC)bPK zsr-j6;}Oc=N?t)!Q4!AXbq;&-(8q6Enkr0ItkDME$(wfw^Wfmyn6n3yxv0Nh<(HT# zwBDLtUz8w?-*Dbq$m-m;I3UyWA&kqpMb*y~u;=I*o04uOWh@gY0}-sTOV|@7k1bjs zw=&kgDzhpCO5$sAYrVRJzplT0!}~c-rz|`&VaBf8vUNy`FKDriMbEx{}nH%))DXu!jI~ zjAkKH9GU+ng>KavZ=U^F7!KvgkS+t$wEvPjgLwOG-yO5+vyGy)DXc4dJ?g^>iH|EAX2zyiB_~*4(r;|Ls{jE@SQjYqBN&lLsr)RY>5~c4)(_HRe z%>6vB9EpD%uPt5u@fbxsHf=OeND;}|;|-#3lS#x{+kd3kA^8ml+9Ba_iIT?i5pB%G zcb+qa&JFn%jWLy3_qJL}JEQ&x6MvRQF7Dfysmwxz*Vo4?tubb7e+iyoj#r(W*FM`W zn1{in23wuIJ3Z+zY_MAb&PK~p#H@NW>3Pn@&-oatc6)U3jOhILq#}ZfK2)*&j z-vc#)sCG`4e)~R`yz#O`(<-}8+)sHAX_++3l{e}Q8DsL8%O#O!Z*6Vuwyl%^;@X5XUw~|v0jgUzYf)bB6+$~Dqx%L)*n4g9sjr5rY z=$g&XP3-#?v0S9|4YsKBseAIfndYu8 zeP{JHlwXKsq}$PWLuJnP&$B8fnni}&$r$jzoFR z@MAs6(!Gs8MmtS{gy_h^CBG{D<=8~nC!SqancvAtK+F-r(Z1Bk_x0s0$e+2{2>D)S_zZDd^f#w~uwY}&$OLF}lA(k@O9=)|b zx+8RsKfKmY*$%+q-j_+vWWkJ*Pj*&+-RvoXJliLum=qfuJIJDa%zL3RS$ixg$9-Z{ zZn-dgy|n3>Jd)B_pOpc>c~oT6+-T-<^KUB27|a>mPUT%;SL52Dbn379jyA~Dq|o|(tm83QOvH#VAVylF zPcjGQ0%^i*KT`ZNu0vsTYUMq@t#>9*J{T0 zFrt84AwUbw&vY_1yZ&h_Wxpb4F!3c|D5G06a%NbJw}6>5%Z)5Fs~8XDT^&FR*Qgd` zXE3)o)rk}183e4xubKJo!03Iw%7ycHNUFOIqCUE67o*INDYNpccc4mUtn82;^s7?} z5_atB>oaEm6Y^-@O-y#S0Kfz1sgT!!WBJjG^B0})B3i2ynt#O50omZ znvuC&Q{6oI)Z?#1kO2nlhEh4)e%I-x^xPNT$v{pp3qWAUfa)0(d0(`&v}9qKvL3hS z&uMeY%OJN|UclQez8wUZF?A`@rs%$3SD^SSZ^%kR>e5wZ4H;_H5tdo~oUIz^zSa7` zd~LPq$r;jeul~FY1|#Sjwzi+xI0LRm@{0QR;k44V3F@;@ua6Td2f|%JW7U9<&=xRU zk+_HoWGNVt>2{vE%^LMG^WOvW>4O%#z-ALX_QCZlUw9QKRJibNt#R+bQ>9ocTiHr= zMChjHI+Z*bQT#kcN6Xp=usp0>^s1p<#U9P&u6BDe*O1|?N8A4N{aVK;OE+-K-HxW9 zIJ~}EQ#R9V@_Squ^xN#d*?lvP>(?J|$_HcaT94xvm=A&AyIGdgZbZoxrn?KiQ_BvP zeTcyrUF~O&Lu9EkYrHw3%boC4OW!&--Ztm*Aqat+)zO(i1qjB+?YiBDLj2}SX1!-E zS338mSl9+>wRCjoieiu_Atf!%8?&8^vE);X%#;kU9GeSZ0}*ltB{twytL(I8fn-JdjW96Cej&h+iF0MxGTbf&aJJw+*e0 ziJ@LUc^M3wZ;@u4Gc1=5`CX1~x81e=l z<8dtHy;8DfZ(`J+@&t-->NA(%|3jw&1LRsL*`@pG?$^vZ2Fwo&c;Lr`1oJPB!7cGF zX{nueIzl=h{C1}fIyqvgvkrhH9-RX{?!a?D}DD>#j%K~{7=9ld?mYw$>Pq8sh4q~l;r zkZ>{%EM_gSzs@(el`iF0W+7qlp@9-`O8+b9PD9cCSiABa&UNbiI>-zZwe2lou_06o z8mYE*i@;kkrKIKQ4Xi+-fvO3!oVM0h*86L87lvKnp()>4@F|YVmUcd#sra&f zq}(k0P3p%^J`c;R_05AKqjb1cDh#%W+%QhweJ4C$4uWh^R#L0{@pDztr-}w{Xs^LZ^ z|G@l6-c3|Z>{RjDg8o2@tT8C)nVFg6sfnK!+lVXW|S|nn?g>E$HYZ3Kv8OQ z^k$CTI(Yw_EhXnwk~9KAsK%v}bJj*eU?H{xNNTY%f@$=0WDA_wBMtIfUJ43t!A!NU zIck2;BOSmeC7#AGLZ^;g5PYdEZ_EYYqf-S#M86?a>Yp0Z0s^{h%VeXx=Dt=^0gM<| zB{3L(U&vd{;>sEz&RUC~2qO8Qroktxd9ciR{b?er2@spNMWdYe5i$(4BWGrLU=bW? z&Cus>&fNK%!mDgyxR1-}R^4*luVc13jY^tZ8&R+*`^mKYR#{tPV`HrQ&u4;DX7j+R zS{IxXpCcPktY_9Sn%4b(BtsEcn#O#(E6K9FZ4;al&V_Z5;y#p&GH_2@-;eIau-oh; zpUQ!4(B_Jk^>hKZR{oUCxA)-3L%#zVNWoydm&T7;EikbGx|ZU5*3=N1@KwON#*A z(^Ve>MrRKQR0ZuB&6U}>&S&(ylFZ~^JUIeZ-waCea@rKoKi`Pr%elN?czA5U?j4(% zK|xXa>(fB3J|y-!Xzmb*fXe209wQvxFyh@klY21i{9JWLn9rsVgsC1KHDGj{C(5mj zu@Q^~!t`cxo`TEA&tp`8|5Ekjh$(2QEH~<4JJdGQ&kPb)a}!2qyw?_z6K1=i9@_tW zrmUn;CEjIq1PO{%@K30~AjowReOM%5Ze-Bx-tts^@tC$W<^*O!ey|Ka?X=Quu@EZv z7M0t+^1w-E;_}i`Df^6g$K4lh^PsZ^%X)yAkinDUMEv0alN}sLplj*;J71&GSNb-( z&bDtqyp{U|OpeSKZlJa(M@cU^&kjNXwSRCEiLuGOvx7k*MC4<@o9{7)%?ScxRgHR- za7anC3Bj8;fXTdr0=&(lf$O%GuqmYV{VW||unhqE^}&wlA%AKLbRe=CHAxL6?(;?t znve8+cqE_WSKl*HD@S5|I{^A>9{mJr?|G0Nw>O(n;au9MZvyr}U1|`9@0j(3#zZyP zGTO_IXm>8j;ctUSld)!-G3M|oEfqQq@i!t_0k%@dCzrv~-M6?V!ux3_<|t0lj9Gg9 z*JnM(kl_A%hTwNJ!!}gSdGew44*73{zsC({I z8WqsgXSY_94zQ`CdDr~W=eF(qOq5OU(h1EBj;3zP&FBs&TmI)=#lSofg=1w0BRW%7 z+1xy#U@bei3*nL$ zhmM}Q@zrn^Z!pXSu$7 zvCKUcr^;&-fQ1lty5X3E?+u}l0X(a*F8i-6P zL|FWwPHxKF-R%%$Vxp$mdJD1&8|W+#bwT&$hOI|)rd7_`R~4VL|FS&VEg+hFP~mSN zl}d%KlVp0qVBkf}Afd3&B z%FsqsHa70NRv!j!U!(<*m*q;DG|ZaL>k_w*`{4$@tDS+{fFBh~wjx|gXKZ^lP7OI^ zCnBv5gIsFMd^?hRws?K6b_k+6C@LzdGD99c5lp_CL()@!=v!qe9?_<4AtniZF3j`8 zILw1r$AM+|nlT=NM*p9(4|l;ImsR@VznG>YFwlT`umBwW|GeH1esP~QFnmk?QB=y#cl&$lXMMJT}LWesGYl<5ER%SPaROtTnyH1iYv0ws*V`E!e+f-c3&sKfg5D?H+kcaJ4aqyip*OeBT zu*i~mX6nlz&OSg176wn0C^eC|@=d$+Frr8xL|;G~hd2KEo5TB#x!ukV|I~yV_~(c~ zZz<$u4KmW0j2Pfv5g`-2xZYp>63oQuvaL=f4rwt11c57?) z+cSbwW!hd4|COYIkry9&W=f3>T!Z9t0z&kjdbR^%B(a%B*W5 zB&XflJ29AYJ0@IW7udyyIG096wvhg-`0Q#{eP*I&7mfweT}Ej!JyJz}!|Ak6=KH~R zP~JMtSbEme0SVDQ9)4Zq5KguiCsoo{UIz&mokHF^^i1^!4yORnG$slM;;6~p% z4npe=HH$^S6l!S6d8MT~jTcBOx^E6haHoH4c9y$o*d*FxB57rFxlF#_vk#i{pm*(8 zhr`Lk;l{__y?aMR^7gZfbM(0enR7Ex`gY7}3x}g_ICS%lWZ$a`1dAM@F-6_;)pB<9 zM(RwiA@q?@k5U`k1%iF(MfCdJ9!EGY!F*M4W18ob{l7=aN{`gPlTLI;t`;JLXv#?R ze$z)Kp^W=_i!VcgJ!;7uK51fM33M-ZAE|Hxg2WV_nYA{NLJlumAHJ9Y#{h?hgTC;- zjTMJNECjQDw-0Ta3Z}=0Uzeeg|3J%xk$6$h<$U=wr)^=J=|H(WBK5YySe zkMo1e^#rJ=zk%ck5UwY&L@E52d4-Zcy8I7Iyn>fCU7G5kf`%_TJ`dS;@Z>|&r$CWW zv%@bz_FxSnJe9Er^{(W~JL>COZiWeT2JyvgmT%8Xp|1T0;c-6j=~4UhQUn^Qvbt>L zSqMSJ?*l_M;$B9uP9=xHZ+~ibS;oM$7nmO`!0WYOuFMTM>mbLX~I50Fa zvod8auRrhcs_^IYYxp9CSyXRry(je%LIwn|i4SfJac{8$6;Pp&(f=YfM1pZ5G_lQFeOnk!J-7@9vc714@&7ZhxbKTbKc=TgEP#V zun|dMB*HLJOZ+p=Xf(K#E(K=cV_7<5v28>ABUJC$|R@z=kz& zu7gD`TGY)Scz#sg#|tq~b_u93V0IvorO*yKi4pG!t|fU@(JSxIi9j<2L=qT28SYVd z+TB#}*1%df6otkX#Q!nyMvMpCa=+aA3Vtn&%n`Xb!F3liTyyU5PT^{t1A>;4o(q8z z%>2+a0M%L;6tgf4sMV&X zY-iw1(XKoC>Y~=%66krm={4U9eTjxj_ByY2nSo7j0%IpE z{9o(wr24XO>^sO=Qh%RN`cqytDOr^t|!Lz)>Ll>O-zx20!G9M?hOrsKq+*lbg~D9i2w`|XDJ zoq38SEII4$*XQxF^6)Mx_JLP}0O1xRmbxj#CL9=!FqM|;4vj1(XvhxgfLR8hPLVZl z^kS6xb)m;mtf?hQJ#uXw-XTq?-@+cT`{td~NL-&5cn`azTPW4nOS!4n0OUG$K%A&qu(B+}N1-HeB42J~3b zNF>hQWB}HYZkeu>%vw597b)>b5U|LCVW%>E!9vruNYX#a0FC;%rO(`(z!KFW|1|@V zr#u-kX>IGNKhQ3_+^ybbz&nii(dQ8ls1bsjkdhW+JNNBcLrMp?gq$lx>uNYGHG1AI zN|deMsB=Wp*v}Nr56RZz3u1URIgb8`X2Ao?U<33*${X*3M`AS44#P9zuyz~Jlfqc( zk=LeZ8zsS~peFouAktv84RFV7k?Q-_VQ79m&C{AqPs&@4s537 z_v+!68bf`YzRXK}14X?GhIWa;eKbDk#B98074?Pc9KB&?8Riw>#)fT0$#29gwcY}4 z73(Y0bCTLz8G2k26(g^o+m@@fxcdYopWc4#B|ETJ$Ply=V`PcJOJkRj^`wt;fk`mL zW!0xo5z&|=a0h(ED9ja@Q9`b9QswG_JRw>MV}wDdc8kf|fR8AQE$)9rzn2tM=r~#f z=8Pb58tRH~lN;v3ctqp5 zzekR9&AKTMw1X>;^7uyXt(LAXPHat;&jr`AnBXs0ws60iXCc2roFGB9{f`O7# zse{flJSUAc3JKzZv6vvdQFAq$vBBd0=ct_`*1$roO!^xZz*~_Jy0?&8!wFTTc!Ey~ zCIF3Su37_EM$pcoNVA@<|(*#M2KXqJKuo~YWQMi}fCqOPVy z$rFgiR?-ZiU5;jdaB$u*3$0z!fcks$C$bT{O|kjM0}5Id(1v8C0yNG8NX3Ml#{qBD z?{e#zG}3O4c-3*+mw}E97S&J}5QJ+x8_+nzaV`cx(9{zDQvE+g?4hh3;=ffg|8CZ* z+rFOyjd9y44`RAqxFqX zr_`9w8QJ7gw1RdW!o7!i(z0|sVHPHQQJc+vC`6)FZ1yaSQB$`^TF5Zb?E{{7a3vL{ zeydSyAyEv{8c+PR@drt^B)7OhO#f-koVQjv~4p=%2{ z0|Vov^ikpPS}}46$Xd5s$B*+2BBlNQn8Z^U->d@k5l2Lw%qf-cTOl6U3a+;=*&ZS7HrFW&vMlHE^s`Iz6ozy;5~@5LC#| z_uOTtOEshoY!a@p8$ite>g2v9~jnx;C*sB<0bZZ?gXjJ!G$x!o*q)@anT4-Fb`xM8K3x z@t}n31%vO1rpEl=+O&WthP=u-Xysxk?Y^-WUv0dkL8~JG72|la*Z4JK{E8}ucN|)? zN&_d}0V7%8pKgBF08qjOycg~HODpb`qk-3o9-E+m8MLVJK^4WjW3 zC#Un9(F1!qt^R<(MpXtk{8D`o4EdCC`wY*XNfSt1i!JH04ApmZ^ zv6uCOu*faY`M$%=Rr}FQbOdR)(k<}5>g!=vR30euiS&^zn921NHGv8AxFkzm*0ad4 zJnzf}HF@mL|LaB|Y=+Ge4+k3Fsx#V&IimnCV6$`>^WY=40(J}Y@gx|K7|b|K`$8S} z*a_4C^E9gb_ecLLcfiLz(eMRwOz);ncQdyvX;`OIn^@|A zhJ)cZauDTl0rg92PR=h)thDumCoO_BYjUdp5slMvw(<-fZ+X81^ImJ`zVbiojGNdT zqi2H>qGm(P*_Ns+<2J16T?L}sG9jxfsgG*888h&hL(x4SuLU+0)iJTwO${DTUI zK)UkU?aRDlPL9ZQb>1<3voG4`c%@aQo^}b|`ok_5HhEFL<5!u7N3DI^(6zzMgjeT1 z`F+ohcCf{KudyUPscStt)B^sVmcY(^7ZkeX^G|M4+Xu&{8wjnv8!AFi2(rybA~U4L z#iP0@Q^SA+0e|2ggmtd-c~h5_UsWBM4;T03XP|T$6T6P-GKV(IhP2%{I&?T26bhtl zW|POSq^N?$XVt30qw@IJFK~U$3)kfuE%ln37BQ~69RMwSb{N4p{gKo7)s)!tSK);9 zj~OvH2u}p1NwBwVKQj|O^IK)q=1Nx?YjrcdM9F5%23H~dB0_2J$z+v5 zIP2mP*?6n)&tn7|C+k!?O!>DP{TL+@c|<70l3Ry8wx2GOq1l*Nu=LQ6+Ai}9_O$UL zT#!qS*5_!GEy(Pswa|q>e>?-A3Z~n+?+jix_q{|qo5WtH4PW}{uRFHU>x_WV)Ocu_ zY{8fN{9$73s1PyANVy7@;#hl`pn zH3V?-)>$3`;d%u3;N=L;!?emBBFn-IKCM4TPa7=c+o&_DxjG7eQosKl8L|`eBLWuq zPI*KKeh{mrXXFV|Ck)A!faH|$@@Inf@Aj)_!CqQd}^sk(}z!Bu(ooK_- z3(gM6vK=3@3doNq{@cg0q$O{Q|MqcC!~ghLPCft7EU0_Xw3wz-;oiyrbt@>WsMy{I zkMy3%!>!@}eBv~G>EM5UX>1{>$jl8qETk!)DrrAvq@-2;=W1(QR-aP@O4#6pIr={A zydOOIi!Vj<-^)uc?P=063l`D+kL#&i0#9rH5&fU*ooHYBpHDZH>N4?45Yev_FU=OJ zaRXUxAqsa=)3q+)-52p+`wyTEQ6t#aId0i|Uk{XHZ? z(A)!4f}Vhv6rgICJMINBrmCouMox^U^7OZxJinMs_ne^WzZq zmP226k}p&Bc42-@z>%k2G3_$-rFi{1X2Y4<%JQP7(|{TCpcQVJM8aD?G%aus*@5|T z0YX3We4ebhk>mC3a8bN<$q2i$EhOI~c(wcvDbtwPy1lPMS{tspyYF_aLLiSVmzrFV60T$A7U;gF%F8(UfPqS$2m$va4h#H^*No7y)4Yghnl#Ec}&uD`rW z#IYwsfz5~!#1D{zFum_2dZoO<5T!eO$XUIgLWbZ2w+IPZ7MLy5|?X|8K2(B zqTaT!wgdAt6h4vCw>=}=`ua!Xl-bAZntzDx9=`V5NOLd{KE0aJXJ>#SBwcGrs&jCW zGmsNtaJZOoTZ5yI!kQ5+CEPhBDfK>2t!grX7(_MP42LZ=DvjKulqPo4sfsg!XuEV) zuNVFd6?@mT3vz@^&jCxAq4!dBQ$r`o(`Ut;Fxs^8_o&i z$?LQ5N-a)?S%>m5z#l*7ZF zCv5HTC(HF&%i9gm#Z=;iT9Onzix1+Gc=LyP+ZcXHO{p7B+=P7NM1das{y+9P_rMOp zIH+Q5DIu1j`rNwMVTZzw+M`;~1I&v!D;}R-IHw3~K??vjvnitJo_1>1RZoP;)T;Hk zI-~kb9ZwVU5}R+Yfnkt3u0egfqZt^WN{y`rUqamTzy*`#hUq5+nLL~aO}RR}YRp0q z9ePN}Nol;V0g@6^d>G6X$w#~Xr zNYIydx^5UNC?0j%>WmZ+rt;gs*>{ZrTwiKfsCt{=`7Z$9R8Y9i(ag%3+Q^cjL#j9(_le%I$vk!j_c>UwbX55wb#w%j`3-O&h}X88rQsP{7{!^ou-t@oWQj3;9u9C=^@}$^y+!864x_+y zqFV4YKFFTSI_ul(yw_*qnNx_?p`!1s>(++=sS$1R4IZ}&%9ZZoIU()|`<*#Q9l8jZ z`oC#v;{WK8kKc<282sNcz4QOaqaz5TGo4?frdJpQ&Rew#MGeX8HC9RR^^5A(Ne z3t{jZ4}as~Z#?{shrioczYF5;g7~{2{#J;;8PRV>^qUd=W<Ah)COr6!maq%Iiv?`L|3|e_gibMPNBwz) SHFzZEisDU$^uKRE`o92f8^Xc> literal 30269 zcmeIbXH=9~*DYGzrZ&<>K$1BiK|lnF-Ad>dltx5A2}%?Ji3()_Wd?~VDj8Z(P$URS z(gLJO5(EJSBuhaNr7Vz~&fIi=?|Hv7#y$6r^XrcD>W@~6s(SXb!&-CAIoH#-{y3_> za@odZ6bfbK@4u-Wqfi#gQYed)e_D)Joi?b82;N9p1zn zJN)s*J(a@KnhWgg?1B^*QckaEbsv70HC!pblg2~&CFbx>8b(Q>m~D3;f0eGq&?%QY zm-6GUUxpU0!e5)HKQ6&vw-v4|z+b-^PndI2c9gfbpN>$Z?LQhJyV|NM%0D?dxu>ts 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultiple.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultiple.png index 1ce0be1f75e23268b9136160bc46a3ccc3469889..d964f72ac5a44ced60d2ae521c2d065418c67551 100644 GIT binary patch literal 28040 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 literal 33777 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultipleBackgroundForeground.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultipleBackgroundForeground.png index c37b6a29f5495bafa4747053e89377b17d251e7a..989e339d90e6ea619b0ebce31a6a27c37fb4658f 100644 GIT binary patch literal 12557 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 E0QL=Rc>n+a literal 63761 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 Lb}Mtf-+uTXs5&*+ diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewOpacity.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewOpacity.png index 158a1ee6b7d8e32a2a458ff6a3f783e9c748d6d2..5a4671cc0a4a7f4a6c63dcd8192329d539a45fa0 100644 GIT binary patch literal 25349 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewRotate.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewRotate.png index 6f799ad72c3bd36c2738ca6916ebfb1651c5f5b3..a0c3ab7d20e6c406852c8c64fd1947ac9acce115 100644 GIT binary patch literal 24657 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 NpnXZJ=ui8<{s+0Z=)eE~ literal 28736 zcmeIaXH=Biwk?Wz8PJ8INLDbDM6x6Uf*BD-GD-=E2nCWQDY_7eDgq)3OE4h_l5<)D zrIJJxkW@%Xk{pZ7>yx$j+55bE+I{Vuc3XS*hwFzdsH*Rq-<)HP(R&|#)}2!))mT?> ztzcncVLftK}2dGKT3(BjqjvB`PiGW@u!aAOgE9F8bhfFE1bTktQ-{d0Sp*KItJo11I%{jWbR z7`;qMk-VCz{~$Q{(2*k_ek)sr_iQ{8wAb3s!J#D3eI%MK*VM$sq%2%UY=f}&@1vum z;*Ncd52Y+>yz|JM?Uu!R{k&>wYGmy?Px0)v>rAF;#c8u~%UJn&d3o&+65^(P!~F}s zY_aycJ~PFn&CX0k$H$ANUHP{5Sg0s(|4#Di0|9HlKVQ6fG5$f_eXV1Y{W*n^PX{#& z4V9WTxunX=%d483n>z;eMLCkqD*0}6`ag-8&cwqVKeLdwqb|?Q<*~e@XoOSioma13 zZS3#z^O2#uN;G@S%_s%Z9U3eLi`J?8z4I%nI5XRbyKQ^DlUtUHi>o!lrH}KIZFW{x zRCKgpeM3WZVxo8eulxqwaK^pt`@7qW+Wc92h1qe&Nke?@x`wpLA~kz^dr3*jRb3Pp z7Z=7zS9PLoM}<>=E^|+0I+dR1#B6e8#Ao+${gfo1Sg?byfB7?vYDarzBtLz;M5Z-= zCVg&0^-J|i*FOHaaaxA!#L&hHvTQH-ZcrYQVY$$!41ZkB^khu9DmnBu%9SK9#HC^m zwQO8~=d!)>EorOm^vyL&-VqTI;*Z(z(c>ZuX}{?to1XYmTg$;C?=kBtDJ>l^=!cii z|NLE6^{~0Qd7}-DH;iFc8NvJR8~$GKWsixv1}E#v4Z`oqC#Jn~PvVIl%h_&w6dum_ z{QMZ^obSn#CmLE>8`iI1FHR}(dleINC)2h=)x@OjuF=cvY>L&k$xqkWS_Q zLhETr7n>dApG$11%W)oV?Vp?NUuK`%ot2%<82J7!kwOcQbs9Kt=jgbmlVO!N(VoEw zkT)Lv`BmJqK4n3Ev&v*+!qkr-b>rH2gTe2&`Bjx7^S=*w|NLppDDh8p=&gShAJ11~ zK_b%*E3!*S$X8$V+(3E9zUI58a<1dn)8E&4cuSa7teT`S`|12s%>F<*+xDV&Ym|9} zwBj~?=4EAc72H@fcw2e;!r@ZaipO&6#PY_CuQX*z@wtsu)n(XFt~yGQXrCRYDVOBA zO)VX@!Nermch@-0jMg#=m+||Rhs&s8Gw�i{*D+&Lh}4x#!jaaX|~6gC1E~S*>5s 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 z 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 U!Kduz0^2AIp00i_>zopr0MWtwumAu6 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewTwoIntersectingOverlays.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewTwoIntersectingOverlays.png index c67a8bd48ed4320adb47a1f04a76bb1e902ba0d7..d56e416e633f40443f3e460876ab0cbdfff1c3b4 100644 GIT binary patch literal 33889 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≈6SxT`dLFve?XoSmou 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 LHq(qvuEGBXnWLUj literal 40144 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 jlIE+V`6}suRwb"; }; 0A57B3C02323C74D00DD9521 /* FlutterEngine+ScenariosTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FlutterEngine+ScenariosTest.h"; sourceTree = ""; }; 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppLifecycleTests.m; sourceTree = ""; }; + 0A97D7BF24BA937000050525 /* FlutterViewControllerInitialRouteTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterViewControllerInitialRouteTest.m; sourceTree = ""; }; 0D14A3FD239743190013D873 /* golden_platform_view_rotate_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_rotate_iPhone SE_simulator.png"; sourceTree = ""; }; 0D8470A2240F0B1F0030B565 /* StatusBarTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StatusBarTest.h; sourceTree = ""; }; 0D8470A3240F0B1F0030B565 /* StatusBarTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StatusBarTest.m; sourceTree = ""; }; @@ -243,6 +245,7 @@ children = ( 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */, 0DB781FC22EA2C0300E9B371 /* FlutterViewControllerTest.m */, + 0A97D7BF24BA937000050525 /* FlutterViewControllerInitialRouteTest.m */, 248D76E522E388380012F0C1 /* Info.plist */, 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */, ); @@ -469,6 +472,7 @@ files = ( 0DB7820222EA493B00E9B371 /* FlutterViewControllerTest.m in Sources */, 0A57B3C22323D2D700DD9521 /* AppLifecycleTests.m in Sources */, + 0A97D7C024BA937000050525 /* FlutterViewControllerInitialRouteTest.m in Sources */, 248FDFC422FE7CD0009CC7CD /* FlutterEngineTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m index 82aa5bf670993..cfa70262f2585 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m @@ -11,7 +11,7 @@ - (instancetype)initWithScenario:(NSString*)scenario NSAssert([scenario length] != 0, @"You need to provide a scenario"); self = [self initWithName:[NSString stringWithFormat:@"Test engine for %@", scenario] project:nil]; - [self runWithEntrypoint:nil]; + [self run]; [self.binaryMessenger setMessageHandlerOnChannel:@"waiting_for_status" binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist b/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist index 032a9620fa3f2..bc23fd5445d4d 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist +++ b/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist @@ -41,7 +41,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - io.flutter.embedded_views_preview - diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m index f875764e99e2b..b43c6bfa15bda 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m @@ -34,7 +34,7 @@ - (void)testChannelSetup { XCTAssertNil(engine.platformChannel); XCTAssertNil(engine.lifecycleChannel); - XCTAssertTrue([engine runWithEntrypoint:nil]); + XCTAssertTrue([engine run]); XCTAssertNotNil(engine.navigationChannel); XCTAssertNotNil(engine.platformChannel); diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m new file mode 100644 index 0000000000000..baf9ad3441960 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m @@ -0,0 +1,84 @@ +// 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 +#import +#import "AppDelegate.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterViewControllerInitialRouteTest : XCTestCase +@property(nonatomic, strong) FlutterViewController* flutterViewController; +@end + +// This test needs to be in its own file with only one test method because dart:ui +// window's defaultRouteName can only be set once per VM. +@implementation FlutterViewControllerInitialRouteTest + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; +} + +- (void)tearDown { + if (self.flutterViewController) { + XCTestExpectation* vcDismissed = [self expectationWithDescription:@"dismiss"]; + [self.flutterViewController dismissViewControllerAnimated:NO + completion:^{ + [vcDismissed fulfill]; + }]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + } + [super tearDown]; +} + +- (void)testSettingInitialRoute { + self.flutterViewController = + [[FlutterViewController alloc] initWithProject:nil + initialRoute:@"myCustomInitialRoute" + nibName:nil + bundle:nil]; + + NSObject* binaryMessenger = self.flutterViewController.binaryMessenger; + __weak typeof(binaryMessenger) weakBinaryMessenger = binaryMessenger; + + FlutterBinaryMessengerConnection waitingForStatusConnection = [binaryMessenger + setMessageHandlerOnChannel:@"waiting_for_status" + binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"driver" + binaryMessenger:weakBinaryMessenger + codec:[FlutterJSONMethodCodec sharedInstance]]; + [channel invokeMethod:@"set_scenario" arguments:@{@"name" : @"initial_route_reply"}]; + }]; + + XCTestExpectation* customInitialRouteSet = + [self expectationWithDescription:@"Custom initial route was set on the Dart side"]; + FlutterBinaryMessengerConnection initialRoutTestChannelConnection = + [binaryMessenger setMessageHandlerOnChannel:@"initial_route_test_channel" + binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { + NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message + options:0 + error:nil]; + NSString* initialRoute = dict[@"method"]; + if ([initialRoute isEqualToString:@"myCustomInitialRoute"]) { + [customInitialRouteSet fulfill]; + } else { + XCTFail(@"Expected initial route to be set to " + @"myCustomInitialRoute. Was set to %@ instead", + initialRoute); + } + }]; + + AppDelegate* appDelegate = (AppDelegate*)UIApplication.sharedApplication.delegate; + UIViewController* rootVC = appDelegate.window.rootViewController; + [rootVC presentViewController:self.flutterViewController animated:NO completion:nil]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + [binaryMessenger cleanupConnection:waitingForStatusConnection]; + [binaryMessenger cleanupConnection:initialRoutTestChannelConnection]; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m index 15acf3fcd7939..48bab99653e0e 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m @@ -6,6 +6,8 @@ #import #import "AppDelegate.h" +FLUTTER_ASSERT_ARC + @interface FlutterViewControllerTest : XCTestCase @property(nonatomic, strong) FlutterViewController* flutterViewController; @end @@ -19,7 +21,12 @@ - (void)setUp { - (void)tearDown { if (self.flutterViewController) { - [self.flutterViewController removeFromParentViewController]; + XCTestExpectation* vcDismissed = [self expectationWithDescription:@"dismiss"]; + [self.flutterViewController dismissViewControllerAnimated:NO + completion:^{ + [vcDismissed fulfill]; + }]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; } [super tearDown]; } diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m index 310f43fae4390..61610c06468a8 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m @@ -1,10 +1,6 @@ -// -// ScenariosTests.m -// ScenariosTests -// -// Created by Dan Field on 7/20/19. -// Copyright © 2019 flutter. All rights reserved. -// +// Copyright 2019 The Chromium 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 diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png index 9ec19ab474f03b6cc7786cb038ee3b3e6d0c51a4..30072dc62164626aad700f7153f9a56b64192038 100644 GIT binary patch literal 20863 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 literal 20295 zcmeHuX*iVa8@HyJnz3Yzr3E$ieMzzvCQH_kA+oDbk8B}3O<5*oDSL~pkUjg(q>u%s=030UI)CT+JD2M|+}G1lqoY1XO+`gTr=hN* zPelcrrJ_PeAz|PX)smMN!DXMTzM2wMUK`sa_>ZTJv4-u13si#O9!Z7RhoRaJJp%qx z?K?(=pxjeYY3^hF_g;S=|DR{Tnjtr+;D4Sm0axfxB=`Z+f3C1(*xx5$v&sAa-Xo-- zN15f^xWEPFtbW;*ii(8?`q`(U&pi)5WV@lFYXGj$#-Yno@DKm)6}p!srTj#Pg6l&K zm2(E3`=@BVmW3Qoo zEAE=V()MiS$ENStBiA{d&TI4$Uu9`u;g8Qjx^L@!$Qxhsl75%>84gx2Zkt9p zRMxIl)orxYi*C=;2XrpKI37oy58lCFSM-=&@xF=oTnrW3#;5$UiQAH?R`gyXGY?cu zrq>A6T!x{2ge|-jD(&p3OzZK`!Cx68%Q=PKgb{}sqw2|M$ zql9LA7VneSbIERwo4HO~Pv=4E+IARkA+J5RTrU5ixIMDd-Gj|3_n70e{kVRPqm%YbIrEf=)%WS0xw@4K>=zfOA>tKgx*wpiLK}#s8`F)Z8tF7a!t7z7<4>CBITKz z!{_m5FWzUiD$~o$dd}A|mHK|YyyH4I(Bw_OI$M<_R7_9vIen|xbfYUnVx`aZZ%(5}4Y;&(4*>&L_jytIj{Ezty%+*$Wvj~__PCB!-R41mQZ@4VB z(*JF1Rc{KP5FV~<_snHf?8gPCb<3b_3%5`07E%0D?E@JS){IU*z3tSj^fc0a9lab$ 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 diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png index b193419929e7b4666e8b753d67d769ea26424595..69ba03a131136f38e4d7e23a8b3349aafd15dea7 100644 GIT binary patch literal 19543 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 UX?g4Ek9yr!xuaaDX!+v50K5E}D*ylh literal 19558 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 bW=EVquwRNbf&U@~0x8O>-7mOj_VWJ#(2mT- diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 36b903e6044ad..f1f23f6fb6cda 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -90,6 +90,14 @@ Future> _getJson(Uri uri) async { void _onBeginFrame(Duration duration) { currentScenario?.onBeginFrame(duration); + + // Render an empty frame to signal first frame in the platform side. + if (currentScenario == null) { + final SceneBuilder builder = SceneBuilder(); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } } void _onDrawFrame() { diff --git a/testing/scenario_app/lib/src/initial_route_reply.dart b/testing/scenario_app/lib/src/initial_route_reply.dart new file mode 100644 index 0000000000000..d53a118e842c9 --- /dev/null +++ b/testing/scenario_app/lib/src/initial_route_reply.dart @@ -0,0 +1,30 @@ +// Copyright 2019 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. + +// @dart = 2.6 +import 'dart:ui'; + +import 'package:scenario_app/src/channel_util.dart'; + +import 'scenario.dart'; + +/// A blank page that just sends back to the platform what the set initial +/// route is. +class InitialRouteReply extends Scenario { + /// Creates the InitialRouteReply. + /// + /// The [window] parameter must not be null. + InitialRouteReply(Window window) + : assert(window != null), + super(window); + + @override + void onBeginFrame(Duration duration) { + sendJsonMethodCall( + window: window, + channel: 'initial_route_test_channel', + method: window.defaultRouteName, + ); + } +} diff --git a/testing/scenario_app/lib/src/scenario.dart b/testing/scenario_app/lib/src/scenario.dart index 4d53abcd0b32c..ccf2be02778de 100644 --- a/testing/scenario_app/lib/src/scenario.dart +++ b/testing/scenario_app/lib/src/scenario.dart @@ -9,11 +9,14 @@ import 'dart:ui'; /// A scenario to run for testing. abstract class Scenario { /// Creates a new scenario using a specific Window instance. - const Scenario(this.window); + Scenario(this.window); /// The window used by this scenario. May be mocked. final Window window; + /// [true] if a screenshot is taken in the next frame. + bool _didScheduleScreenshot = false; + /// Called by the program when a frame is ready to be drawn. /// /// See [Window.onBeginFrame] for more details. @@ -23,7 +26,22 @@ abstract class Scenario { /// flushed. /// /// See [Window.onDrawFrame] for more details. - void onDrawFrame() {} + void onDrawFrame() { + Future.delayed(const Duration(seconds: 1), () { + if (_didScheduleScreenshot) { + window.sendPlatformMessage('take_screenshot', null, null); + } else { + _didScheduleScreenshot = true; + window.scheduleFrame(); + } + }); + } + + /// Called when the current scenario has been unmount due to a + /// new scenario being mount. + void unmount() { + _didScheduleScreenshot = false; + } /// Called by the program when the window metrics have changed. /// diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index 6f4bf28d28e0e..6aa553380d34c 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -6,6 +6,7 @@ import 'dart:ui'; import 'animated_color_square.dart'; +import 'initial_route_reply.dart'; import 'locale_initialization.dart'; import 'platform_view.dart'; import 'poppable_screen.dart'; @@ -41,13 +42,12 @@ Map _scenarios = { 'platform_view_gesture_reject_after_touches_ended': () => PlatformViewForTouchIOSScenario(window, 'platform view touch', id: _viewId++, accept: false, rejectUntilTouchesEnded: true), 'tap_status_bar': () => TouchesScenario(window), 'text_semantics_focus': () => SendTextFocusScemantics(window), + 'initial_route_reply': () => InitialRouteReply(window), }; -Map _currentScenarioParams = { - 'name': 'animated_color_square', -}; +Map _currentScenarioParams = {}; -Scenario _currentScenarioInstance = _scenarios[_currentScenarioParams['name']](); +Scenario _currentScenarioInstance; /// Loads an scenario. /// The map must contain a `name` entry, which equals to the name of the scenario. @@ -55,6 +55,11 @@ void loadScenario(Map scenario) { final String scenarioName = scenario['name'] as String; assert(_scenarios[scenarioName] != null); _currentScenarioParams = scenario; + + if (_currentScenarioInstance != null) { + _currentScenarioInstance.unmount(); + } + _currentScenarioInstance = _scenarios[scenario['name']](); window.scheduleFrame(); print('Loading scenario $scenarioName'); diff --git a/testing/scenario_app/run_android_tests.sh b/testing/scenario_app/run_android_tests.sh index 7234b44caad4e..efca54d75efa4 100755 --- a/testing/scenario_app/run_android_tests.sh +++ b/testing/scenario_app/run_android_tests.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # 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. @@ -7,16 +7,29 @@ set -e -FLUTTER_ENGINE=android_profile_unopt_arm64 - -if [ $# -eq 1 ]; then - FLUTTER_ENGINE=$1 -fi - -cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd - -pushd android - -set -o pipefail && ./gradlew app:verifyDebugAndroidTestScreenshotTest - -popd +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -L "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") + +cd "$SCRIPT_DIR/android" +GRADLE_USER_HOME="$PWD/android/gradle-home/.cache" +set -o pipefail && ./gradlew app:verifyDebugAndroidTestScreenshotTest --gradle-user-home "$GRADLE_USER_HOME" diff --git a/testing/scenario_app/run_ios_tests.sh b/testing/scenario_app/run_ios_tests.sh index a1ad83dff1853..42088ce0d21bc 100755 --- a/testing/scenario_app/run_ios_tests.sh +++ b/testing/scenario_app/run_ios_tests.sh @@ -1,23 +1,44 @@ -#!/bin/sh +#!/bin/bash set -e -FLUTTER_ENGINE=ios_debug_sim_unopt -if [ $# -eq 1 ]; then - FLUTTER_ENGINE=$1 -fi +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH -cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -L "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) -# Delete after LUCI push. -./compile_ios_jit.sh ../../../out/host_debug_unopt ../../../out/$FLUTTER_ENGINE/clang_x64 +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../../.."; pwd -P)" + +FLUTTER_ENGINE="ios_debug_sim_unopt" -pushd ios/Scenarios +if [[ $# -eq 1 ]]; then + FLUTTER_ENGINE="$1" +fi + +# Delete after LUCI push. +"$SCRIPT_DIR/compile_ios_jit.sh" "$SRC_DIR/out/host_debug_unopt" "$SRC_DIR/out/$FLUTTER_ENGINE/clang_x64" +cd ios/Scenarios set -o pipefail && xcodebuild -sdk iphonesimulator \ -scheme Scenarios \ -destination 'platform=iOS Simulator,name=iPhone 8' \ test \ - FLUTTER_ENGINE=$FLUTTER_ENGINE -popd + FLUTTER_ENGINE="$FLUTTER_ENGINE" diff --git a/third_party/txt/src/txt/paragraph_txt.cc b/third_party/txt/src/txt/paragraph_txt.cc index fe50b789378da..264db4bafda79 100644 --- a/third_party/txt/src/txt/paragraph_txt.cc +++ b/third_party/txt/src/txt/paragraph_txt.cc @@ -1045,16 +1045,16 @@ void ParagraphTxt::Layout(double width) { return a.code_units.start < b.code_units.start; }); + double blob_x_pos_start = glyph_positions.front().x_pos.start; + double blob_x_pos_end = run.is_placeholder_run() + ? glyph_positions.back().x_pos.start + + run.placeholder_run()->width + : glyph_positions.back().x_pos.end; line_code_unit_runs.emplace_back( std::move(code_unit_positions), Range(run.start(), run.end()), - Range(glyph_positions.front().x_pos.start, - run.is_placeholder_run() - ? glyph_positions.back().x_pos.start + - run.placeholder_run()->width - : glyph_positions.back().x_pos.end), - line_number, *metrics, run.style(), run.direction(), - run.placeholder_run()); + Range(blob_x_pos_start, blob_x_pos_end), line_number, + *metrics, run.style(), run.direction(), run.placeholder_run()); if (run.is_placeholder_run()) { line_inline_placeholder_code_unit_runs.push_back( @@ -1062,8 +1062,8 @@ void ParagraphTxt::Layout(double width) { } if (!run.is_ghost()) { - min_left_ = std::min(min_left_, glyph_positions.front().x_pos.start); - max_right_ = std::max(max_right_, glyph_positions.back().x_pos.end); + min_left_ = std::min(min_left_, blob_x_pos_start); + max_right_ = std::max(max_right_, blob_x_pos_end); } } // for each in glyph_blobs diff --git a/third_party/txt/src/txt/paragraph_txt.h b/third_party/txt/src/txt/paragraph_txt.h index f5dc174e131ef..4941bb87d3e28 100644 --- a/third_party/txt/src/txt/paragraph_txt.h +++ b/third_party/txt/src/txt/paragraph_txt.h @@ -139,6 +139,7 @@ class ParagraphTxt : public Paragraph { FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, CenterAlignParagraph); FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, JustifyAlignParagraph); FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, JustifyRTL); + FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, InlinePlaceholderLongestLine); FRIEND_TEST_LINUX_ONLY(ParagraphTest, JustifyRTLNewLine); FRIEND_TEST(ParagraphTest, DecorationsParagraph); FRIEND_TEST(ParagraphTest, ItalicsParagraph); @@ -234,6 +235,7 @@ class ParagraphTxt : public Paragraph { end_(e), direction_(d), style_(&st), + is_ghost_(false), placeholder_run_(&placeholder) {} size_t start() const { return start_; } diff --git a/third_party/txt/tests/paragraph_unittests.cc b/third_party/txt/tests/paragraph_unittests.cc index 1626510008bbc..297136172caf5 100644 --- a/third_party/txt/tests/paragraph_unittests.cc +++ b/third_party/txt/tests/paragraph_unittests.cc @@ -1454,6 +1454,35 @@ TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderGetRectsParagraph)) { ASSERT_TRUE(Snapshot()); } +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderLongestLine)) { + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 1; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + txt::PlaceholderRun placeholder_run(50, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 0); + builder.AddPlaceholder(placeholder_run); + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + paragraph->Layout(GetTestCanvasWidth()); + + ASSERT_DOUBLE_EQ(paragraph->width_, GetTestCanvasWidth()); + ASSERT_TRUE(paragraph->longest_line_ < GetTestCanvasWidth()); + ASSERT_TRUE(paragraph->longest_line_ >= 50); +} + #if OS_LINUX // Tests if manually inserted 0xFFFC characters are replaced to 0xFFFD in order // to not interfere with the placeholder box layout. diff --git a/tools/android_lint/bin/main.dart b/tools/android_lint/bin/main.dart index f9a9e4359dc28..9c632d24d108e 100644 --- a/tools/android_lint/bin/main.dart +++ b/tools/android_lint/bin/main.dart @@ -65,7 +65,7 @@ Future runLint(ArgParser argParser, ArgResults argResults) async { await baselineXml.delete(); } } - print('Preparing projext.xml...'); + print('Preparing project.xml...'); final IOSink projectXml = File(projectXmlPath).openWrite(); projectXml.write( ''' @@ -154,7 +154,7 @@ ArgParser setupOptions() { ) ..addOption( 'out', - help: 'The path to write the generated the HTML report to. Ignored if ' + help: 'The path to write the generated HTML report. Ignored if ' '--html is not also true.', defaultsTo: path.join(projectDir, 'lint_report'), ); diff --git a/tools/const_finder/lib/const_finder.dart b/tools/const_finder/lib/const_finder.dart index 481bd24062c6a..88989059308d8 100644 --- a/tools/const_finder/lib/const_finder.dart +++ b/tools/const_finder/lib/const_finder.dart @@ -13,11 +13,11 @@ class _ConstVisitor extends RecursiveVisitor { this.classLibraryUri, this.className, ) : assert(kernelFilePath != null), - assert(classLibraryUri != null), - assert(className != null), - _visitedInstances = {}, - constantInstances = >[], - nonConstantLocations = >[]; + assert(classLibraryUri != null), + assert(className != null), + _visitedInstances = {}, + constantInstances = >[], + nonConstantLocations = >[]; /// The path to the file to open. final String kernelFilePath; @@ -32,9 +32,19 @@ class _ConstVisitor extends RecursiveVisitor { final List> constantInstances; final List> nonConstantLocations; + // A cache of previously evaluated classes. + static Map _classHeirarchyCache = {}; bool _matches(Class node) { - return node.enclosingLibrary.importUri.toString() == classLibraryUri && - node.name == className; + assert(node != null); + final bool result = _classHeirarchyCache[node]; + if (result != null) { + return result; + } + final bool exactMatch = node.name == className + && node.enclosingLibrary.importUri.toString() == classLibraryUri; + _classHeirarchyCache[node] = exactMatch + || node.supers.any((Supertype supertype) => _matches(supertype.classNode)); + return _classHeirarchyCache[node]; } // Avoid visiting the same constant more than once. diff --git a/tools/const_finder/test/const_finder_test.dart b/tools/const_finder/test/const_finder_test.dart index c3b1a8dc20cbe..e68146761b02b 100644 --- a/tools/const_finder/test/const_finder_test.dart +++ b/tools/const_finder/test/const_finder_test.dart @@ -71,11 +71,28 @@ void _checkConsts() { {'stringValue': '10', 'intValue': 10, 'targetValue': null}, {'stringValue': '9', 'intValue': 9}, {'stringValue': '7', 'intValue': 7, 'targetValue': null}, + {'stringValue': '11', 'intValue': 11, 'targetValue': null}, + {'stringValue': '12', 'intValue': 12, 'targetValue': null}, {'stringValue': 'package', 'intValue':-1, 'targetValue': null}, ], 'nonConstantLocations': [], }), ); + + final ConstFinder finder2 = ConstFinder( + kernelFilePath: constsDill, + classLibraryUri: 'package:const_finder_fixtures/target.dart', + className: 'MixedInTarget', + ); + expect( + jsonEncode(finder2.findInstances()), + jsonEncode({ + 'constantInstances': >[ + {'val': '13'}, + ], + 'nonConstantLocations': [], + }), + ); } void _checkNonConsts() { diff --git a/tools/const_finder/test/fixtures/lib/consts.dart b/tools/const_finder/test/fixtures/lib/consts.dart index 4095df150c94e..b5a7e10f1384c 100644 --- a/tools/const_finder/test/fixtures/lib/consts.dart +++ b/tools/const_finder/test/fixtures/lib/consts.dart @@ -31,6 +31,14 @@ void main() { final StaticConstInitializer staticConstMap = StaticConstInitializer(); staticConstMap.useOne(1); + + const ExtendsTarget extendsTarget = ExtendsTarget('11', 11, null); + extendsTarget.hit(); + const ImplementsTarget implementsTarget = ImplementsTarget('12', 12, null); + implementsTarget.hit(); + + const MixedInTarget mixedInTraget = MixedInTarget('13'); + mixedInTraget.hit(); } class IgnoreMe { diff --git a/tools/const_finder/test/fixtures/lib/target.dart b/tools/const_finder/test/fixtures/lib/target.dart index 4046216970ed9..bdaee2d3615cc 100644 --- a/tools/const_finder/test/fixtures/lib/target.dart +++ b/tools/const_finder/test/fixtures/lib/target.dart @@ -13,3 +13,39 @@ class Target { print('$stringValue $intValue'); } } + +class ExtendsTarget extends Target { + const ExtendsTarget(String stringValue, int intValue, Target targetValue) + : super(stringValue, intValue, targetValue); +} + +class ImplementsTarget implements Target { + const ImplementsTarget(this.stringValue, this.intValue, this.targetValue); + + @override + final String stringValue; + @override + final int intValue; + @override + final Target targetValue; + + @override + void hit() { + print('ImplementsTarget - $stringValue $intValue'); + } +} + +mixin MixableTarget { + String get val; + + void hit() { + print(val); + } +} + +class MixedInTarget with MixableTarget { + const MixedInTarget(this.val); + + @override + final String val; +} \ No newline at end of file diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index cbacb46d23c04..cd162d664eefc 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -63,14 +63,18 @@ int main(int argc, char** argv) { hb_blob_create_from_file(input_file_path.c_str())); if (!hb_blob_get_length(font_blob.get())) { std::cerr << "Failed to load input font " << input_file_path - << "; aborting." << std::endl; + << "; aborting. This error indicates that the font is invalid or " + "the current version of Harfbuzz is unable to process it." + << std::endl; return -1; } HarfbuzzWrappers::HbFacePtr font_face(hb_face_create(font_blob.get(), 0)); if (font_face.get() == hb_face_get_empty()) { std::cerr << "Failed to load input font face " << input_file_path - << "; aborting." << std::endl; + << "; aborting. This error indicates that the font is invalid or " + "the current version of Harfbuzz is unable to process it." + << std::endl; return -1; } @@ -103,13 +107,18 @@ int main(int argc, char** argv) { HarfbuzzWrappers::HbFacePtr new_face(hb_subset(font_face.get(), input.get())); if (new_face.get() == hb_face_get_empty()) { - std::cerr << "Failed to subset font; aborting." << std::endl; + std::cerr + << "Failed to subset font; aborting. This error normally indicates " + "the current version of Harfbuzz is unable to process it." + << std::endl; return -1; } HarfbuzzWrappers::HbBlobPtr result(hb_face_reference_blob(new_face.get())); if (!hb_blob_get_length(result.get())) { - std::cerr << "Failed get new font bytes; aborting" << std::endl; + std::cerr << "Failed get new font bytes; aborting. This error may indicate " + "low availability of memory or a bug in Harfbuzz." + << std::endl; return -1; } diff --git a/tools/fuchsia/build_fuchsia_artifacts.py b/tools/fuchsia/build_fuchsia_artifacts.py index 80944056133ee..709b386220123 100755 --- a/tools/fuchsia/build_fuchsia_artifacts.py +++ b/tools/fuchsia/build_fuchsia_artifacts.py @@ -10,6 +10,7 @@ import errno import os import platform +import re import shutil import subprocess import sys @@ -166,26 +167,49 @@ def CopyIcuDepsToBucket(src, dst): deps_bucket_path = os.path.join(_bucket_directory, dst) FindFileAndCopyTo('icudtl.dat', source_root, deps_bucket_path) -def BuildBucket(runtime_mode, arch, product): - out_dir = 'fuchsia_%s_%s/' % (runtime_mode, arch) - bucket_dir = 'flutter/%s/%s/' % (arch, runtime_mode) +def BuildBucket(runtime_mode, arch, optimized, product): + unopt = "_unopt" if not optimized else "" + out_dir = 'fuchsia_%s%s_%s/' % (runtime_mode, unopt, arch) + bucket_dir = 'flutter/%s/%s%s/' % (arch, runtime_mode, unopt) deps_dir = 'flutter/%s/deps/' % (arch) CopyToBucket(out_dir, bucket_dir, product) CopyVulkanDepsToBucket(out_dir, deps_dir, arch) CopyIcuDepsToBucket(out_dir, deps_dir) +def CheckCIPDPackageExists(package_name, tag): + '''Check to see if the current package/tag combo has been published''' + command = [ + 'cipd', + 'search', + package_name, + '-tag', + tag, + ] + stdout = subprocess.check_output(command) + match = re.search(r'No matching instances\.', stdout) + if match: + return False + else: + return True + + def ProcessCIPDPackage(upload, engine_version): # Copy the CIPD YAML template from the source directory to be next to the bucket # we are about to package. cipd_yaml = os.path.join(_script_dir, 'fuchsia.cipd.yaml') CopyFiles(cipd_yaml, os.path.join(_bucket_directory, 'fuchsia.cipd.yaml')) - if upload and IsLinux(): + tag = 'git_revision:%s' % engine_version + already_exists = CheckCIPDPackageExists('flutter/fuchsia', tag) + if already_exists: + print('CIPD package flutter/fuchsia tag %s already exists!' % tag) + + if upload and IsLinux() and not already_exists: command = [ 'cipd', 'create', '-pkg-def', 'fuchsia.cipd.yaml', '-ref', 'latest', '-tag', - 'git_revision:%s' % engine_version + tag, ] else: command = [ @@ -206,23 +230,9 @@ def ProcessCIPDPackage(upload, engine_version): if tries == num_tries - 1: raise -def GetRunnerTarget(runner_type, product, aot): - base = '%s/%s:' % (_fuchsia_base, runner_type) - if 'dart' in runner_type: - target = 'dart_' - else: - target = 'flutter_' - if aot: - target += 'aot_' - else: - target += 'jit_' - if product: - target += 'product_' - target += 'runner' - return base + target - -def BuildTarget(runtime_mode, arch, enable_lto, additional_targets=[]): - out_dir = 'fuchsia_%s_%s' % (runtime_mode, arch) +def BuildTarget(runtime_mode, arch, optimized, enable_lto, enable_legacy, asan, additional_targets=[]): + unopt = "_unopt" if not optimized else "" + out_dir = 'fuchsia_%s%s_%s' % (runtime_mode, unopt, arch) flags = [ '--fuchsia', '--fuchsia-cpu', @@ -231,8 +241,15 @@ def BuildTarget(runtime_mode, arch, enable_lto, additional_targets=[]): runtime_mode, ] + if not optimized: + flags.append('--unoptimized') + if not enable_lto: flags.append('--no-lto') + if not enable_legacy: + flags.append('--no-fuchsia-legacy') + if asan: + flags.append('--asan') RunGN(out_dir, flags) BuildNinjaTargets(out_dir, [ 'flutter' ] + additional_targets) @@ -254,6 +271,12 @@ def main(): required=False, help='Specifies the flutter engine SHA.') + parser.add_argument( + '--unoptimized', + action='store_true', + default=False, + help='If set, disables compiler optimization for the build.') + parser.add_argument( '--runtime-mode', type=str, @@ -263,12 +286,24 @@ def main(): parser.add_argument( '--archs', type=str, choices=['x64', 'arm64', 'all'], default='all') + parser.add_argument( + '--asan', + action='store_true', + default=False, + help='If set, enables address sanitization (including leak sanitization) for the build.') + parser.add_argument( '--no-lto', action='store_true', default=False, help='If set, disables LTO for the build.') + parser.add_argument( + '--no-legacy', + action='store_true', + default=False, + help='If set, disables legacy code for the build.') + parser.add_argument( '--skip-build', action='store_true', @@ -289,7 +324,9 @@ def main(): runtime_modes = ['debug', 'profile', 'release'] product_modes = [False, False, True] + optimized = not args.unoptimized enable_lto = not args.no_lto + enable_legacy = not args.no_legacy for arch in archs: for i in range(3): @@ -297,8 +334,8 @@ def main(): product = product_modes[i] if build_mode == 'all' or runtime_mode == build_mode: if not args.skip_build: - BuildTarget(runtime_mode, arch, enable_lto, args.targets.split(",")) - BuildBucket(runtime_mode, arch, product) + BuildTarget(runtime_mode, arch, optimized, enable_lto, enable_legacy, args.asan, args.targets.split(",")) + BuildBucket(runtime_mode, arch, optimized, product) if args.upload: if args.engine_version is None: diff --git a/tools/fuchsia/merge_and_upload_debug_symbols.py b/tools/fuchsia/merge_and_upload_debug_symbols.py index baa55983e0480..f623a5df82f9c 100755 --- a/tools/fuchsia/merge_and_upload_debug_symbols.py +++ b/tools/fuchsia/merge_and_upload_debug_symbols.py @@ -11,6 +11,7 @@ import json import os import platform +import re import shutil import subprocess import sys @@ -52,12 +53,37 @@ def WriteCIPDDefinition(target_arch, out_dir, symbol_dirs): return yaml_file +def CheckCIPDPackageExists(package_name, tag): + '''Check to see if the current package/tag combo has been published''' + command = [ + 'cipd', + 'search', + package_name, + '-tag', + tag, + ] + stdout = subprocess.check_output(command) + match = re.search(r'No matching instances\.', stdout) + if match: + return False + else: + return True + + def ProcessCIPDPackage(upload, cipd_yaml, engine_version, out_dir, target_arch): _packaging_dir = GetPackagingDir(out_dir) - if upload and IsLinux(): + tag = 'git_revision:%s' % engine_version + package_name = 'flutter/fuchsia-debug-symbols-%s' % target_arch + already_exists = CheckCIPDPackageExists( + package_name, + tag) + if already_exists: + print('CIPD package %s tag %s already exists!' % (package_name, tag)) + + if upload and IsLinux() and not already_exists: command = [ 'cipd', 'create', '-pkg-def', cipd_yaml, '-ref', 'latest', '-tag', - 'git_revision:%s' % engine_version + tag, ] else: command = [ diff --git a/tools/gn b/tools/gn index 6ec42f64e934c..673eac63d936d 100755 --- a/tools/gn +++ b/tools/gn @@ -38,6 +38,9 @@ def get_out_dir(args): if args.linux_cpu is not None: target_dir.append(args.linux_cpu) + if args.windows_cpu != 'x64': + target_dir.append(args.windows_cpu) + if args.target_os == 'fuchsia' and args.fuchsia_cpu is not None: target_dir.append(args.fuchsia_cpu) @@ -133,6 +136,9 @@ def to_gn_args(args): elif args.target_os == 'ios': gn_args['target_os'] = 'ios' gn_args['use_ios_simulator'] = args.simulator + elif args.target_os == 'fuchsia': + gn_args['target_os'] = 'fuchsia' + gn_args['flutter_enable_legacy_fuchsia_embedder'] = args.fuchsia_legacy elif args.target_os is not None: gn_args['target_os'] = args.target_os @@ -163,6 +169,8 @@ def to_gn_args(args): gn_args['target_cpu'] = args.linux_cpu elif args.target_os == 'fuchsia': gn_args['target_cpu'] = args.fuchsia_cpu + elif args.target_os == 'win': + gn_args['target_cpu'] = args.windows_cpu else: # Building host artifacts gn_args['target_cpu'] = 'x64' @@ -172,7 +180,7 @@ def to_gn_args(args): raise Exception('--interpreter is no longer needed on any supported platform.') gn_args['dart_target_arch'] = gn_args['target_cpu'] - if sys.platform.startswith(('cygwin', 'win')): + if sys.platform.startswith(('cygwin', 'win')) and args.target_os != 'win': if 'target_cpu' in gn_args: gn_args['target_cpu'] = cpu_for_target_arch(gn_args['target_cpu']) @@ -297,15 +305,19 @@ def parse_args(args): parser.add_argument('--full-dart-debug', default=False, action='store_true', help='Implies --dart-debug ' + 'and also disables optimizations in the Dart VM making it easier to step through VM code in the debugger.') - parser.add_argument('--target-os', type=str, choices=['android', 'ios', 'linux', 'fuchsia']) + parser.add_argument('--target-os', type=str, choices=['android', 'ios', 'linux', 'fuchsia', 'win']) parser.add_argument('--android', dest='target_os', action='store_const', const='android') parser.add_argument('--android-cpu', type=str, choices=['arm', 'x64', 'x86', 'arm64'], default='arm') parser.add_argument('--ios', dest='target_os', action='store_const', const='ios') parser.add_argument('--ios-cpu', type=str, choices=['arm', 'arm64'], default='arm64') parser.add_argument('--simulator', action='store_true', default=False) parser.add_argument('--fuchsia', dest='target_os', action='store_const', const='fuchsia') + parser.add_argument('--fuchsia-legacy', default=True, action='store_true') + parser.add_argument('--no-fuchsia-legacy', dest='fuchsia_legacy', action='store_false') + parser.add_argument('--linux-cpu', type=str, choices=['x64', 'x86', 'arm64', 'arm']) parser.add_argument('--fuchsia-cpu', type=str, choices=['x64', 'arm64'], default = 'x64') + parser.add_argument('--windows-cpu', type=str, choices=['x64', 'arm64'], default = 'x64') parser.add_argument('--arm-float-abi', type=str, choices=['hard', 'soft', 'softfp']) parser.add_argument('--goma', default=True, action='store_true') diff --git a/vulkan/BUILD.gn b/vulkan/BUILD.gn index c701763ea121f..5a94bf0e48963 100644 --- a/vulkan/BUILD.gn +++ b/vulkan/BUILD.gn @@ -11,7 +11,10 @@ config("vulkan_config") { include_dirs += [ "$fuchsia_sdk_root/vulkan/include" ] defines += [ "VK_USE_PLATFORM_FUCHSIA=1" ] } else { - include_dirs += [ "//third_party/vulkan/src" ] + include_dirs += [ + "//third_party/vulkan/src", + "//third_party/vulkan/include", + ] } } diff --git a/vulkan/vulkan_application.cc b/vulkan/vulkan_application.cc index 0f645af051bf1..3a1e4b1132051 100644 --- a/vulkan/vulkan_application.cc +++ b/vulkan/vulkan_application.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_application.h" @@ -15,7 +14,7 @@ namespace vulkan { VulkanApplication::VulkanApplication( - VulkanProcTable& p_vk, + VulkanProcTable& p_vk, // NOLINT const std::string& application_name, std::vector enabled_extensions, uint32_t application_version, @@ -108,15 +107,15 @@ VulkanApplication::VulkanApplication( } instance_ = {instance, [this](VkInstance i) { - FML_LOG(INFO) << "Destroying Vulkan instance"; + FML_DLOG(INFO) << "Destroying Vulkan instance"; vk.DestroyInstance(i, nullptr); }}; if (enable_instance_debugging) { auto debug_report = std::make_unique(vk, instance_); if (!debug_report->IsValid()) { - FML_LOG(INFO) << "Vulkan debugging was enabled but could not be setup " - "for this instance."; + FML_DLOG(INFO) << "Vulkan debugging was enabled but could not be setup " + "for this instance."; } else { debug_report_ = std::move(debug_report); FML_DLOG(INFO) << "Debug reporting is enabled."; diff --git a/vulkan/vulkan_application.h b/vulkan/vulkan_application.h index 3724929e0ff1b..eb1d23f26901a 100644 --- a/vulkan/vulkan_application.h +++ b/vulkan/vulkan_application.h @@ -24,7 +24,7 @@ class VulkanProcTable; /// create a VkInstance (with debug reporting optionally enabled). class VulkanApplication { public: - VulkanApplication(VulkanProcTable& vk, + VulkanApplication(VulkanProcTable& vk, // NOLINT const std::string& application_name, std::vector enabled_extensions, uint32_t application_version = VK_MAKE_VERSION(1, 0, 0), diff --git a/vulkan/vulkan_backbuffer.cc b/vulkan/vulkan_backbuffer.cc index 15dfb39988d58..d8be29e51d3db 100644 --- a/vulkan/vulkan_backbuffer.cc +++ b/vulkan/vulkan_backbuffer.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_backbuffer.h" diff --git a/vulkan/vulkan_command_buffer.cc b/vulkan/vulkan_command_buffer.cc index 54962bad6d346..c98a345fb3b75 100644 --- a/vulkan/vulkan_command_buffer.cc +++ b/vulkan/vulkan_command_buffer.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_command_buffer.h" diff --git a/vulkan/vulkan_debug_report.cc b/vulkan/vulkan_debug_report.cc index f8a5f3e2ad6b7..1e5478753607b 100644 --- a/vulkan/vulkan_debug_report.cc +++ b/vulkan/vulkan_debug_report.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_debug_report.h" @@ -192,8 +191,9 @@ VulkanDebugReport::VulkanDebugReport( } VkDebugReportFlagsEXT flags = kVulkanErrorFlags; - if (ValidationLayerInfoMessagesEnabled()) + if (ValidationLayerInfoMessagesEnabled()) { flags |= kVulkanInfoFlags; + } const VkDebugReportCallbackCreateInfoEXT create_info = { .sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT, .pNext = nullptr, diff --git a/vulkan/vulkan_device.cc b/vulkan/vulkan_device.cc index 49a8e96e7fe85..b4e0071b7e64a 100644 --- a/vulkan/vulkan_device.cc +++ b/vulkan/vulkan_device.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_device.h" diff --git a/vulkan/vulkan_handle.cc b/vulkan/vulkan_handle.cc index 375e9f9df0009..eb15d9ff13147 100644 --- a/vulkan/vulkan_handle.cc +++ b/vulkan/vulkan_handle.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_handle.h" diff --git a/vulkan/vulkan_image.cc b/vulkan/vulkan_image.cc index 6ba6bc23113c7..7f3b5eb9cdde4 100644 --- a/vulkan/vulkan_image.cc +++ b/vulkan/vulkan_image.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_image.h" diff --git a/vulkan/vulkan_interface.cc b/vulkan/vulkan_interface.cc index a1248061c33ff..5f0e67916e165 100644 --- a/vulkan/vulkan_interface.cc +++ b/vulkan/vulkan_interface.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_interface.h" diff --git a/vulkan/vulkan_native_surface.cc b/vulkan/vulkan_native_surface.cc index 20aba0fd8d692..4d10bf4a35783 100644 --- a/vulkan/vulkan_native_surface.cc +++ b/vulkan/vulkan_native_surface.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_native_surface.h" diff --git a/vulkan/vulkan_proc_table.cc b/vulkan/vulkan_proc_table.cc index 1fe6d1dc86192..9ed7c40b5c9ae 100644 --- a/vulkan/vulkan_proc_table.cc +++ b/vulkan/vulkan_proc_table.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_proc_table.h" diff --git a/vulkan/vulkan_surface.cc b/vulkan/vulkan_surface.cc index ac4406289cc0d..f410d43f81719 100644 --- a/vulkan/vulkan_surface.cc +++ b/vulkan/vulkan_surface.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_surface.h" @@ -11,8 +10,8 @@ namespace vulkan { VulkanSurface::VulkanSurface( - VulkanProcTable& p_vk, - VulkanApplication& application, + VulkanProcTable& p_vk, // NOLINT + VulkanApplication& application, // NOLINT std::unique_ptr native_surface) : vk(p_vk), application_(application), diff --git a/vulkan/vulkan_swapchain.cc b/vulkan/vulkan_swapchain.cc index 1ef045e64df2d..f5a85c25df0ac 100644 --- a/vulkan/vulkan_swapchain.cc +++ b/vulkan/vulkan_swapchain.cc @@ -222,14 +222,12 @@ sk_sp VulkanSwapchain::CreateSkiaSurface( return nullptr; } - const GrVkImageInfo image_info = { - image, // image - GrVkAlloc(), // alloc - VK_IMAGE_TILING_OPTIMAL, // tiling - VK_IMAGE_LAYOUT_UNDEFINED, // layout - surface_format_.format, // format - 1, // level count - }; + GrVkImageInfo image_info; + image_info.fImage = image; + image_info.fImageTiling = VK_IMAGE_TILING_OPTIMAL; + image_info.fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; + image_info.fFormat = surface_format_.format; + image_info.fLevelCount = 1; // TODO(chinmaygarde): Setup the stencil buffer and the sampleCnt. GrBackendRenderTarget backend_render_target(size.fWidth, size.fHeight, 0, diff --git a/vulkan/vulkan_utilities.cc b/vulkan/vulkan_utilities.cc index 2ad904bdfe877..e6aa4180c69d3 100644 --- a/vulkan/vulkan_utilities.cc +++ b/vulkan/vulkan_utilities.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_utilities.h" #include "flutter/fml/build_config.h" diff --git a/web_sdk/test/api_conform_test.dart b/web_sdk/test/api_conform_test.dart index 8b7aee91fd562..ede590f271a01 100644 --- a/web_sdk/test/api_conform_test.dart +++ b/web_sdk/test/api_conform_test.dart @@ -39,7 +39,7 @@ void main() { final Map uiClasses = {}; final Map webClasses = {}; - // Gather all public classes from each library. For now we are skiping + // 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/'); diff --git a/web_sdk/web_engine_tester/lib/golden_tester.dart b/web_sdk/web_engine_tester/lib/golden_tester.dart index 49b40aaf7cc5c..d8cf77059fa59 100644 --- a/web_sdk/web_engine_tester/lib/golden_tester.dart +++ b/web_sdk/web_engine_tester/lib/golden_tester.dart @@ -65,7 +65,7 @@ Future matchGoldenFile(String filename, 'pixelComparison': pixelComparison.toString(), }; - // Chrome on macOS renders slighly differently from Linux, so allow it an + // Chrome on macOS renders slightly differently from Linux, so allow it an // extra 1% to deviate from the golden files. if (maxDiffRatePercent != null) { if (operatingSystem == OperatingSystem.macOs) { From 91ed8f4827718625bda456d92e62b8b0baf4bf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Tue, 25 Aug 2020 20:56:36 +0200 Subject: [PATCH 72/78] Add files via upload --- .../src/engine/canvaskit/canvaskit_api.dart | 79 ++++++++++++++++++- .../lib/src/engine/canvaskit/image.dart | 40 +++++++++- .../lib/src/engine/canvaskit/picture.dart | 10 ++- 3 files changed, 123 insertions(+), 6 deletions(-) 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 0690e52138d48..8a67398f07181 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -4,7 +4,7 @@ /// Bindings for CanvasKit JavaScript API. /// -/// Prefer keeping the originl CanvasKit names so it is easier to locate +/// Prefer keeping the original CanvasKit names so it is easier to locate /// the API behind these bindings in the Skia source code. // @dart = 2.10 @@ -31,6 +31,8 @@ class CanvasKit { external SkBlurStyleEnum get BlurStyle; external SkTileModeEnum get TileMode; external SkFillTypeEnum get FillType; + external SkAlphaTypeEnum get AlphaType; + external SkColorTypeEnum get ColorType; external SkPathOpEnum get PathOp; external SkClipOpEnum get ClipOp; external SkPointModeEnum get PointMode; @@ -62,6 +64,13 @@ class CanvasKit { external SkParagraphStyle ParagraphStyle( SkParagraphStyleProperties properties); external SkTextStyle TextStyle(SkTextStyleProperties properties); + external SkSurface MakeSurface( + int width, + int height, + ); + external Uint8List getSkDataBytes( + SkData skData, + ); // Text decoration enum is embedded in the CanvasKit object itself. external int get NoDecoration; @@ -128,6 +137,7 @@ class SkSurface { external int width(); external int height(); external void dispose(); + external SkImage makeImageSnapshot(); } @JS() @@ -623,6 +633,38 @@ SkTileMode toSkTileMode(ui.TileMode mode) { return _skTileModes[mode.index]; } +@JS() +class SkAlphaTypeEnum { + external SkAlphaType get Opaque; + external SkAlphaType get Premul; + external SkAlphaType get Unpremul; +} + +@JS() +class SkAlphaType { + external int get value; +} + +@JS() +class SkColorTypeEnum { + external SkColorType get Alpha_8; + external SkColorType get RGB_565; + external SkColorType get ARGB_4444; + external SkColorType get RGBA_8888; + external SkColorType get RGB_888x; + external SkColorType get BGRA_8888; + external SkColorType get RGBA_1010102; + external SkColorType get RGB_101010x; + external SkColorType get Gray_8; + external SkColorType get RGBA_F16; + external SkColorType get RGBA_F32; +} + +@JS() +class SkColorType { + external int get value; +} + @JS() @anonymous class SkAnimatedImage { @@ -634,6 +676,8 @@ class SkAnimatedImage { external SkImage getCurrentFrame(); external int width(); external int height(); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); /// Deletes the C++ object. /// @@ -652,6 +696,8 @@ class SkImage { SkTileMode tileModeY, Float32List? matrix, // 3x3 matrix ); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); } @JS() @@ -1662,3 +1708,34 @@ external Object? get _finalizationRegistryConstructor; /// Whether the current browser supports `FinalizationRegistry`. bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; + +@JS() +class SkData { + external int size(); + external bool isEmpty(); + external Uint8List bytes(); +} + +@JS() +@anonymous +class SkImageInfo { + external factory SkImageInfo({ + required int width, + required int height, + SkAlphaType alphaType, + SkColorSpace colorSpace, + SkColorType colorType, + }); + external SkAlphaType get alphaType; + external SkColorSpace get colorSpace; + external SkColorType get colorType; + external int get height; + external bool get isEmpty; + external bool get isOpaque; + external SkRect get bounds; + external int get width; + external SkImageInfo makeAlphaType(SkAlphaType alphaType); + external SkImageInfo makeColorSpace(SkColorSpace colorSpace); + external SkImageInfo makeColorType(SkColorType colorType); + external SkImageInfo makeWH(int width, int height); +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 26006585dd647..8bcb210d961ef 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -75,7 +75,25 @@ class CkAnimatedImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); } } @@ -105,7 +123,25 @@ class CkImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = skImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = skImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index bcce6fe1b7bef..2a0d292ebd45b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -21,9 +21,13 @@ class CkPicture implements ui.Picture { } @override - Future toImage(int width, int height) { - throw UnsupportedError( - 'Picture.toImage not yet implemented for CanvasKit and HTML'); + Future toImage(int width, int height) async { + final SkSurface skSurface = canvasKit.MakeSurface(width, height); + final SkCanvas skCanvas = skSurface.getCanvas(); + skCanvas.drawPicture(skiaObject.skiaObject); + final SkImage skImage = skSurface.makeImageSnapshot(); + skSurface.dispose(); + return CkImage(skImage); } } From 38cf1e100b934e2044cfdbfaa391ed96c905d9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Tue, 25 Aug 2020 20:58:19 +0200 Subject: [PATCH 73/78] Add files via upload --- .../lib/src/engine/html_image_codec.dart | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) 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 a42f4b4f1c746..6a0595d66ae50 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -129,13 +129,13 @@ class HtmlImage implements ui.Image { final int height; @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - return futurize((Callback callback) { - return _toByteData(format.index, (Uint8List? encoded) { - callback(encoded?.buffer.asByteData()); - }); - }); + Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + if (imgElement.src?.startsWith('data:') == true) { + final data = UriData.fromUri(Uri.parse(imgElement.src!)); + return Future.value(data.contentAsBytes().buffer.asByteData()); + } else { + return Future.value(null); + } } // Returns absolutely positioned actual image element on first call and @@ -149,12 +149,4 @@ class HtmlImage implements ui.Image { return imgElement; } } - - // TODO(het): Support this for asset images and images generated from - // `Picture`s. - /// Returns an error message on failure, null on success. - String _toByteData(int format, Callback callback) { - callback(null); - return 'Image.toByteData is not supported in Flutter for Web'; - } } From faf4ba97f6b17c823f1c156a5703eaabe0b5dbaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Tue, 25 Aug 2020 20:58:56 +0200 Subject: [PATCH 74/78] Add files via upload --- .../test/canvaskit/canvaskit_api_test.dart | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 9847ca29a1dd0..8770120ac712c 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1188,4 +1188,29 @@ void _canvasTests() { 20, ); }); + + test('toImage.toByteData', () async { + final SkPictureRecorder otherRecorder = SkPictureRecorder(); + final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + )); + otherCanvas.drawRect( + SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + ), + SkPaint(), + ); + final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture(), null); + final CkImage image = await picture.toImage(1, 1); + final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + expect(rawData, isNotNull); + final ByteData pngData = await image.toByteData(format: ui.ImageByteFormat.png); + expect(pngData, isNotNull); + }); } From b895e66e51b4499ba7c1c3ad2bd126b6f7fe3390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Tue, 25 Aug 2020 20:59:23 +0200 Subject: [PATCH 75/78] Add files via upload --- .../engine/canvas_to_picture_test.dart | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart new file mode 100644 index 0000000000000..3c8d8c64210f9 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -0,0 +1,49 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +void main() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Canvas to Picture', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = await _drawTestPictureWithCircle(region); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + //await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + }); +} + +Picture _drawTestPictureWithCircle(Rect region) { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00)); + return recorder.endRecording(); +} From a57c67439e92c96b307961319120aea4d8f2cd19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Thu, 27 Aug 2020 08:19:45 +0200 Subject: [PATCH 76/78] Update canvas_to_picture_test.dart --- .../test/golden_tests/engine/canvas_to_picture_test.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index 3c8d8c64210f9..48c889ba902f8 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -7,9 +7,14 @@ import 'dart:html' as html; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; +import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; -void main() async { +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { final Rect region = Rect.fromLTWH(0, 0, 500, 500); setUp(() async { From ae9605ab0ea6bc681b1bad54a4b3c65ee1b36a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sun, 30 Aug 2020 21:45:47 +0200 Subject: [PATCH 77/78] Update canvas_to_picture_test.dart --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index e3ecaa78c7de4..016d6bef04c22 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -9,7 +9,6 @@ import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; import 'package:test/test.dart'; -void main() async { import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; From 21feeae99b7329d50a397422a3153fa95c17ec6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor?= Date: Sun, 30 Aug 2020 22:08:06 +0200 Subject: [PATCH 78/78] Update canvas_to_picture_test.dart --- lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart index 016d6bef04c22..02c5a9c16b05a 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -7,7 +7,6 @@ import 'dart:html' as html; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; -import 'package:test/test.dart'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart';

$W&WMwo;d1NvGvh&7hrvIpPlxz}nb>x#d|9|P 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 Q1*$6=*DvM%e(TTw0mC}5yZ`_I literal 28517 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 MdQv6vj|+GH2ecTcR{#J2 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprect.png index a5e2a3fa99cd0b889849525b6509612f15bda3d5..b02a10caf6e9ed358d8ff375ec15a4b72232b45e 100644 GIT binary patch literal 18636 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 literal 21011 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 I&syLAFO3r~p#T5? diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprrect.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewCliprrect.png index e3ff650bc8372dfc1bd23d66d6b73ef410696a24..e97edb37e4fc3950ae0ffab64b5a5e5d4db30ad4 100644 GIT binary patch literal 25946 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 SHFzZEisDU$^uKRE`o92f8^Xc> literal 30269 zcmeIbXH=9~*DYGzrZ&<>K$1BiK|lnF-Ad>dltx5A2}%?Ji3()_Wd?~VDj8Z(P$URS z(gLJO5(EJSBuhaNr7Vz~&fIi=?|Hv7#y$6r^XrcD>W@~6s(SXb!&-CAIoH#-{y3_> za@odZ6bfbK@4u-Wqfi#gQYed)e_D)Joi?b82;N9p1zn zJN)s*J(a@KnhWgg?1B^*QckaEbsv70HC!pblg2~&CFbx>8b(Q>m~D3;f0eGq&?%QY zm-6GUUxpU0!e5)HKQ6&vw-v4|z+b-^PndI2c9gfbpN>$Z?LQhJyV|NM%0D?dxu>ts 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultiple.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultiple.png index 1ce0be1f75e23268b9136160bc46a3ccc3469889..d964f72ac5a44ced60d2ae521c2d065418c67551 100644 GIT binary patch literal 28040 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 literal 33777 zcmeFZXIPW#wk{lHs&h^SCW`c;VnI<65JSfbC@LZf(iNmA0#ZY0(diTsNFo-h5Cuh% zCQ>7TsFVmI2uKf!5}K4?=rw%fVePW6b6wy5&fa@{=bYURtVBY52C=vfTr7Yz(%b@s2T;Ww{ehtFUzw~CJ+J#;SM@o<;rb-T%= zZ=*Sd7H@ja-^aFBRlVVrUAlf}`^|f+-`+WX_|Cyio7Ep2PfPQBk>l*)t=W9uM-yvi z)|5W6wJ9DWwDsVf%|bWtz216*NF?UImoD7*X4_~>*@Ci8pJ%4e<&j2{z~o(f$C51! z896Zn!7TP!QIS%W-}bAk6Q{RfFy%j4L8Fzr(@Zs1>|M}Jf+$&RkQk^7~yQ@Xz{De}P%lPaoDe@a1t zx|OwcYG&r{UAuNUObxf2nwf3z^Yb&c$()CaJTv$vVdzO;^w(Kh7>{R!&Xmg=96w$y zVc*cx)ZQ*RKRrrrwF}xKB`xhav#_*4S@Jn|?p)OSNAOSg-Ty^sdsbLj*i5f7LGpzO zMeZj<^!MAQv@LKq29fXHKg;)Imj(32yQE}fxV@8CbF5m|WEOW8uEiNCVw22XoIYl0 zX}R0JOsCMJORBfGmlJX&Dm6JdStDf5_pz4WuG17f+5UP8**JFZF&7sXQE_oYCnu+K z=H^usKbjoBf4Hqe;4j3fyP1n_+(?{$E`Tw64BtA=W5_d_O!Q^Dy1H)76-%l)M>TGI zU*R)1@nfce94aa+D@)OxYA}?qe0tkS{$0qjm^&JWs{&UsZ z?zQ$ahchkn3^oa1s)%suMixtsK2xdeA%Wecq?B4%c;NTjhkbp0D=H(!Q(wKh8kN1i zitUzDIz=t^YDQOocT*tO`E!C-@vf>PhK7bTUX?WOj{d@>GpVfX$saE$Rmz0fPXRxk z$9BWpClaNxQVlCGQJObz9e0}Mc6sp^W|^GMLifll>!KPHr0xY@398xG}^-1*%iyRW{OJ_Oj!fWkqcIzCu-IbBNiVY^reI9T>So6#a zB5KaHGuQSTxVgFQKX9Pxroft%(o)@NN3x9O`g;AP;NbB@c7EDg%)afg`G~Ojn2tqV z{+$wbznu)WpZOr(J%jq@%8ORo@|$raSSHv0K5rKZM<4MxgPHC>eZ`N zGgmJ$7s;^ibx z`qA`)G2EW#2vNn<);1}qbW!p%!MJ6#0AAT8lMv~e)@#LM{t3Hacf(5%nU#UF_))io zJ$eaqBYNKz0mGo6poMb%g(DOOL;%?(o<5&ZuiNtF%OBb}Zd6wZuAlYMcBn7WlY};U zOQxmTRYExFFL)dc-+?p3OsbmR!q0N1^o^lCHpTlqN=6E66TA3uh7?%AG3blpH$0btz$XVQGTGZ|W~IL^-I$e5y`I8Bm%xiw6h|!W|?FcUr8_ai|SsW$E6CkDXCXN)nc)3Hn<$KZO^>pC>Gh#xvvy3%mPRoh|5*u_4*Y2FeHla;hDC}`Hb6D z!YQ3*MUqG)X+1h59*Cgpr4L*Sk5_XM8XR9}(qA;|%cU>P)5(^3&Vn;GtwG&)cXWSy ze~S`yZEhf%P&{UErWR6P#h=!AJf52yE#!j1 zv?k`?RH#Sz;l+y`5s{IcvEEO-Mq}m5x+Lv2ek27$Qbn|ap~H{H=e*{eP`|?-Sph%4 zy`LHJBy^76>(SX^k{s2a9JJSexJ?AboNMiGoDSc4%<~@j@)UhY8a^O1!I0CqzDY<% zdz3deqFh#-sY@-^%059m+Ab>rysw)sUx!ZXZ~kr5t4pRdB7Vdz(dOm~Unm|l!H zf_#ou!7jDy@a8Lc9LAbbS(ljdJHjq{@Lt+?m=hnNxPyXw8=bK9L@yf-A$p3Ovbn3l zv!Coq@a_c_^LJb2D*~QU70MG&=tvGnrn^G*|wXL?Wpa5b(<-NMZX>CV10|=sQ zPV(6VK%wv4!)U2e5s}5RNvIYGO5X~JabVF{6Y9pch4Gcz-t$**SR5jz02@*YqY zw#tRhe7{f{IO#&Rhg_5#GS#ke{oA|O**GuPQU&Vo?G(LF&#i~azOW85h|~& z#ZW#()I@K#=SG^$Nq@9QzM7d3XWVE2Z)V!-Ll1Q4@Bqzm(qAM?U1v%RE8a&4x7s9B zb&J9g$jjY;+DP#4w{FR{nXeC>%DcoMW=wIrq;qIvHQ|rlofDIkb08F7ato~a< zLPE;Zr<=#y3ZA;|o|u@ZAjxWXQ~Q&p&6&k5fN#31cTi-$%nl9>smbdH{JhAW)4|?X z59GWe>&>|u^$(AX5QX&@_Kmj%5Q`;`<5O3|x@u>pAI4w=RJIvjzs4@p#Ywo^+S&q4 z*#VhE&st&>pyFb34!rL|Pqy3=pS?8yGCN!Dwo+Lx#E-VJkRYzu4U0Y2g}cR+?PSiM zKW}PfC52Kc1UC)u9;3&4AzITMxKA2G`Y?D-sWCmYi7`1a@d`TKG2 zKhvnp+lM!gk0a3HlN3ERHYO4;&VsUmzR;`e8BlHu2@A^(_Ca@c!q_-9BSWb!Sy$SS zB=ZBiUq&}@e{$epdcbflW3H6-zAe|0@iK~8JP1IxI{34(v9V+I{cR}cRaRCuG&Zh> zm_hqqfY5dhVR2}Ni=#`M=r8(mKhHnb^htpj{_54M6u@#QBaP*!}_nPU3XSN;u}qs$1<{Qj&89;KXG24U<0 zgBEI`*PibLXi>xGeHqH(p$h@D*}g>H`!({HlQ|!w6?h-k zDpvs1)!5?ZeRtx8qKS?yMf1%Un;JDc@H)clnmMq4b}Txi%#`fC}$@pC*$=g@({RQuQX&$b1vj0j<;BQPD*1BcrYa( z>>KXB@cK#p5DS+0)TvXs5J{NoHY|AL3-r(sx`K=|z!|`@b?a;e1qE*j$w25C!u1%z z`tRSre_i0$%4U%ht=0uso1cjJC5J;!e^AmkmZ0ti)d1#3y6cJ6qzu^K9Q(x#Li6b7 zgQljY=PWE35TzuCT64%Ql#m11tu%NY=#`1YTj--iAd6DLkMKxRc- zd>Mkw_EYL*7JqU}Myoz!*)oX-+@*4d$i-u&*IR9Im4HbNgA)PSb)XRL!#+kXepArb zeC&K{mbLiuN%|TyByV4Q{gMu#DQFWs2;!qR0f)z-x8@OAwScL17c~lQ=I3`;SJ!X| zlfvKZ52(T-$IS*gLcz*=loRY^r;jHdVHFZw1}DM4|s-k>Y5Zz^~$5;rQD zuN&0aU~1Q6_2%;Nug%RwUNZ(`*6`5gmO}Rl=1d)~Y=d=?hwM-7q&(-(5fJS6e>=&Eud->A(ixW+N{pA|{(q2K;849&# zj~rp(=^clmmMj619wc`321ASDgHbX=Tf3}9=TodQS$^T$8m~_GB4Pj&A}wh&xLVirkO`^ir!2KqD`P;05;HWzv)Yj6O`F2aiu`22i zWvQ^!hd~ye3ob`d9!^TVP7B*Nf#1_L?8z+lGU}I(00h`um9j=IU=vnj!lfzg z6!oTGYqsrT=rBMgwNy)E<4;e-J}d%uS}iJ(n3%Z6j|JVETJzL#IjEAcU+5P1A@^tH?302{iVr(prf%QlHshEI zNGz&kVg*+%eE|N4o2$QGWV7&zzVM0JI*U$N^s2)sLy|g}{LhJ4;i05{>CzH9)Mtl? z>!{`kPM_XlNS2cwk2c3|zE+>COC5^tpfJMW_&RwpFC@$RjuRlhox_M20+HNkOmu=zOodcb6}g_=DZ z;x5WI5BNnZFjwO5-*l{u*jUcL_Y5$3K;P_{( zi=6alTg~kW@QszX*NUr>yI@P+vU@ll2n=!$Yj9=abDTMMYSjr?t@Xa$ZfP9aGQiA^J-W`6U`OmW9lBlL-rseYvP6 zK=@guYzyY%u0(jWt;_O+~;jGi+cCFz`ZW&R}4?n#(udU!| z8>WN*?)I!~$C@pkzf{As-YHE!>gDCdJ(nn5`Gl5bt`C86#jz~tTsrajc7@k0oxf;% zbsyyx=Cls}_)-0sbk533E6sY2^bA@8%*x={J=S8-zLVxCt;D2h-E@?gOSI6DO!KV8kS`axzN%;NB}}GkU<33^R1GQ6;Ha#A zu)}i(pJx#GwY8Oj+9>FS(ZvBU2bPZ?z+zoUtE~a@hcE7xG#1}$6aFGOaM$>fFjP1w zMs&O;gs#A>HvT3N1|h$nznK6n7nL$811qQ=@(T)_7151mrX04-w`c*~G%O=q6>pPs(T9c@y zey>$SiD_fonhI?fpM)A>+UKJ^IrQl6m5%|S@@m!Ud%_lSLMJl>Sd+hEE^hrzru>Ye zsP`Q);hv-qcb@>l1PlgyytXjnfhi$Uk~DGP@nVu^W}3Fj9MrKshC}Oo6Hc%``?9(d zQlHXyN-Fp{TAx4vNSZ#@Qc2zAIUSQ(c_haV_EoK)YPPJ%{f#D2p(ig{P?W-0LUJ3uxMXV>ta8^>0Czxu`&KrxgV4^T7dS|}c3 zsm)v8D>Wyvk`XlA%ZyjAP%~a(jtZz1rNej?g;6b|rdQ`P8~}ZU$g!cD0kBP%ik%d( zD7;;lkgV#{SF2KLss{~6ZZ4cyLD!n9gV1_(CgoEbMzf%MQ~_vn{;zvb&Kknkar0kh zCQiZa)*nlxRmQ^0PAD-u4I#(*U4lL#l)LxD22+H4r;6xAz$VJ$GlR_;^IZ-yn2Y(a ztouXeCTbaspv`qqGiX3zGR&syLx#a-x88q6Gdgc(rnak7&fLPnWi^S%w{y7M3=KC3 zip+lJIoP*e-4%C3#Rqhtoahr=>ie8HFQ}l9_5As^={A%TOqUk0zsmr!&-EYa2!K#D zFXQIw`Y~yJjJj6eoGVHQ&@JZX>DAwn4~>)7fGSFrKx)mjeDGE56>6lY+BbzOnH^XH zP~m@)F1!Mh?GjP5Z{zQ`YZ=y7R&%z$dgh~Ii&YxLAuN!WL?9`OW5YI&Zr?T9#r|H? z>964_`$yxWq9Y%PY3Y|oK8Qrq1Ae$l4(;*J!QP8@!7J<;^S^ZE%9ZMMGM4?-2}1dV zB|fwD`O~MD&a`TXl#bVF^Geu@d&e&Tpwt=rs8G&2AdRLqq)Q1#CHo(1(x-hQ4tC5V1YF(Ep=k1(uYQsMSN98QfZEx(F3N zN1Q%$tS=V;Au0k5f*YJLH=eH72ur8qR2hThR)9lLweRrez)0#i6W}^(5TS^|%}*Gz zh0}Cm4T(nWNmX}xR#4z~ru9$!VZPO*84+m&~2ZOU&pGz5#1blRp0HB~MebTMe&dxXE zUr?lDu=?y+VWh7A@P$mB-cCU?7aXhI<@bU3kn5`$&=@i1NnrSI2 z+vc&(lvPpzTe_jMQ@fjj8VNA^02oxEFV4w<9(>xV#5tW%zl53slDvMh=I@(4a~m6C z#Fw^rbm&9ZYlP(%nDsvad4KKK1l&A?6SadJ)Yn40BP`EnF4c7y1Vdr3P}V$Q z1OS4Fat%7&WPJdK@oTeIVxq7&9pg?X>%`B_TCm_64g%OleW4(VA z$_A7_L>6se=+V&DrtxxUQwY9N(A1spI_#ghapOiO zRyajBXev*lsjDkVQ_Fc%&PYM(!R!t+@8Laz z2A_wHi^BDFM*xy@U(GreWE^TCnp8Z(`7^GHR$qpA_#kMv=ZP<8CUJUeXHMA}$Lzif zwU!=vv^}ps@3v0K+qbH%gQ(y*UQ2T z<8uikpkBb9Lk#&rmP8gu%nw87nao8UQY&q@=uy zd>`u*_%8{3{>sSP?64f@6yp{nX|MO6gDl9R;UVF~X+R=$gqIZ}N5B64gA4W>9B?mQ zccor^wUq8Y^EWBA=Pj*MNG|{ZU-qaD^0RImdhzh6O*{*GufVgfE-8^p9ER@imMwE9 z)gy}E6dJZLqKa577V17aKouCzfPwNeWNBxH_0RsQU zl>7mM19Gc9L3g|jMgyFjrMVo&weQy;;*7P{`~gkXUe-gHAx+H-^&9;FFsFN9J(i8{ ztUJ98zxD_GwNqWEd<1k?CM35I&bNHt@Zb|nmsG_wn2@O9&Z{8P%*%A$ChvS+d&x z!yk1D0q)NXTF|vNr>?`in@qd>4Go0ugpgpVT!f^_;V@zQ6h~`nX8$n=)!XF@{d$mR zY4hBYaxF%|SeQDw%44en3O3ZWyUNCDl*&V2yoJ^nichtUFrPqy^imRK|K*W1Z#YN+ z93ocr2j#nk>V*~|f zdYph6G*`a61UE{8Ro|R64Gn)P;ith-ZhjoU$6ZM?I_!Cb^+lc>wP!b|1Jv{XSut^IE2qbl1343Aa-7P^z)L4a^T`}tul)$DgP@!H;GmR%FHqX>WwTI@sQdm0eB z7bafle_otEW@D-5@Oo?|1Mv8rkPZ7g4tTO2m!#EX+e+=XsV!aldn!MsTOTAO!j-8G_#D6k*l&+7T~ozwxONjbF{KT^=z z4Q=n)dj?@}5Ck=Jgq_?U-1KPG{!*Y@br^N~;e+wf)VFW7d>99Zg-1F< zR#sNsFZ36$0G7D;PQIz7MXX+L{u&*dKMgq4$cA-ea(c};g+K8kV751Q%|x~fPze_J z({!@@(hLK-9pMs~^)(q|Ky924Eib2MmK36w<-K|32wxXes+ zAI(3WAEmRqQ_N1X62w{)jhbR$z>bIzpLO{#MMP6Dc{C>eif0p+7MDjN_5Sy%gQ;Y_ zh<*wo)TmYK-gTG;w~kJISqxzUQNv?~cK40$aC5YZC4I=HHco`BJ9U9>y+QzU@|;;< z^J_SPGx|&YOBE0xI8&XTWEf&N?R^FtPz=&ml!bkWI!@~l#B?=Apm9AUxz%SVId+i;T_=~W)aTRdFQ!+C%>wQpk zLk(UyMH3|g^#F19?A2P;+}dggL8kAmpv4}T=znmR4n{LaXrk+NYW4)8iL9{R%qDKx zJQvLsp&#j@H#%6)pe!x)GkwkjhXzW0?6V;ofKzCYDSlDT_3~Rg09u*$^fLBNH)x+p zJs2tXdrIk(Rj3 zH2T|A0XP?;P|}oqf4c!#He~2S`$T2WELqst$UxcQxiDa;UlURXM}lJk&6My^7Ah9C zt^-C<@PV!NDHi?y`~rl-x!v#3+Py6);UE$X_m7!ef3$kgfLFy8gcrZ@`2hX-R2oeO zwNggM$%37eIWO;PM7#SaDt}VX<|?mcf06H85C*rVzzirhq4lo`Scv7x+$s7yZy=Q{ zg#Y~_kUijiBSw)oMeT7G4iI6x=QqIABY(wNO`xfy9)E_z)TU_x%o(^u zovaWGg7*CBYccTTK!+*2SF-W@`BvfhWTTQys1>VP>HNt7n0nz7&O#YMSJ*#>S_0+5 z`qvnL?z{#y!{Tbv-Tbif0swkq==8vRX)$bc0l|}7tlleCQIfom3gpWkFSI3KDXBM8 zA(DvgzOb*yM;XmTq45u)Z+zx@cePQ%z*=nq%sR#lJ!!v?IbdQStpsqVD7RFAT!Q8| z+T0^#)H8&R_8Nm>8}?jF(VXnzV88G5_h?!G-Ce={#wuG_%sX;{irlwD*&@25h3=i* zfWKvnjH)VLU#`B-ED$!&vD1CxCs$<*D-=B6$wot3;s74S4)#22I!qS3C57#SI+hm{ zcEbjpOR_p3;$OA`6E_U0HN1Gta9lX_=PjT=aqiWlf^-JLnmp=(zE_fd3m2=Lh8MM2 z_b|i9sAiDi|%>$ z692WhU?%Jw*n>LIzh9w%4Tr8>8x8N%mn+)hgfvBo(9D{> z$i>~s|LGE=>C@N4=cni7$WP6}ZUSr!t%3?;%ng-&+g`l1QFm*}3_zV~XgBfD03c|z z6#L|R3HN2K@pMG9s|700o@J^!W`c*KPcoPGKwQi$zNZNHBqg zcY7ClR6?$d1T+zXs_I(X>&*y{Uuu!K$1ZAF4To<6!c=sZ}lw4p4Xar6S zxVXto78Umwz>fOkUwX7r9~Dc6vg}IV){Dl&yA!dT@CJloYG=plUot#9*RcZgEK_mI zcCKvCCL4OmaoE+%9F;yFI!v1XRf+Y@*Wy&yr6_iq4EQUN`R;k z7Y_JjL@29wOj_Y!N||o!E7h*)cuo#+OsjcEmhO|PN3rK z9BY;AZ&2Qt+S}`IIa0pk&%;mufN_@G)R*A6Yp`Yx4JpS8SFgfcJa?*H9SE?P8`Vd? z!5{xd9o?|}%bwq4;CBbd{-+tI|MJfsk)~Hg#lTV(0{~ScH-G&ag@!U1OzM@!)hGm` zQlM&H6@GI=tl2`Xh!^iTy zFB>S4BVAs+bGYDuT@L2Qj_*!33*Xl4WU9DRs>p36=EmK_-!LD&aH(r9GL89R%ogxVb%0F}Xs^%@vHg_O(zskIO=$~G? zV=A;!>cw7v;O0lYtmCrUn!ZR8#Y-HzK6qQ#uEgy7@f&>3Z}_AkZTh(N#KbfGs|qHZ z+jSZtxoQV?Cx{tm5c}Y2W9v0;?7UKcw?!k+xm$UQ*80Fe1%HYwaePl#t`{$#R|Z*2 zV&lp+`=00fPgG=cvKoBx-lvXBhNrq{b)Pjfxn`-eCv9T{WBJyO&aM@YQ>`oXXSm(j zYf@e8b-w9&cEHHbUX9L1T~_Q$3`PNVMA~0X;@TWyd2H(QJQtd>6SKf~qC)zPGUJD_ z8Fm7s53}%u&sqbXsbLYFY`2Lau?HT=`w#fLp=)k9+N9S|fPK|pEuGT5mIg65SUZ#$;~sWs1$HN;rafPG z)WXD*=99V2N;}hJCFbDUUxY56?ZDHT|4g~{rLld3Hd%V)Uw+-#-t^_b^P)dxU-f;m z&F%l|;|k2d1oUxD|1<^Cfg)7GJZc#?*Hnj=jaR3xiUeZy=g8q4bP>~4wtT0Y6z z;qH~*H6e$=Gy-A!K@}^kWgs_g;PvFhL>0^AFA#Ms|NcX+8c?oN{ICW!>5;Zx`R(Um zXEbiUj;S7kgWLO1)k;{Fw#H zlSAJy_x*EAcL;T=JAIY$@E}Ku$*jU`TCsA?y?pZNyV9wj-PU0a{{D;5CQI?opzayV zwU&zmhANvL zz^RE9_nS-i(^QAQRna)_ju4`ESxlp=P(-Bv)$ zOW|)CBed$G;>78V*Ux)JD}#z^LnM4G_2~DweJ2*^AW_;7rEhxk^20~J%KY7BQop4I z{@pE}6J4lDPz>IK!Nj>P-2Q#5qgA@8z~6uT740%j`Q{R%6~#OQSRj>DW=`ha56eGS z*}puf>TEvqNc~?HVP@?{BLlpp*EuVuH}kw$L@KSebp`yxO8DT}zLOw}GL9PyhL{@{ z4)1WMdea^cP1e8vTmY{s>EB+JQy~6$?dxMdisMb~);E>@>l=N%DzBz$etp{R99?+E zNAS!p?@vbBki`G}=hIHdYcuzqa>)Fgsd!iOksS^;$#&QzjrTQg^UD6($#4Un>e21j zF~_Y`U+j;ln%|OkFQs@4cILaI+u*mm9*QvKtX=Z}G9uk{j1mM6|E5Y|Ctl9{h@If8?>3@0!W|Jh} zd)4;aPU^Sgu&KUqLK@?+dAnoc=L9eZ4?}EeykKu3Qh6-x)jA6=(~7ydLC6SSVxOE~u|PY2~xaxV!MV!M2GPid#JFtt5SR?b-D^ z=3obkjEUIP-FyejUKE1QiHZ=se9qSb3e*-sg6>h%9%E~r8;ZK*vNB1TX(MTvpxu4T#5y%Eh?e6D=To#2-f?bb)k|2*wACoDdt~pdNU?PC zdY!8)goSj`yYACi!dS6WC|>l0d*oWoyRQ)B7xZs8rd*)E+~VD=ZyxO5=-_-?_ADHKne zc32b0k{ECFxl@rZ&<*i;?R6Sng$jG~PemQmayEZh(XHv^)%5t!bVUs255QT18&5z&#d>+iy1z6*=N4t(_Jt&iFIlDfgoJm2(#V4zf1YrtcD6H` zgcuF*Ug!bGRLxA_fuF#_h@3Swm)Yo4se-Etu>TESlx_?Yi97#Q$NAp_$p5Uq{<)I> zzg1uV++6?Q&2@q?ztW%vM#qeKNIRGtUPu(q2@!$+#*|rPz?prltJGT2JQcoeoHTM5#UGTp)<^5k_ zl(6*)C=}4V1!i8k0leS}-MDGwB8N0g2~If8Onffh8|heo+x7 zmU=$IaqpQFdjz#c}IhxYcrN{Z;@iEr}{xIFgn|) zJ}y9|#?zF4!(4p#i%?a?P!3&gls0~v!a%YZGEh6igGrzX?I*F2CTWf+y{|bF^eKp+ zhDW+q#~_uc5}3ABf$GW;vy``tlTx7Btqu+bdL>A5)T%55HXxmD z@N@~Ah$K<%c`m7-6aZ$F!L=HR4V_9kwDoBC4E~c!AOUNw;F%o&LMjRS-<~~% zsWo^IK;idZ1L>*Na1GUYuz!$%9p0yxXwL7Livh@7VVTybS!J>EUKxA|1BjnUcYyf# z$VLN-Hq3?u@WhD55`b`b8EghNokGkaFHRNWj_$U6v-O1WVmvf@%I)`J_Eo|NPwT+7 zwm)EM3^E55RxGq+w!#)Wc?j5nxuBU?78(I3niHZ4$?rhAfFXE(P#C}t^}fRdS#2d6 zxFI4Yj4P3i1<-qs!e|)$J&;Z#1r+1e`oCzyP62^EZ~~F+ow5i*eJPlX{p)lNzNQjp za;Yz0x_+Ug0)6BEd`p0%?(QQ`=&gwKfNT*!NrUFGF8zB!rVlFkV{ra=gPa~-50+a# zH2yz$0*JfF4FQ=gspZkMpX{p)y0tVo5CA>9p`$}eL9u?a4f-^oy zHHa)n`sLX9SIj^L&6fB%o81h$VRT^-80=sHX=nw3i5T^uuUKZhu?PHtV^|iZKl5JL z0Z6DKUcD~eSw*Y}#0vzPg(}4!Tnd=3f-;Id>hCB^!ifZoFNh(52!`Lu5O=@dTw#5(-By(DsYOW^$7cy2~(i! z2V;Yr%*-e{2=?Wp?|p&oy0l1Nnyz(390|}is*bmT%$I`{eL7R+^8D*ZTe4@7NE)f^ zAPfq+GDyHKMNS*&>z#o___BW71hn1p3_gE!Ne}VAzxmjX-rbQL4*J1RZtmpJGSNe) z$0u3SXFI=>Fz*SU<&N_A!Yk0|@e%%dAy^1=BliHj-DFTh9|NfXSnensXF}OtD5tCLEnhi?Xpt7eHhAlJ+1_{b~|ukP+<&=JpDK{ES1d zfrexkGT9t@1U@LCN0B;P)SH}?jQ4_Vv8;0v3aw7ibXU+}+%y$E-Hu>1v1o55Y>l zQF8?*JnlX&%i@LG0=N;#`GWKtm|ETPOFVfFa7JX_XGkXl6@6JF@1j57eh;i&;PoP* zNhD=)*oA8S`t&hiDIh_8l?h?4X>%pfEeR}=Lcs%p0Sk?Nf-7G0_X_|0-EcECZisxP z&*zcX$YQgbVVCL8R+}Juni(c1R@t6}$k&M0bf}UzGy=rPWXRB=FLn{b-~oGCubsOz zgi*p(lV<}RE!;$%8jX0q$Y+7TuV2y}DE7+wS7|@U*lVN2T45O@f%@bkha~-Xf@MMe z_BohVQWA6~^<#D{?i5JFHa=K^3A=QHzs+xApkW4`F|Z+MdR^ZP?n*#itOseTtT8%% z;0#oOJP%U(7lBvxH=+E)Th-a{Vv%xOgg``YM6g`-1`FzC0Xzp}WgsAQ?w0JaMsOuT z&uXwW8Eg(JksTBzcKRW?%9Ij$o{PsAk#hXHQax-*o>Y3%hEf z^tNr=c$LbbDiHqemk6AMTbI!f)#3lVqu*0j5&)YJQT#H&kYs!Y?Hzv{ zjse~#98hBot8L`GLh*X7GVKpI_TR!YtD`rHBIP*9L=l*K326wpI|IYRQLxd30#Dr> z;#48)8k96$8&dRg4nZOSn>rK-mfd%VQ6OIrM^QDz;Pyixs>$M*B8aU8AXG2M1QrK5 zk0E(|C>u`zZ5NUWITi*T1zicS4PE%bpwz!p{<{pGO?E8xr~{XhW=|$E9|MCH+4vIw ziK!{DLNOZ#qCYO73FvFGAPyeI{e|SG%a8ap19VtlZgRjgVv!_+_?ckRaftl3SnKeQ zzzp*&sjn$@7|et^yu9;>&Y8n+rVma#KLL?FVzJd}k4B+vm{+ftD7_xCX({5YD8C=J#m{kiPLvZsit+FK~51Md$RsKqm2NxN^Zkqd{XDPXrgM=%@CE;4yQ)ieB3#MZ&Cd3mU)Ni| zWg*IGcPgI;;;<@|Eg&gT`;`IzfkVJHM63i}X#{c^7{`Ogh6HN?BxN3k@1%fUVmuv- zgawF;7?y8d)ogmmvda6f6Gx65S(t03gPj;C!977ohx}Kl^7ar1k<%Mcv-Ocz84Ad1 zoK|DyQc=XEoU;Ya!eDd!O=JLZi;sWX;93k%VSGza20!GP2?h{n`d46{%sqp+?osVTolCIXi#?&U?39iW@5e~&^{576sO4B zK?%NJ;_=Sa6(aZIkp;9t;bjZWJ%-)u1r9m=a`JWD6+6Mg!Gil|K9c@8z|J_LH3}va z-Jk6F-hH{SgaN?L4)bLVfmPYE|6Wg)s%OuT{nFgMmSm7S+y(0OcvyW9xIcgfU-FjT z5-7W4Acv6U$x75t_&q3WJr*ZZ1txcKZas+6JfRYEfxh7_w?G3BsQEJ5z9>z zfIA0YejZ_i0OvIeU)E4E51Cu;>&JU;K%4F7ZuN$Qr6Pajk@yEFCm)o^9Z!a8xQE-F z3$mI+_MK;-a_r$>+YSCV2$nt@7u&e8mmi8migbmKpzm`8>>STnt#)?1TVPGiBPF%g zm(_{eA8rHx34k^x)GGwwHeLeP59CHMPp$?hogUVHF1($#PJJPecbyL&akKxl;HfeC zD_px0;_zeqwf(r>I9JH(qM)$;sN)ZBRXEVJZU`i+Oj8*UoRyQDP#uI=&qFecl3Xic zsLQyx!Q(8k41q^j-3-t;Rurnrz61*jC(>T9oEm##_^_wzT#jwY0o;z%Xn*iMWHzit zyCbaN;}t^FHIMvAQ`Se?&9HC;+rV&RUZ)J|^V&_ZlT00as%=$Z*LEO-UWG@AJ_8!Y zc_Akd4%4l(NzYKp52s9RNt$b(3QrVICiiswvB8%0`5Xce4{yD|V)qf* z#eL?eP(NY=mW8j2v_1#fDr9G7sm4HE#2t;I`ALrT+|G%fOO^?ziEM@ElA8)#4~iSA znY)MRt+7I@MerwEBU*4eN$b(_8;pK8xL*P--r_aPR z@y|$!Fky+58#?&0FbdY~nPEss2ofkjh>w{K;(@IdR>u?MlzpzRVTX|0M%RNc%<2D) zg85597r?X+$rYcT`6z3pa}W3Xjz0m_fsMt-1$Cm4g0htTS>oG zN9=(5t@yi92CeJGSlU+@PY1LUbX6}xU#n)})(k2WudBBM^prvP8yrXp2k11A2<)|e zyWc4-aosVN_E*?mND=^YPV`>@jcengak9^@fPEiSNu*^73;3LU`=^01 zAhY4F5-gvwG$RjEbAK;Ki7}6qG1j7NHqHIcc*}~-ws`Hon63#YTK39_%tpqNVscpy z(}4n#_!w;C7Ef`=8Zou+qWzLHe;kC0w?4!cI+m;bh_D(&Sd9%=V#5FpR4(z^OUO)& zH99AI`VFNN7mv2^(l5Qp7&$H9m-XhQ$QZTX4s`3U ziw>>Ed>aRi|KMm+ZSEYW0CpBKmSYy1g4&~q9U6qNJo16i&gu&U{YgS2xE7td)sUdt z*wLW{DrI-1h~A*FCHtgVH2`1*;jzM_vZpMuiy{^&AD}O1xH#UQ72;c(`3@{QKef0C zKo&IY>&|LrM7V(sYnsWj&)z~aI2QFA^urQ4Wm`6~sW5es!ksRED6Rnt93Rh>1|84` zzF+ca0J0YcQ0eQlOMmp7(yv)7QvL|XYDidLK4S%KZ*WtYqRw`KC>5Fb@NL=#!79_n zrOxkW8Q8NwAPMdTamo59uyT2Qy%3N^+S%~wx0 z?Rw+!&iHTDQ>wQu_WtyI7Knq{doDAqpsn3U+5zo@#oblDIGhWKB>B-TXhiW|1{9dn zmO4KM2WuX0QEMq-0qJ8SsG^}W(VLy1Z(bsGxohUA4c-NpX%KzV9Kn0WSlTaw=L!9Z z(8~`G-MgIxVr=LHyHgm`!|mhkhm)Zh6z}E-T0K2z4Hzs+?zn2;*62mF8Cvi zD!-n<;vV0GV&t>!(iY|2%XfkGWG&wX6pC%2TB0zNZHj-k3kE8)pU!t3azQ$8)c=fUfLS7A#5So* zRyTfV#iJZT3vKZ!_+lQUY#y_4>e5l6)+S<=eR6BSUEnCRRUjxA)#si{JRlfQ(%*0f zYAQJQpLG18KCgA%m;9!s$rXxL`0(ZIkUW?ot)OVTCDfpX1xzR6MRbN{VRD0f0#S^* zL|K07@+yTb2(NCpu9(99HTS?oPqKxIe6gXZ?!(>Fa{Im z{14P`OEsCJI8HsQil1ytSZR)Nm ziFYi0?gD@uO7#6Ww<@IKUiO2A(G0S$L@@!Ht_{#@g$4@}0iq@ej4hmnTI=AHOTl=X znyUI{V*0Jlc&(Zjc=A+%6Ow-C;&WtJnxrj`0`d?p>N%i-;-TykGz|Ex!$%D_)B#+! zs)UNg)ZDzKUlYQN7vw19s4&-c(*=&bn0V5k5U`dFt-8W8kiPSKpGa;b0vIB>?L8~yfz#ukm5*6tJDuWFkEO(v+=*`p5& z!1S3|J@p-|5ZfOL$ykg4C`6{T0Cv2PIujaaXiALa%8&qHpy6Ncx1$d-91a?d61Kdh z9I)%;0(L^9U2w=p;~V6%C9+6^VLf~RB^ftMqw za0o++)#mNYM_P+?!l76vL??o&4^bPqsygL|h}Q^0(M5H~g0_-^IoezEC4`J33a?{$k!#rVYy`wMU!6)k=+3KB(t`t?hAiO4AR$u33nrA|=cz zZ#pEZK*S|RI|%Z1<*beRPoTDO-krEv_!IzjV(>Tgv*!A@n(K{NOyu7?DY8Y66ETi> z;p>etRWt{^KRn((0zOk(zHME0Jp!(NsM!f~YwjN08M~HRg}AU;nvLm~LRyp}N?*vu zNaoG-1oaYtH}>|S8djIl^zHG@O=%V&4jO&l`H3nKYHYP|Zi1byv6dVrh66e_&}9qI zLhnh>7^Eq5Wra5P;XO$AM%v=uDV`ukC;-9`9Ol`!-M7CMxx>eZKn5&kFNEMq?H$i2 z46>)}RxW(oku(~4nswKIue(m#m4gd!pp7-))CiRtX)NN5=Yc5ETW2g9q{ymi@)mKLJB=4h8@K diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultipleBackgroundForeground.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewMultipleBackgroundForeground.png index c37b6a29f5495bafa4747053e89377b17d251e7a..989e339d90e6ea619b0ebce31a6a27c37fb4658f 100644 GIT binary patch literal 12557 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 E0QL=Rc>n+a literal 63761 zcmdSBcRbc_-#@Oss8mXbwu(YBlF^njl9_#`>>aXc(GZoCt*FypnUU35q{!YYk&KXR zXZSu3eXjeye)sqLxPQOL{rl_td0c;V73cXL$MJeS*Xy`@US67MCEH2{1_maXGbb-F zFf0~jU|5>Cd@24WIr+{c1H)v4%*kWQ4k15VFIp+9h5Z=T-KP|&@$li3b?Y7T1%V-ro(GK-f!OJTag zzGu$YAOzO}YVB&}=vckznz9)ohPUe?$swJY4(5&Pwhkhnb5u!3=F(`O=7um$@q_7XEDe89}0di z$GzY`kMGP@^M7FYyyNTNKRlH#STO%5USE$z^M6ne)5j<1U0Skmft|g5Y)Xn^XlQ8P z(2$zDdwxV@B+L5skuP8FeA8-i#lT?SzI&zX@mn53%vO3WUqX*WEn2pkb$sUAru>Jk z7U53>eu;*cN14_q_P)Qn-o(U&ZFo9=j-Ib3D_gp=l#zVhJACb(qfAUp(?3(oYpSZ& z{WcAe|FQVXa`KP6RZlMCxPJ8LmMvR6T`Maq&z?CG@ar%mBV%+@Qh74pwr$7om+K{$ zmGXZ7{=F&FGW?I5`^O}iGiUUi$%|w-cfZGj_G6@1Eq~gdqHAm%V?Wq(xZ`ScbaX|Y zE1i)(JDoJAaq;3s@(~=F7OhoL;cl^og@uZ_76;!+l1~UY!@R|cLGW&IXC>F{FE3`= zgRCskS5)e!UfWOoW3f?D(!;~UeuvJ@|19fbj-$7-Yl3x(RN_?k?%!|T zV80=}T%@v^JYbI%rwSIF^e8Mm7Ak7}E^RKoUPnnr=B}|+x*quu^(`t(t}m~ss=Acz zq~DrtcZ|kF{`$^heD-1^BP0F+&0MFkonID{zojl+ux#yursig*=IW}dF8dsAM`7~! z{8P(CJ(gEAH=8zu1qL225hSnJBjy;sm?70;$AJSDj|~@+A5^-Dr_XTn=1tE5`yAJQ z`%ChV4C?wfj0E}^rZ#q21}*xop4tH}2PX`S|g={TgXTpSalE=jiQmiHQfg`W3>&WBj-kg2wrD^R$xkrei7L z2&aD0Lb!_Ze_vz~JnuZ3^zIs(<<>-ir<5#;&E3cMIM4h_7@xgTP#F>%j z(Z={F$*HdOLPA2f&CTLmRM)85W8?Nr4tE~AxpRk{oLp1Bd;XV@!;#N)-7b#&C>MEK zyhiHJ^BvMYA3KL{iDzKPU2AuZ8p@-~H)ohvXWR7~jNaRiD=lSQ%&`&g+jjPk;}&rT z{+BH52I8E)t2xD^o0>For~BgW-Mzb_lQ$}?eW+ff-6c{fO8$X!e0R5j>u9C4#v7Yg zI!?W>nDgy?FJl3i;K>cSO&qD-VIuw6B;3D!NX^tVZhWxyNTNp+7CM*9uj}>$O|L%O z-y~%=G%!%RN!R7Yy?ghp%SL{6&`iwCYEbp$GHb~`dE}8a%8NaE#%m2%-Rum#n8iXo z7LUhy{O5{hhwAINrM6sW+V6rt(YWD$+8QZFWA@`O`+9rH-=A15a$4gQ*;xDqXu~p?dPt?@ZY_M%~x3f!My+!OLe)pDhliwFq#?*32 z_TdErl9D>BHu9dgv3bq0+Ut*7zL=PpvXavJ(T?1V3{~fe!CFnr++EK;vkdkI+lYmF z3Mn}`J9AmEJ3jgReWh4_id&@nguAMrXO+FTMsf@sh>G< z;%45T-{)wOQPnYAbCbpU_U$|9np= z8@erp?%uuIlw}=>Wq#UK)^w+fYD`QFc|bI!E8Zthos!^n$+qpiKvpJIQ!~sg{K&OL z?2%oW&E{fxpTAe(P2dKiS8Y1bbDuAd|7~J+wg$@kr!QYtcW{#)7=N3MV`Ikz-R{@d zV>C%T*VdeC%a}yMl+T~n%Vv3%m*-X!^;&ilr?0fv>MaKc+y~O@(@bh(ALbWkG@9+( zzrQw7J4ZI@u*0wCPEJmzCF;*SeD*AgRL(fHBu=|5<`v;8Xhm&~KSG~(6!}$ESC193 zTizOVL*woXz1FsZ%|iT>L8|fhPyS6A=E1!}X#Fc46LDc4{-q5bG&J_yT-SGVtJFeo z{QhlIz07Z*Ig^K*+ptFe04ny>)Rd>|rUP26V-D4YsJb;xO(qQ!fG;M!Rj>5fu9ZCA z)%4aem7=RGYo?W}H)FfTaX?5xjG(7#* z;>VXz036n_N!*37tG27_Omy-5)6y%lP~6MMr{?H5rL3@H=ZO<1s6v7^SFfsQXoyj* z&4q)7%^IG^#>P@yRa8_=Oifo!1$Xuq^p6GQDd0_RlQ_RL)osQ}JKI(UK#DdkA1;{? z73Opn$fYye%O9KML`q7EzLPm#G>>XLt6D-(M8x(jk}je>laUuU|K5Xkf%3=hc^C~H z1?7TrjdxilO3&IClbaHkptb}I*1L~48WxW|$6k(1OFKl%v)ZC~_#&>UrnS|)A=9{; z`uz5R6CJir$fw3K6{e@90u+mP{2fQVy@-m~zidYk@V@iR8^sF@?cA5Bn3$l+wpxfV?K#e3cKcp&=jQqbd~Sj5$pV2gtj$14 zZ!)P8m$9r>VxaIY%!lMbHej&FiAXlP%QIl8Pg@6;ghv!S7(`k+81 z;0ExKUW1ytI%}Qt683U49_HB_<$Zh{t2+;#=~(V~<97Jz;uGY}tBUB~_ujZ+gLlqD z${sw-QPj-`Q)2@I160?JeGF_UnO-!?WJPsrs{~bP`)*B5&0yp3iw_>9tQ6Kk1OEN{ z_dBQjGc-fc1qQwCSlc^*nNjHje$fM1E-?C6?p?b^Sw$r#Bg0;+Q%Y~6mHL&WyvBXc zCMO+s?b>yFOb2V^?74GF=gyt`I8vbd>mq450m}u2zb4&j|oR0T-h|R7ZO5cBXKGqCD;%W%4n$i1odicD8^btdP3hr;~-9-L%0% zRdb-rrCLk*iS<&v$f%z`f3C=%i@%t<`wwS_o_$OAN%IpuLqlA(;=eYz zsNzEyb}yWJ7v5dU`R>An3!`6-49WgBqw+obe5Ju+-0FyqL8p9;@clFI{<*B=Q&)ln zomtIHO@q4vWi1VObhK=S5#Ca6H+qTZJ#4Y*U->-=ai(^ z36O~w@#s!Dp)>_2w|))izM!NeM@NI&ZN0l%6HD7z?sl+tpeyh$^vjIe$n<@fi2E*Dw2des>j8AUKSite>*nX9=Z<6pdZVci;;lcNcA zMY!2A6fAYOe0Qx*x!XNGM&=F4g~>@^AKdaGto4&o3gN2g$CP&9$PJq|i6jmiu9M^; zkEr8x0fm`e*bIDC>3U7dSrDnxke|9>#Q-apmHL zV(znV0Y^=p%2x5cJzDrl;P0YYEAJB%#&A5-pp1=xhv~6?&zT?P;q?GI>BiN&SvGFm zw`@OjEn(o#l5zDWg_j$AYdnF_N=$)EP7yKX{mPcVjzWp zQ*4m$Hg68O&$_&N`Em~^8~7nw7Ov~*G@X94ZHJVUE??ecgqd*j$v?{lm;Nm*R$IR& z2Y|Tcj@4;ScljyIBiANeUJt-vB%2gU9_hW^<>69c#`1@~XfM=s98X<8y47g^GT-9$ zqnhr5`~oY zW15speZVf_Y1fi&T0ehVW@cvU#s69Y-kX5Sp%a+U#|Ow>1F@Xx#@OA#DP zDBfD{ihYiDxx_@s%9Sfe@noVC6D>;hp{$sEe!71jFE3*!<1oYD+Z@JiX1#qY{=K_9 z7O4Enl`H#rc>L=7nR?SVZ{AFJsI_&Mdaq*ca<0EGRiEOwA)rs@)(KP7eq{xU#;++C6AKGfXXhO3>Du;o zzVLExJUDDDz}CHo4_`#3FDWSz%;<@z?{}S)M6)aD92R#850Q`hhvEY$fBtSGH)d;i zNRmgdJIm(#y$rL)gS#fWb(yR0+kFmM#6Vp~1crc(%SC@N;7@wIdk)U)HVpqMM*XJ* zv2Zont_*sP|1(wV|I!aSOm$jVeH{u2+WBBxZru2|Jrs?&q$CAbSJ%fc@j{6H0+J`Ba+nX*RL<9Htsf}dZeeNrS*v9Pd#|6GepZks{s}{3nfTZdBdt= z@=|WKd#@5*c-`aE=g)S3Car};M0_1X01!}jP8)s+QKV2_;N3@*mX`8=*^o?K&n+*$ z(L(&Zipr~d_g12hTwZBFAj;ONvS^P;h-!fSL}+;@Hi!*o+gZefI;zrNm*CfGv8I30 z!yjVI=d`4ln7i|uWx=fgfd$2(N3M}F9g~ye{BCeKN@_+10*2Gn_h&3ywn$JW7En|5 z?1U$;lDpT7D81tm31u^XZe|FpTk2anfDZVHs+n2*C4GG%QPJq@*B5}5D1aI~ZXa4R zO(@bnkZgZ79m=j{1}_1K<5E&8lleqMuG!3!8oi=BlOBmY1tt zxbWDxuwq$Mf>w^b916RCai_rB!Kzp1f+U!Rb@E4yMt&UAdP?3`>(;tB3 zp`@x>x1o&ehodi3Wmd|L?R@tlJ-s%02AsVbU(ZDUX`d73KIj~_(%`$7@M=Nr{^K9ZHLyeZ!-cXhhz$jb*3%DZSX2 z@?+2xpKhq+5k*OtqcO=$mngUaXGaJ;*uuq?gz~O}9WUngC#yqf*&0q|d3hhy%YeZJ z>i(dVIy!C23IwwjFs>7-T_13!@ZEzSVa^t%9Yu@4l?fq&9K}QjM70A=vTh|m%fR5x zduh??qa8n*V9m`wbqEZ{A$gNvcmy zf(YHC_%zZ#ARro2LQ`^5db$dbJSAt--=95jX^{`)w(2}!!Q}R&=g$k$hQ1Clu>eG_mygAG}!3vH8BpnnST-(?f)7GZrn5zRdoK!wx zVO8w4AP`=_Mp@q+rcAO>C(99}-Hm3SNejMTL&4^pQJ=V5k7+t{F zSP`WJ&?V~n_3M;yYiL9u)HgX5h%UYL5Hq{gVnJph0SK@DAtCWTK0Xvz_ywJ8Id~n! zpdbXfwRKCCX^TZBRzSK*?Y><*cY62F8d$Vu1AQLo{p8O>ZNARw57nxw>H^R^kAQ%+ z>;xFq^mjgYRm|NzzgSeRUc)UDBSX3xlf2&0%xHJ>} zOSn|hf4CQ{V~MZy)lVPF=NdC@ z1|a^~AP_3LwB3b#1U^#nf1CZZ1)6^XP*|a({o~(Rh7XrlDK);>8%LnO+5bjk0RI z{2BU0Kh#SiexPU2Ua#v`T=VYf)2ACrdioP*>M5InfwYQPL45Co_+h26%}6` zzS`aM=0{j{#Hsu5vP~jVQ+LyCduv|QniCy@XYdYDtYkRi~ ze_ft&{A*ur9925Urx=guovoyYH?mZPe_2R`g^2q2DLtJWE!_nx4 z1C6QNW}Vo@uJnP-&XppZcB4ItP;QJ%lib%nxz+jNk0N0VVG_P z2m4P!M(gH4Y% zWQPS#n+32wPI|hAM$^u6-YgU!T#PiQFL#d}@Z~?v5$&L+zCH>(^*MLO==iwFP+RVF zuY$W+EE_BKgRc8bIyC+DquDhz%4OkFx*$bW6%`Lj`Jhs%&zn~O;@kC9Y#V4u+0iu> zaM-TzNut)9OAVXY+2f%3zTZ>w>?tTxY^?&ATqe2{H$r@p_IJKQA#8~k}a@bA7O zM>MhfZA3YKJLQ50Uyze~0JSG0x##7dm?}`9-bP0u`VFvF({QW?(A67-TU{eZ!B4L z$a&lhRu^76!35+t7J1jzsgego_6W@>UWhF87%cRMF9BSFmU5s7PJg|*CQZM1H8Fr5 z8yb}MkM*H9gC6=J*c(0vZ*PyT#_ye@5ADw z9S1Z-_Cqau_U^4D1ntoyfuEEkYnMl(zCwhoDDmTF(shTgxnDD*G|lKrwD-FQ=!Y68 z;jp?`Ga?L8VHgRvjg5dWhJ`e`eWkGk~frS+ax}eOg)}SP5MtQ#IAq>od$x?Ko`zQ@uii z4b z1dv;D=c)-zRo6RL#X_ftecj`UfLPybCJfc3_x2{8^Ex_VlG8nr#rLjD{sKQE{BMh&@gPpRM&&%Ov_-;!$1p8BV{7~{{Aes zjKO78Y|Var4x@(iHwYrc-B8c;-no0X38*mYXwYb+3YQA1H6SwUc;kS*d3&BVDiGsz zYV&|SS^C`E(wq2nh-Sd3kn{%4XCtrr^Y5RZd5PzLe#RGUF{P1hOMLFlX?b2S2<+`M zblhj`;58p_cNuxmVb|_PPdIz_Y;-XJ!roginOdEh;M%I=GW>!0#C75lhR^nfr@_Pl zqPXSaVknxPnc3fLnJ@J7oSa-O{vZ)MaOmofFSXdya=szrHeDa@H0!}Lf&phVZ#4kc z5ZN7!RyLDV3*Twwjf7znLb~x#ivvufDmW(B?kpnxK>Gbe6`1MSQ|*b>MjZc$29{x{1oiLLb~J z@%IOst~iwy-dt9Tby?+b{b<3VwddsJ>!2$gKcK_L!tx3$-DqZd%yS0bpT1ZiDcS+X zSXd7Shsxah6EdF!1&Mu2Cq{4nY=3^A``pl6LT9mWKU$UFM*9#6=Q^t^H+SOQE2SEi zbB+b<-MiQ6yT2MEUF^sa6-`a{(J_=PQsXJ@_!WOYKf%7k`*rhLMkzC}MIt$eWKc|r z)&QCzJthV}$1u#XC{3!yLm>zw7B*ErNpZ}K zMoHD;(oSUOp`u9pa8xF_&zR11O3g+ROBGcBn}`yRw{1ABmu}H21~1sMRDaWk4MxM? zK6tto=CN?Hu^ntRcm(MP=vAPs7}g5SlgX2~5uG{@lCfA6rL-i^!`x}w)b|$hpX`a0 z^X_xrrNJT!pcJ022FD7F&=K@YhqOfA}%ha9j?sxk*T6X2XH_9N+)fZe57~n-rbjw*;YlYWK@4ImqW(T3yG42 zT2k`e#<=nI6>PH$^78T@g*KnI8R?d*u4RYK2R~rV({6Sa^|wxAvGzY3DC%ZuRBBOL zUIDR4ZLVL~0W%U{Nl<*(%ZP|~wlTA3in5P?TL5pwain{*n_xQuxvc0))0Mt%mm4;H z+P7U$I9QI@s?yTM$$3^GZm*rjOse;b`6a~0ej1%=!z;;J6(SZYPpTh8BKEo;fI&0U zb_p8k`+8UlXJ=W<+gBC3xzneosUP}?52BYHB(T8C);)URWw^XOQ;Za)d?V6O^yTUJVN`Wgr5sm5JO!%mTV*2V6sBzfr2 zOL${!I=Wg~98dk^OP>zc*VUnZ8?|)$`ZxA@^d?Ibm|ZD2X&xH1QZl@JRJ0!Pi#J+{ z$t~Ov6X9*~|BQ=`Wg9L}y*RAX>-MNIrEbt#lmo6C{~tarZbovEk4#P{e08`09R;|K z#k?s^2Ah%)hK`LMddL~z@~GJQCSd6M_U_$WJN^K;x!UUmylBO;T>*2m?Q=N`uNNfS zN4~MQwT+I8`)n0rcKFqMQNH59*S1lZcx~s0iLC z(A2@$WFkWV@&sb31AzEE%uGnj%nTmCt^fJMqR186H_A$WKJluAJO7NSNysM?IadE z)U7A}{;yDdPhT5qs|AFx!__n27ZVl{A<>K)1@~zsl#`{ob!JI(XOe=?6s|OJgPgS9 zZSIWA#|fugxb-!-HDw(g$=d3`j8Y$6L_QvU6r; zhg{|0VB}4HTa`QV<;XEEol>9R5X5-0en_ak;CyjJar@q{4(F4{04t7V>VB0>Eq9Ah z6$}jZyrjCUO9tWr={k8c-YLXGkN67z>dOx z(G|XrhgkK%3IXFXW3y-&uur&{+D)mHiK^aQl{GQgdVkV`#=ATuT51jIZ)A}49%v%a z{|*yB0Q(p8rV96KhakYi;^>%|%DE}2Iq4(zKP$kTi^YkqrMG+A*X1`s!;R2k#caCn zP~6SU6O0Qe<5s4sX4VG-Bw8Rj^&sRB*wv_B4uH?NO6}|gqQ6?UCw6sp$%Od}!GWOJ zr*6IUwzP<;_$e!N{g4oj?=UwAwMSaWc4AOl=+Gf}5eHr-oLjb<;~|n%)e2G)gHz_{-dkXg$qw0S3y9axE9K53OGL`)~c><`>jw;X79!j z$N^Di@~YMYO+qw#^l89_^%mZG07#w#P)P`H#D5KgwIH-0r;24eLckAI&@~Yt>V;NB zxV~|jPo%W(YbT<^;zIm;A>yrBH6}3?-bj-bZ7>tAHeZHoqekJezYOp(&M2cqENPZx z=8Z3$@w@5`8F0_GZIs7``VtnI&1rls?wmIsJVVET^RJ)UsEn9Q_{>j3Y6k^j9mGa= zo6F;iJXlU?6)=;?!9%?a4hgLyr{6a&mfZ(C_qld<&0j^&s=*!6$%b@9bEusNM&34; ze^dlUo0r#?%*VrHim#L-|2D=~`YsZAfVPmsSK%k0hbbvB_h*DU6S?;SxvA2V%$cV? z7^Lonw+6j#bDbK5pRlm774+S$KWn_-zdu>+Cj+{=a@8uPOkspQdSar&*e2dW?Vlay znydW9{*@qQ&hcNNP+|$DUlvP}KVU%u3Dp9A#P{P&VP|JQQj(fNg=dC&z3`gCy z-l|>XQnrPQt@lO%5@H}D9c5e2wDU|A7!BZ~Q2b1x@XKNK*O%{(54Br1)HgRDNlZZ* zhp=YNBf8TVC7bGs+O*!L9QsrB_K+Pc%e08GO-Dhez3zLNlhbVLn4_4>gi4GrVAE*_ zaRi9)ah8Ad(Tx4^PTG!J(w}*c-amBiDX)MT7c1*|M@L8XL1@+#jiF0`i^p{PAw5B} zrpysLLo(b_YXm~3U@NtA*r7;>1a6^l9sIh7m-o;BHfuod%`%4omS<010vIlFH77<} zW)=1{d;Qy;Bj>p#BwB5S?l+#G+U9?yWXU@ko0W=u)CR_imrJy>`tMp`~dW*DL|lh|>C+zK!WdkY`aO#9N5d?y{X3X=z67d9KtM zgXT-|`vA4zr}O{p@9V3AcU~kki$oisj!6T|lx${&6`x8<1QLSvGX{1kqYF?wUNkai z<0cpe?|<|-@tEi$)yw>ex86QG(9Y;TO#$IH_QViJ_ zE%ClUg4nQwI5o*ml;hZ<)iO=Vl$&rzDD7BAkeglzcyS;!(*)c`^D{>#?gfOsJ!U*7 zsP`J}BcTE#5m}lXZORX_H9a%r&NVv1HI#9u*XYAN7Akijhm@O3siq1xPr*og0NH8%vQ~?t)BZ+ zJ{M0g8{{pLUEUb780k+a6u-ZJ7K51Bt25iuqD9cewlZTlqktpTs02)OnEy#wQl=2T zV5=(wpmo?m7bN=B)o$-zbu@9v;hJYgf)Y`Jz}xkmLI#IBi|t@O!u?lmY-~I$E30g4 zn}$2)nstK((f?-(wJWF+?l@uu*4)iqKYo}(#vv9NWezb-X`(d4wNygh68?yn_Q=nL~D#9Jg;wibJehh+jHm6oju^WYnFw`NG_Pi>J$v?4K%kV%{FRoT9+()Ga`4|$Dy`7F*4y6p4E*!Q1tI*;qZ=Y0 zb&tdHP@f0JefO8)D>&h-%8bc}d%srJ`MI(DS+(kFm3Q07 zmFLf7K<+zLt+es@Bg8f5{)Eq^8bA7wqK@W)bio=eS} zXme&>Zy}y+^ZnCvA-&T_>ewycGN*m)Qs)dyc!yvQCRqd%6a`gbPSwIfIh@$3rhn;D zn)8GOWIHi$!JHw2FEh-WH?Rol&jyhdT(s+(nw9uIBy^-8o21zfXpor)$lA+4GY3OD zB~uJYu|r~Bh*;4yhzqG7J=jC~VTM?a!j^4NY$Fu;3f&1;f#Q}9u7V=_K3VllwAO)V z!WJ#jK#QJlwTn#ZFpvY?mMn-Jb*2IbVbj27mIL?%X+wO1B-_(f5MdZA6)g9r3x*{U zgB|dk1^O;-NFc#_-8}#t^TyYwZr{F52@L*6$OL-cf)=6t!Q%n1dh$`w^o7kBAY<@Q z3{qTBya=SEDMjCl1R!E#_Ypz|YNOJFv9Vbu`Nv`M8rAz8Rgtid# z#eGD?9U<oxe_ zvqRfL&yvD(xnvy-i@4{{^<ju9I{yGenH(CeqKd|n7Sg61=ezR-W zEni$yS@{Bx8O^yDoe8n~$AeN^al0gkgk{mGVK1eDT8@26!erO3P0%L3fDej^O?SKn z!|Lee6O@4pNL+V(5(F1Van4hxPVL>j`!u97;%6X_=I!Qty7O3W2w)_>h=iH`w89h! zg2aLr{v<3+viFoaCX3)Rk?H}^fKLIq{b-OXF+NcObS=`ug#cm5Y7SAohKc*Se~!^ah zqfC)Wo2i-Ehq`_b9~O*nAfLAU=H@L{WN2u1>Ic*j*Z5Y{X_j$NRteagj6aI?>EFJ%W&TZc2t+ROf%g%Yo$aJf|LfS4 z?hUBygj`sQkut!kbJ<*Pc`*_Jx2*b-TUL4JaL*d|N!p!`({KopQpWfhNq~ZKL5P+K z+C{GMWX5K8tCqKpN+d*Emj{i<|H%{CLhsYvU^(MIFOm)bskVX)lp*926ISO{7;;V;vh?<{J6ny8+DEfB@Of+5sd1t7zeq{#1H!eQ-0-=Y1xS1|) zDaHAel3^2OB-mBORkQQic4IoTrE?7?_aL%SXCPK+R1|oN%_oD`ata>&Dhl_qWA7$> zUTV$L>(D1%e-$osni;E42PKRN`;tcHdkFfaH9=L1t=DyTZRyNROAAPBQ^zAs(`K4~ zmX8dImT&+lcxo!auUk9*a4YCG9w1k(De^nGasq+jc!CJ9B_4ICL7hW5{_|O%y%iM| zdQF+gvB-pAycY|`Oz^;0QODmeDFPp1`QDnz`T~xQ)V8lFV}5%QfOAjIP~NI({#-1N znOfZ$e=>BENT=BTALc}_-k!MY7G!E{EYOmejBqB|hbs*Z5%jU^+|!;PQgd&RM$vQ9 zCX{ljJ6I-U^Gka3EpMw&^HZs^iJMuhLg2V2yQVdtq26e0NR#k4}6Cc)%@Dy!DXa}GI*2- z>M!)c#2d{%j^##L^FUL!U7Yb8IwEk~ZtK(JXBcF2H#1vdt^{kxUfuc9C1Q&5FA-!5 z$Iwzx*yJ~?LCl|(2+o%vJ(IFKO3D!1kV}$?cb)qE-9|KiY9lXniFyAGedJSFS!m0O zz+Xd#VBhLp1>}AQROHHSz4)bqd}fC5h?MxS$w^ z4<-E7e>eC#p3=YQx(9sMj!DF~s3XQECKs+=O@$3f;<{J~f3vzJ1NJ-s2T+0{XSLZ8 zXaX=MwgXf%FZ~>?13;?#psEu?7cGpJK0!1gy(@y>bUQF$>p1{)7VWXLZ=p=9E+zj+wZBg_{`#2(fG_sReBosWrrP*8A8clNWp zWBkBG{v6TWj&sg+mH3Ha5gX5exQ+G2C3%Xs`4KLOok{}BYDqdwQ+8M#7XT$NM|)_{ z%Zg0oaOTa;(t|`S!*-%W_aOR1J_IHKW74&pR@(75024DbsXsS6#@r{axlc3JC=YKO9>v^o#4}cM9Gm=bgA3p5(zMu5yThi~Bad7$;|5{LiEH{*SCH}}? zcYJ+C23CDvU>TtdI0vV|4@F;krU^sO1MT^`Wd0L4S6M?NHadE9%6${E+TgR!7|Uut z0^IvAQL3Ue6N8;=jwb-){AU2`-{xg$|8-ur@mGKm-Z1g{VAyU}(PoDuP9|mBT_-~E z#Z(Z1VP`U5FMH$88c{rP%y@XE^J!&Sy|=W8O4Yl%#_q#OSgkr+B%EqS_q0{Ava(iS zV$}{>DVd->Mhgji|F0yJbIX58LXAtUf`IWKle3Yw#o(@SP7+MfDk+cE5r2FE5jznIKBU?i4xd?E{(VSG8$;TG0`eUC9!N~$#F z;^uD?^s(Nk@IG+r$AjfFuyr`DWepz%aE6Vy@o5UapCRD>Bnt;((4GST>V8Ka@vj;` zDi$=&(n*F@Nn!+`y~hran?#`CJ7Gc=B^z2T<_se7%Bt((AM~Q`)r0+AnO^(TO7uxU zfKh`8vZYx!;C;XYvwfpQYXao%kig(9lUgZJ;H^1X%QC0_l}%dHLgI{YA;>f=I{gHl zU{%5$kDY+Rj|q@C;-B?|NOvN+xBj(F=XK1FY|a}369@WH!i51N{FBXM{(*}3KOVI8 zP{gqbFe}^BEaM#Ke)nGjL&|{Yp7p?7kkCm+q!wvO#-y0&=<`ss zv1>0k&x#<22&1 zf~W+JW4)e7k5og3rK2wt+6MU=%+h4L&1845B87#Onv{m>lU&oqzj6cE8Zsw9>WLV1 z0lvp^0x?(@KxdI`b0t>#pjB{e6%ngWp3uXTHsaw30aYVFC8uEU#+9DoMsIFkO;5!L zN*ZzwB%6f^%?3JTUS;IU@I5iFUsu_FA+tmjO2@4laZ(@Fb}aC^_~IOt*;}`6$%M%{ zY?e@#dieCI@ZPIg_)KH#VzlR6^v)d?gl7gxx#zXHjFdee+eNG|L^-}>dC_21k<%cO z%Q2rp#zo%PT&bA&V3ElqBBF-e0m+yWD4I3hfel4g$x}XE&Wx#Z6YiS`!^N9V=28kq zfEFr;q35mEFrtXTlHl9SN>I^DBk;BV~-gcMs>vAS_a6t`wAx#{Q@M+O!NV1l^^es z+?o0juIA>JN0}q-rLInx>ZpN~dnJTFS-P(CfDnl^bX6c35|fsujKXj*xBF}1O(do_ zk7<>4VS0IHqP-qHgvMzBABFf=WJq@}D68e$;nQcX${>4mWa#bs-zIzz@(CRVBBR!0 z4uGfY4e9EvTlyuxdp%E0txkr3am9vv-D&8t#XegECH7vy$cvv%-$!&@z-A^oeAPgK zErtDX7?Fypp2c2P^fkT$+2Q>Bj2QH|ICup<#w`FrM%Z1RaClsi0r(kE8 z3yZ-3U0r46L*M>2s5{p1DAgC*0I+05KoQ|Num-HX7?4z{|K(&@Bp!HGE_`*W%F_F^^W_BZQ<0vf-OMHlM5DpkiF*zNiY)i1x1Ld zf5a)5>`B!G4WaH~wC)59dJM}7L=T6Ze*EOgPRxZ*UweO#g{Mf!O;z=2$Jqe1Y2zfAbbD{G4scIt5XRNiRpfJEZ zWrd;no!&1$&!}VX!LhsjosfDl+n$FMD_hJKTf64zYtQgsG!qU66Jc7kI;T}Kc3R-{ z4fFLQ4&w7eL(E{7SyGQj zLOLe#gZhR-$sITbX5PPIAfpU4-j;{E`Mzf1bO+!H!Hx;X-dE4kAg`^+U$a{pOA&!( zMmmf)np4udcgL&sjj+X^JbSjOjxNV-a7;sV;m({^i>6IKhjx?as4uc(k=u zKt3BOF4R{x+xtsS8)RgEh~Lxg5E#1evg)#VIyQfCr7M@8Eu{HcA?ApB(L1#&7 z7X=Ycv_!4a-Q@;xzPtO36jMEJR?i;?q%QM*DMw+(ivX6)_dVp$iT1gfb~I|{Q87>^ zXoKW*2s{$>9O?%h9v&VQMMVMT>JNKl>uZmbr=oYuW08Ix3};`d*#=}G&okOV*Mo|; za>}6%51|YP7MYDzo;2UwJ3p;F|2ty$8n^@W))*G9e7N0~lr&!Xy)sZBoAO+902jC& zRqHS_%h+6Y3}l0kiPvSF?g8$Tl!FpF4Z@$-{^I zC8>9Li6dTacCJEw2|FK_R{R7h8?Bdb+bE2x57c4t%j{GEGsi!N_MSkMJA2G6@fFjF24ry|)nPxo5)&cG8%MH)M z!!5Hv%nuY{8LSp1>#Z4&f!lHSqg@RGIUNp@!bj}wcVjB;E`|QLXV7eXp`+SeyT(2a za?8s*L2l5aWF5KEb##E)fW)H(A8%~oa)Qrm{XiSY5QM|W)LS|*R1^y4JA2=|IHSVQ z>0SwObY0gygxNyE8FWw%LT?g>$wSwBeag0=!#)Sy#4DXXGn*WKTEI^&BWYH6Vz%T@93g(L34a6VJWu%iVTU%S(!@hm?9gfARw#V}GXXZNP1uRHBU-m*)&f2%h6pSQ4-$$ru;- zKWe|7t!)pS;vI=@CFSMyU}2*Bj}vzY4d_mL#5XHZ*?fPTg*LvkeD~{r9I3PF#k}yN z0K;YJ7)?4YqyyJ8%x$`tIjMheP*qd2nRc97-TQvdg3kQd1$&Ww~chfXHkNT5!2 zP0gV`YqbQ8SMw;s{1beETm-!_(xb0l3`;eE;AbyQTk?4Yu?8m#h>JBkI!YP9@3XP8 z2HeM2a0J|bVC^%sv$lfV@6S0tLdGpc$o07-Vx^S5tZYT|g{m!yGhDBk9%HeOP^ z=$~we|>)2>HsBx9%hrZfi~2l!7P)&d>RpVh{EW1W#nl%F^w{ zc>ZG|@Ls_EEASXjG|l>fli-44v$7^ryv$(D5^)WKH#8kGjRnWyP3xdy86O0oq1=fu zNF2@>V<0Q!SW>tT93)uq_2q8kf@Js!Z4|6!RquN?WG?YJ(OFsQ5NWEZMf0Wb)=jyp zQP;=pw-Lg6K7)^#4xbxw$60lfbvOzxCVx6EpPb*KUV6}&$hi1=992SWjLzXbaEej? zVXG5o9uF?+W%PH{KPcBE5eBP#@7m=bzuS-@NK|3?CYd#gF@FtGV!WuSJ%f;mG7E+b zboifR;J&d(c^G6f{?i%xI(M(&GJA8-c?1PScBopNxqwbiRv4w&98Re;^d*?|ArXP6 zsMxx&gXG5!F=DQ94@6TKW=azN$yQ>agpb?Xk1H$8%lXt&Z;gy(B&=ypi%3$?)6{TXHP|~M(f>HnekK1H`juo}ET9U^Rm-+N#M6S@AylaL6hq5dW zebhOqj{V8w@RNzwtHi%!{$KfZpv69XiI9mYPEVpW`GU~_GVcwTm-ZQ0eK&8mLJBZ< zwCbFVD5uoPB4}2iJsUXYuejin!DTc^Vh>^mCXWNe~V3&Z`u6r}V0pFWLEOS4s{ zLsd?IU9Ir1i&8H|DK|*%m70l~d-?Y5+v28W%a)~I8`41%LZPzs@0A-&>zghjBsAX&fc>J34CX&<>xRV~ zbK79I4ZR)tlI%~;WW-8QIyA47Gpt>?&_{abu3f!QxMBcT;mJIC^3ftwp=16K5{7~^ z%SFT1Vuo0x&EZ6_u=(SiWFqRG;XD*$IQ?zj5JCZMdVS?SiT5E&9TP@EsHjp47D%rJ z^A7EspITt>Setz6?<4xR!wLV#Cj+oX{&Px%lEl28?SW$x1;`m7aCK(#(MxZURpcZ_ zu7p1l#+Lq9coYz}WvVcYIrd?bE=ZdE@cdosb#MR5Q1l#AcGz@l=H>uBh`P(wrt*uX)|(AdL&-J%>a^MNbKm*6k5U`rYy< z>{QsKv^<>7gHXFnXn8?FfqpY9C*Xn*2GpwSh4JPv4L~$&lx!TXLQc(+ncJ*)fQRQC z(OhO{09aFwc4FifApa>zyf&I;>N@*4 z$*GdVDk!>8RY`bBTs7@C%sCQV#efJ4%ZGQtz1v|>lS4X41g*G90izc<5DI6H+1xW& zO1?($+)0VOeA?MHX+x-Geojw*L`Iph60~L9j<^`3LKlfigUYFQd;LN{^Z~RE$@m%g zPs;lGof}H$%bY>jot2_E!)#t@!w>)!FG?o(9F#IB_4AUm|D#8g=_a`8f#U>|I$JC! z$^kG0rQzugh^H8^^Nxrl7sS6}`@XMMq8vC_OlTk}K0YMv4F>hg(voo9M&_P4H2@-g zu@IlEHy%9Zv0T-i;Rs-U`0$c>L%}0(SH!~AGUSX8d9s+E>~j=dvOvsj-h~ufy>9lTT5D*)zfq zUp4sW5L3YuU}^w}B&qo`2WO8MGgrVc_(=x38R;0H*Vp*7;gKQ0Fb?|sl01(7jZ-WB zsjypLgSz0bGN#NGXC{2D6Jlak&?Tgs!q%~{q*#iCt;HM}Bq+9VI2DeM4apW|c*OTj zUeU1z@|@kb_bX_5fE31Hc9dM*o!L;?;qNlT&_ z2$ZcMSHZ7^19NepEy*2_-3U0kP|UK=VJ0oSyR)rLGAqc=dhO_7L$$s9~Sqe zmNmZ~+xFM(Cw!@EKnyJ&I|6YLa;S{7U3mM<;5ARzxlZ>#Yl!yMl@d0JVe7cpuYadx zKz=dCyt6)MD&T1TbZrvO*6p^OJ&nrElWp)lghC*-Gz?x*bv|*BPxLSgk{09}`gLc&>yARYzom8mB}@iluFOsA&YdH} zvfBQ`F9EBo*Y=hyo2%?W$Zey=nF+OS95w3g*aAC-HcqTH2?hyz1Ffdza`g`*(G3Yb?4@bGHt=vZX4%-1}ACuS}y2%FIu z@kt(n&Pn2U6B3a3W%H-_AWEB={mdE?;DmBPhWC+m=E|tnC(}DPF63d6VB9H1%!D4@ z%1@=UC$u?iRZpqcgG|=@=^nfpfd_$)9&Pi|pOjAq0muA|p{zb((h%0r+Yh#HKPW27 z`qX@1A!kNe#va+H$aIn+8Dv!%owRY91ZJ>euq#Z!Au&nw;m+7@)C7{6W&XjB*<2d9 zh&=?L2b0s>WH>MGd{MSo1l}{0IZszWQ?4n>g9W~d=B(s<3mQ(^KB`;>VG&1K zF{!a*&L$=+%b`>s(e*%1dSFP1*|e3^MNqsr?7e{TXHT9O;izMbD?cnVaE|BukKty1 zrZXfBiPg14yW=5F)Ie5&c*^R!(5eAYPtYl|C%}3u{CA+~l9b|D?5b227Wjs2_4`7w zFrYZya*>+9sGhPrEF46bi4(D}C`G*_=F^a1JVF8En~n>0PI(ce<-5 z>c$Ju@-81(#w@w=T2QPYcx~GD`R;;S^K*lpPXU?M=dsE9_x4$`Hoh*c1K~I|B35bW z>dNUgl|f~ek1+N34b?L@dl@G|RrBeV6%xOtIE-18+bt&f-8k8O zLf&PfX^)1AN9PYCtxy6Y!>5D8R_w7k>OyF@cErnwidt1@oeMv+%xo|6AN=DY6f8W` z4Jab;-uV;6vVZ(2+$n6@kaKa|<$T&YgKhJ%3O>Jk{r>$iBcqO>BEWeh*FLR_C|`Qz z9;;ivyjeDEcb1i4W%>3qc*Sd>CYd=}CR&wf2VK zQ_twk-MykljI`riY3P@R_R*}O;eAnU* zEKhS=p#T<(Iv6$WK{z*Ab43}y4@$5W4jDK{Js!un@5L)J6K3xVigUOL(@KajgNfZ? zAFzZ6RaK9px5)q}W_8DPpW~YTWKwCj7(E0aoP@jK*0gZ^L`sypX>A7E;L+Q2$yuhX zY+JmHxOk<|AsE4>btEVHq0ZVS-B}6c8rnAsD&6#$ohm=wuAoKP)Oh1vx-U5&4Q^k; zhfoP9R-}8Lda31otlHowCpT=}dZ*5WcSHs(&Vm{Mepie)&?F~OF)?3T z-KYQ_lu&4S1bgtLX`>)^HlJ_J=g$TipQxcXTI+z~=GCiTCWSH4;Tf;=AzVQ}$9Bip zyzj-9rl`rIR#?E>%zpq$0}&b%SYRA??1Prpb@%m&Z>bpDkdh#EucArlPZOSsAvjD3 z*G6wEOI!XXYr4qR&&~06dB^q(Bev3~UocOAcV|#SfljYPpV|Bg7^%b~$~ALR_OyAk zNVgX70cQ49-2Z#py8Iq%w^km)BjeURxT!ivSSq{lk!^bv=z{rOXQWrGUw)k*dk4XF zfuuE@IZnU$a_4Tc(7aC$vbt5h%;zT(+^=^28WN!3rv4{zOgRkA1o_$IOjlQNmtZ7SIw9#9#=-0;h9)yesHyx1m{4U;F{89n%S^K}SwE}{J zm*3gp8*p5E(LRbl{FBpsbGT{!KpP4S!EK28v2b{Nw^GzP7(QPB4*^Qo=F>4Xc%S_3 zjc800os>bJ%$p3l$H((tE+hDS#_xX1)A#JTg?18~FJ2HD7!b z&VZC+yR`7*TsC%g_Tc~tgL}bW{$|#CdF|v+Dk4%oxcYPVF0ETc)zt(7t;Va(lrNs# z{Re0C3E%`x>bKDdwIckL-r(ge+L_Q&S6K=Gblrci)PB&sw#aL z{L=@r8yB%~--1`rn*3KGWMc@JElGd3DQs*dR6u21lV0L?{mUIUL}%*g^q1zkWKdlA z`O_y5=HCKb7`uyA{;EObJ!1b5czkDAoqJ*B${A!dD z!2fxsoudvJp~q?0)xVG#o^HBr$=Fb>ghSc5N)!ZW$c+R=Y)RLi^41Y%LKAPI2pn(h z^DQhWIqO$@Am@c|(ELP4%fgpOp)cHip|7uRq20A^ILKXaZ90M$_9H*eJ*4_YBZR%uGU;Ob>&D&;Qd}1@`I>Q5v!uqXNB+ zZn#fz8g4_&wM`4q4~#aN{OUyA+^ugUx;Mi5TcAvm2ah9qkD|Pc74vy4;-2Y7*-?JL zLS#Z9fJN)VZ$(51rq{@PEazJKtNCqsXmF6zl|}8%qi{u7P*Jo+DLOit)R5Dy}yl z06W7-4E`-6=xHlQg;XT!inc}cOwBibTT;UN>5eaAXntcS*@~==)M`%!G?1xEl zdKHealT{ufQ6NtOAk~h~2LGz5mDE@SCB*Hf^LKE9;XAy28?Qb8>IgsvAGfyp3SL-WkYJJunkWW5Cm~qqE`>TQMobqOQ zkjtQNmXtj+h4UuD6hnUpJW9{LFHmp$<&xpw7mp5Gw}9uf5ivMbN2%bM|(^;P#NXKU{HNcV!>u` zhZWkg?e48xOiBBU8K)tmc8A~wkY78H?_G(T1q8xwv?%1iN7pw2cRf8cy`xE#uxRLY z2I1bd5v|%7SwRpKL_1JZQDq8}PxJP`OrYZ!+EAVpX}AG7IKhE<1Y!gZt2D@sw@0@5f~?LiYoXu!E_~Ab_|a zlK|UBR1=t`@ZnLBD)J)COOWkdDnNZ!GmC9Cq|Tf@3%X1N-_v!fT)M-)XA}ngFs8R%6xu`=KKFcCD73-rHc)EMI;JZq{->^>s!N5I`j4FA?<}05|cw zPMEa;yDR&X`e=-xb`{9{dV~ISUa?O_Ma;3C_kD+$0kY%;H5?c=QdL!D?q~V{yO_t% zP;&z8mq2nMbMwYui`g<89j+glq~rP@5+C1?XFhu6z$w7UKp6%4E-!o8P5;(NQq`75 zNru*$=H_M;aiYD+NSz@<*8_s@2WbUK%dI$#=-tS`U^z5xq&Wdtl8o^o2A^?ioIo2q%U_1EZ>#>k zGkb=Cq`KKVGeE_}n7`fh$7CTw=KLB+PGo)!xe5Mj1^3P%>Ii%nERtUhwt2%=lC;iX z2lM`W3OP!l89{RbaDMcupTlxQ7MaaBO`eFYHJ1?x8oee%Zz1ntM5!I{$Qt!SWN(%Q5~KOgJ=*66_hid_G7 zk8lT)aHKPD#D9grpHR^MjROrzND2T4p*ykRNY^;s0P&z9{p{lyl+Rl>>CIvddH2Fg85Z=)0DvQ5hYPg&G5n=LjneE<$QdBiLe>c}In?FKd-mMC zOxbGE`dO^rHQ*{N8}p!y(4B1N02S`PjfJOeV9XNTOk{itZ+1Q%#)vax1UzW)wO|4o zc8=n_Vta&9d*qV`s&TiIPkBdZ-g5e3UV&%33&_7mQBnD1oPGi=!R)eb;4UG8T2B5i zXfC&V)#eiXFe~dM8aDhfB4A7uzjZ;CUVq96#vD+PPjzbKZtF%t{0RjGGy%Y-X-eto z23^LGE|524dW#lh*yDrcsu(&T@woD!Vigk?XWw{#HtVVNFts7;9U;98HcT}Fk&5CR zZCk)`;DzUO%Ya$jvC|aKMSx?D0O2xXpzI@;b@b#esPSzc7+Y^=hwn5)=pGFP6#`R< z?&Tn+cPg!<0N4YVEAI!C!_7L7%koYKjUj+7d5qE4068a!bbk;XXz4`r zCm)s{EP~NBdAFWFD0VU)EG)>o)4Euor(cw`VX_mo0@B<7@->)pqTw3=6<{NW-p>68 z53U^M@jU|#Cpy@ULE#TjiPyIWZQby{hPnjya~dF*{upQWwA^%eo~h&x2@8PP1DPm? zF@D3Hyvm>oJ4yyP%F3bFmaAk#BLK=lK2T8iGk_#{r#jfP(0 z__#k*$!N5M8XMH^Iq22wdY`w^xTwR<13X4NOGGc?t;lu-z-E%q#ekB|mw)?W<3hA6 zeeTAY2XI!J80o77elrN3g$yfQM@CTwlhyuZ>F^uHAo z4u*5&eLQbc4x_4fP1VcyhUyh=!!Z7hqxl$MmrsYbRt!u`(HKU{&Wc!bzfb?(Jh{C& zz4zt?ECAfO@wz@ILpz0@bH{s9FrJ6(4AJ*py!88a=(x}oR|1M|T|F9hqSw!E!Ka?e)SKg02-=h&2$(1-mv#X(7lrF9u!aNFgJIJb4r2Vu)``R? zPsjs`tVqpN?aQ#;V8?FAC%dW$vv#4Jgyi%NlW5ZbsR$n7ifk4cyTWyf&^rK5uVft6 zRAS1L4ECFv_T^vX^MeQQt~T+{rh^N4Wx3M=xYVcOVg6z`8ZO1n3K@|qfH}Z~M*wou zdU41F#z+G+N$^Cm7VS}|Y_&1U0um!oX}>%Q$Pq}Q0nu>-OtuLz?qPO|Y;kqT*Zc2H z*tG_5P(eHZ%c_^DI(ayQQh_`kUV1Immzqmcx4r?g{|ti{VWq^iEQ*_(`zUZwG)I$h z0+%nQYDCf028atnQ}1$v(h|GCfMGnfp2Ry6l}zMI+v&k1v`HL;Vh8;# zgN9m~rs}#bJVEZ_u0mQqoW-?(toEymb>+s=^kNk{$u9c7t3~rEv?PKqo(66~&SH&s!7%Q{F()g`^ z51?Ec1&YDf2a9ds3Y}qg4;Nt7^lA>7{J2)aq$>_5JQ}c8PoBXwxjZ{`HGj>&i!J^; zukt$`uOQPU9Ua+0+(lE{(jVega45imN*1ae%;`6(swI!wR{Q~t!YKLmokB5y}3BqklDX#&psxm}0=dfCRjw z&P|WiU_5U!e(EXOY~X|GZTAild$SzOoBOb+qIgUtGZ1w_Z=snH0x*(nDYtBe+a~-Y z#JaoCqXpGd@%ASHm?YK-!>LL@hmgVV9gQbbV^~#sk6YnLg46gr_ewG|N9q#AzWv5GjhrC!(PNU5u-p zFG;VY@tz|a5egs7{reQT6Qe&MHY!SAvjIdlMir@UMCNjGBvL7xZk z-SYUD+R>vA&>=o|rSta9@;uat3p=NLp%|!Yv9LWo?@6}J;xM?bn39r0 zL#>`9U(NO-KT4(q22D`K+bhvD4wZL)PQR1aw9A+D*OWhZch7$-v;M@tHx!;~3BI`Q z6-IhaUC%##l4T?JYSpCg++T18ViS=bkvQRP>de+cXhn52`#6k#4WYb-wGHR9bBOfpPP{3* zJ@0LkoYVXl`szVG_!>{neK(6WuKSa?R_kJj#6kBb)l(B47$SHK{BfCQbl%S)b?m1> zsH=a3^rCtM-Iyw)QwfXBYGDkYRM%tu)*$8jloKAM>%#Ylj;C`q9yI+LuleYu@cV;9 z=DzBGNWA-Iq+Vd21xxV-H4Hg>n`LzxR&Dd9`Hi2h+fs8&+D}s~+{Y*R>Pz9E8TbB} z^T%I1cT2u`oLTp2s&rv?ul&dXnfgGdPAy}rq0XE)37KyM?#6ol_PalOcMzu(r*ui0 z^upJ7q_tAwof_}Xvh}B7MWwUV6T*6Y0-?-_wlOVuW@w>6|yrR#FfGQ`bb2FeOQ|UwR z9xwFQb$93f^NQaE3CEIVqo+8ywjLUnBQ$*}qTJp}tR78;ozEWpL8VtDXe=HvFK1O_#(UhPLa3$|Pry%e z1sd9V+LqVR@TkD5`l>2U^nt&5BLm&+BKYO*>(+w6TcuZc2`zS)lk8?^c2?(~QYZky z)C{5Lj_0ZnD0ZXqo?m~#HKd)<%g%y{bxhQFv0Yre7~OR2WWK78Og4Lk^9JU_dcMB# zwF^!bP0an#YryHLhrHMv&XPKHq}=ZZi&{jsjRiQ4qM+Z>XjyO#mxw&zm>xNv>f=}O zW)LnMhAyKj9UaA(9@Ft@RNjNu&TG+`N{aDl$VWdL`vi>&p=tAe^1euU3gVEHw3^?= zDeR4de}~~_`SEgCyd6<3u1Y#B@0Tvu-HpIVMaYPwj{A?8vz%vmtibz~emGUolNx zsUZByT7@2EXUn9S)U6;prcO~mnc<)-0W!?FrxvlNBKdb#f?<+|nJ5oZdDU*rbTy`L z#pCe(B^pw#(IL&RL$>u$xO%I~j;*zd$BYA8q$!ugMy7FIEhYb73<6m@%$Od@^@A*@ z5Vuvo$b0!=P0gyNyCHoaLGm>t1&AFX)AB0;**`ygPu|Vd=(Xxc23ZVO(H7K59+mLi zC`QK{7ftI?zPo*6k?%?_n_6=vxPdBNx^xMRm4&FgFl|tHNEYc+|Kvak8L#V3ZE!T9 zHgy=-W9qz(9s~EY8PjCZhQtk$k%l*K*s=vt4=k_4*uq{ zdBX;Gn2c^h{VA2uS_)g|j`V3Gs5P76QRV{)bG`D<;k(c8u_&=X@==6-#}>_Gy*(<$ z=*qVP&XW7uA;TQ=fes^y5J`qXRjBU^oT0X0LG=sV*>n=p5J_Ugvh8Q4pBVj|557|Gb9bPutkY&xE?uUB?Q>05~U_dYCT{0t?N@Hm&r zF~&+-=$G*w=$BK|(&~R$f&CZ8Qq@d7clh01(al4TbS#RQp+`YSA$8&4T8n#_jU5?d zrgaVx1Fs~)xf!eFV`=FklyHX+9a@A=?88TnEXMlrK+VA^V({*UKqjV#O}0*8bX+Su z1;&3kBFK3jj#Au%mIQBJIazG^xptZIcT4_s^@WzE5K;)1ul3VQ&F$Zq1YerLa?54* zj0}O_B{KHq&D^Q_b1dqaaQr2J%&VEZ4`pIAJXwA4O!i>7$-B@UA~bm#CDLpeQB+kFDg6s)qfP`^d#6tQ|R5G?M6pf4gdU-ikO{v zol6{LNHH3^^Z>8S7DCJ1sU)r=GmEkGTTpGgm9U#3Uj&Mv>cQ392A7Fw&!p+1&;~H_tI6y-Nc!@R z88q*dv-`)I`;L+PH|ODO^XP#=wW&TZRu4z))&8(P2mYL)1XC;fXN44kF^{V&HM|&& z&7453dOv3fal%5k5ZF-*dU0rbQUJlqVI53$si{S^sE68mwng7p=RHD)HqEu0HRDWK zq|Qt8A79^8nDMP?c9WYt{VqZ-S4kfrd(lFt?^m&-l`m8!t?s{zaint5suAQfB9l4y z%rQJ9th-B*guTySozG%7%P}^^tOamVuX%=fdD7|p=6h~Do(M+kh5=~cWRSA6BG9Yx zGt0%ikF~X-n0s3B=x~36spDq;k>z3+G^ODw92>WN8fRqzbQ=~Hg821SUm_Ps07B+~ z+SbDn&H}xI8{YOpz~P0wTrfEi_UOPB!cy$x!b2-*gEn4}_*c29IK(!69 zsL%%b=uz>jX|K=e>+7G_Mc(tFPaKW`6*<^NtzUVNPPrrNlYmvwNi+e$@SF#q__4m8 zhmuSz7bw%cP}wVD&vcrnKmUwuh%B3Nd=y|g=Sts7$7$Z1(c;S~V`oWm4kBLts-$|u z^N5umWF{9YH-OGa8rnz)AFVqCZ6%x{-kic~6k%ot_{AHKOL5mO#^g=xMPJ(BK=2`# zaTwFxwQJXWURh~HZ%#H~7Q#sX@RiNDxR)OiD_!k;@a`E+$rlN;wK86V31hs`e3i0|L6R!{~QT(ep==wJ{?(I0x@Y0N|qQML+j_$jkB#r48N%al-yLrdSPoR9ocSY~K%qdk%UdBcqi= z(+K3LEznNX8sUgCK}cqnA$9C1GsdZ)hc2;X07sl~hEME`_y!+JT)uDBGh>ZT>&QfC zv%K-*qF;xZ(#Qo|7vnqcw7gzJE@Ol-Z7hf4i<$o*cUp`yGq<%0NQF6LYNQy6+xXhw z@MAAnz*Qkl;+C}BAo|;AG@fA*c214aqtc~e**0Vp%#O^=$K4&UY1PxS2OQ8Qd>H7N zts~vPAYr~_Q}|+2&c;7HRo5`3o$hyLIvQ1rgk}4oZkX4=psc2~61q5kNcN~fq9bKh z9D)eets%`wMKDaAsbnZBCRvkH#szcF{U>*nmD}-x zX`vy`8)+w4PCP@uLo%w0(a{#a7F45N@aSvp&ZwrKO%`y74_%+TVC|rfR5|%{zN>Bf zJvU?{OS7G-rXC}Q5@d`Z3u^(Y>w}&n%dsJhlJ&zgEQ(-Ew%NjDo}%-PkQ$)5EfAE- z`>79v%Git51Ax@T1XkLXBM!JO;06{ zgxM-850Hj?;mb)N1M@FG#*W&_6H!kbKPv61)Pd7I9I3f*TfOG;*scQ36fp2# z3G?^001ItKf$odZ*8G&)hyLb_2joQtc2-l6fr#*BjGe@D-A>_dwc+4U;ce0 zW~X9DSA3dDzYW>&;DC2#FAkJ`T}GF4k5xwhF@BX$cU{nGGSddrMLt}~a3`nlX1CPD z;S-M7W{?{1zt|$!^uvL7XjARzV<4V|6n7`bLw4=k%JX+_Clwzkp@zp#kH=AgM^#2( zzRz?#$H$6_CG@8c4aE@Y7GdgZMc0TqQ1pJN62A@((2dMOJEdb<^?cOeQM+DT(I5TZ zlzi&tl`6E|=`pAyj8X>gl~QRDhM#dCJ`|#&T7mc#rXzm()wQ5)XZKU2!?Rn46>g4< zK}Ge#bD+do=rrq3iI|Gl5*0#LP6wM8ikAoFU5NAgNZZYeSK82#(2S}N;Bk3}^%dNr zg1Nj7i|~7A0^9rz@W5cV@uVP!jZd)v#Yq%I>Xcuh4%L>d`$mf&S+n&EgS{wBvZlwo zNFUr`g^bmGqgu2A&I63HRKU+p<~Zyi`y@Clsyf!uVQP~*p-sibjz74tYVEb2)KY+L zm>y0~uN^*o;=~G@`pbq$$JaxOeE-=qQDD5B7&Xx|*am|cP3FbDnkP7TZu1v~$d$hSH3X@6GvVG_PNZ&7^y3M$eUC7z2iP?k zP3;8%Y{)J8$%%#pw${&m^Rde$7l>2vqWM&Ru&=kYtujHjhow&GBLzf{L&c5|0MLGTeruPpUPP$JNWdas75Xq-^w8&)vD=Pr#X8&Zj+hrqm!bWj z80fTrwVYLF1v5mVgz|`cW3p^5H7#|P*kamZRjQGi^kAvHb~u_b<{!H%*%*Y1kJ4q) zuNg4dm~>_|FgPG4_JF;Q9Cl%4iO6IodTAamD-0Dny?gSzn_O0_r$0*2V$?Nykit_* zsstRw&DfWF$6}gege0KBLIqZeDvs_LQQ--aazfa7da|MtjV^@>g>+e&D_h~+N8vQ`Ex##@i8qP9L!-PG zJauG|>eqH~;`9;5Og}18r4nS4VgH?hJ7|%4mb!@56LnVNKU4#GpP-=Mbsey8RX^7s}w zHmG)^?ZOW~N-j5R386$Vb7?E$LY-{Dx`V(GCb3fcwj)Raf*!3O$>Bh*yuUlwqIG|| zirhY}o+C(~<)-`nOqZKD42U+2eXyYk2FVP#5vk5kwmXk~*H2D2=CG|{J(oe@lc1#Y z3=9BgN9RD7MUp@4>OcVap!rg4h?zhm1i!Z_L;xyz7N z6;&Gr0SnmJKoD7auMmA)qkjkao0_UjKou;+A5!>^No+_lRHa%cxzB??+#Q^RjlBwv zWt-0YaU^D$GWFsIgM?OtR{6pSKqXyA1EFc5^!J_Toi%3r;w-Gf>-8FDFO)8AEp3fM zxRb0~b7^w4-^4HYOxc5d{k4}$hT3feDk;mO0>+Voq(0Vy3}R%~<=7e-)VDNtM0-UW z0y7mJ1v%JBxiz`YSf2gh02PsR={Z;Tp+n`nDM~FiHYCVwauZ)lnxDMr?JOIj;Dsuq zNYHug?WiB4U#BQ(i}Kgb&7e> z8iuOCetQIPBqnl5Mu&+SeA1YmQ|FYCWrP>SI%=hH=$|fJo@g*gL9B}|A-VgY#Fi2RxS*u}O zosKS0ZT6yxsnzVLZa?fh{St@Qx-@YyF-4&J&68liLr3r8ibuv%{c%E2G-3DJ6fH2( z=Xr98M*;>dh03F$B~V8Wb+(o)_sG%ezT+BQik8vhan~#aE}s4P-bI+>TS){jZCpJG za#q%Iat%wcjE#%y>hCWnB$9?8JL)y;q$#d0TR0bM9nFaa@f(ijg3$;@8)FAOTb{65 zbMz0}A`~?DVJw0^4k(%FW!cCZ7Zo?0rqp4IFbz-Q5 z%9ET9)tV;+0wSwlk;ErlzWZ9LfWlXGq%9~a*AJ6LM$8DNhgf$Cr=lbkMoCmrdFS@+ z0>DP>`9YMr(V1u4om^~}9R^UPJ#+O+v)b(dNirDI6D_m=LNT}ni=7*Wxt;1&lxUCk zm^yQzJIo!6{ps5pby%CEW?$ z{A(1TJ0(QPs0Or_SxE!npgf+LT;d>=%oe+t$Vkws43ymbXzYVj-{9EP+!lE!WWwCi zwnD(eIIwP(w|CU0mHFyoSGgl-hBhQ(69_l9?{E5yKiVQeB#_@~UGYJbg9IM3=`J}y z6A7q(CZrLwH2OZpDu7=FX{L?-D-JrjsTRj+vS{u?9&c<6!Xwml)i6pNBc=={F}0#& zuxVXS#v7^LjHt?i-q1o#x}%Mk(HEsxeLAuL0l^a2BY6x;NiIVE56a8RgLY@!2JmseHSJr zPQOecq0`eXQ^Q zmTeWG;Epv@3=YBtQV5Ndi)aCV=Ei>2LrXDbT|sc_AqWs)|CTuTYek0q#w~Lc=?0~h z@K2=L>-KohBu3vY!N|Y8GGQ&&+cjso{sjXYE*b*o@OJkzz)8YJWe*5^0ry{ox=yW2 zgdP}G*c|1pTy{@A^?)(t zSU;|hyQQrfq5;h`1C#dC1pwi(nHSJP3!s@;{%sE&57c~G_rr1ZMV0@gUU!<5yt$4! z1XM{M$@f9dh#9$KfqVG9JUuhF_u^c`F@AH|Z^NG=Fdo9y^p zL0q%J{Kyk|{GI|1{tX!5wHE2qXQQfq?DRsM`xiCvnOdBdGnH_fQRV94h}zl*wk?Bi z9MRVsg{_EKSb8rprChy?=fO`3A*V#euU)x16vP?J1(oB;Fd-=zMy^Zw{57t0A;j7O zvfV2t(sZ!>_@WBf$lYuAS#HKpz<^&x&Og;Sx?XK6||zRe{!DF+u%cJ@QHS7qy^bd zrKW(&@#g%)l-YumkaTo32+pf6g_QS>{2J_q%%OklsGHr6(bH?}ADp~4^p zPDNvjWg$o_Lf?Fx#{!)_5bqQLJ0})#okJ-T``1SjUceefsU=*_S(eh}is{sg!pDzk zYH||f+YQr23lzr58ZD$7K(J+>fGySKw`x}ql)_Jl^Ixnvpn+rcp{tvbQaeO3x}3LX zDk&7u_|l5G0aFtr#m-ZS4AK6*mA$ZM@q#byx}l~?9Od3ngVb4sVu$lTX(qZ0-Q_7t zaVkJ=kLYS0I#gq4u{D~qD6(h?`F;D04MSyso-W2G>_)S>*}SV_jg$q&%wZ7b3j~0%2$Nn;`|&)6 z>yjS9@Nv3Rn}%u{-Q7`qJUS2)=glJ{3GgDU3R-n`z$F&xzHj0|3X+0@kfkaTmKHra z93{Cj3UP<$^4FUc(@?b)!-!!eGP$lFS!>n+!RMlZJuN7UbdBX4M$NLOhHL6pbXi|Q z4)$<(s#Vb01NoDc(Zl`N2a`}^dpb`RJ7=BE_*5RSIz0v110_LfH1OdWkqCja)8HLO zyaVBXv6XbphArcmdQ5n?=;VtX>Amssu!`^^j1~aFovK_Qsas3n#p(KZmyPG&8ZdAY zdUEplVrFsTj1@&Z9420H0;cf;x=M7gKGG|Xh2&8~i74WpA5b@|6&Dl~;&rkmv3%-V z$DF#f8a%N`Y;3>#jox?7cOF+|Y^21Hcz>Xl(eh?xco1;DatBdSgwSzhbKoNe*fw{WKqC9{(TaQ(EBR>FVy26sbhWJ!K1p2^6 zJf?CPQ+@ixsY1?!3Q-ZSwa)nVt*?&0ZGMisJN9+m(~NcvpOsQ#Ie(1x+i^3BQLf(ZmtK04cCmWewjWjJ=?E0m5u}^*+fGF7Y=^`+p*R_DMz8WxVQr{ zcf{UE4wVyJIY~_qTuyo1$j*_o&E(2RR~A} z`iYG*d>S7axr%;@Z{3&LtBCqIX7lzkT7>{-LhL+0CVU$vwn@jX<6*@o4)ISF6%NNu zqook4mxCL3PS-{6lKjWWgqQsXZR)HvCRG{bjN5jZ`?q3D7E?h)w=Kl(@7)@dZ%$7z zrgK#Vo>mc7h#`y39Kt{;c(QXqTR2U&@RBmfLM+Bz(CIV%@ap>7j1EF!Q(2U4ipk5n z!szCkaA|47JxE1Jg%m4#-j0;Sq_G<59Q}l}jj0SEZ33&|@O2{Ncpx?46uaT zJ|>Ee6SPj}3gaLSyZ!=pRjTn@;8>{~zqITa6e+~3k#;ID{q33exG23>*Z$~7he-WX zoddVVpH3c+P3V7_ya*0>A*2u@* zbOk7Ade~t0T5xi9`@0Pik;LaGy_g;l>0VF@znF|X3G51{v#OAn#c0IsyG7y~u;6SE zd3ZtTR}3#;wRv})%CSC~^fuaah^FtzGEpKtDR-(S*9Q#%1)wQjEcw$1Isy1Fu_3t_ zum0?Tiw{Dwg`6)c-n+}o?BSc(~taxx6 zJ3kI5f51>7Qh*VyYmP&g5;Oo3He)4)?reN|K&q0>nqGm9`#X{*)J6)n?opW7sk(;4 z?#F^LVki{>Q-He{#>|K%I9o^;0Ba=TsqjDvl|-5iQ_khcsCq}-Zs4HHAekQ08&VP= zT6*C~a35`+NprZtvasW6b$)s}(F;xUKZRtvIX0+cfnJQZW}zfptm?dcKOK2c-1wkg zP{dJ-G_nv{5JEj6_O z=xG${E~<(E1R!c7&>Q7%&3HT^Y1~Pu=k_wcKjaJaiaB_7w+pKQpK!VN|jH>X&Q7u@5nvH#-%;2H< zTZaOAQ|LD(Y1CN}R>h0SyV!5CG>bN`ROst$Uv%OW6Jrfz zPS=eH;}KF-9dN2^qGOjnF+Hbpk|{)?=bbc2i{_ zZ(lXn)iUp6b@hteiMqb~X=%g8Jrp6Mc&FO$G$|Hv)UZv%X+1~vo5s%)(G#>QGdcMC z_Qterwb3xzV#5$%Ms<(=^#oRA{jYt1kx~u_7Cd4pYPv5`q)`53$VD_5)zxUS537!VPyZ zk_KCz(|vko9}r>N83vZdl)lihfSh*A@uzDk5F?&(wHqzCOk@=9SA{d4z z^8Horg6jDsBdBaihn@{AtJ*{a=9a0<7O_&tI<(N=%fgLr9~J4=X25*!F`Slwj6~5Z$5t9`H7P#ezaDN0^+Fc$RDHu@{HbwsTU-*O6*k{ zfw*OLvLix2HVI$>$k{kEEhZG4L!_X(YPP;kSPZfHrI&aQDic?#lHA9QIJh z25M>&4p;e!U?QmG{nHHkE&5ikAtSe#_t@KY2RPN8i3YQom!{QdwBqg@Wd zOs0t2{_~%^zO}U#%8)kprBDB5;9!g)vfW=;-i3Y5n^0#T%&+$&rzf`G#MR zyON**ClS3S6$cMu9#xz~FoFIVtscS|ihyue!U>w)P6rb4B3Cj8IeYHhId_0nBzebk zpn!#7pn(07?p#T7BXG{7#X#TNt3Z9&Ef(V#zmG(5y-Fx25Ye~Y4N%tJI4h6Awctng zUWh=ZBv-Q5Mlz1e0&hPZviAzE{n3*rz9j8|)b>c%QHOGb-WEWN2OX+<-a%KRj}=vS zw*})kn&l?l7&ofKfiI8pGKv}f#urIM+l=%{n}^ZjC+CX^UJ}n36lQoKuF~e|3!$%x zHOtxGNE;7ldt^v$zRVS6cq1D|1Vn+t&oH1FlSuFXV1$W1ma zG>2frBo|9Dr5`gv3UOOn5Mg`}OpDM0qQ`M89f=fEc+7Kgk0ekGZs^?*pg0ISrb|_d zGcaj2c!XZ#FyY4j9)_;%;vDeU0 z{)!>Um&m_hXhVJPxeFJ(Npyz0#vwQQa-rmcORl%*V-O{R{6|Ky^!m{SUW^F}?0B5E z9~fImJdAU(B?6XoNBqiqeDLd3y`ecJL8A#{C=W7KMkU^IomYD!f@3qe^#NKu>r3`fac zaU}%F@k(^(doJ!Hy0T!Y2>!yOrM4o>bU^FGiDts<1b8zLN~pr{hMGVDV%iCl z2xD15xjpp)VLHimmA2ZIxT`(>0bJtQP{Mfh*%MiTDi*{zl*OkZQsqUQ%5FVLLN@@I z9z=Nkyd}6z1znVS1K%cz8eI_12nlGP1lnGp?jl`NEf_}aiJ90RCR`OWm5{5Q54(a3 za0)+YVm18Usyg3ER}(zS`3!<909Ja^M!+{^Pc%YtPiiY7sc?$#&-#}4*KBhj&OsuE zDtZj6oBX7I?A2?)VBn=c4`CuKaR>*F-d!sQouJ+X9oU37xD5lLjQJ4z0v)rw;5rZV z(UW_zKw^7t0gHEVpJ504$>GR60 ze0wtlPFmblvmsFkMY}j4>1JL=Kd;o}7qkcZV$U(9$0$tmCoq|739SJNGORq(L6U8R zAtFvpJotuVQ|R?<-_+fKc;J-0(*0W}bF^VQYGkM2e7x@wFR42L0fjWm>}|qMR|h+_or$a+q1l50(Q2%q*Me~R*VhZ(v#kxa&ju^AIhr_Unco- zMzu9z<4%(Aw&DtP9Dmr1Q>`8X`*1sk&mR6nX%OWM(Ja>*E=1tyC8jitr3_KeaS zr53012mAb+@g2NSjJBT1G6kv>V>f8{oZx5Ru39=HP{xg%dss;qHJ*u;ZEY`&I^3AW zDvzR0!SEJS)v>p_({!w<_UJ+}nfs1(`l4q#4z4aFiWng1;)ybz;ki*v2@=Dyb!N5* z!BCwOS(gXCg)aG>rm0WFH};Tz0C7OZX$sJW=?*6wbyo)rHtDbQ4V`+7guM`Xkll7@ zuS=ir(!R=f@jyPc(w2yHWTVa+EsvO9LWhuAVld(by2Rpizo{XdI1gz!2UKYsKmMh~ zeBj5AW(sIX8m-&A-m2{|5eHjAJvs$Bgo@Ve*^$fabOZ|x_Cs89kffNXRrKNAB*Vi% z4&no%V#vS~hPg&-iKames>tQTxX!Y;ejFa58H=OQ8-*xmKQ)GYlG)~gQ#ZEQZ@5kP zaW-{nz;x3at+BD-dXCoghT@eP!H1f{j^W7h0F>n2jjt_vlwbV3lB7^Ot^UPxGk7lGDj!qGX zVbZklWpfu!5#9lygM9+dqG9Fsq{kWJtj6gY1nGfFpN-VMd1V%Ct6JOIVur1TZBV(C zUXtI|*xr+#lJ12JlzIY=%v(|a0a{8mFM;T#f-5&kO0nAfB5O%L<8){)H`UtPpiBP1 za|>%=+2pcwRwp%F>!Oj^jGLUo56&RYUL zL+j&^&!R~vL^E;yMVg_S9<__h%rfmtR7z`0dJ2To#Z(C3S(S`kHF-ik02C{U6TlJp zSGdZ0Z7}!A*?1nRubv~{FL2S{u+$2L_*2tT7H*{8v5r&En3DB6$y;KG5_CkMBB%C6 z)a=aT7=NV39Z04fYo5qHN3GCZ_d*#3iV9IaB%oycoSMI+Lx1JnZb zMeKHJ^BqfF(ASd#a*RvVEmPhFQKcTxR;KhBx;((RG0W^_K!_vLj1-JMAYS_vCXd`4uPVWI?L(A}LAoV}jCuD}rQe$qi!<-j0_ zdWYmn49pN=_Vyg8SzMup)_9z|q4o3Vn=j~uK!mFuUC+Y<$}EA%X#Xh*xT=aG>->~g z>Z#ZmNetp$1-(JW%0DF9)4!&zqA+q^xk9;V$S7BsC=7PnHlzluCvhF}7ZbnA2wZP7Mro<`I1_F|+GE!904nu~ zOs+Oge5f-}rSG?qjIz%hdAUkdd1}Uk>HxzRf)&| zPzN>0GDtE%e*V1d7YF-`%Phi8cwt%clg`|)9dPqON|Z_iPCsI`s4GA>d>TVDR*0F6u^Z<9c{PUx%>YAd zut9iCbEk$Yt3yyOQ>zA|;LN*U8mXihtpUMA>WT{_M6Yo!^5f+t&K+&Ijy&Sp`V&2ypAn6say{^~!LzsO8FEoT^gvY<_CHG6Z6Cx z-(t`qPQW#N4HUqXPa#9mLtV=R@X$U=XE7SU=013I7G*XGf=QW2AqI>SIVgakBb1DO zPfh8e7;*xH=w@Vp%qH0L=RJ`!Z9pA@EBseOE9%1+$0C@utUh6L+A1Yl&*zEgxN6oOgci5DVcZk_`zLOOQD|JgDD zp%@_Si9RGk@LB9O;YQ3h*ejrw0rr)>YXRkkD7k%RGKb^dm?yufk-CYox2?~~p+o@@ zX*RAXxv7_dm zBW_@)#_tk2dZ3z^`er<~sk1d8ZusQnoM(`?#f(}{mliGnC@oYvN{=>P#Z?2U{1?N; zB*+c}pv972Plz*cUe5Loyl_T3WWogiP3PJXm$s9m>ONSR8UNP|CS)veO2f|J-A+~D z^e&6ccKX4JOk|q6dVEL;jsLMQhwsYYb(w7!37DJAYS`EaW`l}L1hvpyTe@KRK>WNB z02k08Mk@meu{~)-!CP#1Axk69%-^_6X9Iu+W~}It(1U=n8fGa{foBIK0ZohF2XAbf5^c9M3Uxq|imWR{`Cnk^xZ?{ZK75NH#} z!NVlQPc}t2E(wf=yBz48N1KoC;t4t#31ii3AcRr_b2ckI)!_yyK@}5bF?gTO-;(uQ zo7ngqoMuH}{|BB8BXlwQ>7blg{_~;)u6FLb41y@u5iCM?Wt4QT-QS9oojs%pzry_r zzBoH6fU0~`>fHjX9g=^u_Tb%A&=YOV+@(T;^B*%$0TP2&VfHr%dii?OpL0uDZ-Lx) z@dSxU8{tim`~Lx*@2>B#nXlrnEJnW&Zec?@y1mF!z35>9$9+&>f247tPF1@%9rA ztmx>c+Nfe0FmYUYrOmJiktQ+a_xOg!=EeWsgVM52H*aC#UJkYDo2( z(QmR>rU@jVOFh@om`4f@{WqSYaiwBa3vfcmoImfU^lU_LQ&wx2mzP%yn%#}4UZ&1j zVsJ>=E@gdTYs`OZ@7lwuPS>_}vukRk4@0|hXryGCm1b8Xw4{;HfrfM-Y1HhHoKkAZ zshLSAS2HQeBTkBY3;qU7 zXWa4UmJ~Cqjx35`xcXW+!1%fWC@{zv?Dh^`9rR`&qN}37!3l3Y(qhxP|7mA&8E^|4 zeC+?_XA%5mO@Bui9Sc1O(2awPD!;>Pj1O^5dv(U$<|=6eBy7@>1f!VzuhU`B_)YBx;K zJ#mQ52CJ+bRNA6q*9S(X}G`*&dg_=w*7##>Z2GDKa!Rx*(s1YbTa@$QN zWdw9Gb^4GRNXQEt*Ol?YX5x}P?>3W}#=&>(z&iUuK>(`{kD1JoA}(7DDvOxvU?8q( zN~dfc$^FCn626!3%Cu=0v_7ZC6p?M^ePQxyQm^eR)FivO>%ceUxue~qM@bl*^Jpj? zFP{-<({ku-ZyffqpwErX&kP28m z(~-nOi0meX)~Nn4RA7W7l_ODWf~AO>OXW`Eqy- zUalBflmycyh~Z{}H!u>~69?v!mIXA~dyLl$QCgHs5m=~rV7QI0L*FAg z=liZM5t|&|knXCF2~(yXoi&A8NZ@3NBtlGd z8>VzGLTBdG!4Yh041&7Tq(;z$Q#nkITj&tv4#2+D8Xq4ZF=NTB_gDlZmQT*{BR(?) zRl8)VWra{yMX&)0obW&#U{jQQ5Y?!<4Kw(W~Axa3Y=c+N)_(R{>^p6Nvd#TsNt3I2vu|LO8?0MqOGZV6nRtwM%rz7oek*zbn?zMFOPuLjxjRld8gf|{vc zpvz=&D_sJyjE9Q8QuSLISn8CPv1F#6o+Rmg5`waz%|AE`6j9=Md=QMoes%}ipRPK- z$gd@k!OWE=!`~?J$Qo>u9APM$L<=rHP?JZf!kT(L} zVaa3x-PIWU0?Jd!+D^K!Bf`WMNQS7(khp$Gss}t8w~{Pbbo3Y0c6v+vI{!|=*V&%@ z4w6gvL*)gC57}~QX{k&8JdCe8j?60QK+xIOG=oR(H#itOwFH5r!NsH0kkQKioJ__n z;*`&YBW+wxScMQuU~M4GJ<_u{d3$w;#kvNyI$iLq;++?fcOGrsI>(ZrYEv-!L1THt z-R^;Io#<#{+H)<>lLZ+h7m1vz+nicr*5Jk$DTHSJGP@wM^Z}VjT?SP28EH6_hVGx3 zgq6^3v9jMg=@D>p^#%mjHc~%J4-^Id;day9uIGzAvt0SF8nqE&GqaW!-JF4RINXKoxG?$--G;E|$#U5N zY(9g-xe2NPtc=Wx%t|iUez6;fXj3;!jdbcL)ePu+-mDX(Yv;4TXtA@mD(oUA13tQn z7;P#WF8O_GR%4-UMDzv3Bs=dn<=Z4s~8;qgTV)m8_9t z%{o(MP82)DC|FGBeBZn4zr!7Xc#^D8u`?qkjwJ3N-zDsS>M}y>LHX!;sug_s7GpXZ zwTSN^m%sFiX1iRRd~!c)jf12nmlAOIL}M^0K^3i4+Du#dluVy{e)*&XV2oZ$Z{9#sqZ|B zF9V{yxF$491Dun!WH+a>0;6Po2|&GMUOl^AYzW5Qu(L-(LBJ~vX=Xzt@d}Z*h-%DH z$k`na66bI&Z0=&_o|uU)0`c=>0p#V$?}2IJGu-<&v7k_d1Knv=u;RGuC0HpFdaB@Q!ndF$bMJH&Uzmna9 zYo-K8dMjF}Pojj}iDEVzn2>%!H5k0))Dn|9uzC}uo*Nw;PTtn&AhU^ON9J_(ZN9O6 zh4Rd25p^J91+tz<{Kj^fULd9b zRa5&9ghQ1XAj-Q;`&z=m{hRofVdc|O?*p+I{zFc#oo8~AP~y1zS0h(G#F*#h<70RI z;K?uD8yBNNsuDr7whhv>#{#=Wnc3Ovu5aPj3z>6TanDW{OTyG zg_ke)r?6)*wSpO6(2@GFf!p8b&A*~OEf}--?D8=AnbA=w7_>z!oN_{^KbMuE%$G^WJ6i_3G*I#SC{!`vvd<*GEL% zYs%xSPNJ^!^6Ip`NO#FqR7FfoAHYXOqhStLxscr0{TxBRf3cx_nssgHQxt5bTNfdV zFt7K*aT;fZp?Cgh#vATzIsr7A&r5{iSKn@ftz>OV+?L!C(b1nH&}&Z=HlxqdrFwIO zfjaM7s+(V)`Ee~ik$Y7F65FPLKhmmqBHMF|GyrrR?Xr`9v|75+`EI=QAeK%I)6%?^ zaAu^$K_2jBcqFiEwqgAxj!l5#ndh3WOF(~qQu^2~YWnA%)_Kj1rU3}`$ORq=h5?T{ z@OX!u_}`sIDNd{gHXGTU!31;H>30lBwJ%&ip9vIFT|ZKHped{-^U?fr4|?EOEOWQr zqT2I5XM8GB>wKdwn@@}*=I;csk#?_`-h?1yBcObT>%4sPTy)~OK zQHRbqqnU2sq_-H{2Zht#_IX7ioWD<8|6DTxmnZfdsoo{r)9lg)evKotxXj8HpN4{L z%X^O_Hh@r0iis#GYG3+)(RAZky732W@nPJNL}@>VXi-=7;g8j5VLQffr9=$(im%P# zm{QLA`ZFz6a)B;CJ(Xa8q{mOhhJfXWM=7Z5P9&iTmXCW21di&KIOKVQJ#`@oPeDUh zdl=(d+mV-0kV&#=G8Gs)H`n#z7cGr{>vO_xB}xcJdaAqx6*jaJn-%Imw5~$1=f;(n zLa1`bDC6l4PlI0}x&rqKwA%9tGMXOP&{<1XIakF1x42>Hf+yk?1g?d@%D1=SVsl(A zCRAD>xuL46=$7dk`&`hVeaS?xb;y@raGszL*aWnsmVjTlCgY&;FBpv{cVB(f9a6%S z`-YnAo zZbp8ew&E6m_f+bU7lO|*AnMIS5Zk@)_#www8}JT6QaV-n^Y2xFRKeQNOExpE*Y?(K z$}&WTQ-Cins(j*;{1d{iXe$%? zHb=?HDKBN$CbSbOkk9NuV9ylv?+kvl4sY>haVr98DE0{Bm~*9t{El?gI?{S3LQCYO zhEcK3zBA^6!}z%7 zoc-h`fxx^H<(hk#@BIW9+u&;752pif>6qW{d-l6M&L|1e?UH9;Ms!zBA0YqwKj8Be z2cl+sU?b6%j6x+b7rX^s-mxfe`@Qllfw9t9=9lHhMePArkJF&{d!X6~q&|$jV#aKk zw6ru8Wcva3Q#Kpo(v_HH%OB4(rj{NV7+oCc*+naV+c77^5c*;YF!P5O)%wWFZ9_VH zA9sD{si9Odoe@;$_C4Ldx2=@@is?+L zy~0BmmdQQMC3osxa@>}{9EFlnGN?f}Ai$gl*loX1&k=Kmy6*!*O32YlRf({4^U<(< z9Atr(Us+Rsr0Y9nLm8l8lvQ2zE$DxFtQ0Vpi~ zFgov@gF&}k2HF9bh|4V&A@2Xh6u|Zxobqc+g^Q(ZuxxY$T^9QnwkWZ;qu+b1nei9#S-Qsan=w3huqY3cv5 z!G6+;RbKokf`dF__eQF%J`ES_Psp%)>`_tDDt-FK>;LKBvG_2!SUK3tf>SamPAH!! zW_##%+R}gbM=FlY{1pB4|IZW$2w)vFU)%Yv6OlyfoQ3;-ChC61Rn-w zr6g1*jQ=jD6+e}vpV7LzVL-5P_}HId6vjpjgD^H>FbGQjqV+Ovb;eq!<$Aa6=$^J# Lb}Mtf-+uTXs5&*+ diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewOpacity.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewOpacity.png index 158a1ee6b7d8e32a2a458ff6a3f783e9c748d6d2..5a4671cc0a4a7f4a6c63dcd8192329d539a45fa0 100644 GIT binary patch literal 25349 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 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewRotate.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewRotate.png index 6f799ad72c3bd36c2738ca6916ebfb1651c5f5b3..a0c3ab7d20e6c406852c8c64fd1947ac9acce115 100644 GIT binary patch literal 24657 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 NpnXZJ=ui8<{s+0Z=)eE~ literal 28736 zcmeIaXH=Biwk?Wz8PJ8INLDbDM6x6Uf*BD-GD-=E2nCWQDY_7eDgq)3OE4h_l5<)D zrIJJxkW@%Xk{pZ7>yx$j+55bE+I{Vuc3XS*hwFzdsH*Rq-<)HP(R&|#)}2!))mT?> ztzcncVLftK}2dGKT3(BjqjvB`PiGW@u!aAOgE9F8bhfFE1bTktQ-{d0Sp*KItJo11I%{jWbR z7`;qMk-VCz{~$Q{(2*k_ek)sr_iQ{8wAb3s!J#D3eI%MK*VM$sq%2%UY=f}&@1vum z;*Ncd52Y+>yz|JM?Uu!R{k&>wYGmy?Px0)v>rAF;#c8u~%UJn&d3o&+65^(P!~F}s zY_aycJ~PFn&CX0k$H$ANUHP{5Sg0s(|4#Di0|9HlKVQ6fG5$f_eXV1Y{W*n^PX{#& z4V9WTxunX=%d483n>z;eMLCkqD*0}6`ag-8&cwqVKeLdwqb|?Q<*~e@XoOSioma13 zZS3#z^O2#uN;G@S%_s%Z9U3eLi`J?8z4I%nI5XRbyKQ^DlUtUHi>o!lrH}KIZFW{x zRCKgpeM3WZVxo8eulxqwaK^pt`@7qW+Wc92h1qe&Nke?@x`wpLA~kz^dr3*jRb3Pp z7Z=7zS9PLoM}<>=E^|+0I+dR1#B6e8#Ao+${gfo1Sg?byfB7?vYDarzBtLz;M5Z-= zCVg&0^-J|i*FOHaaaxA!#L&hHvTQH-ZcrYQVY$$!41ZkB^khu9DmnBu%9SK9#HC^m zwQO8~=d!)>EorOm^vyL&-VqTI;*Z(z(c>ZuX}{?to1XYmTg$;C?=kBtDJ>l^=!cii z|NLE6^{~0Qd7}-DH;iFc8NvJR8~$GKWsixv1}E#v4Z`oqC#Jn~PvVIl%h_&w6dum_ z{QMZ^obSn#CmLE>8`iI1FHR}(dleINC)2h=)x@OjuF=cvY>L&k$xqkWS_Q zLhETr7n>dApG$11%W)oV?Vp?NUuK`%ot2%<82J7!kwOcQbs9Kt=jgbmlVO!N(VoEw zkT)Lv`BmJqK4n3Ev&v*+!qkr-b>rH2gTe2&`Bjx7^S=*w|NLppDDh8p=&gShAJ11~ zK_b%*E3!*S$X8$V+(3E9zUI58a<1dn)8E&4cuSa7teT`S`|12s%>F<*+xDV&Ym|9} zwBj~?=4EAc72H@fcw2e;!r@ZaipO&6#PY_CuQX*z@wtsu)n(XFt~yGQXrCRYDVOBA zO)VX@!Nermch@-0jMg#=m+||Rhs&s8Gw�i{*D+&Lh}4x#!jaaX|~6gC1E~S*>5s z8TqF-U$dyQm3JAtGTmdQ?At)iSInCoO)%rLkiYk1c*3bSwN~}VFcvqE!^l;9tha%~ zW2V-F_w3oTg1g^UeQjvqmUrm!4mEOj*1i#k-DBuJ(I)HAqdh%pG-rS2%o)EHVY1V_ zQrv?T3)_Y#E?HWN$V%7c?%un1Z|g)yB>z1bW@I|%+iC2ZnM8-22EwGc(qnd*ZEkuE z?_TywMP}ozxoHHJad*v|!B$_f)k0RT%}5@M%#H}|#BN;u_?Etwjr%#o49j@9%Xk+d zRtIB^W_FGB)9L-guV24z4bii@gI#e5(QjvOU;M{Pj@HhqXdA3&Ss;e5tuQw|JorUZ z{gAV>bNleoW5*AK>XZYc!RQ-SKdL`HZCvEy`c76 znx$U;lt|;X?l(4=nZ zd$MTR>LPAxj)ryIvu9iKCd#QLj`>qf+DR9Ww8!k&x^-*f#SizLhCXa(B+~dw8?}c> z`fJK|Tyx2g()Qy+P#Co=Ha3>AOnH1isT^T8g01z2JA5~Y_-v1K-P4yh)mPS$J63OT zu%6pQ^2uKNUG7s2^bI_vZC3}M^t*1)o9U&}%4O*KyN@H)7598gW>^?a=_(ADNrZW* z8oG^sT@b1Ak5##qGdDXGRw@u8qPIaxN=hb2RZUGTXXv9)7=u1LmQJaDabl6ulIKYI zWI6+tXPw$@$?hPD{(8L3hNM3I9U>zB{qD?cairY^MzM&syveT#ss|4qEF2_R5DECU zWM$Q-uV2@TpzSCsaO9~=)QC@SBN$H-@sK>H4#-Cm$k?emFxrW5y^ei*K zl82sRUVS}28S8QB^5tE!W5EfoTdlHt(>9k=zrH>@*m85t1|ADb%U)(hP~sQlco$}$ zh^zxDSP5o8q0<66wPev6g}ZX@)8m8tjcikTnzCh-CVm8YQuNZU+$r|uSTp&kyEdV6 zm4duuAJ51rhOliHYM0VvXY}~DO7{@C=1-sQW>&uIz{YnEHEne1&pjAHQ7OWnUYBfI zzDqXEZEDP2W-G4{Ds5QlHmlsxn$1Rz03>0NE?TF!EEE+L!{~EckA)PK_;cU3Yl@1B z%0RTN2;fz6bvn>ghuoV5qcGlbbMUW~Vk4ZT9m9izH2yW?Cxfa^pFO*tKHV;}GT`gi zudEYUhYlUGDHTaCLdux*J8|pnI-6e6KY`;sDMk|?&n+D>+fmwD>`NKz`L-nVR4+YW zxer-svk^BEZq&o18_}t$QYRyo6#wiP8yQhSElf*GyL9D>pP*Vm(TAYz)Nvb+(Q_VB zE!CPy0k=fN!~(HFR&{d8BwZW*N%FvoyqLMp=)e+1>oOGNFYR~jkN2y5^Z4=U>A@xZ zk*FAN)#Z1JiUyFq7n_h*Z`Miql~8@73qz0S)!c6N4tgU2U-c6HrB zIepbqSD$JjpXd6WJWd#!Mr53$>^F~C}FbBUNo9~Wz(ZkCV z{T<-R2YE^v#dF!H&5Y%Utjk!>_mdU{wTL!-VS_9?6(g!JP#8&}g^6+H$F_S8cT}1M ze66VocyDp$?Ad?h`%z8{b92s5;w2d7y08NAg=CFLLHC;8Cf=RSL=G3XZ7;h}7B?W3 zEd99b5l!VPHkhp2q@ARU4DDO883Vbgs=l7Hv7karEm(nOC2;Fj7-fHO`!o{LyV16BDy@qwKbiUg1U|C?zC?0w&f0 zt2n8f6{V)7nFh>^rRVdG)^Y&JU;l9arijaI!5O9Vo>pkSqIw2kR{oiZP*r zp{1-Q6oe2?{RmMmQiL z822@1-kf}x;Myicu!2&cjFeO{9;20D)zA7XM7X`Z33&cIYPCg(fd7Dx-K@pf>kOIv zi4#0>S1;zg-1D}P?GA3v^8Vq!j`9fSw0mlNXgZoBoW1K4_v zK<Sb`h}DbKY#_+%Y2j=Y%}-S;YhO*e`Lsv;R<<5 zvCsPXtSYuL>uTG_TdX|A#eb+<#K*;v;PspArrTAYh@R!b&7v)*=~dfIXC_An#~P>{ zjn9VPq&NGpa!aci7&QGEuVZLvh?)IeTwkY__sv%En~9bUX`V(6-rjAcp)@3asS&Pk+wv$->05v0BBBdBabRaL5QGEvmiPti2u?<=JwcgKzOP zWzQE8h^wwNaq(1bxHv_%kd|PW%M~hWz%?>GS&cyT_xHc~h^GB5ooZA_D=jUhl!l0e zO<6n>6%s~7j=J0u#)HotY1!zB`En0@=mwY<_41`zL2c}tH|0E~y7A&?W(=rEYl4%y zH|;w=J@pJs&`TG6p&qGJlFTVT(-eHQy(~=p@p6#Oti=JFFJHRkZBiPto<|<#$8~y; zwkS^v_`ZFXLx(zf`*nVId6LAJws9*awtHr5JSH8eCx1Ri@L zZCSsu()7yNbLVIXvlRiojhPaFnk7!Ec;YTLS+5JIE{Rr6S)P=RiYLBL&9xm z*){G2)`JS?czAd!3H%-brZ;I>_h1(eR;ls( z&-1I#g4V05&*cpTmyrNrbqad_{8*@|m2@V4yD+ywIkJ+LX4$YJAUHIX=CfXCseLYj z-!pK{=$FvhfLDrclOuQIotvtn)fm|E8*8MLsLw!$dJ5Lt^jiH{8Y;GCqCT<~0mAP- z{@q)DCBi`fPh*ZH8Z??M_1Gy}gLNn5=94{kR4b5 zdY+pN-bVwxj*5$mtF+xKFVDbE8?4cz78l-JCSha&M0&7UKP6w##K`IUA5NeI#%&*i zBcELV41fvxH8 zat83qbtQssIy*bx=LkjL#R4T{BM~#k2N_|sRsD5|f=bX&oLX)yGoBb~(?BE+IM>dt z%E5cWYF{HStMR*~&?m4^&!Tn+c;!j7Ew9QI3@&K7BHRZ`F)=*jfz~9M4AUMXs&>v>55)>fmYOr!{y7J(o>Qgg; zbA!rz4({I^J(_7$3w5GWY3d3Pc&k_C6B(*uS%jiIktzt0!pMPYZSZkW$;#;rgc&fs zov>lfxL3l0-C9c11Mi$VpWZHU;bPer)mSL*Nq7P^f5xFu@W>i|B_4Xpg9jz8PHr0sjxMuP}SXcE+ z8pt6VxD(eG8e!QN$H21N+S&#S7OlA$kPjBMsQJsD(9+`c{F&YikRi3p*Vt?FzENtW1jHM+wnF_K%!++|5m0U2@5-uNDxjnD9Iz#&{b# zBKihr`aO&q5e?gTZ%}T%(MjI+6G$dNN?N)k-XMFa>l;FapoHIC=_`1m-X4lNs1_4w0K{7Q3zh zAL&15_?O1Mc(E;SZl<3G4ThJRJE~Q9LQ9#tn>xgSFrFJ&Gxs}Di4gisxh2o;PEJme zU5N{ft=tbRSp52I?24r?2oIVV?pQWc;vuQbtklc0GZ%(bLGBLpde+Qt`Z9QifyCYqJz%P3eqlC?6%#OUV9V`8>ICN>+c6r($f*Y zYU~^wYQT48kEDd~Cdj~6`?xa$QmSSfl@pSahm-p~z}V&m!x=GXXDDb;nzg>v)Re^O zB$u>xFWAj4&AEid>tG9(U@_$FjjLV$XEONzP(A-Y`LUxkmxa3xuR%oym7Jd_r$J*p z{2@I(J)*~xiFzz2aoNIx(wyV0p&z#dw3l%G#ly`m{l&+`a?e=z5TWqFg9mgt@{bf`Gh4O!x2X(GulnRv`*-B0&8PF){w?39`}_78diG#Y2Ko6G{66>Ow126_S!6`um>WrM)=>s_b)Wtzti zG-mV>rP*U{wk&WF%k?U>Med?x5G{-dYKa%#d6E1Uot1Sh=j9=lp88ZFQG=`pa(NiZ zd}57I%&z1v#adTI@M)GIbnw=_82Y526j*DF=1YPTD0@Oto=GGXCU!f%S`Xzc@}gmK zVxk6i&UyXcc%mT#DS1QMEAf~~n3D<-HQ3u+9rhNrJsK3Hl0o)73aUUt6(mN2sM^O# zx+kJ~dq%F-M)T*-pA%>}UL84m#(Ekwjk%&R7* zEp@3D=M)AC7HR12&6-mHOrtgDx=7HFFKfyY310@!SP=ku_F{%6rgd`|M3|>fHxtUg z-|aY38y4`Ag@v!?%ag@V58htUd}F|wfV|`3(&bNh7(c$mXogW7(IeSdv3Rz6wET{G zgu*woI7G$C2nD&HPg}5POPE#O85=m0PpaSX&ghQMZiNzJGtyZlrZ7vyn(2vQHM
oe!hsU1A{?rl8w%lc(&`26O^1Lr!K{v#{Ja(>6IoMXDvckkX?5vgo6FbJtZ z4T2o#WBg9AAaGFjRXl8L$ou@G1y%H+2cRP(Q;DPo1N0OPGH8sy{#Z#;^Kg4Pcj9vs z+?2gyh}#P^oRpX8mSxJ`M9>{}4iz&BIA`Q$zsjt{{#=5g$aqGO)kf-!$bxTR>aVP< zU9c{7$z~8z(3)Z0F&hHO2qFzpF%#WquGwHNZMFm0qi~J?JeSY4Mseg+yRAci^OK1N zT~&3hmRE6cJdCgc4lpmX=ABSW>idsueMhKcS)xC;j4BdLYVA@4I_SaOf9QCVGv!!h zd78DVioX1z=XY^&@rdOkU+Jgs620_`p-gw5zqbXq_d=F^K8pj`&YV8I_HP`0o8NsS zFE1}eR`9$4DRS*iDj-G|+PD4r&Iag#G;azMNk;{#Lv>ne0n2&e-*^Oj+QX?83LB&& zE6w=c{B~Rs!41r%Z5>BKedkue@2aJmKE5+%3s|m8p7^GDMz#85un?ucId=)I0?6*? zOTkrV{u>P~vSpxPE}R=@X5`z>$uGxs!uUV2id#(I6xOz`XM9!*y; z`7_}wdDLm4?kQ!S-rnB9u9uOd0jQtnDu=KTXw}^Wh#YPVNB|tc4>)mfg=F-^1pB1S z+l&mSy3!v>v+kh}9(bQYKUgZ!{d44&N%^OwI8tQpE&PAA=-Ste8aWJ?%g}fo8#M;X ztvm=9o$vASFwv|cyG`2~=+AkO=`qK2_Ex!6;v8b~0h^ zT0!kp4zNE7ACkU_kiEd85p>u2m15Fni_G(P-E zuNm}krBXh4kop4@Ki|12k2z{#$6UhPXnx*+*$)&T(k6!Vyk?IPV;Im2H8Ao?+`-xEpS1{G21hed?4hZ5WX{Oft9ZhMU1&cGbr zcC}=={$M#z^q;R^@3OtJi8`QKdSlVD)-@jE2OlfCIDF!F!BRU77IRQY3lAwWD_HRz zk(%+2cXvYsh;EK%9X~|aJJ{H=@C}GR%Qm(8aqY`qNWFxc>=-Y(CL2%xMtac+`LoyZ zH^2S6?;?C)GL-F);s+(@2#XsV00(Wm<7@ z@qE<+%b=UZ#l{K+@W`#j#^JkH`?20duvPa<_zq+y4v%RDKcUuu8>E|+Wp6?B3(`9& z0YF*Ct_LE+k9|jc31&Sg9tT(RAiegklM#qN)QEk&`F3oahY^9zuuL4x+6E=Ih4<%< z&i?d@MY$B?E$ECmO|*qFzL`bxo(65fN9G?2zn3d`+V285_d$16&V8~va1VA4s&4IV!~rc+-$9jeLpRIvD7o(3vslj4&AtikyB{X)b_a`FBKO^_bao zR*UN9H+tzMVjdHv?xDNXGuM|?X!1xGRa%dnJ9ln+C^X*>lvS-LHB%YyCxGgLk$h|Y+Pe_m{NJZ~ zMT1v`h#47L_e2LOaou}ZS6BCtlV0;iPv+Hgv*@QUUv5Xr*c)|$IyXB*M#9>8O-Id@ zU)Fk)tym=YPCI6a{fy;UeI!=6Ox|YO$JvuCER5o{!5W|F+@BL&*8xoa*NtIPmkBa7 z$YLkmSnPWf_eN5)=glOeUo^w0o}f7rwSNJ|aJVw?{zsYLO_6O2X||dAg|(U2MlTm; zn*w}GB&-)7@LwViq2e4g4pzGLIWT6C^v1PlqxqoPcOp!H!Hb|DPPq+V!@hT#o#-GP z=%0>xH;H7YkvrA?2Z!N~aVar$9nphP1zaUO40#H@rFJjd59ot6&}XFy!JUWMpa{Fh zb4cUd($bRjQ&9V$MAAs-N|r9u>?F8B%&@WFfSqyy>iH6ShJ&Lc?Vg~Tac_Opc0FV4%W0 zx+qK}R)@b`LkZf60>n?`$sg2)GA24cuI@e*BzyJ8;Yb>!`ie4V))I)6lWyy5yWgaj zkj5sAG&`J_CmkIfNqJ7A(woh8me~J$Ki>a+1kWFy2aX&$A`DH25a6VwJ%ni`Crf}w zD0I3l%;S-PlFg2M>5Cf7oZuQk{}7rPxYPD#*WvQAARXXQPUYFbz^Un`U63Y-;z}4j z%BfqP60ngCRCU6)Cf#o4qs^*~ZS<FGNCSvnaqDLe8(M9$F1~C=Aa}Ad@E`KC7)BJrmCRO|G`Td1E9DaU@ z*}p7+K?`KMbQ~s8wb%agCr1PQ{P3<)lG6Jezq7FT$=sl@vtK*t)`Ul)U0l6gJ)&&- zLO!TVgzM0UhsqxA`L$pTKFH<8(2+P3@+L30ul1`nbP*xlVH-DFS-$!8$}z+I5et-6 z{>+$rzUV)^0oplEHm(DO%ShX3ea_T(!fy1eGEItPJmccy14u(rE5T5)mG-6P>(^pR zpkhsbct>k%Yo+wr+}^Z?*3kTE6Vpx8Lk!}uB91R0s&MC-p`oFS(HcDpq|5mNUQTs~ zeyJ9~bB7QKfXt->Vg3QooQUv4_#28dDjJlJgR7stRF4cmQud^v%et{P+EyKk2{;sW zCCRblvH6p+?lL+ecxWsLtrlXlHV`dQu7x&!B%XtDwDalxFQOqD3vZMS;8Wb&neQT# zWh>fRADLaaG$3w9X4_wUyO5U3`2cbOl$P`#&9DNor|b!gqkEA?K2g`eS;aicNCGL7 z`Z*8rWMtNuN5uR@IzMOV3^N(Fzr&UK6!U_rmw)>f>iVnh5$gf4oAW2z@+7;i&*Rzo zQ`w|_s;Xf1<;#~MW*#$le-q~^fD-ydh2v7mP$0|kW@cV374?iDeKZCc=#!MYR#;01 z@$8%tS%W;0ChYk*M+I7u3mXP!D5b;<@zk_O|Kx@V>5b&o zp)x~nsBS`N@hzRPK@-ge*K8he4pbb|J|3y$_Ng`>Q?f_74{9+Qdx!NPzqZ#}ZlXS1 z+4b}1M+HR_&=PUzj++e}w-6(Rgs>%oS3-8g47oE*+yO*r#=U3Wd-4|D;MJF_=atwU zs?u1bpIJmo#ubzTYvD_{wwx6>0*b+(kX7ZC04K`^!Lb59D1f(12$ot)pVr6yA+N=@ zXxD_#eAt21nLTZ`iTY`ZG)FGFKY>S4Nr3y7JA<3PF?Dp3L}m>&h&sYO1=hLBxzZaE zU&h!Mhz9p7`inp>P_w$X7Y-IC!cIX$^;qgw9<*Iub>3KU&u~a`Be80D1cnIf?D^p# z=Ii70j}8ArX=CoM@9x+%@jA_IX#Ziz%QfY;J;IpG@-Dcd zx3DyKXGbV@Rav&|(1dHd>9a-lU}BsiyNW4!ANs&p_Q)0?mQ`nJhT;H5*yJPZ zWRn&iFZvV`b8*HQ{UpxXAPRQbBWLDcq@lM%lyA}_0^@j`Nw4xhsi2G9Sx8&PB|s!Q zV2Yo^{aDT81uQSP!u?ijKoP?_Y+el?8z@PoLYi1>J@=jbao_xn zHEViql7a*&r}~w?yv3dJ!+oscRVKOnNizSt?HFg6yVuaUE!gurFtZX@4bw44y)5G% zwiI+Hq4DrBEXw{~e$B%kr9~hiUc9foEZ`I*7Aa!Ei%eXjJiWUQRce8e5GZd%M3QOw zqYO|luf63LN`0gt7W0g`uO(O*4N<4juji)xnl9};U8508lnSU(nfmecKxLQ%3mK`Q z!a8fgBT2hQOu>EV#|K%smRLjWMn^{26_&qBNwKT**bX_?mzZinN{NZ@Po)>AX<)a6 zaH5MBF`+jtHbtF zy<7S}d0$3S0YD?1S6NahjAwZwibBu)i?}mqn-w_r8rmrkFQ3Ng)2FS)o*cZrnpM{N z;u9|Tkb*NCuYh1$Zw7!oV}0dW-|ml-Tu3~`j4{Q!f*3aoPuXA{xR(YT_})I5#|Ivg zh(?@}^_{di}|OA2fXGK+1 zSA$!n#PUX6QjUHP5+`x%gu}#}m;W@BSRw*EYHB%Cs= zv}%RrzUj708gQo}_Ii>-D@jLp(R@E&c^S&qRRv*|f{x?g3Lr0uzgo8#NF*vFLl&+5 zGg?|DaHms%?ua-D|_Vx>4s_h`D#8?24wu z#LHh_E8QT(IT5Z9&zC8{g1c-3SXicCB0Ffk(Or+aS?8oq`C^dcBv)C~?^p zbf9M2gJ}DdOIj@tcf54lokeUPxI9)DyEDjHp%c1+1haH!%CaJD?*0PtyScZLq~#T@ zTlOiSpX$aZmJ$c3wGDDsL&x9&T8o379V+tnsHab#R%$<;k%1sxGINp#?o(43mty{e zD{B^3A0Y86;?*tR9i1^c{rQk+c z!M)VQmM~MUGhS}6B7FTh%r#6QQ$21(^CCjqlq>HV9@$SFTtwF{%)UZ|0Q9Wgq(v0r z0*@?!L})NjPx48vzwrXJ6K_!NUxwF-u#+$w#-!;h*ulL)0XT9*vhL!2$GZyd+3$LH zNrcnf!h-vY!b5#5wK1#}NPlFhZY<;L99}vJMKH`m4Q?6vUBf(aanGfKgM$;1I5j+G zZiuQ2v&#y^TaHXl6o9<0>~lWdY30|ARr$Kznh*?PLUsSXgF!H>OUQ^L;jj9c*bgB6!2N+>8?SK86SP{TQA2ePhwCss-Lc51 zp8#S01UT^rYmJQ`p$8SGoBA4D@%c)U$;KN|UoI}qN(_+-ypy(k2>B%nPh81Zwwh=B zFFNypMps&cRk!2vBiO}}vg_I0#*R(GKQO6ahQ`V|;}|x0T?DOrkHV5J@>g@iy=oLb3E2d1Nes;^XH4mSKLPQ#uMjle;ohOkv!R$&qF$IJ1R} zApO<9+*x7z=oCuOl0p`-?{0#T09pkHh)Pr*0 zD-izxPrciB*2<32biT(oVq#-U8|MUsgy-)Lh%tY6h#FVw-ac)q8)ZENpA`kY_9cFKuAy>$UZY3)e z^GWwY8)s}HY0w!N-aB|)P!4+8x3XMcwUD-@8rrv<^jd0Oa=?=(N0X#ouizj-#s-fa z@t8%y5C z6X~K-nsYjd=o{!&+_rc79Gv>)A{xF+WS%3Oc|V!+F`gpjOVp(xTR_*F$`!QSM0&XB z`T3$%>){Kj)BSr z%WiubC$s;}n>WPEx1YY5Xes0996ZXrD9~CT!FDrvy)5dHM30PgwB`TeYvY)HW35tz zqRXg|@8|h!AI5_ni7w=Yf=bNj%&pbNFlreUvMLD1_ln@aK*FjCIpD1gP zcq3W6DoCs?pvwi@|2*oklGKy(DXI-10?nWHrwZA~h05aI0|z+Pj4;Bb8b`L8q|) zV0@At&M2-m@=zp)wBQR0KyP0KDTtUHAeRx#gRQMCiELQ!GzK>G96-B>9LXDw_d75t z|8oTTrMDWN^A>=DiEmwE#MA{&Ox!z-`4nL(Ny(xaCY_)32MPz#?ePdi@XVh{ITj*( z2i-KK$}Y6ki24V;J2>;mMHS+wyCQm2MSsfD!`!@!Rj>^sCuegglCe+!>0gU{!ZTz- zjya(zDa!{78?-cMNFVI3&7=9ph-0@s6`@Hq4&KiS9q0%U-cR4z4J$jAQ_LE zEoetYBTYrp%#om4@LQnQ;Q;|u35_64>uCU58=zVgu(onCZ|I34Md(H-}hy8$CLI; ze&y8A(8D`+ACH2U&HLM+Ou93h;5+POuJ#uuhQM(~pc>lE6_Q~WfqHG<6^PfBi_)sW zv|AyF`xv32MB4T4kuKnW`y+pog83F2?{+UYwPOfW)0TgnZ(Kc5k6jsv?#BG7HKoT+ z3t4ub9k>6qg^(ap@Kt+siRNXDrd(KMW?mTmJk+zDm?N3TG5*ZSsBtlTgp{%D#a9GCp?t_ephWB)w%k4}GXskGthaD&S3iM{4R8D2 zg7bDeay}v{)#`Z5!ynSrW*&Io)_A$C=F9%6dJn2R-RDER!{b!qWtZI zTQ9fmGcQjqISfM18P0z;m#u|JT;#Lx2WHsb^dzU>0=4}!96qZ19D4);K{Qs_;AXes zyxZHfLTEZJ-Z=1YZBMbB4;5`!30)w@j!1F{@;Cz8{S$Q?G1Frb+{w8DVq!IEj&f^K zS;bQ;@?U!DiXK4?`FBs)|Xwp zAEG!{|IRJZFo>Watm6>X_a@zYD5?;ZN%x+#Prw?#eEsTQO1%!3I)gN@FyX4u7|~c$ z{YQ5l0E8Thh>1Q;8vBIrL@M(WGXQ#+#0-FK`M&MyKTqGHitV@jv_kWUnOTRKMY@h5eAm19r6s!R&|PpY7OnwOB((m;G$pRvXPjHfKUP)SSbXva+x& z+G@?Rswz03?^4!h-tu!Jasa)wKX?n-jA{gUk8%PzHr$-uhQNoo0OgWG_fDi>5e&O!-qoavl*Ym(#AWF z>j}q*1Qw^7Xc$~rj?)~Pp#=*DwuUxjGd_KDeVp--zdxq&6jg=Cvd=rZoHx6(67PZWMa2zeUQ4%2gxlz1Xlk@&Mj)X&D&+Xf6EZj1m3*qiNad#CSiwYVo zGEFVd#q-S}PEWxTyf&=|l(fpWtCH=kX3eOBU>`AcWvRa-6p)sk%4Q})78|~Cmk&>nqTSivkjG}^iG>6W|TDH zklArVylZQ((zgw?3ias|DryJRsYUTYmNqe|@--q4rJJi^tkT$Q&7yf!FK@d;#3kPs zu|XC`KJMd@j|>!=h^WgruY0@{-)~_DZ=%Y?a*l_4L{sc(+_w`>e#o#fs1A;IV!Z#F z@u^<1Srb=cLpW&um09{^u;yNDRsA;!%!K2ci3q3e_g`flO@4aeG|D!9Jm3fy7Wz%} zn<}a00oQqV8Yt!_&UAh~;&=^b&B(kTzuY0}Skge#rMYmeJ58k)HP9MAEMO`4vB{bx zsOFEfJ||{S+F(S3N_h@9XwhgT$$4?-#J_d$f4<`O|Xl zO8m4(@;DCgVXp)Jx3UAnfuZjbK4h-{fmw9>W#f(A`uq`AfkM0?Ox$Gy?jkw%__8mb zi_dC3b#-cwoCy1ZzyHo2fCES0O2?Wu?lco~EKW2%b{5&=cEN%zV)U=vQxz$JOv!Ce z0+lTTT|@PZ@zPDa)W46S_9Hu&jo*BtuUYofrFU8^ET1t6N3t8M)~P*ywC8ewq+^i@ zcfdlHyDT>r9jrP-Hj{dRkUgK^xv)4aA&2nqYVcCeL2s*qyXtV z+?3*4E{JX?TUhQt=>O?Y8Ef7sfv0`^Y9UL(t!=nPlB7w`#G^$lyYJy6_fP6~^7QXK zE|=Giu)2tAKVJF9pz3q8VsrJwtGd`k2P_b)IElStt?cz5k2AMe`EeMzV7+%?SSKmI z&(|`FYx!`)qqyPK=h&%^8NTOp^!qvkgz{!_&3(A$gBL>0`hhZZTn571qQggr*N5u| z{Ok9Re`qBTk`oiL!&M9!?b71an>d>_)p_zb@K}=n@r^HsRc)%ADzA6@__|8qZQA5* zTUXF-X|(2JUmV}+vPlGxW9O}npANTmzII2r$f~5X23FFJxA6B*h!y{?i3OW+pHZJ9-3qR}- z6ACyQt{0H~QQzi4=!6nxE0MhMW1RVOhTvIXIi8I!Z+g?yA<<8d*uJK?S~3s@dr$0N$wnmzDmvcC}ik6@^1yo^Qt zS8!CseZ2oF{_Sz&xU=UTAj2CuVr<*6`k(*OK~ntd0mEW7?m2Ll_MMOgPw*F zpUKvWGo0ooTMs2h<+-EdR z4B_8?DA|4{w0P|?5A17iKtXExOSFh1h{eEq@HXWKY@-DdEKyZFT3?hVI%>vv7rtkSYPYaXY! z+j4$n2Qjjs&%G;O9x@5@XkGex&pDoSF?JbzC^T~R#u0Pfcv)$Yc)!oa;Q`Hu&22QU z=$_%MzB-Fsxi4#4a^-Q|HL)K~_EbP$E~j z&+1ju;3bta?MGYtp7-HU(fO-`K}x$#${uO0WlEnp(It4%BK9bw`S}=mZ3w${rwIn! zXZgx!t5h39F<3kubAWfsi7a8ce#HFeEZ4e+pKC;5%?&P2oI0a7G8VwRqI%9oLnH2q z5u2~YRfZx?@NJYLMN=gH!y(L6k6aTU5^(_2PPO=P*YiZv=9G+>Mgx3Nz;P?Q?RnD~ zLmtPb)0}GI_%ec*mQ~*rnLwOIwTxp59>6GCUW%eZ0TRxjc5ImWIxrhsgZKA z_gzv-nu66Eu|WM8iVgO`USOQL?dNpaE!xi&Mb#(C+DY?o7i*`v9KD(JH;#zP`fK@4 z+*xtqvVzbv*ap46FN%}oGG*%@1Ts3dJ9KKF*5o_(^D3}OBqH(!%Gsua_J@wd8SNG! z@IhVFuH?hYiA&~@8^i6$=RI{y5-(|0oMwk83wYvn_4%R1D-NO|kTr ze3e#L5)l5;mttkFQ}#rIODcGF0*>&?;dOfoHkG{=_SXr1o;cY)l+joPco#p@|2Mw# z>$ErRV*ZV}#CNp=tsN+b{S75~M z9{;h5f8op#RrUVVjtv}#9R;bs{3|K^N(#S{!mp(8D=GY99skX*zyDP_ewB`2rQ^R{ zI>zT35*Oj?Q zBj?rUzohHeM=%$^W(H!vB9jF_-0eUWU_e9sS`XB99z8sS@|Q3eL<6MrdR(DjU)9rB7ZTcWNmdXO()PgV^URX=VsQeW5A zVV#{JtY<$}XKZutMCs6R^_)@;Q7#j_jjaatcy8H6ZH<#hFFZI$NXse``e%ecX07e^ z%P-Cgag+ovMcP`M!pg~o)il0HueCTAZi z;MeosTN&ZErwPCR>Gx{@Vg-XrU$l<#tBhui;X|oa4?ikM|tf&&nF9KsJ1L z02r{V(pE-Y?Ml;#A&-$-d>N~tYU5OT;*EA?c;BJtCb~&X^EFFteGb)g#zWX#Vffo9 zbhY*;LJ4p+uh$qs0Rl_F>LrdV@DS~14c~2H`#GsZlacJZ`Oux$h5Yp{*641$Ug{J0 zruW|a+hnI^i*XV8zT+aiio^8l#S1x|7m}xvI>mige`v;!y<_AL^II7zzla`>bS2^4 z7G9?&HUs5KQ-_06o79B>52C5uT#3%3w5<#b#LAq}K=+t4chvWAoy)+NUC-YSq(O*#w=$ zb=?&E{Q6CAQwXWn`H>#BLk@cIwnXEtj0!z7`SpC;cI>f+Yjfh$wG!QP(_YDYIh*<| zceltd6v;0(9uT+b%Gk@h8zSSmXDE|Gn`rsq{&D9@?Lp6>9IErgEgFL( zB6>eg#5(d}`F!kD+P1?t;&<_ojzhq@54rWK4ydPIA9NqEze=X|l-G4&`py6yZ~W9X!>0g_Xera0`6dfJ1d|x76D`{EG4d0y|jvOzWu-dJanxY7sm} zw54q<^6lhXv5VQ})GQq=XhK(yt&BtcM0`q$sTlU9iNjZGpG@5lp+w)JPn+neo)a-& z#Iap8dE1^GN%QJ&YaC*xeG_9b#Y=5+9@Cz%a`T#_Ndw-i--5>4FR|E@t5XG=r?d0< zQm>!b!OYb)yUEOdy1&Onk4v%K67tWwQ#|*zteZ_b4xYW;Y!{EK9EYjX1qf3^k0G~# zk^s27qwg4Z?tCXKE^(mTF~#zwTiatu;EpsSr=-Dr24H>q(=j{ z^}7xI_54^91VQh-7rb_r;QfcX^VJJ=eRjpIA$%#9jEz(AukX5=dQYWdAiO+Q>3ec< z^zb8oDp74Qe!(}LC@%XQ(8=q{>ocjJ$bRInLS=mxp_+*!bR^o!xJw@1u8`bs%b}ne zq-)vT+`$*HvA9Z9Q!*}Gf_vWn)9G5VSj~_3s~eL_<|IyD=gqZA?0{qa&~3`MAbviP z+>+}}`5qIm?@ChEjZrdP98Ee_`{VuY#m$xadbs^t5q|Cj*Q4xf9=LgWJE7-e=ORuxcfr}@x@8anSlN$CETqjNG(7Zms zI3eyhTw~~1yUY}4^Yz%3SNqC`y={v}^nBpibCBiXEqwpsd+LN#{S#h~!9sl5#QO$g z+gvtt>a;<%bynGlY4;Z|zr;$N(l_^8zL>|%z;Nx)`wuThwTxSpYsH*9(Hy}$Fzcbx z^zd-33x;AUo5N0augj06PO6@5CEJFg=g zW$|P59sLn2oNhf|2)S;XEg3VW&jzoO&MHm3em4%I>bn#>l0)0*&^XmZT^ZpwajbQ- zEBk7d%QtZP@Ig%V=e|Nq%Z|5yT`gMxG%IXkH#xbq8hWZu`t!Imfc|mCl}cPwjBTD# zxVWHz1*}UUUX@#KZUY{t6rij{p4?}N5GFXdVz#J-0AztkeToBcAZ)P+%Au zXnb;jyD2<~&pYFjVyr{eJ>SixM8}d`QP$f%IY#jb`3f(;=n6;m^GdzsDTgxEv`Z*% zUv^HBI+gsj_%8O8|6-_m_}9xWv>JLQh)8FI%;Z&zp~@xowa&G|aDD0hqupaAotn0b zl+^O>j)w|@0w~rz*T$n^nIOq9ACCl;R0SAp7>U)b5@INmPzSHw(|5Xtfz13`QCKqJ zKQ)g~Cn!ks)#*3?r0BImCbOkp&s20!u31UKL9LxFmU>AHX~6W|3ep>|K`X92nq=Wn zPbEVwjaS5>NV^oYH#|5PcC{4mQChM8n@2sxO#bIbK~aAtx9KiU1#edl&91Hr%otNG zpw#)&T|By|h|<;)X_}}8YP|-~SQ)?(3T&E zGd-bycF}yjBYHZ(pI=AoNZikfmGv(#YwcS@{dUFGKFGeI?d&-cw-ZKzVlL#3P}Hy+Sx_+F~?#*YssgXNWsA)BjMr3kO3 ziEc-w@fSMNj<%UEtE#H968K>)c5Y>~hiV_)gdim(ZiYs!r*Q7S@rpU#e*?PM#)y*``1 zS#}5&!S|xVC7)VO)teSpu(B3X=Bw2*b#0tY)Lbwry27Go9j?Sm<~9XA)9(!2vW?5{ zMv1j{C}l5?^l3!MShWoYgM$qnD#r;>S7!gQh3Ix=6Op#=hUMCaU_9GDe!{+4G2aTYe4zm9wV%iUbEgrNRS)a6Qd-AH5`ZS> z(rQ{I2O1N6=8b4;q)shIjGMZPCQwpCz&K^)IMcZs#exH9 zlo9)WkMSsjh)XOACEb2pn}-13XFN%GHIt3H6qL}b!z*>YQ%i8|g8;t6h?lGJdEGwJ zD;A}W`SFesTQ!q6Fzs)NjR=SGDM2v<=(AWPGzY}jZ!YJ1OoyN16}S5G>Xm|zd+12H z#Fd&=LMVp1-0QI}-$MAcM*E}zJe6o`I5U3}wM$w`RM%hd-q~Y-ty^s7#y~)I2Pi^L zTN2yf#K#r*#l)88IzBKWfI4v?OTGa*FC{r(SKJoX`%wuXYITH09SMnNp$DWIiwCHL z@|oy)fs%ugdu549oHEY0VASqf)Um$7|}H-J=BXSb>1p&0#LWoFSuydtpCyH7LZm9^R%~i>Ih3|pe z_GZw$fI6|o=88&=_%3nEdpgCAHNz!_2_m<-9qPY5<`zD8O9wcdL3bOfr}p|zq|-WS zSDsvWxPwhWH9Ll)63{Bq8$iTv&%BUT*C zqtpHR^LWumNT0WVI*}M!Yo7={6fNr$faOTs%O3!=PuD5tv&{_mEv_v0K|?}XtV0MY z=^A-ZqpqEB(4K0ZucOTOS;g7|wQ6a7B`zpdMNC#5pjTT)rPX&2j;$-vfYgjZpjsFC zMVq-l0UeQgb7@oRU88Y8$Ox4HnoflkTCLlr1!$BT6EC#2ra;|#e%j>hTgFidjp9^V406>+@ zaKc^*7`?-{ZY<{q&T(2>$H2@@uLfXvIdKKjx$pM#tFo?n z5%SS*Yq1;az4Z}}{!6iQb$g-eu;z>sk3q+1aJ3`4PM5^_i1=~oWXNeKQ0s|7K)AnE zur3Ziyey;{*LRx&p?tt+zVa@0@-b~snr1v#c5az*Z2sbuR)Fy+Y+6UbgA{I0KuL=z zJ^d5jVPYtQ=@D|90Ka=OzETSCFKxEbyPMK-C)%v%&G9p?#P(pK;WZ=s z(ryA0Ix8rC%vzZu-rn$;E9I1Sx{0@Rt(+J>xF26;K5Y9<|K$mtv_@RO=90Ua&*sJ& zruooa&;r>8`Kx_cMRxRX!NCYw3qeWg+4VfGLHs4dZL8Xu@qb`_F)TJl-d;h90FCFAEdlJVgClf;B z-TO0zC!r;n@yC6^XOM5CosVWdo?41F_xD1D!faBkA5wmw)S%O)=X0ps>uAaVbw66n z@A5@l-1En1))&`z+c_HVrN`?>xgZ($vf)_+5`iWTm3Q!#U11Olry>;BiINK=r%m^8 zOP<^-^Zn^M4&jjHaE({%yvd7*L3I}p){@>n>ZKL*x76c1WBdW}@o{IPK|^(0jHOYw zDOWQPnXyQvI)Sr#NN0B1H3C$9Kl4Gu0BNI(NTiGIm$vEpz@o$h)Xo}j`S!YS6hguI zc1$aEFbbFkidK7f$^}MV-3H}WSlYP|KG^FdqpfP;GPI2)L@8itO-RQx&+R!d8Rw#z zvd{xON-QDGFD6u&k;m!$)j_dN9^3=?md&#U?Mn#(n`=m5h4Km3F8Hrhhby9jo=02T zSi&t3?!C0H9Div`s?-&3QsuVSZ9ix4G8Qhyt^nHZyr5qwUtn+|ZDVrrla_xGQM|(& zO3O^Z4cpw(gjMSN|`sne#u1uZSjfNZG`+WP$6Pq~zjdm#3YTM4oD9#5Zh0(QS@>I&!Hp-Sy*H`}lZZy|EYCRj z-JpJ5??uZUOv+YJh2`ss{9fOmS*_gj9Qf?kYt~Dw2CCdktNC;+R@bxYvq+SBOBgqE zyjzcv-fK&!2pFKk2IGT}C3D>(0_+?L%35Pb!lQa$O1&v5!~KIRUB8Jt8WZc-B3f=o zW>4?bloMqK$v<5fbJaEqywFo;%{?dS6FNK?1~$ z9n>$C$PW9`JKNQ46_+!$Bz)SS`e)O%Cu<_C5YJ} z;!=4%bxzW3sn0Spm$o)S+w^#p?u`R4;zsGfO?cma_0fzX_=qFY00$LRBRe}pkI&(q zmOsFg?gzgD_5(j*tKUIrqIn2?-NMMTFFUWwCN^@&weu<^5J$h_@t3+dtWZAQGgxWW3@C1xN~~tgCfOc}0oaW3u!@%@P3!g_g21 z*j+-JvQux}d1;`!8UYiySy`dfg&?(>yd=5^P}U6Z`Hf!iZyM!{W;{TQiJ*D`)-uJQ zK{7oTWBLB?U9Aoa-Ps0!kcbd*m{aa{dX?IY3xSWi zAKx(u9BA>!2bS_x>MLcM2Bj=DWw^vV%2o%dR^biwE-mhDF@dB@E~6+w1uOl$C~$RY<&lC)^)U46j`|IiHTkl zukXw&xlDXHhOzv>ER{ntY;i@Da!q0jSRyT#R0qC+G7UxEflB0s?WW)Xg%67rDW{q* z8lO08^(8n|tSV!330&K8L#SQcb4vhaSRg@N-}`_>NmMmi-E*v2RLv>+EX)>LdL&yK zxImtOQwbi7HFiPOu;bo)Wbgg|{ub0UvD!x|g8q<{&4|dl^CH&{n^!&GPgv1-(6&;y zQtJs(+WUGjW*Oj6s0TdNkVH_AH}7r5o-y@ZY{HD*XJl8{!S*ACpL%>4cJtVPty{OM zi$le?$DZFFarBos+6Lt9I&iEJ?BD(gs@xdhLEb0E0)UC3&<1?SlLJETpKK+jxHM>6 z6K{2^YS2@4IiFcnM7Mmt{{H7;+*QraBL5L@ zll#2_T*_oA_oGNcPACwY=XZyU%B#Hd|Vo5)Ux2 z2&sMUYaN7W)3I2){RsB~XwCrH=_gWbRQ8r<0Vs6~I4Ohhhi-<=6%Cd@=fHZH1-xH; z$Krqbm}VR&k}KQ_yHkuE1*d|^^0t;>lwg3*_6C3=%C*d|C1)NoF$cz}*BdD?LQ#Nu~;CoZ^5iEw(!P7sF*Q)UP(3BBVy&umSt# z9Rb~tkc|M2i5CDy$tCP~u^BDX4fM&*z+gIt{ zP)13e3QK$UCa`KOGPFpXT^WxtDO!n&$ek~psz7h1=(MqvNXu!q&dut|Hm?>yUd)$R zE&rd-&74JnN8BZf-yZ`nEqvQv=_Gi(Ta$?a{wX;7aBdC2+VroJ?gwwBgiB77Mplku z-g1=>dM2Gh#*c!4Q_aTD7Kx=%HJe_j_hvK~HUCU=#3q5r<&G-b8wf`Uv;XN(yWI2Y z^sS53Nx+yW6Ll0vhM>De{BNMUPn(e#x>2&LRhqI(5%iuc zy4xWoIpy5#el4l|ko)!^gZd6_Ab8X57-n-0lfM!Ef50Mc6RuxyauHaQLXV+kcw1O^ z7C>=|4I+)+FC93f=fMZ)^QX*(@;z*n-7g&CFJX7LM3SOsVAh>Rm9-=^J`s2R{*hij zh%!~JqQY4I0AIu!9*V6LCsK3plP3#PzPZS8$wDJ$rk?xs&GRYL>|3B@F`te`>mq`1 z9)}kZ2r)A--@>A}deaUv9$?EE_0R_4#;mM(FwiAy&n)K@w}n7M=3Kx2%>A*20GiJR zxxC)K$Pb7{WJ;uB)h9CS02V?8`Z~q|bvE)J4+52uxt>2N{mV=W<<@P~H?h~FEkOs1 z0F5{Y%m{o1929j9DX4eF|Ef_4E@p1_ZH#+!Xud$(b?=mc(~%>Ln34D65)tVH@99iK z*g60Z2kWWJi;qtE9#IYBWJN?l<5cZAtJawAw6PdUT?T_Qme->Z9r4H}UG__cV$9or)SaOBA&0kmXn5;p~0YF>m!u`B%GRr`Wj;kZb7ES}Ww zc+{w8d7!9PbW#R(hbwnF<4<~u^FEtqj{=JfUUR7Z&82~j7HD$Vh&!CD3{QRY*zxFz z&mdL0r`R|T2x;bgIo(=8+oWW>J3w4f)E8s=z_}53AMsm(n+F-~-$M7{d+RrWN%~R9 z;UFbWxDR_xO7af$%$cR8^qPYmlY&Eb&-S!?!w7glwz3dlld}3HkW-aD;i!Egx8SL#zYh?= zQ5N>zV_giCSdPzs_1m$5a=048d@g$ti=)bFAa~PS%Y#cMub!N{!v+kcU0;Gtdh=&= zeIrue$=E>2nUBSiT>d~jGx{)CUJ*H6q0)2zv#tRZVkS@{W_cvHf)IjfWL4Aked#gc zCxu)ldTAs2+Xy&?9XflgZoNAnN?XZS6%Mn5P<}zqC;E|}fM1Z@ZHYK_1$fH@AYr#i z3nTT$f@K5nEkbgrm`#!2zy7wQ&y^3HKi6;piaq85)DRr-Z;vJpK*nEpy1~T4gOgth zqg~VxE?s~8qRsjq`MH*&jQ5HI)xLu{GPg%*8$Ci=uJ_qmf@7%e(bW+>RGrNmQ)e zoNNls?$uC&wuIgs0uO&PcGRl~NQZ`0HS83JOq4INqgrJ=|lag-1AT(G~OldcKS_Ftgj%LMqP z`}ta1d#XJaq}RSbA8;GgR^w$8$tk!PWaT-XnQj42?M!&OlZ zM;`nTw)7F5ejQzRC;wq^!|M+6+M`}=zwQHfCO|d5N3Jg0yhw}6_f7R50byxiRiKGb z+Y%h{t7YXv*K|c3l$BABdXow^gtD^GHZ17h6j0GoG21(ArlQFpl8-?nda(3^?q!#N zwENo_n;~&~zq#nHm&GC{BzY3P`E&Z(XvNw@timV@7l^!Ye=l>(a0$YUIn6sASM9k- zK&&kt*U6V2?{0k3w4&e>ncVy&bp=c%rQs);#*!dC4Bfi*vYi1q4@bH)MnItsmq{8R zei42I(y|mknHRy$ivQDL53lSw)L(IrNBy;M#g*x95u_v9B6@P_{3y*5y{2PZumC+D zO4p90AZ3m!@8_}exKdZnLy~reJkB4FJZkH}8|uuN=){cOPl3-Cm(GX^ccxtpt@D$k znqE)34;}O~;Bu7dudFo+DY7EaM27D7D2r386DE?X9t~IL>IrD z#R1J6i;!#1&`#m9mji-j`R>8?Dzl)_SWWbNb^5oOXHm0?+RB``q8khn=4-jJRpDSI@^9J0Rd=M*RwgB zL3J{Or(tD9qZz4_fes6(R}$ymfA4Bp0J%8PY$7Y{@kp=5sG;{}YRVO#);O87kYrVR zc`5f}$%J19g^35-=>e0vF@#IUNLA~ebM~?0F)YD6Qp8%#-3QWQ~Ia{xsmFD`{plxlYQ9sKM@LveN zOWBMmb7GVBckro^&jSlJpF#8e{!FbIs&TZ}Akw@5+-A@Zc6?27Ketr@jEdIbpU76z ziuazJ!TFPm>D}H;qoe_FqQm_!01{E^jL2u7ElH4ic2Ac< zABnaGnj;b^oSQ`vwsSo%z%#{GV8;6W+F&c1Sk-t~FFoEf_1%?*MJWsn*mV?*%;Af*UZE zqtJ(Xx%%aOfeOQFxXnagk77ayyFw@Jh1bUgQj z=pkrt@_^JwITMoT^A}j^o-kA->ly(~M=bJsSEmi?jWYp-1G#hx8(3Kvp&}{*Rf*L5 z`J&yjYdtIcag}pgX^*i;+LhNZD295@LAWK99oGfnO(Zfb$}>(b!dj?8BT3jxo!Eoi zh*ye!-p}XE78%-%&jS8bfRY$P{dLm9vr3oAW~jn)ew)sfJlu8#7t(PMPhANij1fB> z_3oE_F0o1JH};vcI~vX_CmA6-ok6FY$2Ik~0yQ0E1uf#2s}=#Z1`>3SIw>1AZE5NF(enZ#rg0I=Z~jBsEI&1=m?SD3C3&yo zEt+M^a`%Tonb8wgINC+tuCSqDIG)AvL{iYJZ#P1+Jj6sm1k~e5tSg?H3rEK0XuN z{pvUYo+%PV<|9*RVP)hDch@z5!ZMrFV35e8_nLs{-vYzR0%GFiY(EstJ9>WG;dTeK zjVZ)xnlO+g7Dl2>7|<$weZ73!zv(SJnjEYCWxb>`o#h^6L<;`(37 zxLC=r47x1B&`dK7I31!=R`P%R9j*Y-RtQYnV3`Z<<>qEIx(b~PF3McFxq&IuaYGl@ zF`%3mPJ-(%%C`h&b~9k{$o!Iih#n8z%_Mp|gm&}|?-OVwE3b;@TGCN*i$`qw!6^~4 zSyw4#O)$~N0}RpuT5MprFIj*Wk-v(zD^GhlC`_`XO?Jk0d_3?$yz}Q1xy|9w|Hy2r z&^9pOfD-HjMXCf(MqP%%N*597ai1M~IQJoAPM9t@ZC$4zvsV1y90G}HaASYQ1t5Z$ zV%)@9vs4{^Rvp(tKyj64)6IS7=?*r4Ye=1&2Il)S#L-a8;=2#1=ob&DMeuwj zMil{9G10T~0e+C}jC5^PmAsSk>Z!Nwo#8O~ho6Mm0|aB#NtZU6^si&6YYDy=bhWa+ zNZR!kNJFuq*WV;>rd@C^7Nrd*?dTm$B#`bk_{pQ?F|*)wE)Q6>#eIzult6u24sSoW z50z;*4|BllE#48(oav&~tPJ}|t>=Ank;qz8?<+$iS8sYj4D_cLK<;9|c4GJPO1f!c zU#j=q9)wB;Z4F%B`Og@xL(oJ0_6&HVb@jzT3GtK13ZP-AV61lA2LwEGwOQ!lj{9jcf|yKSur_@jO1c0eG~Xfb~!| z1#=G6Q`b|(79huGloEk{g(nQ*VK|Kl^90dfItJ~}K~&9UK50PHhNr;uF(z@I=a7b= zn+d1`7HmAY017^v<^>}c;dK<48|%9b6#F2wZ>_c;v&%9SGeFk(`SC90(cs`daG*o^ zwh*@>B%$I!DB8J=Jcb#`U_^_Gao-RpgPGKSG{mMh0!0y#`lTS3Y>vSbBYp-??0O$H zEH=?LE={!By4OE;)cpztiH=!CIK(xe*yhqqbEWcUVUkc!26-fhY6UDX_)F)`dHwv3 z+P-*820clz>Rz3@cRj@(IuK^4DHfhkkB>oWB(HvReI5632XhDz+OlL9$#Wj_gJ5Au z{6e2G8yiUKF`C70S99>GozTovc$vb+j}Jf!JxbPin8#F=btQm-?1AZsyqce%vu*%z z5?{sahKPcyW{wwRFuMtQ7%>%M_;C%j$d@OnCnF{jTD6&B?!qzJ6n51*F{WDO&Mrq$ z2IK3x(CGzr6U^dAykzEm72?fJqVj>DKyqGQ9+5SWOV<@mU)+h(BMiFW=6=oqkr2)P zijoC{qX+WxRU)@^1sex*Gw*^Da!t<09pMt6S=lO8yxspyUKL^{vjER|ake>DW zbp1o=BBA1kPf??fw1TS@XtXpE{&k7HX>fa}<+lJ@4>=^bu2%5n%!u5Ml?C2yly2AJ zl!s{jJQtfdW|Yd=rH~Rw|(}b^X~C=as(UW%9E%dQn+690>`%X<>?21Wv2ZPCDuUJ zg}u2HB-s!2N?hEBJgbj$A-)M}hItgzaMmG27$uS%@!Lw8_R7tI<*(34Xkyjblu$~m zDLp(KQ(G{E#Tiu`pQx!lo>o_TIKkIXGpES6-q5zmU)=YRe~aR)n6A?f$ZZvej_oSY zdM?0Zt?D+NT-0M;dN6n$cCAUh5cY)tws+)17DePvLuka#67+x-&q*cyc9ii zV1}9du#3;a6fdi#1~m{}GN$xS66E>eAeHaccbb!cR6WO~wlgBX@dJosm)0=Q$Q0OLb_A zlX=txgdec|1ahUtYL786{7BG6Tn0F0Vv{0eR)@st+VS3l18bSz_C#v4IH}-}YM%UI zNiM~Lf|+7F(9Fp63gU6T9J^vp0L%ylZNEvYM`H!&#lf5k4hErDivP#e`E|V$M*ipL z*;NBZJ^|x=|CaseJtGqlj1XAUGk!K4fIBN zOPufo&k9un3_86ipTIuPZ#@A$h0I%noZt_}5}H9s=|veKIcZ>uQK4G}B`3#GoI~IF zD4JOYmny!w{)}82ho_0UMh~~-VM){OIpIU$pkJ+U31+m)(n$w-P|6rUj_!aPcjMg% zq2?PF#gqgnD)e!c4=!|HxJtht(4J2^=F8uiE@P?Ay$aV#YRgPruH_Ua_kOhc30cqt zBeXN?xYnj{u-8!MeTX0vBbWBAEJ87Z1PzV5Xkv#~7F3pXy{@CSk+GRs{KsdFUF95I zDCa$=mt_h&MimQ9X)in+mM@Dog{K=?qzT=GXD8d8Nyq(hp2?IWBE75&sv=_)FmBnDd}gTTFkIM1T#|7n)95d;@WpK z{ym18lxHF-;P1Dm1syc7U_+z{u9*Fng@@HaFVhWoHJ1C7X8v3^H?w^;Imyt?t30c# zi2O$ov<2kqV63j3!u4Ob=u)p8acwd&64}P|`FHzX(H??1%mS-Kp5HwoYRYrq01Cjr z94BJR)9-0mufFG~+XpcPWGe{mvbsoa7jX$BZ|9pOyMlK+{x=YxG*{Jx_7$lPU~m8~ zsq#wpH)>A%Ip8fz2FU!J2R{toWmC&FmUSl04M=Ii;Cq&N3u0o|s;jOdDWWdA9)PPo z;Id!d$MRl6llH(%c~eqn;-+hN{xS?1qFKOew91rcW6*Pn0J&FuQ3rBOV}>25HU>h8 zO<^nZ6$@duj43&|6_~Xau%Da41C> zf-H_(A0D$Q>|kB|p6z1s(_{&TjX(SwKLfhnMPPu)B*3N~8=%8mX}U=+sWY^H=^nsb%4~3`EDP|gu*Qbc;5zxVH25)bOZfH%o4VVw1;bnM6ywy?j z+QP=1wFO9Fvzvg0Amg=s2?wqUsD8L>?Rw5>&!IDaqAs92I_i+*H6#g~U}G^d;H3B8 zd?J*&6B=JM*4a7xVRk3s36KMr#y0z?4^zyLqV`49f)9JMKbE3dqL4TMX-}>4T9rFb z=)G2j>tv~nlZE3kElpe^{6&>pdbV*1Ms zvmEk9MHITiA$)Jtrp3DLv^~}yUR!{knVsZi-#mEic5&$S zC|BsEW-Bb=D#fQ-y;LIc_XhG5j_aG%nL`5q^{-!6`HB3W3}rv9Z3&i%=`8eZfdeow zWWr!tV4gE{Hn1pBx^`qOV7Glb zw?oAnBCGe`Q05w){ak3v#+Yl7l@GH`WZ_uBbigK(#Dyl;+oZ?BG!t!MrXFbzRA*GI zpxS%)i3)3gIY6`S%sxC7hfV(y(Ysj-tuw=OFpo3Qs)MR~=Hvy!X__O==_?Vkx9=?LZPQ|R2(1x=$7?cL?&;(t{j3T~j zNOp_qu1tJ=y~)0;d+)m{7q5HWKoHd{(x#5v0@zO)(HGL;Yhq zR#;owje=R;-9?|KTbda4oo&^%@Evo-2oFznl7o7bmQX6RD+K2M7eETqISbTACW4`BF@JrPzL(3ArFba1>z z8(yrR*&9--e{!OdgF8?5+ThLCFfzpn$C3!puKn}ocD3C%U|7NvbR?XfQF#CT(9&4t zJr~Q(>TX(g1tF=f5dULKVaLtL#5Zo>ectt*(lCcU89;UT9})ThSu-a+Jupyu{jYet zIOw%06*w+%%GjOH(V4t@qQn8(0%%;IfG7?U)!jK%T?7rW!tRT&FHf=Yb15EHtort| zaJb;Uc!6VnNSw#oeLsumkiK-}}@f}#I;INz0iCR>SkNUVP2r!=^(p?pZ| z{dqry87BHhkk+^cvUxi)WYM9zWGt+{u=1$2V77D;1w_sjXp5xL@67b`DPB?*sszjx z65lFOr?ddhgrFC2Uh4LCH8~*P+X|s}b13X+*Zj{~q<>Mk=s^lEIvkL!#?U_ngQ^@- zu<(rVB2!t}dBWfcFhd&)BE1P64oFsm7_SO!z-xk9HAM%ih~k0zrZDzg_-+UHK?T_F z_DAv$Qy#+Ds0|J#L{UozAP6qW0CPm`a1`)5h{kPT9A%=6Q+UP36Mq^*Xhd^j&{d5j zLwi}_SGyP9mC0(mnot;zOc9YaNlgw4DMG!&Fvy}w0qSW^+Sxva-V7l(nwi9>z;`6z zC<5DQzjYFE$VB9U4VcGiAYaD9|P1HcumQdKQFc!oBiKO&f_Mjz}(*Lnm}Qq|1NjK{4@ zTO|_e$Dc=>blU+1*`Wb?1zyZ1+SK{UKrM>UCH5#`nC^mWO`%gti-nPXIrOb~i^kl* zjjtlI9*(N2lEt84I4B~W^&RH>gC%+lQ;6b8X;kn^f`5&9z||bik7fvf>$fz~7B9j8 zH)b1$%R>{)Uyro&nhSujB)Uw7G+bsDW)MOkUhCj8PH2+Kmw1W)SE9v26nsYGPswV5 z@2Lci`h5@ufmzcgBnE|dh-J{{(Dx9XPu2Xm2vakA>B~t%dwL&Dki-78phJE1<-7v{ zu-hw}N#Pw@;a?+7=v8hFCU-nSeQ#vOhVX66N~1dva3j9YqQYj@KsCUF$_N2x+yGjr z3`|FzTno{;S-83dh$mh^l!UWzrGuKILBV!YuB%Ta{S{BXA(laD@C(9wxFvf^~ zx>4Oi1K@Bo*f3TRX(ngrF91C$834{N2G@%JtURMD5S+!MaB353 z=(YTR-;<#qGzC`yw6Hl8wkp%WI*`p z>|G@j(oWJf)ZIiy2YdO_#kD6 zQPYJ|v|5EQ&?r4tW=ZD$? zBV+KSx={_<9|p57-t9)m%pu&)M>~b8GjqdG4_@$T7pNHeqAG;2@kltp{8}OKAs90F z3w5g&QR52*Izv?)`e&F&hOw`ID!_C=@05hyFTH;i#;bHlJG%wj@KN7VDq5ATGyW<(*LZ1Du8M9gGqULnrNe~A2mGtg2q!3=K+$e zg3d!i7}CT4+R|Y1KQF)%DK8)KmqV>DdiY)b0MqxVJpv1>?hixRAwWtQ<^YB*z;up; zV_#^Z@XVdhFZ!T;Nr(KyM@6Lf4?u4}lp5(umlhgU)#P_O==NfLt>&e)_}xWG79|ycW$5u=h@&+v6yUidqj6F zfPvESJkSBh6yY!d6Ev21!5`*QL$1%!b=plZc1}?t$U@-g+^;hsXsOR#8YOP2gTAvW8dMA`K}5=s z?l{7C1NA+3sIdq*m*CJ8H9E#hkEZq=aQ^2oI`f4We2b1sJwZY61dhRcxDqcbvA(hZ zR7he5rZ&SOeZLFR7)T0O!QS>lj zU;}tz>FFG1cLt$Ff5f2|4fqdKQo53;_upAQ&_)ECSPabz(4j3!{f`K~dFTrS1H3`) z&ze9TGT_l=^nA-WgBv|8P&HzrM;~W|``#dNqW1vkh0|onK_s#Y1MJh2i+C#s&aR<@ z9xydWLG}b{YW6|8(zicw>rS3?|19c9tu0MrZL)Lq>6beAQB?SxGxV$36fRb8%zM-y zKtWOP;9(k?wuF%|QSUJQT4(?i(uFAG0x$xmy)kGwB9!lq9tJ5fEu)=Pcm(OW zxt~U`0ii~nK(@PVWqM(-{Zqgjz;HS6!tS)fOs^|OWxp}*8cFpLz+BaDKU z=Lv6^>0_{Y6%^KyMOmmp&RDDCb3XY^GCZF=*}u_ zm*i{`~$)ueKSDgd4)oSjhGUWMHT| zg&R$31zy&KlL4lKOdEP*jKP~7j!hP$oM%%Qf%C$<>30f&<#B&4jj=#&HJwRhua5<6 zP6h~{-3J%@n~kTW6Fe?Cw+$Et`x6zUYZ!Dy#20}b>UIveeL=H=^Oo>WNx6f`7lUBr zNdd;V2DU=+WDxlw@1XtW1DX-GN=!!z-w@8(?tTTK z9hmT$X|JpuOg}jkA-VMHrzgO&c!2Hga-*E98Eq@ma28Q3Y5-Q^VB~*bUl|jPYq|D$Us?vX{ MXe#Gjy!qh&1MAsveEFw>?T?)YyZlNH?IOSO|(zq-aDCMUY;kL{LGH4hl#ynivpxQE8$SrFTR? zKq=8s1QetTA|QxJ6X_j(bG_y}-}%nD3K|O&o?o-% z+wYpLv99~>jP?fAfFp-i{_wgXCZ&O)WtsJOZ)0ZITxg|!ZQIM9efE-;RimR0swFX6 zrHPgXiCSz;%fG!5-~VFwChvo9&MLkbp1C%}A#iH#@Q1RnJVTG6mMS-o!Asnuck^Zp z?jDgli)Ucj{6->VHT}Wn@j7n&<6y^jJR!@KB6S=3Td%lw`o~(8XW!r-o5Qaz!at5m zEnbR$+;mz_zqx7XpI`iEasG*%e@@3gspFsYh=TA>?f9oY{sSKV0Z9L#$Nvuz@_pVD zVUqi5SrJy8G3QuSLwbIAz3xIwj!32SM9Yg;uU>I~Vd<#Q-Q8_u2K3(#1MWGV*y+6{&)!uKvBDv?%=a$Lw z>_UTqnWd{Z*DZWmvv9LS4i9=Ur`aX%RANJ zX?Xoc>w>9st{?YCBt$WP$A62^$rWp)M^CO}XOAoQ9UdMQzu2#nQo<==cg8>-)Whud z%l0dr&3VrDA3l7Dma%RtkiK!_Mx=I{p@>yWj-H=I0;tx(fskk4)Q`U0TP0^}Yn#}z zZPzZ}rVP{4x&(ET&Jte{i-zPk1#Xv9N*Bh`JPfo|@mzPg_ln==#F{0!w&vKHi8_j6oobv6~XZq3sg6!FG0+`outsNJ@6XR?|Vw~VX!@)avO(@S{|JZSM0 zH+0RL`TR7?z3uucF7vsacoy~8_ACCAaURog+pzLGQgitE`Gt1uFsV<}9HMm{`gqS; zD|e#ixUg@U`&d#!%#OY1UV2Jj9{?E9V5wfz=V|~HX zc^(V%cSKC&cN#p&EO(=yyn6NOMp3hqq81H1gLdk!t&&}Em6Kb!@uy7%lf}aF8RuS^ z2R>6Uc(Mv~^cgL<=-ZWTNmy(0 zrTtlgsdkFq`ifxTG{>ML-W%$tb@3swFIKMh^jyC~OU}^Hu-6_7y>9*b;^r)iq!z4K zckzcZ|ENoYt%5wJ6QAFSIQG`jF6_5#E)pvk@-=(=*fM30^z4_vhGZAI7n18UC}MoC zUfVkG{trIJJ?jFAvE1b3of~%RZ=~gT^yrb$1cT?m{t_P!wq2(lOAXwi$XT~x0|FAO zVBAsUl~rlV-2Aa^;%1K?UuasJqL+2dt@Vkc1#>sgRs^wqo_o$-@I$bn%lWL5T|0Jo zEATpRer(;w%gWj|Kb{?h4bLjiF)qHbQOrs|B^>c@cC7!|A^(xLZS(sP8|q#1ii(Oa z?}!NSx_o-zAH>DV>dh|9KmeNiw&gjGhLR_aea1r_#r5tgX@>b~9`mCz zy!*x)J?0us$D}Bh?zi7pbQ;KNjKoKcO}&0_cnEP*%%&p6`t;(!F$3$`*b}aw?IXN} z-G{uZbM3qD{B-(f^VrhN^R>qKmUpG<$7-F}smeQa8>3 z=YhpbeVZ4ijTUy`Wlbi(yic&WJn`8#K|QYIz+j$wSL^A&t{2nxZQOI_XsCVVkjFkr zhfg<~H;=x!B_QH9J!&4f{*To>H)?8XJUzDw3(NLJn2dg^HkF3hanK~T{eHFH!(R@~ zHbu-eEo(gB@aYFcxsuyw981@p<3=~I>r>GRPYf?MowXe*W>=6adUUqJxT`$S)OF(W zV1DqleeiCBNZpKcS(D2WoAGwKet7EFI@`s~9OefX<_cBy^+ScW$fV#$Zbd}!Nk~YD zxQu?vS(u-R!eNQy9>gX|isHTExs)pLRvW zhazqjsUf*AA4I_GH{x4^-?-nw`1RLbQwyi!gz99(h34X3P&cckU5Wi2&5>-v6$ zk!N0BUfo1sU|_n{yC+UlUBT*?9g#Go=KHJWcj9DeT#M)!O`I9QBa3cXzMAuQqxs?B zp{6SL{V9X-@$sJB;g@#@2^%36b#-+`&b+w!qN2hZE2)!hWpJT2FSmTJdaROHQtFf5 z023`OEqVM@rdf4lv&8u4cU*2W<0+9+8sOX%X~7LK!L&sG<>&3r;u)_15rlCVlq*0PkrqpG8` zajMh_Teq(0{-JB;jsg81v!xzIZ3}a4jb}BGv*grlZhqf3(>l?ivM{Q$ zmnTXK329wg!L+$6j-zI(k!|sM5%<|iv)b5Ky1v$wPOe|*=KU1u z_%f%FS7J%}gEcY7mBOW_0xmn$)lslwi5>TBK(wnNC?C$ZEzEcf33?0*hA>o3C^THg zdbbG)-N?T@p*3aI|0Z4J+{-&=O^?oo6^#5LJ9~QeZQaW z_V7fNj1RW)?KQ3RK{40JZN&%Nk#e^4RXLWhjMn)|tkSbX*M^gf=7g+r-d|TLjZa5b z(s|-=K5x9)!WKulZmhq*f1|9stItM}jsU@=xAFp$(U?Pof(5KA$zRvk#9g zJ85Xhi;_?sdBl5VtYk(c)`gh=%;|Mwp`oGMwr%r#c5P{?M0cqA zU`VCx!mNv%AeQj({q{{J^Q8-ud_&nDGueW3R^$CmRJ(;5A7x~i)I=*{x3udN@@kv^ zTaFiML{`fM44w$;Tk)9r>pUkuwx1_^1 zRF@43e!M5##GHpeYy{qLE0Q`y=|1d$n4!nqm{8*Z$KG{|7cZ_(Gb%J|NN!S$NU7Q) z7L-Wo9hJ*vENx*r&30jKB2FeaD5&JY;XfiB`x>efHIsxUcmWgivaNWi(o%XDtJgMZ zD{y0FwXKq#kgE!p?h|n?LRpV``0%HT1I?OU)A6cN#t4vVR0MySV6QD=2s$s+jhM3+ zu-(^>K0B1}+#PnQBhq{K4Np&lrx)pf8cz*(CCu%Zo*3jaDe>{*m39doouYpy>@ig# z?mB7np{uLdZQ`A_HP=n6);tMbx6x{$#wQmCIFVZ<&bF$Q7u~*Rv@qQ`lw$3+W0wh` z4$o~Fz;Ie3i1~(Ud#%ws=LkgHGY47c1SP9 z;OTzJi~V<|_4W0+0gV<^V_H5T*<@O`$?Ugk*{Hza_VV<^FPB6s`YVh79vvOM*&Q29 zxo{L`UyWIdDK8TD5eoZ5d~ zYK$~>%io_35$SQc{DgF$`e8%6kv|u$DUzM;bju01&htkbytQ7?h?ais!Y8p z@?taj^U5~~S0go(v{X-?`~f)?*!+BLY%xl4q7;Q96%+p2(Gga~HnQyEsZse~O|LK(+OK?rwu8a(F{IIqZOdR+?d0kSkE8$K4=X%9LvtFLnHg+U|q% z%N7*qnj9I)pS4+lU)iNx0a&o3*&@yNn?+0HoSmKXhRb(o9=n#^>ars%FUF!VHO^Jr zZTs%sGN)y4AzF*Eoa%9jFTXRG`mSNFXO?`mRC&`eB>6 zxJBDk@_xK-%`=ra+CrDLLoaCkRXTAZTuOqD!l}m>yt3Qe49{-8Gx1_*tRNpdU}TJ zH7ybFlUkc$TBYpPmU1e4ql~N5uCLW!*OkMIBbqf@MK>coYnM=|BnR42^q6A2)ymzK!OojH59cyg$- zlX2j)=lH}=Xh&**DnobVVtVUtzsLn{(f0UCe z3fQjRk(iV!*_HpV!+k3(jECpI#pFFZVoqgS*)|$ns;@{60MMX=+T4bWaWY~Zw?+k$ znaTl+$XKE}#W&UumCDYG09C3u8-e@@$jZv@E}R{%s|oUNdU*OUaC}K+*a2X&$^(Nw zGH%m#x~=L>`Ij%vcCZ(EqJ+FCEAvWo?b=Ztb3E9;b*5T@uei!%encok4W}zNw=3wB z@^n@9qepwP;uH)}eS$l77)>1u^IL(GzkT^?<-Rh(2p{mHOkX1{tqnLqUZxRlWw>ugp2_rt5HJP z!V?l&T3Rw(c)=!rRJE}=7I#lqRr}bnV;uy-IZE4?cHs$GhcMzFE{U-IwNvauhZk0)Sae zECO2@mlubf7cnmKr1z?Fi9PICH+K6Tl^G!UC{*~}Px~Jcdd2CARn|Y>n&<4=$<-#_ zr@L`sDq?}dV~E|uR?7sDEBf8?)oSza`R0F|9~NGC)Y|qG48v)5sMMs$la-R>t9xRy zJ#JlHUFm?jcVygLxTRf09rICFZBarM5K1N}p0qA?YBzz3N&#qGo1aJT4jZ_APvx$d zwV~3z{R7c$t2p*--l}xxq@ZkYj1Z9#U<-bJ-~hI3OIDS?nQ22U1*Ckl-E==^lw5Ee~4 zw-uRyfX)2eOk>m9(6PS8i1AT&WPr^lZA5KlC!ek909Z4RH3_Z3PZ~eFx|oS<_VPDp zTk`>Ah}g9M=|3N7q^rx3mOrkCg9)-p(3GfY^GL(FvR85WAT~hEI$ax^JUc6_3TC^6 z6~OQMMD68@hyYH0zEpUsyA$G09s6Phw#R46Zs)XLU9pF!Ghn}UEBEie|DKK*5o%0B z{?7qS6c7~b&aC2-u-oiD>>p8t(nnNrzC(Dv!>6k5_uk#PQ<;DLv4)tE@%!h$Yo>~U z-k`ek^LG@?d``0a`MAL&5{-^NvfsmVWm=3l|L)yZ54%dJie@z$dMog`Wgg4D6&%b? zh)f@-s$NKlQYV#L&(7mAfa3v=Uq;kDy)-!4Zz{{>FQ}5Ll{+-#_0t-e$zKBP1a|D$ z0Se96Hv3U5%D%gjm6a$1O7ijJpZ)xv(3eN{&Z$(0a&n1T`Pf%Ui`9xKTYwddc+AfV zHJ12tvExuC49ReIhmN%IwBnHVZ8mb$cc1F2e*!VdjaaNsfva<1UxbYOMfA;wKKQGY zAa{1)OnM^18l5R!H%{F__>LWURxKwrH(D!l`uT6b+(E!+#W#OEY}`{5gID&uv*+wF z2PiWoo!nARKT(CAZUG#8;^VAUUQuBJqAz0EoGB#uJhXRX6X;M0(tMOrp?lV}sj2~v zDHB_Ioms=!dLOm4meu?MNIY=}s!~}K4=1CqEnQQBw`3>MB*$&^dFYk3dq(;B9U*E! zP_(bh1fSP^*w}>lJpv-7oMWh}%1ST_Z`9BmaZ0WTo7n*>o5ECJLL%GEisrxkAuF2O zDT}41d_oI6Gcl-Q5CJVg-)*d(L+N4*WS-6@(}p)wfEPjTS zDwUmEUA(G@ZAX#NjP04{fb+(^bqNVdfRu7jbRS^OA<<)zC2qRm6s=VeGMNk z9USO%cTm6-2k{4I*CB5sAFOP{wmxh~?L|ElgoBdX#5qHV{UF8RI}J58G@{2d#O*#D z?R5bv1)-Cm47Rt@Cw>iyPmM+%()^5wFLo8XvgHch=gg%l32QfJo@ZqxmLZO_dVa8~ zvXTLnLJZZ)d^EgmVSd6E6sSR*58t>I-{{3R_ep`^{uxR6{I^*a+EnyVCBY(rO_c76 zZgjV;mOrMpjv;`(M&6ohrQhq2FBn{B2Hi0eS{UK zGF5_hMeG(#j6(6$O4a8$a^#3+cvX4%4OGRF_1gNknsqWy+}m$EH`b_t47XGF(Pk8S zc0SiHhal}l$q(wM8y_L5a9<_Z^2s*N2J0(zFAlGfUWWiak=pX+(b?lt<8MrcfHyU# zj(EIzczVd!+Ew8CY95zzyUs7~|Dqx#bX>Q$0-RmV!@VyhyD}&9gFm1Sd*RHx!45)x z%c=r{gG)g&ot`Ibe|2}CcVFt$-F9|%z%lnrj%)DqW7RtVF>HM!P}SnCgW@;WeSCEf z8BMTOq*oI(j#4vaZ_v0)pSckzaqp@Kp;EA6pA37Oft9&QcJLN15Vs7G9q<)v^^A;+ z^z%%QEoH-GUhX5-!hTNg4Y*+?RGCMnaN7lo=zcv5BZY0+nP$T@n20RGR z?vgJWGCqeRWhxEV&cP>lp55AnJqlsmwB_1=!LyXlLkeVb1Et)C@U<#3Q)iOF$Hs6_ zDv7sY*^~^p4qUvTf9aj$`7(WVWL@vYD>hIDiNyJH%vX<7P6!c-XxTYBQMll?Q!jIy zyw65foF2DkWuGa;@CJx33jsmtIQf-9R|UQy;rzyq)E5puOH9Bb2pReBE}hK8E|lOS z=IXjnS?BLtg0=!}IpVk|qe|wUkmTnleW{<`J-yUyr(KF&CejShqxB!8nJrwx>B}$u zEx8$nLCrdeD1xFI`#CKCobQR4-wq%e$Q7~2?Dc(!xX`uzZr{Ew)CdJ2Xxbh{p!4?e z-HNYl2*SLzdb%s?Hv)2+=alN==*b8Rl4uZU7X_*`RG;R)Z~5}&%6e9Ea+#>7t>diQ z0SSXAP$238q{TVo#af*NrpE{F^=$M?M-6tGpXyf8F&{3=mr&OF;lsy|CHorn*?`jh zz&}dpeV`zZ-InN>tWG1OiWq#8VXD&WLJ*TnY&Vb^M^jVNSWvf8fF(Ba@Yy8_i$*&6 z7Q~sawa*saObT={bHGlM>B8}3-l?Q7T1wiA&gr(5uxb>X(`$R2a!T} zz~LC4o(TG|lQzhGTgJM3H>_}s0WPJV)5$n@LMc>S1p0zbO807Bsf{Z(?DYJ71$+3z z!55`2_-S5%`6m`xO4ld%73g3yZ2;bcBD$hqg{mqI{8W-$xOYBUBrW+s8JduQ9$B*q`F3;|cggciez+Zgp!@XOV8+)<3Xuh! z5-r{9Y7h=c{CE3|GQ#N}-awtw(QmqK`g85%787`w(r0rrg?H%62$~@o9pg~pwkgS}8 zY%Vy~4Jx8JWd(vTlw0MtTi%z`@wq)TKgpg0U|1dKiQqdfZYWC{1Q$SuZY7{UGq%C{ zw;%ULtd&AN;^#NU3uuO?9q-`;EWf{X9rswf$6VUNdZI_JlV6k+&Ib@OCArG8Gh;7F zfNY`pSeyN+d#25g~v|0pOUAs$vM7H2Hpw$41%N_iH*m76uo1 zg!8`q9C&uOqb0S&aIWjRNyx%8asvZObY^Jl+a4jIi=TgkuwiJBE8^CdAj5eMED4|MKkWWQOcahEl27D8U_yLOHpYA$tX5RU&w)^8z%kQ|RtSEh@*sIKNxcA>fKd zvr{9T?a!{!w{8Q$BhjPEeLO=4&6r-DW|U4HXiQ4zE;pna+zX1Zgi@yp4VaY`OYMtZ zagju~#-PzVkeyGBX-0C~ZR*c{^rLV(%>D_=Pdq*IpMLCb$owJxJ^S$85E3KCj!k0z0o%(OJd-k)i{27Acwggh8 zn$?+uR#sLmwxv9p%de{hae+|!owCZ^f?T7WGPFDQ!)=X_FJ*R7NFYNo!fxB=$E_Fk z7t9StxUCPp z8xk$)dz$-MUjcrhO;!5tQX7*QffdQ2+IA8hheUyvCkizYlK6}W5_`tFfkGQ5rzn^9 z)+Y&cOe!&OAnG(T{Q;{B0vni*RlGJ(@(~Kui^|GMhm{?Z!oyMWzBg2(~3JfE##hp@08faaIq(g9XB;4B+Z1xU^PcvsROa=5H(%O7j_QmZqd2WpP< zQmV_E_$U5lJQB@Y$Ge3p5i+c-=@29k#R(1eP(+xr5@2h{ z6Oi|Zk`<1U+J??&^Yh?NZufuLF1~^Za1Qby87feF8aX#npHI@k4E-wwq{K*^GA6@A zrR+?}ZoMI5QWE-;vcU$fh)NRb#epGq>gRm%;c_ODGul&Y0_K}wLFzh{sV!|~I*DMN zbz+MZb}2>3(X&p@9eYC>fPAj$;_CC4jmfpYZT7fZb}H_QN?a=;KX7q zcqEW_3fD#V8WaIS6zMp1Bz0t3H158>lEdz~8+8r9`sm{VeUHqH4{#wXB?Tu(jE>f- zln!LK84eEOA@0V)7IH~JM%3=VwYaWHK16+nNacgA*JO%XROO)vPXA^~We~V7xj`hu z(Gs6KRADr~$5H4dkht;d`-e#9-m+y2iJIWVQ_C`LELpkn2PiD|f%#H0DTqAF%l6DB z5D5(XcIoH*gvSi**%%r_KnX?>V-OV0)Jp@+`(l+s)=ImKzBu`4?w~Jp}m{^!Mnk|c%Rg5*U z38LBxgs7^au?~W-q5;tt+s=|=^y=8sRr`TdD5!_#X9gpN!vRK0>$Qz|!XzCfyQB&E zK7anaPA%xb=Nv+5r@Ea5_`}`Cn z5sog&7H;v$$vz0}BG77MY%f(gFaaxvo|-K#Fp%KSpSMRyJA66`+`pq0!uD6hPPMOj z&nZHu(f6))ZifHEsCboF%gvsRbq^?O;*4*^DPw4*=p6xZSji_d8&Dx8lY*K8oG~jKED9vK$>s>ygc2GD|8#EvwAapbwItcYv^lhFkcI3#e z?0=grOr3!pTFK%AZlvXa7KuYKuDRWbZS?)OD!-huFiZ>=8xYpBF|Ba8T zoIF6qTR^!<347wHCrFA302B!{Lb8WZCrm(PC_-_>)3MOh-U0%@(6r|>@+zs(dV6%4 zD|_5uSp468#1O`zMRpMRn2kZaYpPv7l7KDsGhk%NG_Mx|(Bq~ySACMUf}9-0@lFzx zuiYu5gFyQ0Dz5z+x$gLllHZoGMKZNP93JoP2oDml)_oOmUINT8>Q1hcmasb^`}hyg17M7&97dfzwiU zA70!MJ+B6DhJ>l>IQXC^u}TQ1q$iE(*^uN*N-Z8F%}!caSQu<85(b8P&TeGU1Mf}Y zeN>+4IO%=Q)5H&7RyX zTFIcs_>9iUyRrWrpjb%RKLF)PkmW(7z<0G3f)8jDpPJl+(g7 z`1oz;ZnLqn;?y$*D`Fw2nU6_Ojo<70r2SvbO2qHCuA|io>39)O&m6$0&b*Pzq#;V= zN^`lA3e^pPSXfFI`slk7memv9E`eYmR0w03TGupUGXeyhzoW5m-jSI`^-AE;xf>*Z z%7!R&bH$4L|A{N&7~Yl;0|QNT`VLc+mhVV|X^RRI&_10B5IgNDE3BgJt9^}iM~eLK zVo#g@{#0!nA_mm0P%7PYS|&H|Lv7)Q2t}NJdM0MTd2MgJIM~WVf(D-6-@On_nS@v$ z&V52?s)PmAYBr=9@v$4Z+A`ZZqPxl_cO`QcjD!usAvShSZS}F?WFump7FFQvT3h^&@?dd{qF3 zDI+_PH2u(EyI)yKbvEd${nFF|TSh&>Gm=Y|Wn&%4YXjl#`ESdpI}tHH4_SdKIS|B8 zj+S^;5;HSDN}WRg#1}OL=w}V!2#^!O5iDzxz7dc(x=SmdEW+b4IUHQb4X*DPUU5AN zXsYSx=-NO^7q)F~6K5rt_!4pae}NeebWLK*I1HX(d6->4uyeRcfGMe0fo^c9gMA(J zq$5DCUNh%tkC^F!zSDuxLH^ z8mTV_ThJuNQ4x3yL_+(bzNNH>I;r5K>;ei5AK(tQf(3_kn^K-j(E~5){U!dRI*$ww zlK@){QNi4AtcXp8+!;79DC7y%8T(F?HRvRlmtN!}EEj1I$Z0Re&6JA9?gfEr}u zEB=ad&xT;dl-zpaZa~jQ(H}`2dHVUu4sthA{~Sp12tFhf_?kMSWT&D(3rcCDZ?r=@ z)d$)Um4U#)C2M%O3BkL~eUH$BgJmN?Fp0TXp7lU6wh*NnokD@KiqIrckEqIw?z%v- z?oimEtKdA~R@?c@K-I4&MJKliMYQ&@a3W!<8h(Dg#}_!LmE_RZuuDC?>XSx>swCuHFmo^1@kk@oWBB-bl0;jw|18e#?YL*_ zxyhvbgU3{`2fpm&E|N4zV?!T0u#dcLa6eppK7KL zuk@435e*HEy0>a*C2wltM8I6*o843}U7vPNa*0O8|I<31EfZ$GzZ^ghnoeQ)g#b@a zYAva&NC!@L+h9-SBV^oqr5a)J;w0Zgg|phuwhcI} zE_S&W^BR9i7STY9tPtO|wzH;FZ_s7c5S&u}NG=BQu*7LtFIGTCcgtU1 z6HBqx6{rpC{nFLko*aA672V!)JW<~ReY?JtcJd-Bh3whF>oGf&ao3k{vJNn8uRU6N zRg+2(3IMPll?>hxFF4fK6>MafWD2ZRvTNV~PXSCs<*Ph9?|JV=TH$s5@{FzA7nOz7 zZS^!~VoOHTHNhRJPd_JW=8`3Ga&FLr4;t_>gTBVJw(}9&QFh5%20xEZ=fn|iqtzp6 zu+(L_z))${QgiV}`6V=jCP~p?JowS}zrh9dR)2ITNX`JV5{vINksUZwVZqN&9ura) zE;pNY13{9n1!*A%l9ELp_eT?Dnn;k@wQF%H)T!D>)tfdmHO#s3w<7i*U!szoFsS<4 z;}Kt&9bxvWm`(#f$Soz7+3z-BD_?5#t4r1o-$fZ9=4R6@U@Ugw%@2OE^G?0?;QYWZ ziCPk5zUeIWOQSC9!A_01#ia{VvI~L)&7W|tUTcYpMtxT5ss;8@_5y)21Cb2uBQ^$| z??-FilZ|MeghJxg*&6K*73`bju+H8w$IA2?-aqf6V|sZ;!~e8SHkNgX_N zxya^X9@g_;R@0OSq$F z@o_yCcgj#583vh3W+2(uFCQ&Fs7YvCHYnyoA19&-q^B=vf^9ClsI{1SAY7y8JWm=y z9b1RIdjkfvr{!aa32N1&ua=?L375-O;_Qe?KLCXy*kN=AO#5o^hLG|_$qj;qs^>I? z;y_N|x^76yjV^7z{^!5nOUn862l%GFRZ?s?z>Z;y*IG~mq1+dMgM=5vLvQkUH8o6l zqLT`MMkhhcbYhTkKfyi&!wkx)>45XtsK=DR;?VoUDCe3sI4-XnxJ^>b5KIc zcQ)6zpP>?E8Q7KMC1l__x`LPbv4-ak*& zfpk3(kxo>@gg*5282@+mTT(q;B?&(1`4QQLy=2Q_WzEga9STfy-2%-@Jx#*S-o9_l zjlSz-=T%5ythXUFDIMlEoBg{1tT`Uow=SVOAEKVbfw!!ZnYjR-oyzDuNM|mVBuZcb?Ob=1rxLh@xG=wA%H2s8RzN)D z8>_;(@r^(Ia!~J4g9gf(6wX0H%zlz$>Bu{UqGJGt!GO+LOgbDgpzX{+c7)9ZOGl;% zapHKEpVwR_83*@s|Grn;Wz-Cw)~DtD*F9-?gWCLX`6o4-7|=+YgxE)~A8(S^)+1klS`@)<7JLoixIc~B5d9}Uwj`JM5^(+^q1y1|`XXBso3*eT4u&KftCHjFrS~dS{?<6{Dj^jlguAEGM9a zq95By>%FhP6;LOp}u!cFgaCsj4nP*t7;qkn<_hI}#E( zImie`*;gji&zB1BL#3jK)HdxzreCEVwvPM16!5-HtG}d^GK2mvX*f9AYK6@&;&?Bf=7um*ze89bif;4Yz8SuHp58v{(0J@+X=AJa5Sb_ZJOA4^h#6B42c0c(PNdi#fOB~k2 zq<%IJCSv{v%7zF!e^9$dJt0Iy>HYp9E-)zQT>l$AKyCLr-Dj*|ouMf-*8#DR-UVn@ zy)gr`5U_&UV&bT=XV|xS8|K<5Z4W(L$_H-$H!UQomQeK(G~6nmcs4n)GT^{nHBPT^t5mrS}~W;NgluilUDDMyM#rI&i9GH<+BY)1^racSp%Ay;3rp0)``;vvTorh(wmv=;nOFYw>G#Vjq~SH9i4aYOf#pjwFpyJ#>GtRU zoYTqpgcs3k9gBcB6~@#dc4ou%MQG+aZZhLA6~(^O52{Jxmz^K-qniI8vml(7B_SGA zq(G&igdN*;*h}2Kw+zzSSf*ucuLdSA&0KpO)-e3uVF8Ge_=JzLtiq7mE9J=vQn5({ zfXv}|U9CP1>UpUT$6m4!YNk%$q(beinQrqVO;l6cAdp&&SEhr5IJo(MrIb@pI+(pi zrdK->^_D|fLske@V|)Oa4Z>{HUXtcP?PXEx@Sx9GH;mw_9(p(EE;cZTAhIzZgA z+s~mfhLq-m_&aV;HL55wSTJJ+TU$bF8%%d~`I+q8tjAWHl|M%3fhMlNBVx+OuG^{k z=yDSUiuQPLSq{UjqUd?4@uhvs=}GDtG-+~f!E2i7Z%qCc`q}U6DpoSS&k(=AT^G%6 z^3eg1C+f@ADvD6M3OVcz+Pkn(i6g3*Y#JF#4v}9IbO|}w7K&D^r7OVGTyIMIWH6Kf zYW5i7OZS*T?jQOT<)@X~0A05>e4Rbc1~oqN#u`r9B6Iq;)?$)?YYD+Pma4y zJ@_C`PYU2D)2i^XG09DfNPMKJC=6`L{oA?YT;p#{yYqJ^1E#|l9HmxZUCdSoK(=I+ z8@`25)mM(~4r)jTUvx3?H4;gHKm~+_2csuQ7&mn83M4rsu#YByiG(a>*Z%>tBR42z zPm#6z$UXQ7 z=9aa+Ly9AbGt|ZnfNzKlY~b}YMnv@7tTpfRgd$U;zTFc#?fxE946n$n>x}D zhA}NPl5E~tt-edU2U{KjPy;|G`|?&tW|`9%G1F8q97+kN+)_vjepa%j%MTU8(oz zjACyU1uHe8x5q$T=tYzLeA&bb9tio@MvviB;UZkTWESF0jEY3XPvZ--?x{O<~E_ zL%r#ME;NJ*<*l@DtgPsq-T>!`leOVC62gawy5d9l`DxVutmL;R*FcmDkvwu9%OAH> zf>+9u-LV>uIF9=MAh7$LeuAHMGi3TJM$kFmy-35r2kkG zma$*ZG6Bk5Lbgj~=QeW60Ukpkf;wC`MswoeYPwymG^7EwYCQ^d^30#CGzm=3lq*ka zH!X*!0G_@5wX9cMe!>Z>`xr?Oi#k1{1=LstOYgNOP5JR#D>_1&xiniu)@*A3(<)l# zMu1c>$w_Gb5_R+$rAA*L4q>oF>r#7=d>NjdmoJd(atISFp`&ja(bz9J8*H$F8Wt_U zuOcuT3bk3|z6L{dW$L;?yU^=6qG#9L-5p0K;J%fpw`cj@R!zTFOz06p!26#k(>_U% z>JiAuToln@_)?BHjuj2X?8KmnCm>LBDU3CHc`x^;Gkc)i!iQ-NNa(VCT^0z;%pe=K z=;SK%B?uU3NqZ46OOYIWsoOzfDJv_X8_etrf)#6Y72sHepS0o@x7ma&&=y_p3zuq9 zx6DLtCJnp;l%Ko%4a>nr_3h`d4$J`^GD+5oTye31Ttw0t*+eK*pl~V=RNL+$j72tj z&~E@sbnr+Ipc3PDMA_0EO1dcbsz*2=q+@}?{D{w55)Cac++%*~D{UmN9do_`0!Ht? zQi7t$%VX2Z@Og}KwyX{G8$;&@JG>+seQzY5a3N-E2NGD?gq;9CfySXIzPWg2F$&eY zwhIA^$j0e0+eDOz=6Qie3b;w5qDM4wCMDk|ALc7@)obv(q=WSzLCxO7v*^YBu8QE& zLHCKLU{dfJytOWrhIIgwWfL$hlf>=IB~}7-*15uH^7%5Zz=q$USQMk_ZNMDI*w@&; z0znBSTS?*mH{u$4tbZIY@BEZrzHKJ0FzXe6dy<#z-Al&!cz}oiHG^G}8D@^RfY;m=jxn6AkJ` z*17Yb*3hIIjZoC^H0^|a`o%+h2P-Qf$>Y0oH`j{9n><||RD5GVq4UpbbzzbC9e6p` z;ENXPC7Oa#MbA((hK5!I=Th;0)qoJhWjY{H`QWW%3f^LbdA3)(knT=k4!gk$GNvcB zO#-FSeyygBoK9lRo4kqBiplSsGY#899F;RR)}&}U3 z^gcxVi@oU@2+I1v?;I5)pvgy|e(xQ32lz}}o6Gv_;pxBj@cgPLsb$DvUC{PZIcUa- z%g0U1f&}R4sS}adLS_rHBgExp!>CRnORV|jJu%^3&UAYYEO`4|JvmezTQDlasy^X4L4043V7>p07*?z9Gy)W63zT=+@L{8o#a9G19-?voX#= zccP&>Kgw6Z-x!sD=#7aVx%4PGQd%PBC^vg4fxK{p{$Tl7h&BNmk|m7GW%c2iw2!A64Q|hbG@vqa5T*3H0E^%}5W4F?dfmeU*<>}1SJG0N* zVkVp`h3Ch&?L4rwk}Zi^!sJ^dPa<%Jd)6^uQ0Z7kh%}@~6&L>Fs;a7Ar_@Ly3BTA> z(L-*=p-UbUmzWBEd`J;Hw~F+b2Dz|nMZ|q?lzWCGtuA?QvOa`iC(oW)#i$T|)MIv7 z3x?(@sS%}Qqe7Tr$>)li!*uwdnULA(m36@HoTPk~LH%CA;pVIt4l9+(fk#tMMM&f{ zt_;bHbSAjspX!yq41WnxsFO|?e84iy(glB-Z>ess-B}E8JT9pMi`u6?vS6M6P5G-= zns%Zmx0DL4B?T<`t!84o*?k7DG$(kzM9(GiMiDYN!fvn*I7V~4KHito8lsqhI#XLg z=Z|Tx7!oJ{yvO7p9-h>zlt_|-1E6Q$4HC~h;9jD^+;WGNGH2`m*pj~WG~vM)h7z`h zr?w~@&V+%J*>44&^o24}Lc=YT4PdpD^k~H2bU+^PA$=3-bSGW+Ch` zoB?9P8F_xknvz?Ot`|;}#2LIUIHK>a`flO*%cdl7VQ5YYh%5BeW9HnB2krDC4$03? zTSElXV&8!eWV5IOfp+8p;hz-t?alF|q7=gTQ_wYML5IHrhc#u!uQ+{JzIJY(5Gl$q zRn{LT$q-^p-BX}5=Cwoh;@IEh3LPv!oM<8~0Lct1fo5w(aK9BFw-?SluJdTPKJmh9 z#Q~kz8jV2E7@E?m43*GK&4#9rYiq3KfH^GorIJE*lh>Dth>%FUE(U7F;QpqfGBBXkBZ zJb#xUCtW?kS!hioseRLDTvI8T1 zzH}23+7;9%gJ?x2hy(#MVYGOudjWcobslIl%f%Guu9Jq`oBn`Wq{eCa3>_EDxu2E% z+21kx);ewlzkQ5|Yiu;PMZPcC$D-crmG0rZ~M!i0^6M<-VNzCKqE(PL>kAp}rAN``4?wAQ{oYuazmp zUpZlt7@#hhDRpBgd&=tom>&bctZ@>BwO!8Fg+MV5V?fXMC)7;NzbfadSo1Ts93yiW z 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 U!Kduz0^2AIp00i_>zopr0MWtwumAu6 diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewTwoIntersectingOverlays.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.PlatformViewUiTests__testPlatformViewTwoIntersectingOverlays.png index c67a8bd48ed4320adb47a1f04a76bb1e902ba0d7..d56e416e633f40443f3e460876ab0cbdfff1c3b4 100644 GIT binary patch literal 33889 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≈6SxT`dLFve?XoSmou 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 LHq(qvuEGBXnWLUj literal 40144 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 jlIE+V`6}suRwb"; }; 0A57B3C02323C74D00DD9521 /* FlutterEngine+ScenariosTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FlutterEngine+ScenariosTest.h"; sourceTree = ""; }; 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppLifecycleTests.m; sourceTree = ""; }; + 0A97D7BF24BA937000050525 /* FlutterViewControllerInitialRouteTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterViewControllerInitialRouteTest.m; sourceTree = ""; }; 0D14A3FD239743190013D873 /* golden_platform_view_rotate_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_rotate_iPhone SE_simulator.png"; sourceTree = ""; }; 0D8470A2240F0B1F0030B565 /* StatusBarTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StatusBarTest.h; sourceTree = ""; }; 0D8470A3240F0B1F0030B565 /* StatusBarTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StatusBarTest.m; sourceTree = ""; }; @@ -243,6 +245,7 @@ children = ( 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */, 0DB781FC22EA2C0300E9B371 /* FlutterViewControllerTest.m */, + 0A97D7BF24BA937000050525 /* FlutterViewControllerInitialRouteTest.m */, 248D76E522E388380012F0C1 /* Info.plist */, 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */, ); @@ -469,6 +472,7 @@ files = ( 0DB7820222EA493B00E9B371 /* FlutterViewControllerTest.m in Sources */, 0A57B3C22323D2D700DD9521 /* AppLifecycleTests.m in Sources */, + 0A97D7C024BA937000050525 /* FlutterViewControllerInitialRouteTest.m in Sources */, 248FDFC422FE7CD0009CC7CD /* FlutterEngineTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m index 82aa5bf670993..cfa70262f2585 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m @@ -11,7 +11,7 @@ - (instancetype)initWithScenario:(NSString*)scenario NSAssert([scenario length] != 0, @"You need to provide a scenario"); self = [self initWithName:[NSString stringWithFormat:@"Test engine for %@", scenario] project:nil]; - [self runWithEntrypoint:nil]; + [self run]; [self.binaryMessenger setMessageHandlerOnChannel:@"waiting_for_status" binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist b/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist index 032a9620fa3f2..bc23fd5445d4d 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist +++ b/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist @@ -41,7 +41,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - io.flutter.embedded_views_preview - diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m index f875764e99e2b..b43c6bfa15bda 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m @@ -34,7 +34,7 @@ - (void)testChannelSetup { XCTAssertNil(engine.platformChannel); XCTAssertNil(engine.lifecycleChannel); - XCTAssertTrue([engine runWithEntrypoint:nil]); + XCTAssertTrue([engine run]); XCTAssertNotNil(engine.navigationChannel); XCTAssertNotNil(engine.platformChannel); diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m new file mode 100644 index 0000000000000..baf9ad3441960 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m @@ -0,0 +1,84 @@ +// 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 +#import +#import "AppDelegate.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterViewControllerInitialRouteTest : XCTestCase +@property(nonatomic, strong) FlutterViewController* flutterViewController; +@end + +// This test needs to be in its own file with only one test method because dart:ui +// window's defaultRouteName can only be set once per VM. +@implementation FlutterViewControllerInitialRouteTest + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; +} + +- (void)tearDown { + if (self.flutterViewController) { + XCTestExpectation* vcDismissed = [self expectationWithDescription:@"dismiss"]; + [self.flutterViewController dismissViewControllerAnimated:NO + completion:^{ + [vcDismissed fulfill]; + }]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + } + [super tearDown]; +} + +- (void)testSettingInitialRoute { + self.flutterViewController = + [[FlutterViewController alloc] initWithProject:nil + initialRoute:@"myCustomInitialRoute" + nibName:nil + bundle:nil]; + + NSObject* binaryMessenger = self.flutterViewController.binaryMessenger; + __weak typeof(binaryMessenger) weakBinaryMessenger = binaryMessenger; + + FlutterBinaryMessengerConnection waitingForStatusConnection = [binaryMessenger + setMessageHandlerOnChannel:@"waiting_for_status" + binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"driver" + binaryMessenger:weakBinaryMessenger + codec:[FlutterJSONMethodCodec sharedInstance]]; + [channel invokeMethod:@"set_scenario" arguments:@{@"name" : @"initial_route_reply"}]; + }]; + + XCTestExpectation* customInitialRouteSet = + [self expectationWithDescription:@"Custom initial route was set on the Dart side"]; + FlutterBinaryMessengerConnection initialRoutTestChannelConnection = + [binaryMessenger setMessageHandlerOnChannel:@"initial_route_test_channel" + binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { + NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message + options:0 + error:nil]; + NSString* initialRoute = dict[@"method"]; + if ([initialRoute isEqualToString:@"myCustomInitialRoute"]) { + [customInitialRouteSet fulfill]; + } else { + XCTFail(@"Expected initial route to be set to " + @"myCustomInitialRoute. Was set to %@ instead", + initialRoute); + } + }]; + + AppDelegate* appDelegate = (AppDelegate*)UIApplication.sharedApplication.delegate; + UIViewController* rootVC = appDelegate.window.rootViewController; + [rootVC presentViewController:self.flutterViewController animated:NO completion:nil]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + [binaryMessenger cleanupConnection:waitingForStatusConnection]; + [binaryMessenger cleanupConnection:initialRoutTestChannelConnection]; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m index 15acf3fcd7939..48bab99653e0e 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m @@ -6,6 +6,8 @@ #import #import "AppDelegate.h" +FLUTTER_ASSERT_ARC + @interface FlutterViewControllerTest : XCTestCase @property(nonatomic, strong) FlutterViewController* flutterViewController; @end @@ -19,7 +21,12 @@ - (void)setUp { - (void)tearDown { if (self.flutterViewController) { - [self.flutterViewController removeFromParentViewController]; + XCTestExpectation* vcDismissed = [self expectationWithDescription:@"dismiss"]; + [self.flutterViewController dismissViewControllerAnimated:NO + completion:^{ + [vcDismissed fulfill]; + }]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; } [super tearDown]; } diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m index 310f43fae4390..61610c06468a8 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m @@ -1,10 +1,6 @@ -// -// ScenariosTests.m -// ScenariosTests -// -// Created by Dan Field on 7/20/19. -// Copyright © 2019 flutter. All rights reserved. -// +// Copyright 2019 The Chromium 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 diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png index 9ec19ab474f03b6cc7786cb038ee3b3e6d0c51a4..30072dc62164626aad700f7153f9a56b64192038 100644 GIT binary patch literal 20863 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 literal 20295 zcmeHuX*iVa8@HyJnz3Yzr3E$ieMzzvCQH_kA+oDbk8B}3O<5*oDSL~pkUjg(q>u%s=030UI)CT+JD2M|+}G1lqoY1XO+`gTr=hN* zPelcrrJ_PeAz|PX)smMN!DXMTzM2wMUK`sa_>ZTJv4-u13si#O9!Z7RhoRaJJp%qx z?K?(=pxjeYY3^hF_g;S=|DR{Tnjtr+;D4Sm0axfxB=`Z+f3C1(*xx5$v&sAa-Xo-- zN15f^xWEPFtbW;*ii(8?`q`(U&pi)5WV@lFYXGj$#-Yno@DKm)6}p!srTj#Pg6l&K zm2(E3`=@BVmW3Qoo zEAE=V()MiS$ENStBiA{d&TI4$Uu9`u;g8Qjx^L@!$Qxhsl75%>84gx2Zkt9p zRMxIl)orxYi*C=;2XrpKI37oy58lCFSM-=&@xF=oTnrW3#;5$UiQAH?R`gyXGY?cu zrq>A6T!x{2ge|-jD(&p3OzZK`!Cx68%Q=PKgb{}sqw2|M$ zql9LA7VneSbIERwo4HO~Pv=4E+IARkA+J5RTrU5ixIMDd-Gj|3_n70e{kVRPqm%YbIrEf=)%WS0xw@4K>=zfOA>tKgx*wpiLK}#s8`F)Z8tF7a!t7z7<4>CBITKz z!{_m5FWzUiD$~o$dd}A|mHK|YyyH4I(Bw_OI$M<_R7_9vIen|xbfYUnVx`aZZ%(5}4Y;&(4*>&L_jytIj{Ezty%+*$Wvj~__PCB!-R41mQZ@4VB z(*JF1Rc{KP5FV~<_snHf?8gPCb<3b_3%5`07E%0D?E@JS){IU*z3tSj^fc0a9lab$ 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 diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png index b193419929e7b4666e8b753d67d769ea26424595..69ba03a131136f38e4d7e23a8b3349aafd15dea7 100644 GIT binary patch literal 19543 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 UX?g4Ek9yr!xuaaDX!+v50K5E}D*ylh literal 19558 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 bW=EVquwRNbf&U@~0x8O>-7mOj_VWJ#(2mT- diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 36b903e6044ad..f1f23f6fb6cda 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -90,6 +90,14 @@ Future> _getJson(Uri uri) async { void _onBeginFrame(Duration duration) { currentScenario?.onBeginFrame(duration); + + // Render an empty frame to signal first frame in the platform side. + if (currentScenario == null) { + final SceneBuilder builder = SceneBuilder(); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } } void _onDrawFrame() { diff --git a/testing/scenario_app/lib/src/initial_route_reply.dart b/testing/scenario_app/lib/src/initial_route_reply.dart new file mode 100644 index 0000000000000..d53a118e842c9 --- /dev/null +++ b/testing/scenario_app/lib/src/initial_route_reply.dart @@ -0,0 +1,30 @@ +// Copyright 2019 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. + +// @dart = 2.6 +import 'dart:ui'; + +import 'package:scenario_app/src/channel_util.dart'; + +import 'scenario.dart'; + +/// A blank page that just sends back to the platform what the set initial +/// route is. +class InitialRouteReply extends Scenario { + /// Creates the InitialRouteReply. + /// + /// The [window] parameter must not be null. + InitialRouteReply(Window window) + : assert(window != null), + super(window); + + @override + void onBeginFrame(Duration duration) { + sendJsonMethodCall( + window: window, + channel: 'initial_route_test_channel', + method: window.defaultRouteName, + ); + } +} diff --git a/testing/scenario_app/lib/src/scenario.dart b/testing/scenario_app/lib/src/scenario.dart index 4d53abcd0b32c..ccf2be02778de 100644 --- a/testing/scenario_app/lib/src/scenario.dart +++ b/testing/scenario_app/lib/src/scenario.dart @@ -9,11 +9,14 @@ import 'dart:ui'; /// A scenario to run for testing. abstract class Scenario { /// Creates a new scenario using a specific Window instance. - const Scenario(this.window); + Scenario(this.window); /// The window used by this scenario. May be mocked. final Window window; + /// [true] if a screenshot is taken in the next frame. + bool _didScheduleScreenshot = false; + /// Called by the program when a frame is ready to be drawn. /// /// See [Window.onBeginFrame] for more details. @@ -23,7 +26,22 @@ abstract class Scenario { /// flushed. /// /// See [Window.onDrawFrame] for more details. - void onDrawFrame() {} + void onDrawFrame() { + Future.delayed(const Duration(seconds: 1), () { + if (_didScheduleScreenshot) { + window.sendPlatformMessage('take_screenshot', null, null); + } else { + _didScheduleScreenshot = true; + window.scheduleFrame(); + } + }); + } + + /// Called when the current scenario has been unmount due to a + /// new scenario being mount. + void unmount() { + _didScheduleScreenshot = false; + } /// Called by the program when the window metrics have changed. /// diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index 6f4bf28d28e0e..6aa553380d34c 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -6,6 +6,7 @@ import 'dart:ui'; import 'animated_color_square.dart'; +import 'initial_route_reply.dart'; import 'locale_initialization.dart'; import 'platform_view.dart'; import 'poppable_screen.dart'; @@ -41,13 +42,12 @@ Map _scenarios = { 'platform_view_gesture_reject_after_touches_ended': () => PlatformViewForTouchIOSScenario(window, 'platform view touch', id: _viewId++, accept: false, rejectUntilTouchesEnded: true), 'tap_status_bar': () => TouchesScenario(window), 'text_semantics_focus': () => SendTextFocusScemantics(window), + 'initial_route_reply': () => InitialRouteReply(window), }; -Map _currentScenarioParams = { - 'name': 'animated_color_square', -}; +Map _currentScenarioParams = {}; -Scenario _currentScenarioInstance = _scenarios[_currentScenarioParams['name']](); +Scenario _currentScenarioInstance; /// Loads an scenario. /// The map must contain a `name` entry, which equals to the name of the scenario. @@ -55,6 +55,11 @@ void loadScenario(Map scenario) { final String scenarioName = scenario['name'] as String; assert(_scenarios[scenarioName] != null); _currentScenarioParams = scenario; + + if (_currentScenarioInstance != null) { + _currentScenarioInstance.unmount(); + } + _currentScenarioInstance = _scenarios[scenario['name']](); window.scheduleFrame(); print('Loading scenario $scenarioName'); diff --git a/testing/scenario_app/run_android_tests.sh b/testing/scenario_app/run_android_tests.sh index 7234b44caad4e..efca54d75efa4 100755 --- a/testing/scenario_app/run_android_tests.sh +++ b/testing/scenario_app/run_android_tests.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # 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. @@ -7,16 +7,29 @@ set -e -FLUTTER_ENGINE=android_profile_unopt_arm64 - -if [ $# -eq 1 ]; then - FLUTTER_ENGINE=$1 -fi - -cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd - -pushd android - -set -o pipefail && ./gradlew app:verifyDebugAndroidTestScreenshotTest - -popd +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -L "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") + +cd "$SCRIPT_DIR/android" +GRADLE_USER_HOME="$PWD/android/gradle-home/.cache" +set -o pipefail && ./gradlew app:verifyDebugAndroidTestScreenshotTest --gradle-user-home "$GRADLE_USER_HOME" diff --git a/testing/scenario_app/run_ios_tests.sh b/testing/scenario_app/run_ios_tests.sh index a1ad83dff1853..42088ce0d21bc 100755 --- a/testing/scenario_app/run_ios_tests.sh +++ b/testing/scenario_app/run_ios_tests.sh @@ -1,23 +1,44 @@ -#!/bin/sh +#!/bin/bash set -e -FLUTTER_ENGINE=ios_debug_sim_unopt -if [ $# -eq 1 ]; then - FLUTTER_ENGINE=$1 -fi +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH -cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -L "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) -# Delete after LUCI push. -./compile_ios_jit.sh ../../../out/host_debug_unopt ../../../out/$FLUTTER_ENGINE/clang_x64 +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../../.."; pwd -P)" + +FLUTTER_ENGINE="ios_debug_sim_unopt" -pushd ios/Scenarios +if [[ $# -eq 1 ]]; then + FLUTTER_ENGINE="$1" +fi + +# Delete after LUCI push. +"$SCRIPT_DIR/compile_ios_jit.sh" "$SRC_DIR/out/host_debug_unopt" "$SRC_DIR/out/$FLUTTER_ENGINE/clang_x64" +cd ios/Scenarios set -o pipefail && xcodebuild -sdk iphonesimulator \ -scheme Scenarios \ -destination 'platform=iOS Simulator,name=iPhone 8' \ test \ - FLUTTER_ENGINE=$FLUTTER_ENGINE -popd + FLUTTER_ENGINE="$FLUTTER_ENGINE" diff --git a/third_party/txt/src/txt/paragraph_txt.cc b/third_party/txt/src/txt/paragraph_txt.cc index fe50b789378da..264db4bafda79 100644 --- a/third_party/txt/src/txt/paragraph_txt.cc +++ b/third_party/txt/src/txt/paragraph_txt.cc @@ -1045,16 +1045,16 @@ void ParagraphTxt::Layout(double width) { return a.code_units.start < b.code_units.start; }); + double blob_x_pos_start = glyph_positions.front().x_pos.start; + double blob_x_pos_end = run.is_placeholder_run() + ? glyph_positions.back().x_pos.start + + run.placeholder_run()->width + : glyph_positions.back().x_pos.end; line_code_unit_runs.emplace_back( std::move(code_unit_positions), Range(run.start(), run.end()), - Range(glyph_positions.front().x_pos.start, - run.is_placeholder_run() - ? glyph_positions.back().x_pos.start + - run.placeholder_run()->width - : glyph_positions.back().x_pos.end), - line_number, *metrics, run.style(), run.direction(), - run.placeholder_run()); + Range(blob_x_pos_start, blob_x_pos_end), line_number, + *metrics, run.style(), run.direction(), run.placeholder_run()); if (run.is_placeholder_run()) { line_inline_placeholder_code_unit_runs.push_back( @@ -1062,8 +1062,8 @@ void ParagraphTxt::Layout(double width) { } if (!run.is_ghost()) { - min_left_ = std::min(min_left_, glyph_positions.front().x_pos.start); - max_right_ = std::max(max_right_, glyph_positions.back().x_pos.end); + min_left_ = std::min(min_left_, blob_x_pos_start); + max_right_ = std::max(max_right_, blob_x_pos_end); } } // for each in glyph_blobs diff --git a/third_party/txt/src/txt/paragraph_txt.h b/third_party/txt/src/txt/paragraph_txt.h index f5dc174e131ef..4941bb87d3e28 100644 --- a/third_party/txt/src/txt/paragraph_txt.h +++ b/third_party/txt/src/txt/paragraph_txt.h @@ -139,6 +139,7 @@ class ParagraphTxt : public Paragraph { FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, CenterAlignParagraph); FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, JustifyAlignParagraph); FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, JustifyRTL); + FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, InlinePlaceholderLongestLine); FRIEND_TEST_LINUX_ONLY(ParagraphTest, JustifyRTLNewLine); FRIEND_TEST(ParagraphTest, DecorationsParagraph); FRIEND_TEST(ParagraphTest, ItalicsParagraph); @@ -234,6 +235,7 @@ class ParagraphTxt : public Paragraph { end_(e), direction_(d), style_(&st), + is_ghost_(false), placeholder_run_(&placeholder) {} size_t start() const { return start_; } diff --git a/third_party/txt/tests/paragraph_unittests.cc b/third_party/txt/tests/paragraph_unittests.cc index 1626510008bbc..297136172caf5 100644 --- a/third_party/txt/tests/paragraph_unittests.cc +++ b/third_party/txt/tests/paragraph_unittests.cc @@ -1454,6 +1454,35 @@ TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderGetRectsParagraph)) { ASSERT_TRUE(Snapshot()); } +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderLongestLine)) { + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 1; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + txt::PlaceholderRun placeholder_run(50, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 0); + builder.AddPlaceholder(placeholder_run); + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + paragraph->Layout(GetTestCanvasWidth()); + + ASSERT_DOUBLE_EQ(paragraph->width_, GetTestCanvasWidth()); + ASSERT_TRUE(paragraph->longest_line_ < GetTestCanvasWidth()); + ASSERT_TRUE(paragraph->longest_line_ >= 50); +} + #if OS_LINUX // Tests if manually inserted 0xFFFC characters are replaced to 0xFFFD in order // to not interfere with the placeholder box layout. diff --git a/tools/android_lint/bin/main.dart b/tools/android_lint/bin/main.dart index f9a9e4359dc28..9c632d24d108e 100644 --- a/tools/android_lint/bin/main.dart +++ b/tools/android_lint/bin/main.dart @@ -65,7 +65,7 @@ Future runLint(ArgParser argParser, ArgResults argResults) async { await baselineXml.delete(); } } - print('Preparing projext.xml...'); + print('Preparing project.xml...'); final IOSink projectXml = File(projectXmlPath).openWrite(); projectXml.write( ''' @@ -154,7 +154,7 @@ ArgParser setupOptions() { ) ..addOption( 'out', - help: 'The path to write the generated the HTML report to. Ignored if ' + help: 'The path to write the generated HTML report. Ignored if ' '--html is not also true.', defaultsTo: path.join(projectDir, 'lint_report'), ); diff --git a/tools/const_finder/lib/const_finder.dart b/tools/const_finder/lib/const_finder.dart index 481bd24062c6a..88989059308d8 100644 --- a/tools/const_finder/lib/const_finder.dart +++ b/tools/const_finder/lib/const_finder.dart @@ -13,11 +13,11 @@ class _ConstVisitor extends RecursiveVisitor { this.classLibraryUri, this.className, ) : assert(kernelFilePath != null), - assert(classLibraryUri != null), - assert(className != null), - _visitedInstances = {}, - constantInstances = >[], - nonConstantLocations = >[]; + assert(classLibraryUri != null), + assert(className != null), + _visitedInstances = {}, + constantInstances = >[], + nonConstantLocations = >[]; /// The path to the file to open. final String kernelFilePath; @@ -32,9 +32,19 @@ class _ConstVisitor extends RecursiveVisitor { final List> constantInstances; final List> nonConstantLocations; + // A cache of previously evaluated classes. + static Map _classHeirarchyCache = {}; bool _matches(Class node) { - return node.enclosingLibrary.importUri.toString() == classLibraryUri && - node.name == className; + assert(node != null); + final bool result = _classHeirarchyCache[node]; + if (result != null) { + return result; + } + final bool exactMatch = node.name == className + && node.enclosingLibrary.importUri.toString() == classLibraryUri; + _classHeirarchyCache[node] = exactMatch + || node.supers.any((Supertype supertype) => _matches(supertype.classNode)); + return _classHeirarchyCache[node]; } // Avoid visiting the same constant more than once. diff --git a/tools/const_finder/test/const_finder_test.dart b/tools/const_finder/test/const_finder_test.dart index c3b1a8dc20cbe..e68146761b02b 100644 --- a/tools/const_finder/test/const_finder_test.dart +++ b/tools/const_finder/test/const_finder_test.dart @@ -71,11 +71,28 @@ void _checkConsts() { {'stringValue': '10', 'intValue': 10, 'targetValue': null}, {'stringValue': '9', 'intValue': 9}, {'stringValue': '7', 'intValue': 7, 'targetValue': null}, + {'stringValue': '11', 'intValue': 11, 'targetValue': null}, + {'stringValue': '12', 'intValue': 12, 'targetValue': null}, {'stringValue': 'package', 'intValue':-1, 'targetValue': null}, ], 'nonConstantLocations': [], }), ); + + final ConstFinder finder2 = ConstFinder( + kernelFilePath: constsDill, + classLibraryUri: 'package:const_finder_fixtures/target.dart', + className: 'MixedInTarget', + ); + expect( + jsonEncode(finder2.findInstances()), + jsonEncode({ + 'constantInstances': >[ + {'val': '13'}, + ], + 'nonConstantLocations': [], + }), + ); } void _checkNonConsts() { diff --git a/tools/const_finder/test/fixtures/lib/consts.dart b/tools/const_finder/test/fixtures/lib/consts.dart index 4095df150c94e..b5a7e10f1384c 100644 --- a/tools/const_finder/test/fixtures/lib/consts.dart +++ b/tools/const_finder/test/fixtures/lib/consts.dart @@ -31,6 +31,14 @@ void main() { final StaticConstInitializer staticConstMap = StaticConstInitializer(); staticConstMap.useOne(1); + + const ExtendsTarget extendsTarget = ExtendsTarget('11', 11, null); + extendsTarget.hit(); + const ImplementsTarget implementsTarget = ImplementsTarget('12', 12, null); + implementsTarget.hit(); + + const MixedInTarget mixedInTraget = MixedInTarget('13'); + mixedInTraget.hit(); } class IgnoreMe { diff --git a/tools/const_finder/test/fixtures/lib/target.dart b/tools/const_finder/test/fixtures/lib/target.dart index 4046216970ed9..bdaee2d3615cc 100644 --- a/tools/const_finder/test/fixtures/lib/target.dart +++ b/tools/const_finder/test/fixtures/lib/target.dart @@ -13,3 +13,39 @@ class Target { print('$stringValue $intValue'); } } + +class ExtendsTarget extends Target { + const ExtendsTarget(String stringValue, int intValue, Target targetValue) + : super(stringValue, intValue, targetValue); +} + +class ImplementsTarget implements Target { + const ImplementsTarget(this.stringValue, this.intValue, this.targetValue); + + @override + final String stringValue; + @override + final int intValue; + @override + final Target targetValue; + + @override + void hit() { + print('ImplementsTarget - $stringValue $intValue'); + } +} + +mixin MixableTarget { + String get val; + + void hit() { + print(val); + } +} + +class MixedInTarget with MixableTarget { + const MixedInTarget(this.val); + + @override + final String val; +} \ No newline at end of file diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index cbacb46d23c04..cd162d664eefc 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -63,14 +63,18 @@ int main(int argc, char** argv) { hb_blob_create_from_file(input_file_path.c_str())); if (!hb_blob_get_length(font_blob.get())) { std::cerr << "Failed to load input font " << input_file_path - << "; aborting." << std::endl; + << "; aborting. This error indicates that the font is invalid or " + "the current version of Harfbuzz is unable to process it." + << std::endl; return -1; } HarfbuzzWrappers::HbFacePtr font_face(hb_face_create(font_blob.get(), 0)); if (font_face.get() == hb_face_get_empty()) { std::cerr << "Failed to load input font face " << input_file_path - << "; aborting." << std::endl; + << "; aborting. This error indicates that the font is invalid or " + "the current version of Harfbuzz is unable to process it." + << std::endl; return -1; } @@ -103,13 +107,18 @@ int main(int argc, char** argv) { HarfbuzzWrappers::HbFacePtr new_face(hb_subset(font_face.get(), input.get())); if (new_face.get() == hb_face_get_empty()) { - std::cerr << "Failed to subset font; aborting." << std::endl; + std::cerr + << "Failed to subset font; aborting. This error normally indicates " + "the current version of Harfbuzz is unable to process it." + << std::endl; return -1; } HarfbuzzWrappers::HbBlobPtr result(hb_face_reference_blob(new_face.get())); if (!hb_blob_get_length(result.get())) { - std::cerr << "Failed get new font bytes; aborting" << std::endl; + std::cerr << "Failed get new font bytes; aborting. This error may indicate " + "low availability of memory or a bug in Harfbuzz." + << std::endl; return -1; } diff --git a/tools/fuchsia/build_fuchsia_artifacts.py b/tools/fuchsia/build_fuchsia_artifacts.py index 80944056133ee..709b386220123 100755 --- a/tools/fuchsia/build_fuchsia_artifacts.py +++ b/tools/fuchsia/build_fuchsia_artifacts.py @@ -10,6 +10,7 @@ import errno import os import platform +import re import shutil import subprocess import sys @@ -166,26 +167,49 @@ def CopyIcuDepsToBucket(src, dst): deps_bucket_path = os.path.join(_bucket_directory, dst) FindFileAndCopyTo('icudtl.dat', source_root, deps_bucket_path) -def BuildBucket(runtime_mode, arch, product): - out_dir = 'fuchsia_%s_%s/' % (runtime_mode, arch) - bucket_dir = 'flutter/%s/%s/' % (arch, runtime_mode) +def BuildBucket(runtime_mode, arch, optimized, product): + unopt = "_unopt" if not optimized else "" + out_dir = 'fuchsia_%s%s_%s/' % (runtime_mode, unopt, arch) + bucket_dir = 'flutter/%s/%s%s/' % (arch, runtime_mode, unopt) deps_dir = 'flutter/%s/deps/' % (arch) CopyToBucket(out_dir, bucket_dir, product) CopyVulkanDepsToBucket(out_dir, deps_dir, arch) CopyIcuDepsToBucket(out_dir, deps_dir) +def CheckCIPDPackageExists(package_name, tag): + '''Check to see if the current package/tag combo has been published''' + command = [ + 'cipd', + 'search', + package_name, + '-tag', + tag, + ] + stdout = subprocess.check_output(command) + match = re.search(r'No matching instances\.', stdout) + if match: + return False + else: + return True + + def ProcessCIPDPackage(upload, engine_version): # Copy the CIPD YAML template from the source directory to be next to the bucket # we are about to package. cipd_yaml = os.path.join(_script_dir, 'fuchsia.cipd.yaml') CopyFiles(cipd_yaml, os.path.join(_bucket_directory, 'fuchsia.cipd.yaml')) - if upload and IsLinux(): + tag = 'git_revision:%s' % engine_version + already_exists = CheckCIPDPackageExists('flutter/fuchsia', tag) + if already_exists: + print('CIPD package flutter/fuchsia tag %s already exists!' % tag) + + if upload and IsLinux() and not already_exists: command = [ 'cipd', 'create', '-pkg-def', 'fuchsia.cipd.yaml', '-ref', 'latest', '-tag', - 'git_revision:%s' % engine_version + tag, ] else: command = [ @@ -206,23 +230,9 @@ def ProcessCIPDPackage(upload, engine_version): if tries == num_tries - 1: raise -def GetRunnerTarget(runner_type, product, aot): - base = '%s/%s:' % (_fuchsia_base, runner_type) - if 'dart' in runner_type: - target = 'dart_' - else: - target = 'flutter_' - if aot: - target += 'aot_' - else: - target += 'jit_' - if product: - target += 'product_' - target += 'runner' - return base + target - -def BuildTarget(runtime_mode, arch, enable_lto, additional_targets=[]): - out_dir = 'fuchsia_%s_%s' % (runtime_mode, arch) +def BuildTarget(runtime_mode, arch, optimized, enable_lto, enable_legacy, asan, additional_targets=[]): + unopt = "_unopt" if not optimized else "" + out_dir = 'fuchsia_%s%s_%s' % (runtime_mode, unopt, arch) flags = [ '--fuchsia', '--fuchsia-cpu', @@ -231,8 +241,15 @@ def BuildTarget(runtime_mode, arch, enable_lto, additional_targets=[]): runtime_mode, ] + if not optimized: + flags.append('--unoptimized') + if not enable_lto: flags.append('--no-lto') + if not enable_legacy: + flags.append('--no-fuchsia-legacy') + if asan: + flags.append('--asan') RunGN(out_dir, flags) BuildNinjaTargets(out_dir, [ 'flutter' ] + additional_targets) @@ -254,6 +271,12 @@ def main(): required=False, help='Specifies the flutter engine SHA.') + parser.add_argument( + '--unoptimized', + action='store_true', + default=False, + help='If set, disables compiler optimization for the build.') + parser.add_argument( '--runtime-mode', type=str, @@ -263,12 +286,24 @@ def main(): parser.add_argument( '--archs', type=str, choices=['x64', 'arm64', 'all'], default='all') + parser.add_argument( + '--asan', + action='store_true', + default=False, + help='If set, enables address sanitization (including leak sanitization) for the build.') + parser.add_argument( '--no-lto', action='store_true', default=False, help='If set, disables LTO for the build.') + parser.add_argument( + '--no-legacy', + action='store_true', + default=False, + help='If set, disables legacy code for the build.') + parser.add_argument( '--skip-build', action='store_true', @@ -289,7 +324,9 @@ def main(): runtime_modes = ['debug', 'profile', 'release'] product_modes = [False, False, True] + optimized = not args.unoptimized enable_lto = not args.no_lto + enable_legacy = not args.no_legacy for arch in archs: for i in range(3): @@ -297,8 +334,8 @@ def main(): product = product_modes[i] if build_mode == 'all' or runtime_mode == build_mode: if not args.skip_build: - BuildTarget(runtime_mode, arch, enable_lto, args.targets.split(",")) - BuildBucket(runtime_mode, arch, product) + BuildTarget(runtime_mode, arch, optimized, enable_lto, enable_legacy, args.asan, args.targets.split(",")) + BuildBucket(runtime_mode, arch, optimized, product) if args.upload: if args.engine_version is None: diff --git a/tools/fuchsia/merge_and_upload_debug_symbols.py b/tools/fuchsia/merge_and_upload_debug_symbols.py index baa55983e0480..f623a5df82f9c 100755 --- a/tools/fuchsia/merge_and_upload_debug_symbols.py +++ b/tools/fuchsia/merge_and_upload_debug_symbols.py @@ -11,6 +11,7 @@ import json import os import platform +import re import shutil import subprocess import sys @@ -52,12 +53,37 @@ def WriteCIPDDefinition(target_arch, out_dir, symbol_dirs): return yaml_file +def CheckCIPDPackageExists(package_name, tag): + '''Check to see if the current package/tag combo has been published''' + command = [ + 'cipd', + 'search', + package_name, + '-tag', + tag, + ] + stdout = subprocess.check_output(command) + match = re.search(r'No matching instances\.', stdout) + if match: + return False + else: + return True + + def ProcessCIPDPackage(upload, cipd_yaml, engine_version, out_dir, target_arch): _packaging_dir = GetPackagingDir(out_dir) - if upload and IsLinux(): + tag = 'git_revision:%s' % engine_version + package_name = 'flutter/fuchsia-debug-symbols-%s' % target_arch + already_exists = CheckCIPDPackageExists( + package_name, + tag) + if already_exists: + print('CIPD package %s tag %s already exists!' % (package_name, tag)) + + if upload and IsLinux() and not already_exists: command = [ 'cipd', 'create', '-pkg-def', cipd_yaml, '-ref', 'latest', '-tag', - 'git_revision:%s' % engine_version + tag, ] else: command = [ diff --git a/tools/gn b/tools/gn index 6ec42f64e934c..673eac63d936d 100755 --- a/tools/gn +++ b/tools/gn @@ -38,6 +38,9 @@ def get_out_dir(args): if args.linux_cpu is not None: target_dir.append(args.linux_cpu) + if args.windows_cpu != 'x64': + target_dir.append(args.windows_cpu) + if args.target_os == 'fuchsia' and args.fuchsia_cpu is not None: target_dir.append(args.fuchsia_cpu) @@ -133,6 +136,9 @@ def to_gn_args(args): elif args.target_os == 'ios': gn_args['target_os'] = 'ios' gn_args['use_ios_simulator'] = args.simulator + elif args.target_os == 'fuchsia': + gn_args['target_os'] = 'fuchsia' + gn_args['flutter_enable_legacy_fuchsia_embedder'] = args.fuchsia_legacy elif args.target_os is not None: gn_args['target_os'] = args.target_os @@ -163,6 +169,8 @@ def to_gn_args(args): gn_args['target_cpu'] = args.linux_cpu elif args.target_os == 'fuchsia': gn_args['target_cpu'] = args.fuchsia_cpu + elif args.target_os == 'win': + gn_args['target_cpu'] = args.windows_cpu else: # Building host artifacts gn_args['target_cpu'] = 'x64' @@ -172,7 +180,7 @@ def to_gn_args(args): raise Exception('--interpreter is no longer needed on any supported platform.') gn_args['dart_target_arch'] = gn_args['target_cpu'] - if sys.platform.startswith(('cygwin', 'win')): + if sys.platform.startswith(('cygwin', 'win')) and args.target_os != 'win': if 'target_cpu' in gn_args: gn_args['target_cpu'] = cpu_for_target_arch(gn_args['target_cpu']) @@ -297,15 +305,19 @@ def parse_args(args): parser.add_argument('--full-dart-debug', default=False, action='store_true', help='Implies --dart-debug ' + 'and also disables optimizations in the Dart VM making it easier to step through VM code in the debugger.') - parser.add_argument('--target-os', type=str, choices=['android', 'ios', 'linux', 'fuchsia']) + parser.add_argument('--target-os', type=str, choices=['android', 'ios', 'linux', 'fuchsia', 'win']) parser.add_argument('--android', dest='target_os', action='store_const', const='android') parser.add_argument('--android-cpu', type=str, choices=['arm', 'x64', 'x86', 'arm64'], default='arm') parser.add_argument('--ios', dest='target_os', action='store_const', const='ios') parser.add_argument('--ios-cpu', type=str, choices=['arm', 'arm64'], default='arm64') parser.add_argument('--simulator', action='store_true', default=False) parser.add_argument('--fuchsia', dest='target_os', action='store_const', const='fuchsia') + parser.add_argument('--fuchsia-legacy', default=True, action='store_true') + parser.add_argument('--no-fuchsia-legacy', dest='fuchsia_legacy', action='store_false') + parser.add_argument('--linux-cpu', type=str, choices=['x64', 'x86', 'arm64', 'arm']) parser.add_argument('--fuchsia-cpu', type=str, choices=['x64', 'arm64'], default = 'x64') + parser.add_argument('--windows-cpu', type=str, choices=['x64', 'arm64'], default = 'x64') parser.add_argument('--arm-float-abi', type=str, choices=['hard', 'soft', 'softfp']) parser.add_argument('--goma', default=True, action='store_true') diff --git a/vulkan/BUILD.gn b/vulkan/BUILD.gn index c701763ea121f..5a94bf0e48963 100644 --- a/vulkan/BUILD.gn +++ b/vulkan/BUILD.gn @@ -11,7 +11,10 @@ config("vulkan_config") { include_dirs += [ "$fuchsia_sdk_root/vulkan/include" ] defines += [ "VK_USE_PLATFORM_FUCHSIA=1" ] } else { - include_dirs += [ "//third_party/vulkan/src" ] + include_dirs += [ + "//third_party/vulkan/src", + "//third_party/vulkan/include", + ] } } diff --git a/vulkan/vulkan_application.cc b/vulkan/vulkan_application.cc index 0f645af051bf1..3a1e4b1132051 100644 --- a/vulkan/vulkan_application.cc +++ b/vulkan/vulkan_application.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_application.h" @@ -15,7 +14,7 @@ namespace vulkan { VulkanApplication::VulkanApplication( - VulkanProcTable& p_vk, + VulkanProcTable& p_vk, // NOLINT const std::string& application_name, std::vector enabled_extensions, uint32_t application_version, @@ -108,15 +107,15 @@ VulkanApplication::VulkanApplication( } instance_ = {instance, [this](VkInstance i) { - FML_LOG(INFO) << "Destroying Vulkan instance"; + FML_DLOG(INFO) << "Destroying Vulkan instance"; vk.DestroyInstance(i, nullptr); }}; if (enable_instance_debugging) { auto debug_report = std::make_unique(vk, instance_); if (!debug_report->IsValid()) { - FML_LOG(INFO) << "Vulkan debugging was enabled but could not be setup " - "for this instance."; + FML_DLOG(INFO) << "Vulkan debugging was enabled but could not be setup " + "for this instance."; } else { debug_report_ = std::move(debug_report); FML_DLOG(INFO) << "Debug reporting is enabled."; diff --git a/vulkan/vulkan_application.h b/vulkan/vulkan_application.h index 3724929e0ff1b..eb1d23f26901a 100644 --- a/vulkan/vulkan_application.h +++ b/vulkan/vulkan_application.h @@ -24,7 +24,7 @@ class VulkanProcTable; /// create a VkInstance (with debug reporting optionally enabled). class VulkanApplication { public: - VulkanApplication(VulkanProcTable& vk, + VulkanApplication(VulkanProcTable& vk, // NOLINT const std::string& application_name, std::vector enabled_extensions, uint32_t application_version = VK_MAKE_VERSION(1, 0, 0), diff --git a/vulkan/vulkan_backbuffer.cc b/vulkan/vulkan_backbuffer.cc index 15dfb39988d58..d8be29e51d3db 100644 --- a/vulkan/vulkan_backbuffer.cc +++ b/vulkan/vulkan_backbuffer.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_backbuffer.h" diff --git a/vulkan/vulkan_command_buffer.cc b/vulkan/vulkan_command_buffer.cc index 54962bad6d346..c98a345fb3b75 100644 --- a/vulkan/vulkan_command_buffer.cc +++ b/vulkan/vulkan_command_buffer.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_command_buffer.h" diff --git a/vulkan/vulkan_debug_report.cc b/vulkan/vulkan_debug_report.cc index f8a5f3e2ad6b7..1e5478753607b 100644 --- a/vulkan/vulkan_debug_report.cc +++ b/vulkan/vulkan_debug_report.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_debug_report.h" @@ -192,8 +191,9 @@ VulkanDebugReport::VulkanDebugReport( } VkDebugReportFlagsEXT flags = kVulkanErrorFlags; - if (ValidationLayerInfoMessagesEnabled()) + if (ValidationLayerInfoMessagesEnabled()) { flags |= kVulkanInfoFlags; + } const VkDebugReportCallbackCreateInfoEXT create_info = { .sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT, .pNext = nullptr, diff --git a/vulkan/vulkan_device.cc b/vulkan/vulkan_device.cc index 49a8e96e7fe85..b4e0071b7e64a 100644 --- a/vulkan/vulkan_device.cc +++ b/vulkan/vulkan_device.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_device.h" diff --git a/vulkan/vulkan_handle.cc b/vulkan/vulkan_handle.cc index 375e9f9df0009..eb15d9ff13147 100644 --- a/vulkan/vulkan_handle.cc +++ b/vulkan/vulkan_handle.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_handle.h" diff --git a/vulkan/vulkan_image.cc b/vulkan/vulkan_image.cc index 6ba6bc23113c7..7f3b5eb9cdde4 100644 --- a/vulkan/vulkan_image.cc +++ b/vulkan/vulkan_image.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_image.h" diff --git a/vulkan/vulkan_interface.cc b/vulkan/vulkan_interface.cc index a1248061c33ff..5f0e67916e165 100644 --- a/vulkan/vulkan_interface.cc +++ b/vulkan/vulkan_interface.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_interface.h" diff --git a/vulkan/vulkan_native_surface.cc b/vulkan/vulkan_native_surface.cc index 20aba0fd8d692..4d10bf4a35783 100644 --- a/vulkan/vulkan_native_surface.cc +++ b/vulkan/vulkan_native_surface.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_native_surface.h" diff --git a/vulkan/vulkan_proc_table.cc b/vulkan/vulkan_proc_table.cc index 1fe6d1dc86192..9ed7c40b5c9ae 100644 --- a/vulkan/vulkan_proc_table.cc +++ b/vulkan/vulkan_proc_table.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_proc_table.h" diff --git a/vulkan/vulkan_surface.cc b/vulkan/vulkan_surface.cc index ac4406289cc0d..f410d43f81719 100644 --- a/vulkan/vulkan_surface.cc +++ b/vulkan/vulkan_surface.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_surface.h" @@ -11,8 +10,8 @@ namespace vulkan { VulkanSurface::VulkanSurface( - VulkanProcTable& p_vk, - VulkanApplication& application, + VulkanProcTable& p_vk, // NOLINT + VulkanApplication& application, // NOLINT std::unique_ptr native_surface) : vk(p_vk), application_(application), diff --git a/vulkan/vulkan_swapchain.cc b/vulkan/vulkan_swapchain.cc index 1ef045e64df2d..f5a85c25df0ac 100644 --- a/vulkan/vulkan_swapchain.cc +++ b/vulkan/vulkan_swapchain.cc @@ -222,14 +222,12 @@ sk_sp VulkanSwapchain::CreateSkiaSurface( return nullptr; } - const GrVkImageInfo image_info = { - image, // image - GrVkAlloc(), // alloc - VK_IMAGE_TILING_OPTIMAL, // tiling - VK_IMAGE_LAYOUT_UNDEFINED, // layout - surface_format_.format, // format - 1, // level count - }; + GrVkImageInfo image_info; + image_info.fImage = image; + image_info.fImageTiling = VK_IMAGE_TILING_OPTIMAL; + image_info.fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; + image_info.fFormat = surface_format_.format; + image_info.fLevelCount = 1; // TODO(chinmaygarde): Setup the stencil buffer and the sampleCnt. GrBackendRenderTarget backend_render_target(size.fWidth, size.fHeight, 0, diff --git a/vulkan/vulkan_utilities.cc b/vulkan/vulkan_utilities.cc index 2ad904bdfe877..e6aa4180c69d3 100644 --- a/vulkan/vulkan_utilities.cc +++ b/vulkan/vulkan_utilities.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "vulkan_utilities.h" #include "flutter/fml/build_config.h" diff --git a/web_sdk/test/api_conform_test.dart b/web_sdk/test/api_conform_test.dart index 8b7aee91fd562..ede590f271a01 100644 --- a/web_sdk/test/api_conform_test.dart +++ b/web_sdk/test/api_conform_test.dart @@ -39,7 +39,7 @@ void main() { final Map uiClasses = {}; final Map webClasses = {}; - // Gather all public classes from each library. For now we are skiping + // 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/'); diff --git a/web_sdk/web_engine_tester/lib/golden_tester.dart b/web_sdk/web_engine_tester/lib/golden_tester.dart index 49b40aaf7cc5c..d8cf77059fa59 100644 --- a/web_sdk/web_engine_tester/lib/golden_tester.dart +++ b/web_sdk/web_engine_tester/lib/golden_tester.dart @@ -65,7 +65,7 @@ Future matchGoldenFile(String filename, 'pixelComparison': pixelComparison.toString(), }; - // Chrome on macOS renders slighly differently from Linux, so allow it an + // Chrome on macOS renders slightly differently from Linux, so allow it an // extra 1% to deviate from the golden files. if (maxDiffRatePercent != null) { if (operatingSystem == OperatingSystem.macOs) { From 90d0b696b95ec0826293cdddede326bdbc9c26f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20DE=C3=81K=20JAHN?= Date: Sun, 23 Aug 2020 11:41:42 +0200 Subject: [PATCH 71/78] Squash 2. --- .cirrus.yml | 20 +- BUILD.gn | 16 +- DEPS | 53 +- build/generate_coverage.py | 8 +- ci/analyze.sh | 149 +- ci/bin/format.dart | 937 +++++++++++ ci/bin/lint.dart | 18 +- ci/build.sh | 53 +- ci/check_gn_format.py | 51 - ci/check_roll.sh | 35 +- ci/dev/README.md | 32 + ci/dev/prod_builders.json | 68 + ci/dev/try_builders.json | 74 + ci/firebase_testlab.sh | 30 +- ci/format.bat | 32 + ci/format.sh | 121 +- ci/licenses.sh | 199 ++- ci/licenses_golden/licenses_flutter | 146 +- ci/licenses_golden/licenses_fuchsia | 29 +- ci/licenses_golden/licenses_skia | 382 +++-- ci/licenses_golden/licenses_third_party | 316 +++- ci/lint.sh | 29 +- ci/pubspec.yaml | 7 +- ci/test.sh | 4 - common/config.gni | 119 +- common/settings.h | 21 +- flow/BUILD.gn | 67 +- flow/compositor_context.cc | 15 +- flow/compositor_context.h | 22 +- flow/embedded_views.cc | 4 + flow/embedded_views.h | 14 + flow/instrumentation.cc | 32 +- flow/instrumentation.h | 4 +- flow/layers/child_scene_layer.cc | 16 +- flow/layers/container_layer.cc | 68 +- flow/layers/fuchsia_layer_unittests.cc | 74 +- flow/layers/layer.cc | 52 +- flow/layers/layer.h | 17 +- flow/layers/layer_tree.cc | 90 +- flow/layers/layer_tree.h | 22 +- flow/layers/layer_tree_unittests.cc | 19 +- flow/layers/opacity_layer.h | 1 - flow/layers/performance_overlay_layer.cc | 12 +- flow/layers/physical_shape_layer.cc | 28 +- flow/layers/physical_shape_layer.h | 5 +- flow/layers/physical_shape_layer_unittests.cc | 4 +- flow/layers/picture_layer.cc | 16 +- flow/layers/picture_layer.h | 3 +- flow/layers/picture_layer_unittests.cc | 15 +- flow/layers/platform_view_layer.cc | 4 +- flow/layers/transform_layer.cc | 10 +- flow/matrix_decomposition.cc | 17 +- flow/matrix_decomposition_unittests.cc | 13 +- flow/paint_utils.cc | 1 - flow/raster_cache.cc | 56 +- flow/raster_cache.h | 40 +- flow/rtree_unittests.cc | 15 +- flow/scene_update_context.cc | 297 ++-- flow/scene_update_context.h | 209 +-- flow/skia_gpu_object.cc | 2 +- flow/skia_gpu_object.h | 4 +- flow/testing/layer_test.h | 9 +- flow/testing/mock_layer.cc | 1 - flow/testing/mock_layer.h | 2 - flow/testing/mock_layer_unittests.cc | 3 - flow/view_holder.cc | 15 +- flow/view_holder.h | 11 +- fml/file.cc | 11 +- fml/logging.cc | 3 +- fml/memory/ref_counted_unittest.cc | 16 +- fml/memory/weak_ptr_unittest.cc | 4 + fml/message_loop_task_queues.cc | 2 +- fml/message_loop_task_queues_unittests.cc | 7 + fml/raster_thread_merger.cc | 71 +- fml/raster_thread_merger.h | 36 +- fml/raster_thread_merger_unittests.cc | 93 ++ lib/io/dart_io.cc | 30 +- lib/io/dart_io.h | 4 +- lib/ui/BUILD.gn | 62 +- lib/ui/channel_buffers.dart | 2 +- lib/ui/compositing.dart | 12 +- lib/ui/compositing/scene.cc | 7 +- lib/ui/compositing/scene_builder.cc | 2 +- lib/ui/dart_ui.cc | 4 +- lib/ui/fixtures/ui_test.dart | 64 +- lib/ui/geometry.dart | 2 +- lib/ui/hooks.dart | 6 +- lib/ui/painting.dart | 284 +++- lib/ui/painting/canvas.cc | 13 +- lib/ui/painting/image.cc | 2 +- lib/ui/painting/multi_frame_codec.cc | 4 +- lib/ui/painting/multi_frame_codec.h | 5 +- lib/ui/painting/picture.cc | 2 +- lib/ui/text/font_collection.cc | 8 +- lib/ui/text/paragraph_builder.cc | 8 +- lib/ui/ui_dart_state.cc | 19 +- lib/ui/ui_dart_state.h | 13 +- lib/ui/window.dart | 99 +- lib/ui/window/platform_configuration.cc | 437 +++++ lib/ui/window/platform_configuration.h | 421 +++++ .../platform_configuration_unittests.cc | 139 ++ lib/ui/window/viewport_metrics.cc | 28 +- lib/ui/window/viewport_metrics.h | 36 +- lib/ui/window/window.cc | 417 +---- lib/ui/window/window.h | 81 +- lib/web_ui/CODE_CONVENTIONS.md | 62 + lib/web_ui/dev/README.md | 24 +- lib/web_ui/dev/browser_lock.yaml | 20 +- lib/web_ui/dev/chrome_installer.dart | 62 +- lib/web_ui/dev/common.dart | 4 +- lib/web_ui/dev/driver_manager.dart | 54 +- lib/web_ui/dev/driver_version.yaml | 1 - lib/web_ui/dev/felt_windows.bat | 6 +- lib/web_ui/dev/goldens_lock.yaml | 2 +- lib/web_ui/dev/macos_info.dart | 2 + lib/web_ui/dev/test_platform.dart | 31 +- lib/web_ui/dev/test_runner.dart | 187 ++- lib/web_ui/lib/assets/houdini_painter.js | 1069 ------------- lib/web_ui/lib/src/engine.dart | 112 +- lib/web_ui/lib/src/engine/bitmap_canvas.dart | 8 +- .../lib/src/engine/browser_detection.dart | 29 + .../{compositor => canvaskit}/canvas.dart | 7 +- .../canvaskit_api.dart | 282 +++- .../canvaskit_canvas.dart} | 27 +- .../color_filter.dart | 2 +- .../embedded_views.dart | 2 +- .../{compositor => canvaskit}/fonts.dart | 60 +- .../lib/src/engine/canvaskit/image.dart | 185 +++ .../image_filter.dart | 2 +- .../initialization.dart | 2 +- .../{compositor => canvaskit}/layer.dart | 0 .../layer_scene_builder.dart | 0 .../{compositor => canvaskit}/layer_tree.dart | 10 +- .../mask_filter.dart | 2 +- .../n_way_canvas.dart | 0 .../{compositor => canvaskit}/painting.dart | 12 +- .../{compositor => canvaskit}/path.dart | 4 +- .../path_metrics.dart | 0 .../{compositor => canvaskit}/picture.dart | 12 +- .../picture_recorder.dart | 0 .../platform_message.dart | 0 .../raster_cache.dart | 0 .../{compositor => canvaskit}/rasterizer.dart | 0 .../lib/src/engine/canvaskit/shader.dart | 186 +++ .../skia_object_cache.dart | 125 +- .../{compositor => canvaskit}/surface.dart | 47 +- .../{compositor => canvaskit}/text.dart | 60 +- .../{compositor => canvaskit}/util.dart | 0 .../{compositor => canvaskit}/vertices.dart | 57 +- .../viewport_metrics.dart | 0 lib/web_ui/lib/src/engine/color_filter.dart | 4 - .../lib/src/engine/compositor/image.dart | 117 -- lib/web_ui/lib/src/engine/engine_canvas.dart | 124 ++ lib/web_ui/lib/src/engine/houdini_canvas.dart | 370 ----- .../{surface => html}/backdrop_filter.dart | 0 .../src/engine/{surface => html}/canvas.dart | 18 +- .../src/engine/{surface => html}/clip.dart | 2 + .../debug_canvas_reuse_overlay.dart | 0 .../{surface => html}/image_filter.dart | 0 .../src/engine/{surface => html}/offset.dart | 0 .../src/engine/{surface => html}/opacity.dart | 0 .../engine/{surface => html}/painting.dart | 0 .../engine/{surface => html}/path/conic.dart | 0 .../engine/{surface => html}/path/cubic.dart | 0 .../engine/{surface => html}/path/path.dart | 6 - .../{surface => html}/path/path_metrics.dart | 0 .../{surface => html}/path/path_ref.dart | 2 +- .../{surface => html}/path/path_to_svg.dart | 0 .../{surface => html}/path/path_utils.dart | 12 +- .../{surface => html}/path/path_windings.dart | 18 +- .../{surface => html}/path/tangent.dart | 0 .../src/engine/{surface => html}/picture.dart | 483 +++--- .../{surface => html}/platform_view.dart | 0 .../{surface => html}/recording_canvas.dart | 319 +--- .../{surface => html}/render_vertices.dart | 6 +- .../src/engine/{surface => html}/scene.dart | 0 .../{surface => html}/scene_builder.dart | 5 +- .../lib/src/engine/{ => html}/shader.dart | 120 +- .../src/engine/{surface => html}/surface.dart | 0 .../{surface => html}/surface_stats.dart | 6 +- .../engine/{surface => html}/transform.dart | 0 .../lib/src/engine/html_image_codec.dart | 30 +- lib/web_ui/lib/src/engine/rrect_renderer.dart | 1 - .../lib/src/engine/text/line_breaker.dart | 179 ++- .../lib/src/engine/text/measurement.dart | 105 +- lib/web_ui/lib/src/engine/text/paragraph.dart | 135 +- lib/web_ui/lib/src/engine/text/ruler.dart | 28 + .../text_editing/text_capitalization.dart | 2 +- .../src/engine/text_editing/text_editing.dart | 289 +++- lib/web_ui/lib/src/engine/util.dart | 25 + lib/web_ui/lib/src/engine/window.dart | 24 +- lib/web_ui/lib/src/ui/annotations.dart | 30 - lib/web_ui/lib/src/ui/canvas.dart | 599 +------ lib/web_ui/lib/src/ui/channel_buffers.dart | 81 +- lib/web_ui/lib/src/ui/compositing.dart | 299 ---- lib/web_ui/lib/src/ui/geometry.dart | 981 +----------- lib/web_ui/lib/src/ui/hash_codes.dart | 52 +- lib/web_ui/lib/src/ui/initialization.dart | 24 +- lib/web_ui/lib/src/ui/lerp.dart | 2 - lib/web_ui/lib/src/ui/natives.dart | 17 +- lib/web_ui/lib/src/ui/painting.dart | 1419 +---------------- lib/web_ui/lib/src/ui/path.dart | 241 +-- lib/web_ui/lib/src/ui/path_metrics.dart | 108 -- lib/web_ui/lib/src/ui/pointer.dart | 240 +-- lib/web_ui/lib/src/ui/semantics.dart | 452 +----- lib/web_ui/lib/src/ui/test_embedding.dart | 14 +- lib/web_ui/lib/src/ui/text.dart | 1052 +----------- lib/web_ui/lib/src/ui/tile_mode.dart | 44 - lib/web_ui/lib/src/ui/window.dart | 751 +-------- lib/web_ui/pubspec.yaml | 1 + lib/web_ui/test/alarm_clock_test.dart | 5 + lib/web_ui/test/canvas_test.dart | 6 +- .../test/canvaskit/canvaskit_api_test.dart | 66 +- lib/web_ui/test/canvaskit/image_test.dart | 45 + .../test/canvaskit/path_metrics_test.dart | 5 + lib/web_ui/test/canvaskit/shader_test.dart | 81 + .../canvaskit/skia_objects_cache_test.dart | 19 +- lib/web_ui/test/canvaskit/test_data.dart | 29 + lib/web_ui/test/canvaskit/vertices_test.dart | 62 + lib/web_ui/test/clipboard_test.dart | 7 +- lib/web_ui/test/color_test.dart | 7 +- lib/web_ui/test/dom_renderer_test.dart | 7 +- .../test/engine/frame_reference_test.dart | 5 + lib/web_ui/test/engine/history_test.dart | 5 + .../engine/image/html_image_codec_test.dart | 29 +- lib/web_ui/test/engine/navigation_test.dart | 5 + lib/web_ui/test/engine/path_metrics_test.dart | 7 +- .../test/engine/pointer_binding_test.dart | 8 +- lib/web_ui/test/engine/profiler_test.dart | 5 + .../test/engine/recording_canvas_test.dart | 7 +- .../engine/semantics/accessibility_test.dart | 7 +- .../semantics/semantics_helper_test.dart | 8 +- .../test/engine/semantics/semantics_test.dart | 5 + .../engine/services/serialization_test.dart | 7 +- .../surface/path/path_iterator_test.dart | 90 ++ .../surface/path}/path_winding_test.dart | 7 +- .../engine/surface/scene_builder_test.dart | 12 +- .../test/engine/surface/surface_test.dart | 5 + lib/web_ui/test/engine/ulps_test.dart | 5 + lib/web_ui/test/engine/util_test.dart | 8 +- .../test/engine/web_experiments_test.dart | 5 + lib/web_ui/test/engine/window_test.dart | 5 + lib/web_ui/test/geometry_test.dart | 9 +- .../engine/backdrop_filter_golden_test.dart | 9 +- .../engine/canvas_arc_golden_test.dart | 11 +- .../engine/canvas_blend_golden_test.dart | 8 +- .../engine/canvas_clip_path_test.dart | 9 +- .../engine/canvas_context_test.dart | 9 +- .../engine/canvas_draw_image_golden_test.dart | 31 +- .../engine/canvas_draw_picture_test.dart | 9 +- .../engine/canvas_draw_points_test.dart | 10 +- .../engine/canvas_golden_test.dart | 9 +- .../engine/canvas_image_blend_mode_test.dart | 9 +- .../engine/canvas_lines_golden_test.dart | 9 +- .../engine/canvas_mask_filter_test.dart | 9 +- .../engine/canvas_rect_golden_test.dart | 9 +- .../engine/canvas_reuse_test.dart | 9 +- .../engine/canvas_rrect_golden_test.dart | 9 +- .../canvas_stroke_joins_golden_test.dart | 11 +- .../canvas_stroke_rects_golden_test.dart | 11 +- .../engine/canvas_to_picture_test.dart | 49 + .../engine/canvas_winding_rule_test.dart | 9 +- .../engine/compositing_golden_test.dart | 33 +- .../engine/conic_golden_test.dart | 9 +- .../engine/draw_vertices_golden_test.dart | 9 +- .../engine/linear_gradient_golden_test.dart | 9 +- .../multiline_text_clipping_golden_test.dart | 7 +- .../engine/path_metrics_test.dart | 9 +- .../engine/path_to_svg_golden_test.dart | 9 +- .../engine/path_transform_test.dart | 9 +- .../engine/picture_golden_test.dart | 7 +- .../engine/radial_gradient_golden_test.dart | 9 +- .../engine/recording_canvas_golden_test.dart | 9 +- .../test/golden_tests/engine/scuba.dart | 14 +- .../engine/shadow_golden_test.dart | 9 +- .../engine/text_overflow_golden_test.dart | 7 +- .../engine/text_placeholders_test.dart | 79 + .../engine/text_style_golden_test.dart | 9 +- .../golden_failure_smoke_test.dart | 5 + .../golden_success_smoke_test.dart | 7 +- lib/web_ui/test/gradient_test.dart | 9 +- lib/web_ui/test/hash_codes_test.dart | 5 + lib/web_ui/test/keyboard_test.dart | 8 +- lib/web_ui/test/locale_test.dart | 8 +- lib/web_ui/test/paragraph_builder_test.dart | 8 +- lib/web_ui/test/paragraph_test.dart | 49 +- lib/web_ui/test/path_test.dart | 5 + lib/web_ui/test/rect_test.dart | 8 +- lib/web_ui/test/rrect_test.dart | 8 +- .../test/text/font_collection_test.dart | 9 +- lib/web_ui/test/text/font_loading_test.dart | 7 +- lib/web_ui/test/text/line_breaker_test.dart | 89 +- lib/web_ui/test/text/measurement_test.dart | 10 +- lib/web_ui/test/text/word_breaker_test.dart | 5 + lib/web_ui/test/text_editing_test.dart | 415 ++++- lib/web_ui/test/text_test.dart | 7 +- lib/web_ui/test/title_test.dart | 7 +- lib/web_ui/test/window_test.dart | 5 + runtime/BUILD.gn | 44 +- runtime/dart_isolate.cc | 29 +- runtime/dart_isolate.h | 7 +- runtime/dart_isolate_unittests.cc | 89 +- runtime/dart_service_isolate.cc | 2 +- runtime/dart_vm.cc | 10 +- runtime/{window_data.cc => platform_data.cc} | 6 +- runtime/{window_data.h => platform_data.h} | 14 +- runtime/runtime_controller.cc | 158 +- runtime/runtime_controller.h | 53 +- runtime/service_protocol.cc | 36 +- runtime/service_protocol.h | 9 +- shell/common/BUILD.gn | 94 +- shell/common/animator.cc | 21 +- shell/common/animator.h | 1 + shell/common/animator_unittests.cc | 6 +- shell/common/engine.cc | 63 +- shell/common/engine.h | 40 +- shell/common/engine_unittests.cc | 234 +++ shell/common/pointer_data_dispatcher.h | 2 +- shell/common/rasterizer.cc | 80 +- shell/common/rasterizer.h | 85 +- shell/common/serialization_callbacks.cc | 4 +- shell/common/shell.cc | 190 +-- shell/common/shell.h | 46 +- shell/common/shell_benchmarks.cc | 6 +- shell/common/shell_test.cc | 62 +- shell/common/shell_test.h | 6 +- .../shell_test_external_view_embedder.cc | 29 +- .../shell_test_external_view_embedder.h | 24 +- shell/common/shell_test_platform_view_gl.cc | 2 +- shell/common/shell_test_platform_view_gl.h | 2 +- shell/common/shell_unittests.cc | 558 ++++++- shell/common/skp_shader_warmup_unittests.cc | 6 +- shell/common/switches.cc | 7 +- shell/common/switches.h | 15 +- shell/common/vsync_waiter.cc | 3 - shell/gpu/BUILD.gn | 61 +- shell/gpu/gpu.gni | 26 - shell/gpu/gpu_surface_gl.cc | 21 +- shell/gpu/gpu_surface_gl_delegate.h | 13 +- shell/platform/android/BUILD.gn | 4 + .../android/android_external_texture_gl.cc | 5 +- .../platform/android/android_shell_holder.cc | 23 +- shell/platform/android/android_shell_holder.h | 2 +- shell/platform/android/android_surface_gl.cc | 2 +- shell/platform/android/android_surface_gl.h | 2 +- .../external_view_embedder.cc | 5 + .../external_view_embedder.h | 2 + .../external_view_embedder_unittests.cc | 8 + .../android/FlutterFragmentActivity.java | 20 +- .../embedding/android/FlutterView.java | 2 +- .../engine/loader/ApplicationInfoLoader.java | 161 ++ .../engine/loader/FlutterApplicationInfo.java | 46 + .../engine/loader/FlutterLoader.java | 122 +- .../systemchannels/PlatformChannel.java | 16 +- .../systemchannels/TextInputChannel.java | 61 + .../plugin/common/JSONMethodCodec.java | 11 + .../flutter/plugin/common/MethodChannel.java | 15 +- .../io/flutter/plugin/common/MethodCodec.java | 14 + .../plugin/common/StandardMethodCodec.java | 18 + .../editing/InputConnectionAdaptor.java | 7 + .../plugin/editing/TextInputPlugin.java | 10 + .../plugin/platform/PlatformPlugin.java | 11 +- .../platform/PlatformViewsController.java | 166 +- .../io/flutter/view/AccessibilityBridge.java | 11 + .../android/io/flutter/view/FlutterView.java | 12 +- .../android/surface/android_surface_mock.cc | 2 +- .../android/surface/android_surface_mock.h | 2 +- .../test/io/flutter/FlutterTestSuite.java | 2 + .../android/FlutterFragmentActivityTest.java | 60 +- .../embedding/engine/PluginComponentTest.java | 8 +- .../loader/ApplicationInfoLoaderTest.java | 193 +++ .../systemchannels/PlatformChannelTest.java | 45 + .../common/StandardMethodCodecTest.java | 24 + .../editing/InputConnectionAdaptorTest.java | 312 ++++ .../plugin/editing/TextInputPluginTest.java | 80 + .../plugin/platform/PlatformPluginTest.java | 26 + .../platform/PlatformViewsControllerTest.java | 164 +- .../flutter/view/AccessibilityBridgeTest.java | 35 + .../common/cpp/client_wrapper/BUILD.gn | 56 +- .../basic_message_channel_unittests.cc | 5 +- .../client_wrapper/binary_messenger_impl.h | 50 + ...tream_wrappers.h => byte_buffer_streams.h} | 51 +- .../client_wrapper/core_implementations.cc | 150 ++ .../cpp/client_wrapper/core_wrapper_files.gni | 20 +- .../encodable_value_unittests.cc | 184 +-- .../client_wrapper/engine_method_result.cc | 46 +- .../include/flutter/byte_streams.h | 85 + .../include/flutter/encodable_value.h | 194 ++- .../include/flutter/event_channel.h | 4 +- .../include/flutter/event_sink.h | 33 +- .../include/flutter/method_result.h | 46 +- .../include/flutter/plugin_registrar.h | 11 +- .../flutter/standard_codec_serializer.h | 76 + .../include/flutter/standard_message_codec.h | 25 +- .../include/flutter/standard_method_codec.h | 27 +- .../method_channel_unittests.cc | 6 +- .../method_result_functions_unittests.cc | 7 +- .../cpp/client_wrapper/plugin_registrar.cc | 120 +- .../common/cpp/client_wrapper/publish.gni | 4 +- .../cpp/client_wrapper/standard_codec.cc | 398 +++-- .../standard_codec_serializer.h | 54 - .../standard_message_codec_unittests.cc | 89 +- .../standard_method_codec_unittests.cc | 37 +- .../testing/encodable_value_utils.cc | 89 -- .../testing/encodable_value_utils.h | 22 - .../testing/stub_flutter_api.cc | 8 - .../client_wrapper/testing/stub_flutter_api.h | 3 - .../testing/test_codec_extensions.cc | 80 + .../testing/test_codec_extensions.h | 89 ++ .../platform/common/cpp/json_method_codec.cc | 12 +- .../cpp/public/flutter_plugin_registrar.h | 13 - shell/platform/darwin/ios/BUILD.gn | 2 + .../darwin/ios/framework/Headers/Flutter.h | 46 - .../ios/framework/Headers/FlutterEngine.h | 53 +- .../framework/Headers/FlutterViewController.h | 32 +- .../framework/Source/FlutterDartProject.mm | 54 +- .../Source/FlutterDartProjectTest.mm | 85 + .../Source/FlutterDartProject_Internal.h | 6 +- .../ios/framework/Source/FlutterEngine.mm | 118 +- .../ios/framework/Source/FlutterEngineTest.mm | 22 +- .../framework/Source/FlutterEngine_Internal.h | 4 +- .../ios/framework/Source/FlutterEngine_Test.h | 10 + .../framework/Source/FlutterPlatformPlugin.mm | 14 + .../Source/FlutterPlatformPluginTest.mm | 51 + .../framework/Source/FlutterPlatformViews.mm | 119 +- .../Source/FlutterPlatformViewsTest.mm | 382 +++++ .../Source/FlutterPlatformViews_Internal.h | 49 +- .../Source/FlutterPlatformViews_Internal.mm | 118 +- .../Source/FlutterTextInputPlugin.mm | 49 +- .../Source/FlutterTextInputPluginTest.m | 343 ++-- .../framework/Source/FlutterViewController.mm | 54 +- .../Source/FlutterViewControllerTest.mm | 8 +- .../ios/framework/Source/SemanticsObject.mm | 1 + .../framework/Source/SemanticsObjectTest.mm | 1 + .../framework/Source/accessibility_bridge.h | 12 +- .../framework/Source/accessibility_bridge.mm | 54 +- .../Source/accessibility_bridge_ios.h | 7 + .../Source/accessibility_bridge_test.mm | 231 ++- .../darwin/ios/ios_external_texture_gl.h | 13 + .../darwin/ios/ios_external_texture_gl.mm | 108 +- .../darwin/ios/ios_external_texture_metal.h | 5 + .../darwin/ios/ios_external_texture_metal.mm | 162 +- shell/platform/darwin/ios/ios_surface.h | 5 +- shell/platform/darwin/ios/ios_surface.mm | 22 +- shell/platform/darwin/ios/ios_surface_gl.h | 2 +- shell/platform/darwin/ios/ios_surface_gl.mm | 4 +- .../platform/darwin/ios/ios_surface_metal.mm | 2 +- .../darwin/ios/ios_surface_software.mm | 2 +- .../platform/darwin/ios/platform_view_ios.mm | 10 +- .../macos/framework/Source/FlutterEngine.mm | 50 + .../framework/Source/FlutterViewController.mm | 1 + shell/platform/embedder/embedder.cc | 32 +- shell/platform/embedder/embedder.h | 123 +- shell/platform/embedder/embedder_engine.h | 1 - .../platform/embedder/embedder_surface_gl.cc | 4 +- shell/platform/embedder/embedder_surface_gl.h | 4 +- .../embedder/tests/embedder_config_builder.cc | 21 +- .../embedder/tests/embedder_config_builder.h | 6 + .../embedder/tests/embedder_test_context.cc | 7 +- .../embedder/tests/embedder_test_context.h | 6 +- .../embedder/tests/embedder_unittests.cc | 55 + .../fuchsia/dart-pkg/fuchsia/lib/fuchsia.dart | 21 +- .../dart-pkg/zircon/lib/src/handle.dart | 1 - .../zircon/lib/src/handle_waiter.dart | 1 - .../dart-pkg/zircon/lib/src/system.dart | 91 +- .../fuchsia/dart-pkg/zircon/lib/zircon.dart | 1 - shell/platform/fuchsia/dart_runner/BUILD.gn | 3 + .../fuchsia/dart_runner/embedder/builtin.dart | 3 +- .../fuchsia/dart_runner/kernel/BUILD.gn | 2 +- shell/platform/fuchsia/flutter/BUILD.gn | 485 +++--- shell/platform/fuchsia/flutter/component.cc | 6 + .../fuchsia/flutter/compositor_context.cc | 180 ++- .../fuchsia/flutter/compositor_context.h | 36 +- shell/platform/fuchsia/flutter/engine.cc | 306 ++-- shell/platform/fuchsia/flutter/engine.h | 34 +- .../fuchsia/flutter/engine_flutter_runner.gni | 150 -- .../platform/fuchsia/flutter/kernel/BUILD.gn | 2 +- .../platform/fuchsia/flutter/platform_view.cc | 236 +-- .../platform/fuchsia/flutter/platform_view.h | 38 +- .../fuchsia/flutter/platform_view_unittest.cc | 285 +++- .../fuchsia/flutter/session_connection.cc | 57 +- .../fuchsia/flutter/session_connection.h | 54 +- shell/platform/fuchsia/flutter/surface.cc | 9 +- shell/platform/fuchsia/flutter/surface.h | 4 +- .../tests/session_connection_unittests.cc | 25 +- .../fuchsia/flutter/vulkan_surface.cc | 21 +- .../platform/fuchsia/flutter/vulkan_surface.h | 89 +- .../fuchsia/flutter/vulkan_surface_pool.cc | 96 +- .../fuchsia/flutter/vulkan_surface_pool.h | 33 +- .../flutter/vulkan_surface_producer.cc | 25 +- .../fuchsia/flutter/vulkan_surface_producer.h | 47 +- shell/platform/glfw/client_wrapper/BUILD.gn | 2 + .../include/flutter/plugin_registrar_glfw.h | 12 +- .../testing/stub_flutter_glfw_api.cc | 8 + .../testing/stub_flutter_glfw_api.h | 3 + shell/platform/glfw/flutter_glfw.cc | 3 + shell/platform/glfw/public/flutter_glfw.h | 16 + .../linux/fl_basic_message_channel.cc | 21 +- shell/platform/linux/fl_binary_messenger.cc | 21 +- shell/platform/linux/fl_engine.cc | 56 +- shell/platform/linux/fl_json_method_codec.cc | 9 +- shell/platform/linux/fl_method_channel.cc | 21 +- shell/platform/linux/fl_method_codec.cc | 3 +- shell/platform/linux/fl_renderer.cc | 96 +- shell/platform/linux/fl_renderer.h | 65 +- shell/platform/linux/fl_renderer_headless.cc | 15 +- shell/platform/linux/fl_renderer_x11.cc | 73 +- shell/platform/linux/fl_renderer_x11.h | 9 - .../linux/fl_standard_message_codec.cc | 113 +- .../linux/fl_standard_method_codec.cc | 37 +- shell/platform/linux/fl_string_codec.cc | 4 +- shell/platform/linux/fl_text_input_plugin.cc | 37 +- shell/platform/linux/fl_text_input_plugin.h | 4 +- shell/platform/linux/fl_value.cc | 88 +- shell/platform/linux/fl_view.cc | 41 +- shell/platform/linux/testing/mock_renderer.cc | 24 +- shell/platform/windows/BUILD.gn | 10 + .../platform/windows/angle_surface_manager.cc | 32 +- .../platform/windows/angle_surface_manager.h | 17 +- .../platform/windows/client_wrapper/BUILD.gn | 18 +- .../windows/client_wrapper/flutter_engine.cc | 83 + .../flutter_engine_unittests.cc | 106 ++ .../client_wrapper/flutter_view_controller.cc | 68 +- .../flutter_view_controller_unittests.cc | 45 +- .../include/flutter/dart_project.h | 1 + .../include/flutter/flutter_engine.h | 94 ++ .../include/flutter/flutter_view_controller.h | 38 +- .../flutter/plugin_registrar_windows.h | 80 +- .../plugin_registrar_windows_unittests.cc | 129 +- .../testing/stub_flutter_windows_api.cc | 111 +- .../testing/stub_flutter_windows_api.h | 50 +- shell/platform/windows/cursor_handler.cc | 21 +- .../windows/flutter_project_bundle.cc | 81 + .../platform/windows/flutter_project_bundle.h | 65 + shell/platform/windows/flutter_windows.cc | 358 ++--- .../windows/flutter_windows_engine.cc | 259 +++ .../platform/windows/flutter_windows_engine.h | 126 ++ .../platform/windows/flutter_windows_view.cc | 123 +- shell/platform/windows/flutter_windows_view.h | 46 +- .../platform/windows/public/flutter_windows.h | 167 +- shell/platform/windows/system_utils.h | 36 + .../windows/system_utils_unittests.cc | 75 + shell/platform/windows/system_utils_win32.cc | 93 ++ .../windows/win32_dpi_utils_unittests.cc | 4 + .../windows/win32_flutter_window_unittests.cc | 4 + .../windows/win32_platform_handler.cc | 13 +- .../win32_window_proc_delegate_manager.cc | 42 + .../win32_window_proc_delegate_manager.h | 58 + ..._window_proc_delegate_manager_unittests.cc | 168 ++ .../windows/win32_window_unittests.cc | 4 + shell/platform/windows/window_state.h | 76 +- shell/testing/tester_main.cc | 9 +- sky/packages/sky_engine/LICENSE | 155 +- testing/BUILD.gn | 14 +- testing/dart/canvas_test.dart | 93 +- .../dart/window_hooks_integration_test.dart | 8 - testing/dart/window_test.dart | 10 +- testing/fuchsia/run_tests.sh | 54 +- testing/fuchsia/test_fars | 4 - testing/ios/IosUnitTests/App/Info.plist | 20 + .../IosUnitTests.xcodeproj/project.pbxproj | 27 + testing/ios/IosUnitTests/run_tests.sh | 7 +- testing/run_tests.py | 15 +- testing/scenario_app/.gitignore | 3 + testing/scenario_app/README.md | 13 + testing/scenario_app/android/app/build.gradle | 5 +- .../flutter/scenariosui/ScreenshotUtil.java | 6 +- .../scenarios/TestableFlutterActivity.java | 19 +- .../scenarios/TextPlatformViewActivity.java | 18 +- testing/scenario_app/android/build.gradle | 3 +- .../scenario_app/android/gradle-home/.vpython | 11 + .../android/gradle-home/bin/python | 14 + ...atformTextureUiTests__testPlatformView.png | Bin 33464 -> 29557 bytes ...xtureUiTests__testPlatformViewClippath.png | Bin 30028 -> 19560 bytes ...xtureUiTests__testPlatformViewCliprect.png | Bin 21585 -> 18389 bytes ...tureUiTests__testPlatformViewCliprrect.png | Bin 23520 -> 20209 bytes ...xtureUiTests__testPlatformViewMultiple.png | Bin 53141 -> 25037 bytes ...atformViewMultipleBackgroundForeground.png | Bin 11251 -> 11452 bytes ...estPlatformViewMultipleWithoutOverlays.png | Bin 62517 -> 43916 bytes ...extureUiTests__testPlatformViewOpacity.png | Bin 31570 -> 22231 bytes ...TextureUiTests__testPlatformViewRotate.png | Bin 33464 -> 22559 bytes ...tureUiTests__testPlatformViewTransform.png | Bin 24157 -> 22950 bytes ...estPlatformViewTwoIntersectingOverlays.png | Bin 27067 -> 30760 bytes ....PlatformViewUiTests__testPlatformView.png | Bin 39235 -> 32713 bytes ...mViewUiTests__testPlatformViewClippath.png | Bin 28517 -> 24012 bytes ...mViewUiTests__testPlatformViewCliprect.png | Bin 21011 -> 18636 bytes ...ViewUiTests__testPlatformViewCliprrect.png | Bin 30269 -> 25946 bytes ...mViewUiTests__testPlatformViewMultiple.png | Bin 33777 -> 28040 bytes ...atformViewMultipleBackgroundForeground.png | Bin 13277 -> 12557 bytes ...estPlatformViewMultipleWithoutOverlays.png | Bin 63761 -> 50031 bytes ...rmViewUiTests__testPlatformViewOpacity.png | Bin 30299 -> 25349 bytes ...ormViewUiTests__testPlatformViewRotate.png | Bin 28736 -> 24657 bytes ...ViewUiTests__testPlatformViewTransform.png | Bin 33845 -> 26722 bytes ...estPlatformViewTwoIntersectingOverlays.png | Bin 40144 -> 33889 bytes testing/scenario_app/assemble_apk.sh | 42 +- .../build_and_run_android_tests.sh | 63 +- .../scenario_app/build_and_run_ios_tests.sh | 54 +- testing/scenario_app/compile_android_aot.sh | 56 +- testing/scenario_app/compile_android_jit.sh | 100 ++ testing/scenario_app/compile_ios_aot.sh | 60 +- testing/scenario_app/compile_ios_jit.sh | 52 +- testing/scenario_app/firebase_xctest.sh | 54 +- .../Scenarios.xcodeproj/project.pbxproj | 4 + .../Scenarios/FlutterEngine+ScenariosTest.m | 2 +- .../ios/Scenarios/Scenarios/Info.plist | 2 - .../ScenariosTests/FlutterEngineTest.m | 2 +- .../FlutterViewControllerInitialRouteTest.m | 84 + .../FlutterViewControllerTest.m | 9 +- .../Scenarios/ScenariosTests/ScenariosTests.m | 10 +- ...tform_view_clippath_iPhone 8_simulator.png | Bin 20295 -> 20863 bytes ...form_view_cliprrect_iPhone 8_simulator.png | Bin 19558 -> 19543 bytes testing/scenario_app/lib/main.dart | 8 + .../lib/src/initial_route_reply.dart | 30 + testing/scenario_app/lib/src/scenario.dart | 22 +- testing/scenario_app/lib/src/scenarios.dart | 13 +- testing/scenario_app/run_android_tests.sh | 41 +- testing/scenario_app/run_ios_tests.sh | 43 +- third_party/txt/src/txt/paragraph_txt.cc | 18 +- third_party/txt/src/txt/paragraph_txt.h | 2 + third_party/txt/tests/paragraph_unittests.cc | 29 + tools/android_lint/bin/main.dart | 4 +- tools/const_finder/lib/const_finder.dart | 24 +- .../const_finder/test/const_finder_test.dart | 17 + .../test/fixtures/lib/consts.dart | 8 + .../test/fixtures/lib/target.dart | 36 + tools/font-subset/main.cc | 17 +- tools/fuchsia/build_fuchsia_artifacts.py | 85 +- .../fuchsia/merge_and_upload_debug_symbols.py | 30 +- tools/gn | 16 +- vulkan/BUILD.gn | 5 +- vulkan/vulkan_application.cc | 9 +- vulkan/vulkan_application.h | 2 +- vulkan/vulkan_backbuffer.cc | 1 - vulkan/vulkan_command_buffer.cc | 1 - vulkan/vulkan_debug_report.cc | 4 +- vulkan/vulkan_device.cc | 1 - vulkan/vulkan_handle.cc | 1 - vulkan/vulkan_image.cc | 1 - vulkan/vulkan_interface.cc | 1 - vulkan/vulkan_native_surface.cc | 1 - vulkan/vulkan_proc_table.cc | 1 - vulkan/vulkan_surface.cc | 5 +- vulkan/vulkan_swapchain.cc | 14 +- vulkan/vulkan_utilities.cc | 1 - web_sdk/test/api_conform_test.dart | 2 +- .../web_engine_tester/lib/golden_tester.dart | 2 +- 646 files changed, 19921 insertions(+), 16075 deletions(-) create mode 100644 ci/bin/format.dart create mode 100644 ci/dev/README.md create mode 100644 ci/dev/prod_builders.json create mode 100644 ci/dev/try_builders.json create mode 100644 ci/format.bat delete mode 100755 ci/test.sh create mode 100644 lib/ui/window/platform_configuration.cc create mode 100644 lib/ui/window/platform_configuration.h create mode 100644 lib/ui/window/platform_configuration_unittests.cc create mode 100644 lib/web_ui/CODE_CONVENTIONS.md delete mode 100644 lib/web_ui/lib/assets/houdini_painter.js rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/canvas.dart (96%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/canvaskit_api.dart (83%) rename lib/web_ui/lib/src/engine/{compositor/canvas_kit_canvas.dart => canvaskit/canvaskit_canvas.dart} (95%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/color_filter.dart (96%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/embedded_views.dart (99%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/fonts.dart (69%) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/image.dart rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/image_filter.dart (92%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/initialization.dart (97%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/layer.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/layer_scene_builder.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/layer_tree.dart (92%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/mask_filter.dart (91%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/n_way_canvas.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/painting.dart (94%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/path.dart (99%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/path_metrics.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/picture.dart (65%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/picture_recorder.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/platform_message.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/raster_cache.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/rasterizer.dart (100%) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/shader.dart rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/skia_object_cache.dart (69%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/surface.dart (80%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/text.dart (90%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/util.dart (100%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/vertices.dart (65%) rename lib/web_ui/lib/src/engine/{compositor => canvaskit}/viewport_metrics.dart (100%) delete mode 100644 lib/web_ui/lib/src/engine/compositor/image.dart delete mode 100644 lib/web_ui/lib/src/engine/houdini_canvas.dart rename lib/web_ui/lib/src/engine/{surface => html}/backdrop_filter.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/canvas.dart (97%) rename lib/web_ui/lib/src/engine/{surface => html}/clip.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/debug_canvas_reuse_overlay.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/image_filter.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/offset.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/opacity.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/painting.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/conic.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/cubic.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_metrics.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_ref.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_to_svg.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_utils.dart (97%) rename lib/web_ui/lib/src/engine/{surface => html}/path/path_windings.dart (96%) rename lib/web_ui/lib/src/engine/{surface => html}/path/tangent.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/picture.dart (87%) rename lib/web_ui/lib/src/engine/{surface => html}/platform_view.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/recording_canvas.dart (87%) rename lib/web_ui/lib/src/engine/{surface => html}/render_vertices.dart (99%) rename lib/web_ui/lib/src/engine/{surface => html}/scene.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/scene_builder.dart (98%) rename lib/web_ui/lib/src/engine/{ => html}/shader.dart (64%) rename lib/web_ui/lib/src/engine/{surface => html}/surface.dart (100%) rename lib/web_ui/lib/src/engine/{surface => html}/surface_stats.dart (98%) rename lib/web_ui/lib/src/engine/{surface => html}/transform.dart (100%) create mode 100644 lib/web_ui/test/canvaskit/image_test.dart create mode 100644 lib/web_ui/test/canvaskit/shader_test.dart create mode 100644 lib/web_ui/test/canvaskit/test_data.dart create mode 100644 lib/web_ui/test/canvaskit/vertices_test.dart create mode 100644 lib/web_ui/test/engine/surface/path/path_iterator_test.dart rename lib/web_ui/test/{ => engine/surface/path}/path_winding_test.dart (99%) create mode 100644 lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart create mode 100644 lib/web_ui/test/golden_tests/engine/text_placeholders_test.dart rename runtime/{window_data.cc => platform_data.cc} (63%) rename runtime/{window_data.h => platform_data.h} (81%) create mode 100644 shell/common/engine_unittests.cc create mode 100644 shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java create mode 100644 shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java create mode 100644 shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h rename shell/platform/common/cpp/client_wrapper/{byte_stream_wrappers.h => byte_buffer_streams.h} (58%) create mode 100644 shell/platform/common/cpp/client_wrapper/core_implementations.cc create mode 100644 shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h create mode 100644 shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h delete mode 100644 shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h delete mode 100644 shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.cc delete mode 100644 shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h create mode 100644 shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.cc create mode 100644 shell/platform/common/cpp/client_wrapper/testing/test_codec_extensions.h create mode 100644 shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm create mode 100644 shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h create mode 100644 shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm delete mode 100644 shell/platform/fuchsia/flutter/engine_flutter_runner.gni create mode 100644 shell/platform/windows/client_wrapper/flutter_engine.cc create mode 100644 shell/platform/windows/client_wrapper/flutter_engine_unittests.cc create mode 100644 shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h create mode 100644 shell/platform/windows/flutter_project_bundle.cc create mode 100644 shell/platform/windows/flutter_project_bundle.h create mode 100644 shell/platform/windows/flutter_windows_engine.cc create mode 100644 shell/platform/windows/flutter_windows_engine.h create mode 100644 shell/platform/windows/system_utils.h create mode 100644 shell/platform/windows/system_utils_unittests.cc create mode 100644 shell/platform/windows/system_utils_win32.cc create mode 100644 shell/platform/windows/win32_window_proc_delegate_manager.cc create mode 100644 shell/platform/windows/win32_window_proc_delegate_manager.h create mode 100644 shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc create mode 100644 testing/scenario_app/android/gradle-home/.vpython create mode 100755 testing/scenario_app/android/gradle-home/bin/python create mode 100755 testing/scenario_app/compile_android_jit.sh create mode 100644 testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m create mode 100644 testing/scenario_app/lib/src/initial_route_reply.dart diff --git a/.cirrus.yml b/.cirrus.yml index 5731441e4e0be..f5ca07262b00b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -4,9 +4,13 @@ web_shard_template: &WEB_SHARD_TEMPLATE only_if: "changesInclude('.cirrus.yml', 'DEPS', 'lib/web_ui/**', 'web_sdk/**') || $CIRRUS_PR == ''" environment: # As of March 2020, the Web shards needed 16G of RAM and 4 CPUs to run all framework tests with goldens without flaking. + # The tests are encountering a flake in Chrome. Increasing the number of shards to decrease race conditions. + # Shard number kept at 8 for the engine since 12 shards exhausted the Cirrus limits. + # https://github.com/flutter/flutter/issues/62510 + WEB_SHARD_COUNT: 8 CPU: 4 MEMORY: 16G - WEB_SHARD_COUNT: 4 + CHROME_NO_SANDBOX: true compile_host_script: | cd $ENGINE_PATH/src ./flutter/tools/gn --unoptimized --full-dart-sdk @@ -108,7 +112,19 @@ task: - name: web_tests-2-linux << : *WEB_SHARD_TEMPLATE - - name: web_tests-3_last-linux # last Web shard must end with _last + - name: web_tests-3-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-4-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-5-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-6-linux + << : *WEB_SHARD_TEMPLATE + + - name: web_tests-7_last-linux # last Web shard must end with _last << : *WEB_SHARD_TEMPLATE - name: build_test diff --git a/BUILD.gn b/BUILD.gn index 90ca39d027844..0f1c68a8990c5 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -19,18 +19,12 @@ config("config") { cflags = [ "/WX" ] # Treat warnings as errors. } } -} -# This "fuchsia_legacy" configuration includes old, non-embedder API sources and -# defines the LEGACY_FUCHSIA_EMBEDDER symbol. This config and its associated -# template are both transitional and will be removed after the embedder API -# transition is complete. -# -# See `source_set_maybe_fuchsia_legacy` in //flutter/common/config.gni -# -# TODO(fxb/54041): Remove when no longer neccesary. -config("fuchsia_legacy") { - if (is_fuchsia) { + # This define is transitional and will be removed after the embedder API + # transition is complete. + # + # TODO(bugs.fuchsia.dev/54041): Remove when no longer neccesary. + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { defines = [ "LEGACY_FUCHSIA_EMBEDDER" ] } } diff --git a/DEPS b/DEPS index 892cd355c9f5f..c711df19fece4 100644 --- a/DEPS +++ b/DEPS @@ -26,7 +26,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', - 'skia_revision': '8cc118dce81392f94da2a05de41a48fb34f54b1f', + 'skia_revision': '370cbc70e080ada9dd75b416d019d91304e1b168', # When updating the Dart revision, ensure that all entries that are # dependencies of Dart are also updated to match the entries in the @@ -34,7 +34,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS. # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'bd528bfbd69deecd3e8ad21e634da495bf0c09bb', + 'dart_revision': '3367c66051ce0b17c9d895ae3600f0c107d7a0cf', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -42,23 +42,21 @@ vars = { 'dart_boringssl_gen_rev': '429ccb1877f7987a6f3988228bc2440e61293499', 'dart_boringssl_rev': '4dfd5af70191b068aebe567b8e29ce108cee85ce', 'dart_collection_rev': '583693680fc067e34ca5b72503df25e8b80579f9', - 'dart_dart2js_info_tag': '0.6.0', 'dart_dart_style_tag': '1.3.6', 'dart_http_retry_tag': '0.1.1', 'dart_http_throttle_tag': '1.0.2', 'dart_intl_tag': '0.16.1', - 'dart_linter_tag': '0.1.117', + 'dart_linter_tag': '0.1.118', 'dart_oauth2_tag': '1.6.0', 'dart_protobuf_rev': '3746c8fd3f2b0147623a8e3db89c3ff4330de760', - 'dart_pub_rev': '04b054b62cc437cf23451785fdc50e49cd9de139', + 'dart_pub_rev': 'cf9795f3bb209504c349e20501f0b4b8ae31530c', 'dart_pub_semver_tag': 'v1.4.4', 'dart_quiver-dart_tag': '246e754fe45cecb6aa5f3f13b4ed61037ff0d784', 'dart_resource_rev': 'f8e37558a1c4f54550aa463b88a6a831e3e33cd6', - 'dart_root_certificates_rev': '16ef64be64c7dfdff2b9f4b910726e635ccc519e', + 'dart_root_certificates_rev': '7e5ec82c99677a2e5b95ce296c4d68b0d3378ed8', 'dart_shelf_packages_handler_tag': '2.0.0', 'dart_shelf_proxy_tag': '0.1.0+7', 'dart_shelf_static_rev': 'v0.2.8', - 'dart_shelf_tag': '0.7.3+3', 'dart_shelf_web_socket_tag': '0.2.2+3', 'dart_sse_tag': 'e5cf68975e8e87171a3dc297577aa073454a91dc', 'dart_stack_trace_tag': 'd3813ca0a77348e0faf0d6af0cc17913e36afa39', @@ -107,7 +105,7 @@ allowed_hosts = [ ] deps = { - 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'fe3b46e595e7ce1350e11aa0c90365976051f4a3', + 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'a6c0959d1ac8cdfe6f9ff87892bc4905a73699fe', # Fuchsia compatibility # @@ -125,7 +123,7 @@ deps = { Var('fuchsia_git') + '/third_party/rapidjson' + '@' + 'ef3564c5c8824989393b87df25355baf35ff544b', 'src/third_party/harfbuzz': - Var('fuchsia_git') + '/third_party/harfbuzz' + '@' + 'f5c000538699a4e40649508a44f41d37035e6c35', + Var('fuchsia_git') + '/third_party/harfbuzz' + '@' + '9c55f4cf3313d68d68f68419e7a57fb0771fcf49', 'src/third_party/libcxx': Var('fuchsia_git') + '/third_party/libcxx' + '@' + '7524ef50093a376f334a62a7e5cebf5d238d4c99', @@ -161,9 +159,6 @@ deps = { # WARNING: Unused Dart dependencies in the list below till "WARNING:" marker are removed automatically - see create_updated_flutter_deps.py. - 'src/third_party/dart/pkg/analysis_server/language_model': - {'packages': [{'version': 'lIRt14qoA1Cocb8j3yw_Fx5cfYou2ddam6ArBm4AI6QC', 'package': 'dart/language_model'}], 'dep_type': 'cipd'}, - 'src/third_party/dart/third_party/pkg/args': Var('dart_git') + '/args.git' + '@' + Var('dart_args_tag'), @@ -177,28 +172,28 @@ deps = { Var('dart_git') + '/boolean_selector.git@665e6921ab246569420376f827bff4585dff0b14', 'src/third_party/dart/third_party/pkg/charcode': - Var('dart_git') + '/charcode.git@af1e2d59a9c383da94f99ea51dac4b93fb0626c4', + Var('dart_git') + '/charcode.git@4a685faba42d86ebd9d661eadd1e79d0a1c34c43', 'src/third_party/dart/third_party/pkg/cli_util': - Var('dart_git') + '/cli_util.git@0.1.4', + Var('dart_git') + '/cli_util.git@0.2.0', 'src/third_party/dart/third_party/pkg/collection': Var('dart_git') + '/collection.git' + '@' + Var('dart_collection_rev'), 'src/third_party/dart/third_party/pkg/convert': - Var('dart_git') + '/convert.git@49bde5b371eb5c2c8e721557cf762f17c75e49fc', + Var('dart_git') + '/convert.git@c1b01f832835d3d8a06b0b246a361c0eaab35d3c', 'src/third_party/dart/third_party/pkg/crypto': - Var('dart_git') + '/crypto.git@7422fb2f6584fe1839eb30bc4ca56e9f9760b801', + Var('dart_git') + '/crypto.git@f7c48b334b1386bc5ab0f706fbcd6df8496a87fc', 'src/third_party/dart/third_party/pkg/csslib': Var('dart_git') + '/csslib.git@451448a9ac03f87a8d0377fc0b411d8c388a6cb4', 'src/third_party/dart/third_party/pkg/dart2js_info': - Var('dart_git') + '/dart2js_info.git' + '@' + Var('dart_dart2js_info_tag'), + Var('dart_git') + '/dart2js_info.git@94ba36cb77067f28b75a4212e77b810a2d7385e9', 'src/third_party/dart/third_party/pkg/dartdoc': - Var('dart_git') + '/dartdoc.git@6d5396c2b4bc415ab9cb3d8212b87ecffd90a272', + Var('dart_git') + '/dartdoc.git@291ebc50072746bc59ccab59115a298915218428', 'src/third_party/dart/third_party/pkg/ffi': Var('dart_git') + '/ffi.git@454ab0f9ea6bd06942a983238d8a6818b1357edb', @@ -231,7 +226,7 @@ deps = { Var('dart_git') + '/intl.git' + '@' + Var('dart_intl_tag'), 'src/third_party/dart/third_party/pkg/json_rpc_2': - Var('dart_git') + '/json_rpc_2.git@d589e635d8ccb7cda6a804bd571f88abbabab146', + Var('dart_git') + '/json_rpc_2.git@995611cf006c927d51cc53cb28f1aa4356d5414f', 'src/third_party/dart/third_party/pkg/linter': Var('dart_git') + '/linter.git' + '@' + Var('dart_linter_tag'), @@ -240,13 +235,13 @@ deps = { Var('dart_git') + '/logging.git@9561ba016ae607747ae69b846c0e10958ca58ed4', 'src/third_party/dart/third_party/pkg/markdown': - Var('dart_git') + '/markdown.git@acaddfe74217f62498b5cf0cf5429efa6a700be3', + Var('dart_git') + '/markdown.git@dbeafd47759e7dd0a167602153bb9c49fb5e5fe7', 'src/third_party/dart/third_party/pkg/matcher': Var('dart_git') + '/matcher.git@9cae8faa7868bf3a88a7ba45eb0bd128e66ac515', 'src/third_party/dart/third_party/pkg/mime': - Var('dart_git') + '/mime.git@179b5e6a88f4b63f36dc1b8fcbc1e83e5e0cd3a7', + Var('dart_git') + '/mime.git@0.9.7', 'src/third_party/dart/third_party/pkg/mockito': Var('dart_git') + '/mockito.git@d39ac507483b9891165e422ec98d9fb480037c8b', @@ -282,7 +277,7 @@ deps = { Var('dart_git') + '/resource.git' + '@' + Var('dart_resource_rev'), 'src/third_party/dart/third_party/pkg/shelf': - Var('dart_git') + '/shelf.git' + '@' + Var('dart_shelf_tag'), + Var('dart_git') + '/shelf.git@289309adc6c39aab0a63db676d550c517fc1cc2d', 'src/third_party/dart/third_party/pkg/shelf_packages_handler': Var('dart_git') + '/shelf_packages_handler.git' + '@' + Var('dart_shelf_packages_handler_tag'), @@ -324,7 +319,7 @@ deps = { Var('dart_git') + '/term_glyph.git@6a0f9b6fb645ba75e7a00a4e20072678327a0347', 'src/third_party/dart/third_party/pkg/test': - Var('dart_git') + '/test.git@c6b3fe63eda87da1687580071cad1eefd575f851', + Var('dart_git') + '/test.git@e37a93bbeae23b215972d1659ac865d71287ff6a', 'src/third_party/dart/third_party/pkg/test_reflective_loader': Var('dart_git') + '/test_reflective_loader.git' + '@' + Var('dart_test_reflective_loader_tag'), @@ -354,7 +349,7 @@ deps = { Var('dart_git') + '/package_config.git@9c586d04bd26fef01215fd10e7ab96a3050cfa64', 'src/third_party/dart/tools/sdks': - {'packages': [{'version': 'version:2.10.0-0.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, + {'packages': [{'version': 'version:2.10.0-3.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, # WARNING: end of dart dependencies list that is cleaned up automatically - see create_updated_flutter_deps.py. @@ -430,7 +425,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/sdk/build-tools/${{platform}}', - 'version': 'version:29.0.1' + 'version': 'version:30.0.1' } ], 'condition': 'download_android_deps', @@ -441,7 +436,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/sdk/platform-tools/${{platform}}', - 'version': 'version:29.0.2' + 'version': 'version:30.0.4' } ], 'condition': 'download_android_deps', @@ -452,7 +447,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/sdk/platforms', - 'version': 'version:29r1' + 'version': 'version:30r2' } ], 'condition': 'download_android_deps', @@ -521,7 +516,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/mac-amd64', - 'version': 'T2xc0OuiKH4DXmYwnM0GRhzMP1k2DGJQ3yccE4ld2hoC' + 'version': 'KGZUxRgWxdxH_PR6zLSQrUHqKZ-vWeFDNZ76kG9nDDIC' } ], 'condition': 'host_os == "mac"', @@ -541,7 +536,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'd_5wDVmBdmLoZFwim5qFY4G9xXel2vAMP6s_DY65ulsC' + 'version': 'LhYt1i9FPQeIhMU1ReI1t1fk4Zx0F3uQwhrr4NkLpuMC' } ], 'condition': 'host_os == "linux"', diff --git a/build/generate_coverage.py b/build/generate_coverage.py index fb6ad9d74514c..6286d30a43612 100755 --- a/build/generate_coverage.py +++ b/build/generate_coverage.py @@ -42,7 +42,7 @@ def RemoveIfExists(path): def main(): parser = argparse.ArgumentParser(); - + parser.add_argument('-t', '--tests', nargs='+', dest='tests', required=True, help='The unit tests to run and gather coverage data on.') parser.add_argument('-o', '--output', dest='output', @@ -64,19 +64,19 @@ def main(): # Run all unit tests and collect raw profiles. for test in args.tests: absolute_test_path = os.path.abspath(test) - + if not os.path.exists(absolute_test_path): print("Path %s does not exist." % absolute_test_path) return -1 binaries.append(absolute_test_path) - + raw_profile = absolute_test_path + ".rawprofile" RemoveIfExists(raw_profile) print "Running test %s to gather profile." % os.path.basename(absolute_test_path) - + subprocess.check_call([absolute_test_path], env={ "LLVM_PROFILE_FILE": raw_profile }) diff --git a/ci/analyze.sh b/ci/analyze.sh index 536d933289865..6eb2a529c949a 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -1,81 +1,94 @@ #!/bin/bash -echo "Analyzing dart:ui library..." +# +# 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. + +set -e + +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH -echo "Using analyzer from `which dartanalyzer`" +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -L "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) -dartanalyzer --version +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +FLUTTER_DIR="$SRC_DIR/flutter" +DART_BIN="$SRC_DIR/third_party/dart/tools/sdks/dart-sdk/bin" +PUB="$DART_BIN/pub" +DART_ANALYZER="$DART_BIN/dartanalyzer" -RESULTS=`dartanalyzer \ - --options flutter/analysis_options.yaml \ - --enable-experiment=non-nullable \ - "$1out/host_debug_unopt/gen/sky/bindings/dart_ui/ui.dart" \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+out/host_debug_unopt/gen/sky/bindings/dart_ui/ui\.dart"` +echo "Using analyzer from $DART_ANALYZER" -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +"$DART_ANALYZER" --version + +function analyze() ( + local last_arg="${!#}" + local results + # Grep sets its return status to non-zero if it doesn't find what it's + # looking for. + set +e + results="$("$DART_ANALYZER" "$@" 2>&1 | + grep -Ev "No issues found!" | + grep -Ev "Analyzing.+$last_arg")" + set -e + echo "$results" + if [ -n "$results" ]; then + echo "Failed analysis of $last_arg" + return 1 + else + echo "Success: no issues found in $last_arg" + fi + return 0 +) + +echo "Analyzing dart:ui library..." +analyze \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + --enable-experiment=non-nullable \ + "$SRC_DIR/out/host_debug_unopt/gen/sky/bindings/dart_ui/ui.dart" echo "Analyzing flutter_frontend_server..." -RESULTS=`dartanalyzer \ - --packages=flutter/flutter_frontend_server/.dart_tool/package_config.json \ - --options flutter/analysis_options.yaml \ - flutter/flutter_frontend_server \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+frontend_server"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +analyze \ + --packages="$FLUTTER_DIR/flutter_frontend_server/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + "$FLUTTER_DIR/flutter_frontend_server" echo "Analyzing tools/licenses..." -(cd flutter/tools/licenses && pub get) -RESULTS=`dartanalyzer \ - --packages=flutter/tools/licenses/.dart_tool/package_config.json \ - --options flutter/tools/licenses/analysis_options.yaml \ - flutter/tools/licenses \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+tools/licenses"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +(cd "$FLUTTER_DIR/tools/licenses" && "$PUB" get) +analyze \ + --packages="$FLUTTER_DIR/tools/licenses/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/tools/licenses/analysis_options.yaml" \ + "$FLUTTER_DIR/tools/licenses" echo "Analyzing testing/dart..." -flutter/tools/gn --unoptimized -ninja -C out/host_debug_unopt sky_engine sky_services -(cd flutter/testing/dart && pub get) -RESULTS=`dartanalyzer \ - --packages=flutter/testing/dart/.dart_tool/package_config.json \ - --options flutter/analysis_options.yaml \ - flutter/testing/dart \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+testing/dart"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi +"$FLUTTER_DIR/tools/gn" --unoptimized +ninja -C "$SRC_DIR/out/host_debug_unopt" sky_engine sky_services +(cd "$FLUTTER_DIR/testing/dart" && "$PUB" get) +analyze \ + --packages="$FLUTTER_DIR/testing/dart/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + "$FLUTTER_DIR/testing/dart" echo "Analyzing testing/scenario_app..." -(cd flutter/testing/scenario_app && pub get) -RESULTS=`dartanalyzer \ - --packages=flutter/testing/scenario_app/.dart_tool/package_config.json \ - --options flutter/analysis_options.yaml \ - flutter/testing/scenario_app \ - 2>&1 \ - | grep -Ev "No issues found!" \ - | grep -Ev "Analyzing.+testing/scenario_app"` -echo "$RESULTS" -if [ -n "$RESULTS" ]; then - echo "Failed." - exit 1; -fi \ No newline at end of file +(cd "$FLUTTER_DIR/testing/scenario_app" && "$PUB" get) +analyze \ + --packages="$FLUTTER_DIR/testing/scenario_app/.dart_tool/package_config.json" \ + --options "$FLUTTER_DIR/analysis_options.yaml" \ + "$FLUTTER_DIR/testing/scenario_app" diff --git a/ci/bin/format.dart b/ci/bin/format.dart new file mode 100644 index 0000000000000..1426edd54f97b --- /dev/null +++ b/ci/bin/format.dart @@ -0,0 +1,937 @@ +// 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 and fixes format on files with changes. +// +// Run with --help for usage. + +// TODO(gspencergoog): Support clang formatting on Windows. +// TODO(gspencergoog): Support Java formatting on Windows. +// TODO(gspencergoog): Convert to null safety. + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:isolate/isolate.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:process_runner/process_runner.dart'; +import 'package:process/process.dart'; + +class FormattingException implements Exception { + FormattingException(this.message, [this.result]); + + final String message; + final ProcessResult /*?*/ result; + + int get exitCode => result?.exitCode ?? -1; + + @override + String toString() { + final StringBuffer output = StringBuffer(runtimeType.toString()); + output.write(': $message'); + final String stderr = result?.stderr as String ?? ''; + if (stderr.isNotEmpty) { + output.write(':\n$stderr'); + } + return output.toString(); + } +} + +enum MessageType { + message, + error, + warning, +} + +enum FormatCheck { + clang, + java, + whitespace, + gn, +} + +FormatCheck nameToFormatCheck(String name) { + switch (name) { + case 'clang': + return FormatCheck.clang; + case 'java': + return FormatCheck.java; + case 'whitespace': + return FormatCheck.whitespace; + case 'gn': + return FormatCheck.gn; + } + assert(false, 'Unknown FormatCheck type $name'); + return null; +} + +String formatCheckToName(FormatCheck check) { + switch (check) { + case FormatCheck.clang: + return 'C++/ObjC'; + case FormatCheck.java: + return 'Java'; + case FormatCheck.whitespace: + return 'Trailing whitespace'; + case FormatCheck.gn: + return 'GN'; + } + assert(false, 'Unhandled FormatCheck type $check'); + return null; +} + +List formatCheckNames() { + List allowed; + if (!Platform.isWindows) { + allowed = FormatCheck.values; + } else { + allowed = [FormatCheck.gn, FormatCheck.whitespace]; + } + return allowed + .map((FormatCheck check) => check.toString().replaceFirst('$FormatCheck.', '')) + .toList(); +} + +Future _runGit( + List args, + ProcessRunner processRunner, { + bool failOk = false, +}) async { + final ProcessRunnerResult result = await processRunner.runProcess( + ['git', ...args], + failOk: failOk, + ); + return result.stdout; +} + +typedef MessageCallback = Function(String message, {MessageType type}); + +/// Base class for format checkers. +/// +/// Provides services that all format checkers need. +abstract class FormatChecker { + FormatChecker({ + ProcessManager /*?*/ processManager, + @required this.baseGitRef, + @required this.repoDir, + @required this.srcDir, + this.allFiles = false, + this.messageCallback, + }) : _processRunner = ProcessRunner( + defaultWorkingDirectory: repoDir, + processManager: processManager ?? const LocalProcessManager(), + ); + + /// Factory method that creates subclass format checkers based on the type of check. + factory FormatChecker.ofType( + FormatCheck check, { + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) { + switch (check) { + case FormatCheck.clang: + return ClangFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + case FormatCheck.java: + return JavaFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + case FormatCheck.whitespace: + return WhitespaceFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + case FormatCheck.gn: + return GnFormatChecker( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + break; + } + assert(false, 'Unhandled FormatCheck type $check'); + return null; + } + + final ProcessRunner _processRunner; + final Directory srcDir; + final Directory repoDir; + final bool allFiles; + MessageCallback /*?*/ messageCallback; + final String baseGitRef; + + /// Override to provide format checking for a specific type. + Future checkFormatting(); + + /// Override to provide format fixing for a specific type. + Future fixFormatting(); + + @protected + void message(String string) => messageCallback?.call(string, type: MessageType.message); + + @protected + void error(String string) => messageCallback?.call(string, type: MessageType.error); + + @protected + Future runGit(List args) async => _runGit(args, _processRunner); + + /// Converts a given raw string of code units to a stream that yields those + /// code units. + /// + /// Uses to convert the stdout of a previous command into an input stream for + /// the next command. + @protected + Stream> codeUnitsAsStream(List input) async* { + yield input; + } + + @protected + Future applyPatch(List patches) async { + final ProcessPool patchPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('patch'), + ); + final List jobs = patches.map((String patch) { + return WorkerJob( + ['patch', '-p0'], + stdinRaw: codeUnitsAsStream(patch.codeUnits), + failOk: true, + ); + }).toList(); + final List completedJobs = await patchPool.runToCompletion(jobs); + if (patchPool.failedJobs != 0) { + error('${patchPool.failedJobs} patch${patchPool.failedJobs > 1 ? 'es' : ''} ' + 'failed to apply.'); + completedJobs + .where((WorkerJob job) => job.result.exitCode != 0) + .map((WorkerJob job) => job.result.output) + .forEach(message); + } + return patchPool.failedJobs == 0; + } + + /// Gets the list of files to operate on. + /// + /// If [allFiles] is true, then returns all git controlled files in the repo + /// of the given types. + /// + /// If [allFiles] is false, then only return those files of the given types + /// that have changed between the current working tree and the [baseGitRef]. + @protected + Future> getFileList(List types) async { + String output; + if (allFiles) { + output = await runGit([ + 'ls-files', + '--', + ...types, + ]); + } else { + output = await runGit([ + 'diff', + '-U0', + '--no-color', + '--diff-filter=d', + '--name-only', + baseGitRef, + '--', + ...types, + ]); + } + return output.split('\n').where((String line) => line.isNotEmpty).toList(); + } + + /// Generates a reporting function to supply to ProcessRunner to use instead + /// of the default reporting function. + @protected + ProcessPoolProgressReporter namedReport(String name) { + return (int total, int completed, int inProgress, int pending, int failed) { + final String percent = + total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3); + final String completedStr = completed.toString().padLeft(3); + final String totalStr = total.toString().padRight(3); + final String inProgressStr = inProgress.toString().padLeft(2); + final String pendingStr = pending.toString().padLeft(3); + final String failedStr = failed.toString().padLeft(3); + + stderr.write('$name Jobs: $percent% done, ' + '$completedStr/$totalStr completed, ' + '$inProgressStr in progress, ' + '$pendingStr pending, ' + '$failedStr failed.${' ' * 20}\r'); + }; + } + + /// Clears the last printed report line so garbage isn't left on the terminal. + @protected + void reportDone() { + stderr.write('\r${' ' * 100}\r'); + } +} + +/// Checks and formats C++/ObjC files using clang-format. +class ClangFormatChecker extends FormatChecker { + ClangFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ) { + /*late*/ String clangOs; + if (Platform.isLinux) { + clangOs = 'linux-x64'; + } else if (Platform.isMacOS) { + clangOs = 'mac-x64'; + } else { + throw FormattingException( + "Unknown operating system: don't know how to run clang-format here."); + } + clangFormat = File( + path.join( + srcDir.absolute.path, + 'buildtools', + clangOs, + 'clang', + 'bin', + 'clang-format', + ), + ); + } + + /*late*/ File clangFormat; + + @override + Future checkFormatting() async { + final List failures = await _getCFormatFailures(); + failures.map(stdout.writeln); + return failures.isEmpty; + } + + @override + Future fixFormatting() async { + message('Fixing C++/ObjC formatting...'); + final List failures = await _getCFormatFailures(fixing: true); + if (failures.isEmpty) { + return true; + } + return await applyPatch(failures); + } + + Future _getClangFormatVersion() async { + final ProcessRunnerResult result = + await _processRunner.runProcess([clangFormat.path, '--version']); + return result.stdout.trim(); + } + + Future> _getCFormatFailures({bool fixing = false}) async { + message('Checking C++/ObjC formatting...'); + const List clangFiletypes = [ + '*.c', + '*.cc', + '*.cxx', + '*.cpp', + '*.h', + '*.m', + '*.mm', + ]; + final List files = await getFileList(clangFiletypes); + if (files.isEmpty) { + message('No C++/ObjC files with changes, skipping C++/ObjC format check.'); + return []; + } + if (verbose) { + message('Using ${await _getClangFormatVersion()}'); + } + final List clangJobs = []; + for (String file in files) { + if (file.trim().isEmpty) { + continue; + } + clangJobs.add(WorkerJob([clangFormat.path, '--style=file', file.trim()])); + } + final ProcessPool clangPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('clang-format'), + ); + final Stream completedClangFormats = clangPool.startWorkers(clangJobs); + final List diffJobs = []; + await for (final WorkerJob completedJob in completedClangFormats) { + if (completedJob.result != null && completedJob.result.exitCode == 0) { + diffJobs.add( + WorkerJob(['diff', '-u', completedJob.command.last, '-'], + stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw), failOk: true), + ); + } + } + final ProcessPool diffPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('diff'), + ); + final List completedDiffs = await diffPool.runToCompletion(diffJobs); + final Iterable failed = completedDiffs.where((WorkerJob job) { + return job.result.exitCode != 0; + }); + reportDone(); + if (failed.isNotEmpty) { + final bool plural = failed.length > 1; + if (fixing) { + message('Fixing ${failed.length} C++/ObjC file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + } else { + error('Found ${failed.length} C++/ObjC file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + for (final WorkerJob job in failed) { + stdout.write(job.result.stdout); + } + } + } else { + message('Completed checking ${diffJobs.length} C++/ObjC files with no formatting problems.'); + } + return failed.map((WorkerJob job) { + return job.result.stdout; + }).toList(); + } +} + +/// Checks the format of Java files uing the Google Java format checker. +class JavaFormatChecker extends FormatChecker { + JavaFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ) { + googleJavaFormatJar = File( + path.absolute( + path.join( + srcDir.absolute.path, + 'third_party', + 'android_tools', + 'google-java-format', + 'google-java-format-1.7-all-deps.jar', + ), + ), + ); + } + + /*late*/ File googleJavaFormatJar; + + Future _getGoogleJavaFormatVersion() async { + final ProcessRunnerResult result = await _processRunner + .runProcess(['java', '-jar', googleJavaFormatJar.path, '--version']); + return result.stderr.trim(); + } + + @override + Future checkFormatting() async { + final List failures = await _getJavaFormatFailures(); + failures.map(stdout.writeln); + return failures.isEmpty; + } + + @override + Future fixFormatting() async { + message('Fixing Java formatting...'); + final List failures = await _getJavaFormatFailures(fixing: true); + if (failures.isEmpty) { + return true; + } + return await applyPatch(failures); + } + + Future _getJavaVersion() async { + final ProcessRunnerResult result = + await _processRunner.runProcess(['java', '-version']); + return result.stderr.trim().split('\n')[0]; + } + + Future> _getJavaFormatFailures({bool fixing = false}) async { + message('Checking Java formatting...'); + final List formatJobs = []; + final List files = await getFileList(['*.java']); + if (files.isEmpty) { + message('No Java files with changes, skipping Java format check.'); + return []; + } + String javaVersion = ''; + String javaFormatVersion = ''; + try { + javaVersion = await _getJavaVersion(); + } on ProcessRunnerException { + error('Cannot run Java, skipping Java file formatting!'); + return const []; + } + try { + javaFormatVersion = await _getGoogleJavaFormatVersion(); + } on ProcessRunnerException { + error('Cannot find google-java-format, skipping Java format check.'); + return const []; + } + if (verbose) { + message('Using $javaFormatVersion with Java $javaVersion'); + } + for (String file in files) { + if (file.trim().isEmpty) { + continue; + } + formatJobs.add( + WorkerJob( + ['java', '-jar', googleJavaFormatJar.path, file.trim()], + ), + ); + } + final ProcessPool formatPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('Java format'), + ); + final Stream completedClangFormats = formatPool.startWorkers(formatJobs); + final List diffJobs = []; + await for (final WorkerJob completedJob in completedClangFormats) { + if (completedJob.result != null && completedJob.result.exitCode == 0) { + diffJobs.add( + WorkerJob( + ['diff', '-u', completedJob.command.last, '-'], + stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw), + failOk: true, + ), + ); + } + } + final ProcessPool diffPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('diff'), + ); + final List completedDiffs = await diffPool.runToCompletion(diffJobs); + final Iterable failed = completedDiffs.where((WorkerJob job) { + return job.result.exitCode != 0; + }); + reportDone(); + if (failed.isNotEmpty) { + final bool plural = failed.length > 1; + if (fixing) { + error('Fixing ${failed.length} Java file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + } else { + error('Found ${failed.length} Java file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + for (final WorkerJob job in failed) { + stdout.write(job.result.stdout); + } + } + } else { + message('Completed checking ${diffJobs.length} Java files with no formatting problems.'); + } + return failed.map((WorkerJob job) { + return job.result.stdout; + }).toList(); + } +} + +/// Checks the format of any BUILD.gn files using the "gn format" command. +class GnFormatChecker extends FormatChecker { + GnFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ) { + gnBinary = File( + path.join( + repoDir.absolute.path, + 'third_party', + 'gn', + Platform.isWindows ? 'gn.exe' : 'gn', + ), + ); + } + + /*late*/ File gnBinary; + + @override + Future checkFormatting() async { + message('Checking GN formatting...'); + return (await _runGnCheck(fixing: false)) == 0; + } + + @override + Future fixFormatting() async { + message('Fixing GN formatting...'); + await _runGnCheck(fixing: true); + // The GN script shouldn't fail when fixing errors. + return true; + } + + Future _runGnCheck({@required bool fixing}) async { + final List filesToCheck = await getFileList(['*.gn', '*.gni']); + + final List cmd = [ + gnBinary.path, + 'format', + if (!fixing) '--dry-run', + ]; + final List jobs = []; + for (final String file in filesToCheck) { + jobs.add(WorkerJob([...cmd, file])); + } + final ProcessPool gnPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('gn format'), + ); + final List completedJobs = await gnPool.runToCompletion(jobs); + reportDone(); + final List incorrect = []; + for (final WorkerJob job in completedJobs) { + if (job.result.exitCode == 2) { + incorrect.add(' ${job.command.last}'); + } + if (job.result.exitCode == 1) { + // GN has exit code 1 if it had some problem formatting/checking the + // file. + throw FormattingException( + 'Unable to format ${job.command.last}:\n${job.result.output}', + ); + } + } + if (incorrect.isNotEmpty) { + final bool plural = incorrect.length > 1; + if (fixing) { + message('Fixed ${incorrect.length} GN file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + } else { + error('Found ${incorrect.length} GN file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly:'); + incorrect.forEach(stderr.writeln); + } + } else { + message('All GN files formatted correctly.'); + } + return incorrect.length; + } +} + +@immutable +class _GrepResult { + const _GrepResult(this.file, this.hits, this.lineNumbers); + final File file; + final List hits; + final List lineNumbers; +} + +/// Checks for trailing whitspace in Dart files. +class WhitespaceFormatChecker extends FormatChecker { + WhitespaceFormatChecker({ + ProcessManager /*?*/ processManager, + @required String baseGitRef, + @required Directory repoDir, + @required Directory srcDir, + bool allFiles = false, + MessageCallback messageCallback, + }) : super( + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: allFiles, + messageCallback: messageCallback, + ); + + @override + Future checkFormatting() async { + final List failures = await _getWhitespaceFailures(); + return failures.isEmpty; + } + + static final RegExp trailingWsRegEx = RegExp(r'[ \t]+$', multiLine: true); + + @override + Future fixFormatting() async { + final List failures = await _getWhitespaceFailures(); + if (failures.isNotEmpty) { + for (File file in failures) { + stderr.writeln('Fixing $file'); + String contents = file.readAsStringSync(); + contents = contents.replaceAll(trailingWsRegEx, ''); + file.writeAsStringSync(contents); + } + } + return true; + } + + static Future<_GrepResult> _hasTrailingWhitespace(File file) async { + final List hits = []; + final List lineNumbers = []; + int lineNumber = 0; + for (final String line in file.readAsLinesSync()) { + if (trailingWsRegEx.hasMatch(line)) { + hits.add(line); + lineNumbers.add(lineNumber); + } + lineNumber++; + } + if (hits.isEmpty) { + return null; + } + return _GrepResult(file, hits, lineNumbers); + } + + Stream<_GrepResult> _whereHasTrailingWhitespace(Iterable files) async* { + final LoadBalancer pool = + await LoadBalancer.create(Platform.numberOfProcessors, IsolateRunner.spawn); + for (final File file in files) { + yield await pool.run<_GrepResult, File>(_hasTrailingWhitespace, file); + } + } + + Future> _getWhitespaceFailures() async { + final List files = await getFileList([ + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.dart', + '*.gn', + '*.gni', + '*.gradle', + '*.h', + '*.java', + '*.json', + '*.m', + '*.mm', + '*.py', + '*.sh', + '*.yaml', + ]); + if (files.isEmpty) { + message('No files that differ, skipping whitespace check.'); + return []; + } + message('Checking for trailing whitespace on ${files.length} source ' + 'file${files.length > 1 ? 's' : ''}...'); + + final ProcessPoolProgressReporter reporter = namedReport('whitespace'); + final List<_GrepResult> found = <_GrepResult>[]; + final int total = files.length; + int completed = 0; + int inProgress = Platform.numberOfProcessors; + int pending = total; + int failed = 0; + await for (final _GrepResult result in _whereHasTrailingWhitespace( + files.map( + (String file) => File( + path.join(repoDir.absolute.path, file), + ), + ), + )) { + if (result == null) { + completed++; + } else { + failed++; + found.add(result); + } + pending--; + inProgress = pending < Platform.numberOfProcessors ? pending : Platform.numberOfProcessors; + reporter(total, completed, inProgress, pending, failed); + } + reportDone(); + if (found.isNotEmpty) { + error('Whitespace check failed. The following files have trailing spaces:'); + for (final _GrepResult result in found) { + for (int i = 0; i < result.hits.length; ++i) { + message(' ${result.file.path}:${result.lineNumbers[i]}:${result.hits[i]}'); + } + } + } else { + message('No trailing whitespace found.'); + } + return found.map((_GrepResult result) => result.file).toList(); + } +} + +Future _getDiffBaseRevision(ProcessManager processManager, Directory repoDir) async { + final ProcessRunner processRunner = ProcessRunner( + defaultWorkingDirectory: repoDir, + processManager: processManager ?? const LocalProcessManager(), + ); + String upstream = 'upstream'; + final String upstreamUrl = await _runGit( + ['remote', 'get-url', upstream], + processRunner, + failOk: true, + ); + if (upstreamUrl.isEmpty) { + upstream = 'origin'; + } + await _runGit(['fetch', upstream, 'master'], processRunner); + String result = ''; + try { + // This is the preferred command to use, but developer checkouts often do + // not have a clear fork point, so we fall back to just the regular + // merge-base in that case. + result = await _runGit( + ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], + processRunner, + ); + } on ProcessRunnerException { + result = await _runGit(['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner); + } + return result.trim(); +} + +void _usage(ArgParser parser, {int exitCode = 1}) { + stderr.writeln('format.dart [--help] [--fix] [--all-files] ' + '[--check <${formatCheckNames().join('|')}>]'); + stderr.writeln(parser.usage); + exit(exitCode); +} + +bool verbose = false; + +Future main(List arguments) async { + final ArgParser parser = ArgParser(); + parser.addFlag('help', help: 'Print help.', abbr: 'h'); + parser.addFlag('fix', + abbr: 'f', + help: 'Instead of just checking for formatting errors, fix them in place.', + defaultsTo: false); + parser.addFlag('all-files', + abbr: 'a', + help: 'Instead of just checking for formatting errors in changed files, ' + 'check for them in all files.', + defaultsTo: false); + parser.addMultiOption('check', + abbr: 'c', + allowed: formatCheckNames(), + defaultsTo: formatCheckNames(), + help: 'Specifies which checks will be performed. Defaults to all checks. ' + 'May be specified more than once to perform multiple types of checks. ' + 'On Windows, only whitespace and gn checks are currently supported.'); + parser.addFlag('verbose', help: 'Print verbose output.', defaultsTo: verbose); + + ArgResults options; + try { + options = parser.parse(arguments); + } on FormatException catch (e) { + stderr.writeln('ERROR: $e'); + _usage(parser, exitCode: 0); + } + + verbose = options['verbose'] as bool; + + if (options['help'] as bool) { + _usage(parser, exitCode: 0); + } + + final File script = File.fromUri(Platform.script).absolute; + final Directory repoDir = script.parent.parent.parent; + final Directory srcDir = repoDir.parent; + if (verbose) { + stderr.writeln('Repo: $repoDir'); + stderr.writeln('Src: $srcDir'); + } + + void message(String message, {MessageType type = MessageType.message}) { + switch (type) { + case MessageType.message: + stderr.writeln(message); + break; + case MessageType.error: + stderr.writeln('ERROR: $message'); + break; + case MessageType.warning: + stderr.writeln('WARNING: $message'); + break; + } + } + + const ProcessManager processManager = LocalProcessManager(); + final String baseGitRef = await _getDiffBaseRevision(processManager, repoDir); + + bool result = true; + final List checks = options['check'] as List; + try { + for (final String checkName in checks) { + final FormatCheck check = nameToFormatCheck(checkName); + final String humanCheckName = formatCheckToName(check); + final FormatChecker checker = FormatChecker.ofType(check, + processManager: processManager, + baseGitRef: baseGitRef, + repoDir: repoDir, + srcDir: srcDir, + allFiles: options['all-files'] as bool, + messageCallback: message); + bool stepResult; + if (options['fix'] as bool) { + message('Fixing any $humanCheckName format problems'); + stepResult = await checker.fixFormatting(); + if (!stepResult) { + message('Unable to apply $humanCheckName format fixes.'); + } + } else { + message('Performing $humanCheckName format check'); + stepResult = await checker.checkFormatting(); + if (!stepResult) { + message('Found $humanCheckName format problems.'); + } + } + result = result && stepResult; + } + } on FormattingException catch (e) { + message('ERROR: $e', type: MessageType.error); + } + + exit(result ? 0 : 1); +} diff --git a/ci/bin/lint.dart b/ci/bin/lint.dart index 658388f4e9773..04ff295acd5de 100644 --- a/ci/bin/lint.dart +++ b/ci/bin/lint.dart @@ -1,9 +1,13 @@ -/// Runs clang-tidy on files with changes. -/// -/// usage: -/// dart lint.dart [clang-tidy checks] -/// -/// User environment variable FLUTTER_LINT_ALL to run on all files. +// 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. + +// Runs clang-tidy on files with changes. +// +// usage: +// dart lint.dart [clang-tidy checks] +// +// User environment variable FLUTTER_LINT_ALL to run on all files. import 'dart:async' show Completer; import 'dart:convert' show jsonDecode, utf8, LineSplitter; @@ -241,7 +245,7 @@ void main(List arguments) async { final ProcessPool pool = ProcessPool(); await for (final WorkerJob job in pool.startWorkers(jobs)) { - if (job.result.stdout.isEmpty) { + if (job.result?.stdout.isEmpty ?? true) { continue; } print('❌ Failures for ${job.name}:'); diff --git a/ci/build.sh b/ci/build.sh index 6e63dd9c9fd62..5555fc4efd5e3 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -1,21 +1,50 @@ #!/bin/bash -set -ex +# +# 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. -PATH="$HOME/depot_tools:$PATH" -cd .. +set -e -PATH=$(pwd)/third_party/dart/tools/sdks/dart-sdk/bin:$PATH +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +FLUTTER_DIR="$SRC_DIR/flutter" + +set -x + +PATH="$SRC_DIR/third_party/dart/tools/sdks/dart-sdk/bin:$HOME/depot_tools:$PATH" + +cd "$SRC_DIR" # Build the dart UI files -flutter/tools/gn --unoptimized -ninja -C out/host_debug_unopt generate_dart_ui +"$FLUTTER_DIR/tools/gn" --unoptimized +ninja -C "$SRC_DIR/out/host_debug_unopt" generate_dart_ui # Analyze the dart UI -flutter/ci/analyze.sh -flutter/ci/licenses.sh +"$FLUTTER_DIR/ci/analyze.sh" +"$FLUTTER_DIR/ci/licenses.sh" # Check that dart libraries conform -cd flutter/web_sdk -pub get -cd .. -dart web_sdk/test/api_conform_test.dart +(cd "$FLUTTER_DIR/web_sdk"; pub get) +(cd "$FLUTTER_DIR"; dart "web_sdk/test/api_conform_test.dart") \ No newline at end of file diff --git a/ci/check_gn_format.py b/ci/check_gn_format.py index 2257a58984dc6..e69de29bb2d1d 100755 --- a/ci/check_gn_format.py +++ b/ci/check_gn_format.py @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -# 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 sys -import subprocess -import os -import argparse -import errno -import shutil - -def GetGNFiles(directory): - directory = os.path.abspath(directory) - gn_files = [] - assert os.path.exists(directory), "Directory must exist %s" % directory - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith(".gn") or file.endswith(".gni"): - gn_files.append(os.path.join(root, file)) - return gn_files - -def main(): - parser = argparse.ArgumentParser(); - - parser.add_argument('--gn-binary', dest='gn_binary', required=True, type=str) - parser.add_argument('--dry-run', dest='dry_run', default=False, action='store_true') - parser.add_argument('--root-directory', dest='root_directory', required=True, type=str) - - args = parser.parse_args() - - gn_binary = os.path.abspath(args.gn_binary) - assert os.path.exists(gn_binary), "GN Binary must exist %s" % gn_binary - - gn_command = [ gn_binary, 'format'] - - if args.dry_run: - gn_command.append('--dry-run') - - for gn_file in GetGNFiles(args.root_directory): - if subprocess.call(gn_command + [ gn_file ]) != 0: - print "ERROR: '%s' is incorrectly formatted." % os.path.relpath(gn_file, args.root_directory) - print "Format the same with 'gn format' using the 'gn' binary in third_party/gn/gn." - print "Or, run ./ci/check_gn_format.py without '--dry-run'" - return 1 - - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/ci/check_roll.sh b/ci/check_roll.sh index 296300d3eaef8..ffb2cca9728d1 100755 --- a/ci/check_roll.sh +++ b/ci/check_roll.sh @@ -1,4 +1,36 @@ #!/bin/bash +# +# 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. + +set -e + +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +FLUTTER_DIR="$(cd "$SCRIPT_DIR/.."; pwd -P)" + +cd "$FLUTTER_DIR" if git remote get-url upstream >/dev/null 2>&1; then UPSTREAM=upstream/master @@ -7,7 +39,7 @@ else fi; FLUTTER_VERSION="$(curl -s https://raw.githubusercontent.com/flutter/flutter/master/bin/internal/engine.version)" -BEHIND="$(git rev-list $FLUTTER_VERSION..$UPSTREAM --oneline | wc -l)" +BEHIND="$(git rev-list "$FLUTTER_VERSION".."$UPSTREAM" --oneline | wc -l)" MAX_BEHIND=16 # no more than 4 bisections to identify the issue if [[ $BEHIND -le $MAX_BEHIND ]]; then @@ -18,4 +50,3 @@ else echo " please roll engine into flutter first before merging more commits into engine." exit 1 fi - diff --git a/ci/dev/README.md b/ci/dev/README.md new file mode 100644 index 0000000000000..6f5701397bfe6 --- /dev/null +++ b/ci/dev/README.md @@ -0,0 +1,32 @@ +This directory contains resources that the Flutter team uses during +the development of engine. + +## Luci builder file +`try_builders.json` and `prod_builders.json` contains the +supported luci try/prod builders for engine. It follows format: +```json +{ + "builders":[ + { + "name":"yyy", + "repo":"engine", + "enabled":true + } + ] +} +``` +for `try_builders.json`, and follows format: +```json +{ + "builders":[ + { + "name":"yyy", + "repo":"engine" + } + ] +} +``` +for `prod_builders.json`. `try_builders.json` will be mainly used in +[`flutter/cocoon`](https://github.com/flutter/cocoon) to trigger/update pre-submit +engine luci tasks, whereas `prod_builders.json` will be mainly used in `flutter/cocoon` +to push luci task statuses to GitHub. \ No newline at end of file diff --git a/ci/dev/prod_builders.json b/ci/dev/prod_builders.json new file mode 100644 index 0000000000000..435046df1bcd8 --- /dev/null +++ b/ci/dev/prod_builders.json @@ -0,0 +1,68 @@ +{ + "builders":[ + { + "name":"Linux Android AOT Engine", + "repo":"engine" + }, + { + "name":"Linux Android Debug Engine", + "repo":"engine" + }, + { + "name":"Linux Host Engine", + "repo":"engine" + }, + { + "name":"Linux Fuchsia", + "repo":"engine" + }, + { + "name":"Linux Fuchsia FEMU", + "repo":"engine" + }, + { + "name":"Linux Web Engine", + "repo":"engine" + }, + { + "name":"Mac Android AOT Engine", + "repo":"engine" + }, + { + "name":"Mac Android Debug Engine", + "repo":"engine" + }, + { + "name":"Mac Host Engine", + "repo":"engine" + }, + { + "name":"Mac iOS Engine", + "repo":"engine" + }, + { + "name":"Mac iOS Engine Profile", + "repo":"engine" + }, + { + "name":"Mac iOS Engine Release", + "repo":"engine" + }, + { + "name":"Mac Web Engine", + "repo":"engine" + }, + { + "name":"Windows Android AOT Engine", + "repo":"engine" + }, + { + "name":"Windows Host Engine", + "repo":"engine" + }, + { + "name":"Windows Web Engine", + "repo":"engine" + } + ] +} diff --git a/ci/dev/try_builders.json b/ci/dev/try_builders.json new file mode 100644 index 0000000000000..b4d5bbb807f76 --- /dev/null +++ b/ci/dev/try_builders.json @@ -0,0 +1,74 @@ +{ + "builders":[ + { + "name":"Linux Android AOT Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Android Debug Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Android Scenarios", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Fuchsia", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Host Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Linux Web Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Android AOT Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Android Debug Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Host Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac iOS Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Mac Web Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Windows Android AOT Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Windows Host Engine", + "repo":"engine", + "enabled": true + }, + { + "name":"Windows Web Engine", + "repo":"engine", + "enabled": true + } + ] +} diff --git a/ci/firebase_testlab.sh b/ci/firebase_testlab.sh index 76540ebcb2a6e..45631d9bfd025 100755 --- a/ci/firebase_testlab.sh +++ b/ci/firebase_testlab.sh @@ -1,16 +1,26 @@ #!/bin/bash +# +# 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. set -e -if [[ ! -f $1 ]]; then - echo "File $1 not found." - exit -1 +APP="$1" +if [[ -z "$APP" ]]; then + echo "Application must be specified as the first argument to the script." + exit 255 fi -GIT_REVISION=${2:-$(git rev-parse HEAD)} -BUILD_ID=${3:-$CIRRUS_BUILD_ID} +if [[ ! -f "$APP" ]]; then + echo "File '$APP' not found." + exit 255 +fi + +GIT_REVISION="${2:-$(git rev-parse HEAD)}" +BUILD_ID="${3:-$CIRRUS_BUILD_ID}" -if [[ ! -z $GCLOUD_FIREBASE_TESTLAB_KEY ]]; then +if [[ -n $GCLOUD_FIREBASE_TESTLAB_KEY ]]; then # New contributors will not have permissions to run this test - they won't be # able to access the service account information. We should just mark the test # as passed - it will run fine on post submit, where it will still catch @@ -21,8 +31,8 @@ if [[ ! -z $GCLOUD_FIREBASE_TESTLAB_KEY ]]; then exit 0 fi - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json + echo "$GCLOUD_FIREBASE_TESTLAB_KEY" > "${HOME}/gcloud-service-key.json" + gcloud auth activate-service-account --key-file="${HOME}/gcloud-service-key.json" fi # Run the test. @@ -32,8 +42,8 @@ fi # See https://firebase.google.com/docs/test-lab/android/game-loop gcloud --project flutter-infra firebase test android run \ --type game-loop \ - --app $1 \ + --app "$APP" \ --timeout 2m \ --results-bucket=gs://flutter_firebase_testlab \ - --results-dir=engine_scenario_test/$GIT_REVISION/$BUILD_ID \ + --results-dir="engine_scenario_test/$GIT_REVISION/$BUILD_ID" \ --no-auto-google-login diff --git a/ci/format.bat b/ci/format.bat new file mode 100644 index 0000000000000..12a6bb8d7af89 --- /dev/null +++ b/ci/format.bat @@ -0,0 +1,32 @@ +@ECHO off +REM Copyright 2013 The Flutter Authors. All rights reserved. +REM Use of this source code is governed by a BSD-style license that can be +REM found in the LICENSE file. + +REM ---------------------------------- NOTE ---------------------------------- +REM +REM Please keep the logic in this file consistent with the logic in the +REM `format.sh` script in the same directory to ensure that it continues to +REM work across all platforms! +REM +REM -------------------------------------------------------------------------- + +SETLOCAL ENABLEDELAYEDEXPANSION + +FOR %%i IN ("%~dp0..\..") DO SET SRC_DIR=%%~fi + +REM Test if Git is available on the Host +where /q git || ECHO Error: Unable to find git in your PATH. && EXIT /B 1 + +SET repo_dir=%SRC_DIR%\flutter +SET ci_dir=%repo_dir%\flutter\ci +SET dart_sdk_path=%SRC_DIR%\third_party\dart\tools\sdks\dart-sdk +SET dart=%dart_sdk_path%\bin\dart.exe +SET pub=%dart_sdk_path%\bin\pub.bat + +cd "%ci_dir%" + +REM Do not use the CALL command in the next line to execute Dart. CALL causes +REM Windows to re-read the line from disk after the CALL command has finished +REM regardless of the ampersand chain. +"%pub%" get & "%dart%" --disable-dart-dev bin\format.dart %* & exit /B !ERRORLEVEL! diff --git a/ci/format.sh b/ci/format.sh index ff45d2ad8b4ba..6725635583f98 100755 --- a/ci/format.sh +++ b/ci/format.sh @@ -1,100 +1,41 @@ #!/bin/bash # -# Code formatting presubmit -# -# This presubmit script ensures that code under the src/flutter directory is -# formatted according to the Flutter engine style requirements. On failure, a -# diff is emitted that can be applied from within the src/flutter directory -# via: -# -# patch -p0 < diff.patch +# 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. set -e -echo "Checking formatting..." - -case "$(uname -s)" in - Darwin) - OS="mac-x64" - ;; - Linux) - OS="linux-x64" - ;; - *) - echo "Unknown operating system." - exit -1 - ;; -esac - -# Tools -CLANG_FORMAT="../buildtools/$OS/clang/bin/clang-format" -$CLANG_FORMAT --version - -# Compute the diffs. -CLANG_FILETYPES="*.c *.cc *.cpp *.h *.m *.mm" -DIFF_OPTS="-U0 --no-color --name-only" - -if git remote get-url upstream >/dev/null 2>&1; then - UPSTREAM=upstream -else - UPSTREAM=origin -fi; +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH -BASE_SHA="$(git fetch $UPSTREAM master > /dev/null 2>&1 && \ - (git merge-base --fork-point FETCH_HEAD HEAD || git merge-base FETCH_HEAD HEAD))" -# Disable glob matching otherwise a file in the current directory that matches -# $CLANG_FILETYPES will cause git to query for that exact file instead of doing -# a match. -set -f -CLANG_FILES_TO_CHECK="$(git ls-files $CLANG_FILETYPES)" -set +f -FAILED_CHECKS=0 -for f in $CLANG_FILES_TO_CHECK; do - set +e - CUR_DIFF="$(diff -u "$f" <("$CLANG_FORMAT" --style=file "$f"))" - set -e - if [[ ! -z "$CUR_DIFF" ]]; then - echo "$CUR_DIFF" - FAILED_CHECKS=$(($FAILED_CHECKS+1)) - fi -done - -GOOGLE_JAVA_FORMAT="../third_party/android_tools/google-java-format/google-java-format-1.7-all-deps.jar" -if [[ -f "$GOOGLE_JAVA_FORMAT" && -f "$(which java)" ]]; then - java -jar "$GOOGLE_JAVA_FORMAT" --version 2>&1 - JAVA_FILETYPES="*.java" - JAVA_FILES_TO_CHECK="$(git diff $DIFF_OPTS $BASE_SHA -- $JAVA_FILETYPES)" - for f in $JAVA_FILES_TO_CHECK; do - set +e - CUR_DIFF="$(diff -u "$f" <(java -jar "$GOOGLE_JAVA_FORMAT" "$f"))" - set -e - if [[ ! -z "$CUR_DIFF" ]]; then - echo "$CUR_DIFF" - FAILED_CHECKS=$(($FAILED_CHECKS+1)) - fi +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" done -else - echo "WARNING: Cannot find google-java-format, skipping Java file formatting!" -fi - -if [[ $FAILED_CHECKS -ne 0 ]]; then - echo "" - echo "ERROR: Some files are formatted incorrectly. To fix, run \`./ci/format.sh | patch -p0\` from the flutter/engine/src/flutter directory." - exit 1 -fi + echo "$file" +) -FILETYPES="*.dart" - -set +e -TRAILING_SPACES=$(git diff $DIFF_OPTS $BASE_SHA..HEAD -- $FILETYPES | xargs grep --line-number --with-filename '[[:blank:]]\+$') -set -e -if [[ ! -z "$TRAILING_SPACES" ]]; then - echo "$TRAILING_SPACES" - echo "" - echo "ERROR: Some files have trailing spaces. To fix, try something like \`find . -name "*.dart" -exec sed -i -e 's/\s\+$//' {} \;\`." - exit 1 -fi +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +DART_SDK_DIR="${SRC_DIR}/third_party/dart/tools/sdks/dart-sdk" +DART="${DART_SDK_DIR}/bin/dart" +PUB="${DART_SDK_DIR}/bin/pub" -# Check GN format consistency -./ci/check_gn_format.py --dry-run --root-directory . --gn-binary "third_party/gn/gn" +cd "$SCRIPT_DIR" +"$PUB" get && "$DART" \ + --disable-dart-dev \ + bin/format.dart \ + "$@" diff --git a/ci/licenses.sh b/ci/licenses.sh index 58b06ea8a179e..48843bf58d761 100755 --- a/ci/licenses.sh +++ b/ci/licenses.sh @@ -1,81 +1,140 @@ #!/bin/bash +# +# 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. + set -e shopt -s nullglob +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH + +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +DART_BIN="$SRC_DIR/third_party/dart/tools/sdks/dart-sdk/bin" +PATH="$DART_BIN:$PATH" + echo "Verifying license script is still happy..." -echo "Using pub from `which pub`, dart from `which dart`" +echo "Using pub from $(command -v pub), dart from $(command -v dart)" -exitStatus=0 +untracked_files="$(cd "$SRC_DIR/flutter"; git status --ignored --short | grep -E "^!" | awk "{print\$2}")" +untracked_count="$(echo "$untracked_files" | wc -l)" +if [[ $untracked_count -gt 0 ]]; then + echo "" + echo "WARNING: There are $untracked_count untracked/ignored files or directories in the flutter repository." + echo "False positives may occur." + echo "You can use 'git clean -dxf' in the flutter dir to clean out these files." + echo "BUT, be warned that this will recursively remove all these files and directories:" + echo "$untracked_files" + echo "" +fi dart --version -# These files trip up the script on Mac OS X. -find . -name ".DS_Store" -exec rm {} \; - -(cd flutter/tools/licenses; pub get; dart --enable-asserts lib/main.dart --src ../../.. --out ../../../out/license_script_output --golden ../../ci/licenses_golden) - -for f in out/license_script_output/licenses_*; do - if ! cmp -s flutter/ci/licenses_golden/$(basename $f) $f - then - echo "============================= ERROR =============================" - echo "License script got different results than expected for $f." - echo "Please rerun the licenses script locally to verify that it is" - echo "correctly catching any new licenses for anything you may have" - echo "changed, and then update this file:" - echo " flutter/sky/packages/sky_engine/LICENSE" - echo "For more information, see the script in:" - echo " https://github.com/flutter/engine/tree/master/tools/licenses" - echo "" - diff -U 6 flutter/ci/licenses_golden/$(basename $f) $f - echo "=================================================================" - echo "" - exitStatus=1 - fi -done - -echo "Verifying license tool signature..." -if ! cmp -s flutter/ci/licenses_golden/tool_signature out/license_script_output/tool_signature -then - echo "============================= ERROR =============================" - echo "The license tool signature has changed. This is expected when" - echo "there have been changes to the license tool itself. Licenses have" - echo "been re-computed for all components. If only the license script has" - echo "changed, no diffs are typically expected in the output of the" - echo "script. Verify the output, and if it looks correct, update the" - echo "license tool signature golden file:" - echo " ci/licenses_golden/tool_signature" - echo "For more information, see the script in:" - echo " https://github.com/flutter/engine/tree/master/tools/licenses" - echo "" - diff -U 6 flutter/ci/licenses_golden/tool_signature out/license_script_output/tool_signature - echo "=================================================================" - echo "" - exitStatus=1 -fi +# Collects the license information from the repo. +# Runs in a subshell. +function collect_licenses() ( + cd "$SRC_DIR/flutter/tools/licenses" + pub get + dart --enable-asserts lib/main.dart \ + --src ../../.. \ + --out ../../../out/license_script_output \ + --golden ../../ci/licenses_golden +) -echo "Checking license count in licenses_flutter..." -actualLicenseCount=`tail -n 1 flutter/ci/licenses_golden/licenses_flutter | tr -dc '0-9'` -expectedLicenseCount=2 # When changing this number: Update the error message below as well describing all expected license types. - -if [ "$actualLicenseCount" -ne "$expectedLicenseCount" ] -then - echo "=============================== ERROR ===============================" - echo "The total license count in flutter/ci/licenses_golden/licenses_flutter" - echo "changed from $expectedLicenseCount to $actualLicenseCount." - echo "It's very likely that this is an unintentional change. Please" - echo "double-check that all newly added files have a BSD-style license" - echo "header with the following copyright:" - echo " Copyright 2013 The Flutter Authors. All rights reserved." - echo "Files in 'third_party/txt' may have an Apache license header instead." - echo "If you're absolutely sure that the change in license count is" - echo "intentional, update 'flutter/ci/licenses.sh' with the new count." - echo "=================================================================" - echo "" - exitStatus=1 -fi +# Verifies the licenses in the repo. +# Runs in a subshell. +function verify_licenses() ( + local exitStatus=0 + cd "$SRC_DIR" -if [ "$exitStatus" -eq "0" ] -then - echo "Licenses are as expected." -fi -exit $exitStatus + # These files trip up the script on Mac OS X. + find . -name ".DS_Store" -exec rm {} \; + + collect_licenses + + for f in out/license_script_output/licenses_*; do + if ! cmp -s "flutter/ci/licenses_golden/$(basename "$f")" "$f"; then + echo "============================= ERROR =============================" + echo "License script got different results than expected for $f." + echo "Please rerun the licenses script locally to verify that it is" + echo "correctly catching any new licenses for anything you may have" + echo "changed, and then update this file:" + echo " flutter/sky/packages/sky_engine/LICENSE" + echo "For more information, see the script in:" + echo " https://github.com/flutter/engine/tree/master/tools/licenses" + echo "" + diff -U 6 "flutter/ci/licenses_golden/$(basename "$f")" "$f" + echo "=================================================================" + echo "" + exitStatus=1 + fi + done + + echo "Verifying license tool signature..." + if ! cmp -s "flutter/ci/licenses_golden/tool_signature" "out/license_script_output/tool_signature"; then + echo "============================= ERROR =============================" + echo "The license tool signature has changed. This is expected when" + echo "there have been changes to the license tool itself. Licenses have" + echo "been re-computed for all components. If only the license script has" + echo "changed, no diffs are typically expected in the output of the" + echo "script. Verify the output, and if it looks correct, update the" + echo "license tool signature golden file:" + echo " ci/licenses_golden/tool_signature" + echo "For more information, see the script in:" + echo " https://github.com/flutter/engine/tree/master/tools/licenses" + echo "" + diff -U 6 "flutter/ci/licenses_golden/tool_signature" "out/license_script_output/tool_signature" + echo "=================================================================" + echo "" + exitStatus=1 + fi + + echo "Checking license count in licenses_flutter..." + + local actualLicenseCount + actualLicenseCount="$(tail -n 1 flutter/ci/licenses_golden/licenses_flutter | tr -dc '0-9')" + local expectedLicenseCount=2 # When changing this number: Update the error message below as well describing all expected license types. + + if [[ $actualLicenseCount -ne $expectedLicenseCount ]]; then + echo "=============================== ERROR ===============================" + echo "The total license count in flutter/ci/licenses_golden/licenses_flutter" + echo "changed from $expectedLicenseCount to $actualLicenseCount." + echo "It's very likely that this is an unintentional change. Please" + echo "double-check that all newly added files have a BSD-style license" + echo "header with the following copyright:" + echo " Copyright 2013 The Flutter Authors. All rights reserved." + echo "Files in 'third_party/txt' may have an Apache license header instead." + echo "If you're absolutely sure that the change in license count is" + echo "intentional, update 'flutter/ci/licenses.sh' with the new count." + echo "=================================================================" + echo "" + exitStatus=1 + fi + + if [[ $exitStatus -eq 0 ]]; then + echo "Licenses are as expected." + fi + return $exitStatus +) + +verify_licenses \ No newline at end of file diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 4cd5031ebf219..99124c9effdbb 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -399,6 +399,9 @@ FILE: ../../../flutter/lib/ui/ui_benchmarks.cc FILE: ../../../flutter/lib/ui/ui_dart_state.cc FILE: ../../../flutter/lib/ui/ui_dart_state.h FILE: ../../../flutter/lib/ui/window.dart +FILE: ../../../flutter/lib/ui/window/platform_configuration.cc +FILE: ../../../flutter/lib/ui/window/platform_configuration.h +FILE: ../../../flutter/lib/ui/window/platform_configuration_unittests.cc FILE: ../../../flutter/lib/ui/window/platform_message.cc FILE: ../../../flutter/lib/ui/window/platform_message.h FILE: ../../../flutter/lib/ui/window/platform_message_response.cc @@ -416,7 +419,6 @@ FILE: ../../../flutter/lib/ui/window/viewport_metrics.cc FILE: ../../../flutter/lib/ui/window/viewport_metrics.h FILE: ../../../flutter/lib/ui/window/window.cc FILE: ../../../flutter/lib/ui/window/window.h -FILE: ../../../flutter/lib/web_ui/lib/assets/houdini_painter.js FILE: ../../../flutter/lib/web_ui/lib/src/engine.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/assets.dart @@ -424,42 +426,69 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_location.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvas_pool.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/vertices.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/viewport_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/clipboard.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/color_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/color_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/embedded_views.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/fonts.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/image.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/image_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/initialization.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_tree.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/painting.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path_metrics.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/platform_message.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/raster_cache.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/rasterizer.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/surface.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/text.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/util.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/vertices.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/viewport_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/history.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/houdini_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/clip.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/image_filter.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 +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/conic.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/cubic.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_metrics.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_ref.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_to_svg.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_utils.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/path_windings.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/path/tangent.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/picture.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/platform_view.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/recording_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shader.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/keyboard.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart @@ -486,34 +515,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/buffers.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/message_codec.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/message_codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/serialization.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/shader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/shadow.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/clip.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/debug_canvas_reuse_overlay.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/image_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/offset.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/opacity.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/painting.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/conic.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/cubic.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_ref.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_utils.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_windings.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/tangent.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/picture.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/platform_view.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/recording_canvas.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/render_vertices.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene_builder.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/surface.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/surface_stats.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/transform.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/test_embedding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/font_collection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_break_properties.dart @@ -575,6 +577,8 @@ FILE: ../../../flutter/runtime/dart_vm_unittests.cc FILE: ../../../flutter/runtime/embedder_resources.cc FILE: ../../../flutter/runtime/embedder_resources.h FILE: ../../../flutter/runtime/fixtures/runtime_test.dart +FILE: ../../../flutter/runtime/platform_data.cc +FILE: ../../../flutter/runtime/platform_data.h FILE: ../../../flutter/runtime/ptrace_ios.cc FILE: ../../../flutter/runtime/ptrace_ios.h FILE: ../../../flutter/runtime/runtime_controller.cc @@ -587,8 +591,6 @@ FILE: ../../../flutter/runtime/skia_concurrent_executor.cc FILE: ../../../flutter/runtime/skia_concurrent_executor.h FILE: ../../../flutter/runtime/test_font_data.cc FILE: ../../../flutter/runtime/test_font_data.h -FILE: ../../../flutter/runtime/window_data.cc -FILE: ../../../flutter/runtime/window_data.h FILE: ../../../flutter/shell/common/animator.cc FILE: ../../../flutter/shell/common/animator.h FILE: ../../../flutter/shell/common/animator_unittests.cc @@ -597,6 +599,7 @@ FILE: ../../../flutter/shell/common/canvas_spy.h FILE: ../../../flutter/shell/common/canvas_spy_unittests.cc FILE: ../../../flutter/shell/common/engine.cc FILE: ../../../flutter/shell/common/engine.h +FILE: ../../../flutter/shell/common/engine_unittests.cc FILE: ../../../flutter/shell/common/fixtures/shell_test.dart FILE: ../../../flutter/shell/common/fixtures/shelltest_screenshot.png FILE: ../../../flutter/shell/common/input_events_unittests.cc @@ -726,6 +729,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -831,12 +836,15 @@ FILE: ../../../flutter/shell/platform/android/surface/android_surface_mock.h FILE: ../../../flutter/shell/platform/android/vsync_waiter_android.cc FILE: ../../../flutter/shell/platform/android/vsync_waiter_android.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/byte_stream_wrappers.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/binary_messenger_impl.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/byte_buffer_streams.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/core_implementations.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/engine_method_result.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/event_channel_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/basic_message_channel.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/byte_streams.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/engine_method_result.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/event_channel.h @@ -851,6 +859,7 @@ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registry.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_codec_serializer.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/method_call_unittests.cc @@ -859,7 +868,6 @@ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/method_result_fu FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_codec.cc -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_codec_serializer.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_message_codec_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/incoming_message_dispatcher.cc @@ -914,11 +922,13 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryM FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache_Internal.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm @@ -926,6 +936,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlay FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -1321,17 +1332,24 @@ FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/flutter_linux.h FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.cc FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc +FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine.cc +FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc FILE: ../../../flutter/shell/platform/windows/cursor_handler.cc FILE: ../../../flutter/shell/platform/windows/cursor_handler.h +FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.cc +FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.h FILE: ../../../flutter/shell/platform/windows/flutter_windows.cc +FILE: ../../../flutter/shell/platform/windows/flutter_windows_engine.cc +FILE: ../../../flutter/shell/platform/windows/flutter_windows_engine.h FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.cc FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.h FILE: ../../../flutter/shell/platform/windows/key_event_handler.cc @@ -1341,6 +1359,9 @@ FILE: ../../../flutter/shell/platform/windows/public/flutter_windows.h FILE: ../../../flutter/shell/platform/windows/string_conversion.cc FILE: ../../../flutter/shell/platform/windows/string_conversion.h FILE: ../../../flutter/shell/platform/windows/string_conversion_unittests.cc +FILE: ../../../flutter/shell/platform/windows/system_utils.h +FILE: ../../../flutter/shell/platform/windows/system_utils_unittests.cc +FILE: ../../../flutter/shell/platform/windows/system_utils_win32.cc FILE: ../../../flutter/shell/platform/windows/text_input_plugin.cc FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h FILE: ../../../flutter/shell/platform/windows/win32_dpi_utils.cc @@ -1355,6 +1376,9 @@ FILE: ../../../flutter/shell/platform/windows/win32_task_runner.cc FILE: ../../../flutter/shell/platform/windows/win32_task_runner.h FILE: ../../../flutter/shell/platform/windows/win32_window.cc FILE: ../../../flutter/shell/platform/windows/win32_window.h +FILE: ../../../flutter/shell/platform/windows/win32_window_proc_delegate_manager.cc +FILE: ../../../flutter/shell/platform/windows/win32_window_proc_delegate_manager.h +FILE: ../../../flutter/shell/platform/windows/win32_window_proc_delegate_manager_unittests.cc FILE: ../../../flutter/shell/platform/windows/win32_window_unittests.cc FILE: ../../../flutter/shell/platform/windows/window_binding_handler.h FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h diff --git a/ci/licenses_golden/licenses_fuchsia b/ci/licenses_golden/licenses_fuchsia index 454747e3fd392..c851dcd928364 100644 --- a/ci/licenses_golden/licenses_fuchsia +++ b/ci/licenses_golden/licenses_fuchsia @@ -1,4 +1,4 @@ -Signature: 9e13cc16b79c2ae5720973f66c12df77 +Signature: cca1700ca777f682864d7baed336e5bc UNUSED LICENSES: @@ -471,6 +471,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.ethernet/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.images/meta.json @@ -503,6 +504,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.testing/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json @@ -1315,6 +1317,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/goldfish_pipe.fi FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/light.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/admin.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/hwinfo.fidl @@ -1385,6 +1388,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/options.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/server.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json @@ -2129,6 +2133,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.ethernet/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.images/meta.json @@ -2161,6 +2166,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.testing/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.dhcp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json @@ -2595,9 +2601,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/module_manifest.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/puppet_master.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_command.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_options.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_visibility_state.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/client.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/connectivity.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/net.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.process/launcher.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.process/resolver.fidl @@ -2892,12 +2896,11 @@ FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/limits.h FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/namespace.h FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/private.h FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/unsafe.h -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/decoding.cc +FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/decoding_and_validating.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/encoding.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/formatting.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/coding.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/internal.h -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/validating.cc FILE: ../../../fuchsia/sdk/linux/pkg/fit/include/lib/fit/function.h FILE: ../../../fuchsia/sdk/linux/pkg/fit/include/lib/fit/function_internal.h FILE: ../../../fuchsia/sdk/linux/pkg/memfs/include/lib/memfs/memfs.h @@ -3057,9 +3060,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.math/math.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.playback/problem.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.playback/seeking_reader.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/audio.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/focus.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/module_context.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/module_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/session_shell.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_info.fidl @@ -3150,18 +3151,24 @@ FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/noop_view_p FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/view_provider.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/module_interceptor.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/module_with_view_provider_impl.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/src/child_view_render_box_2.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/component.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/device.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/diagnostics.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/feedback_data_provider.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/power.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/repository_manager.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/tiles.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/time.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/trace_processing/metrics/gpu_metrics.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/trace_processing/metrics/total_trace_wall_time.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/update.dart FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.gesture/gesture_listener.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.a2dp/audio_mode.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.le/connection_options.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.sys/configuration.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.sys/pairing_options.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.sys/security_mode.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/device.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/device_watcher.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/stream.fidl @@ -3177,6 +3184,11 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory.wlan/iovar.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/crash_register.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/data_register.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/last_reboot_info.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/device.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/frames.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/instance.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/mac.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/session.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.report/consumer_control.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.report/device_ids.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input/keys.fidl @@ -3190,11 +3202,11 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.drm/properties.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.drm/types.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.target/target_discovery.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/activity_reporter.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/audio_errors.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/profile_provider.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.memorypressure/memorypressure.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.session/launcher.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/session_restart_controller.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/interfaces.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/routes.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/socket.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.posix/error.fidl @@ -3208,6 +3220,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.input3/pointer.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.pointerinjector/config.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.pointerinjector/device.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.pointerinjector/event.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.policy/display_backlight.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/view_ref.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/view_ref_focused.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/view_ref_installed.fidl diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index 6099a770e54ab..9b18c12e1d4b7 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: c7ee484f0bf49aed48999011c8ec793c +Signature: 2590d5f2ed193cd53addb25bee19db33 UNUSED LICENSES: @@ -970,6 +970,11 @@ FILE: ../../../third_party/skia/bench/skpbench.json FILE: ../../../third_party/skia/build/fuchsia/skqp/skqp.cmx FILE: ../../../third_party/skia/build/fuchsia/skqp/test_manifest.json FILE: ../../../third_party/skia/demos.skia.org/demos/hello_world/index.html +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/garbage.svg +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/index.html +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/main.js +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/shared.js +FILE: ../../../third_party/skia/demos.skia.org/demos/path_performance/worker.js FILE: ../../../third_party/skia/demos.skia.org/demos/web_worker/index.html FILE: ../../../third_party/skia/demos.skia.org/demos/web_worker/main.js FILE: ../../../third_party/skia/demos.skia.org/demos/web_worker/shared.js @@ -1452,15 +1457,21 @@ FILE: ../../../third_party/skia/specs/web-img-decode/proposed/index.html FILE: ../../../third_party/skia/src/core/SkOrderedReadBuffer.h FILE: ../../../third_party/skia/src/ports/SkTLS_pthread.cpp FILE: ../../../third_party/skia/src/ports/SkTLS_win.cpp +FILE: ../../../third_party/skia/src/sksl/generated/sksl_fp.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_frag.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_geom.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_gpu.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_interp.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_pipeline.dehydrated.sksl +FILE: ../../../third_party/skia/src/sksl/generated/sksl_vert.dehydrated.sksl FILE: ../../../third_party/skia/src/sksl/lex/sksl.lex -FILE: ../../../third_party/skia/src/sksl/sksl_blend.inc -FILE: ../../../third_party/skia/src/sksl/sksl_fp.inc -FILE: ../../../third_party/skia/src/sksl/sksl_frag.inc -FILE: ../../../third_party/skia/src/sksl/sksl_geom.inc -FILE: ../../../third_party/skia/src/sksl/sksl_gpu.inc -FILE: ../../../third_party/skia/src/sksl/sksl_interp.inc -FILE: ../../../third_party/skia/src/sksl/sksl_pipeline.inc -FILE: ../../../third_party/skia/src/sksl/sksl_vert.inc +FILE: ../../../third_party/skia/src/sksl/sksl_fp_raw.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_frag.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_geom.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_gpu.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_interp.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_pipeline.sksl +FILE: ../../../third_party/skia/src/sksl/sksl_vert.sksl ---------------------------------------------------------------------------------------------------- Copyright (c) 2011 Google Inc. All rights reserved. @@ -3325,7 +3336,6 @@ FILE: ../../../third_party/skia/src/core/SkCanvasPriv.cpp FILE: ../../../third_party/skia/src/core/SkColorSpaceXformSteps.cpp FILE: ../../../third_party/skia/src/core/SkColorSpaceXformSteps.h FILE: ../../../third_party/skia/src/core/SkContourMeasure.cpp -FILE: ../../../third_party/skia/src/core/SkCoverageModePriv.h FILE: ../../../third_party/skia/src/core/SkCubicMap.cpp FILE: ../../../third_party/skia/src/core/SkCubicSolver.h FILE: ../../../third_party/skia/src/core/SkDeferredDisplayList.cpp @@ -3421,7 +3431,6 @@ FILE: ../../../third_party/skia/src/gpu/gradients/GrLinearGradientLayout.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrRadialGradientLayout.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrSingleIntervalGradientColorizer.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrSweepGradientLayout.fp -FILE: ../../../third_party/skia/src/gpu/gradients/GrTextureGradientColorizer.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrTiledGradientEffect.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrTwoPointConicalGradientLayout.fp FILE: ../../../third_party/skia/src/gpu/gradients/GrUnrolledBinaryGradientColorizer.fp @@ -3437,8 +3446,6 @@ FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSingleIntervalGrad FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSingleIntervalGradientColorizer.h FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSweepGradientLayout.cpp FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrSweepGradientLayout.h -FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTextureGradientColorizer.cpp -FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTextureGradientColorizer.h FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTiledGradientEffect.cpp FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTiledGradientEffect.h FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTwoPointConicalGradientLayout.cpp @@ -3831,7 +3838,6 @@ FILE: ../../../third_party/skia/src/sksl/lex/RegexNode.cpp FILE: ../../../third_party/skia/src/sksl/lex/RegexNode.h FILE: ../../../third_party/skia/src/sksl/lex/RegexParser.cpp FILE: ../../../third_party/skia/src/sksl/lex/RegexParser.h -FILE: ../../../third_party/skia/src/sksl/sksl_enums.inc FILE: ../../../third_party/skia/src/utils/SkFloatToDecimal.cpp FILE: ../../../third_party/skia/src/utils/SkFloatToDecimal.h FILE: ../../../third_party/skia/src/utils/SkJSONWriter.cpp @@ -3892,6 +3898,7 @@ FILE: ../../../third_party/skia/include/gpu/GrBackendSurfaceMutableState.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DBackendContext.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypes.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypesMinimal.h +FILE: ../../../third_party/skia/include/ports/SkImageGeneratorNDK.h FILE: ../../../third_party/skia/include/private/GrD3DTypesPriv.h FILE: ../../../third_party/skia/include/private/SkIDChangeListener.h FILE: ../../../third_party/skia/include/private/SkSLSampleUsage.h @@ -3901,6 +3908,7 @@ FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/SIMD/simd_float_cap FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/SIMD/simd_int_capabilities.cpp FILE: ../../../third_party/skia/src/core/SkIDChangeListener.cpp FILE: ../../../third_party/skia/src/core/SkMatrixProvider.h +FILE: ../../../third_party/skia/src/core/SkRuntimeEffectPriv.h FILE: ../../../third_party/skia/src/core/SkVM_fwd.h FILE: ../../../third_party/skia/src/gpu/GrBackendSemaphore.cpp FILE: ../../../third_party/skia/src/gpu/GrBackendSurfaceMutableStateImpl.h @@ -3917,6 +3925,7 @@ FILE: ../../../third_party/skia/src/gpu/GrStencilMaskHelper.h FILE: ../../../third_party/skia/src/gpu/GrUniformDataManager.cpp FILE: ../../../third_party/skia/src/gpu/GrUniformDataManager.h FILE: ../../../third_party/skia/src/gpu/GrUnrefDDLTask.h +FILE: ../../../third_party/skia/src/gpu/GrUtil.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DCaps.cpp @@ -3967,11 +3976,18 @@ FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDeviceSpaceEffect.cp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDeviceSpaceEffect.h FILE: ../../../third_party/skia/src/gpu/geometry/GrShape.cpp FILE: ../../../third_party/skia/src/gpu/geometry/GrShape.h +FILE: ../../../third_party/skia/src/gpu/gl/webgl/GrGLMakeNativeInterface_webgl.cpp FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLUniformHandler.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkManagedResource.h FILE: ../../../third_party/skia/src/image/SkRescaleAndReadPixels.cpp FILE: ../../../third_party/skia/src/image/SkRescaleAndReadPixels.h +FILE: ../../../third_party/skia/src/ports/SkImageEncoder_NDK.cpp +FILE: ../../../third_party/skia/src/ports/SkImageGeneratorNDK.cpp +FILE: ../../../third_party/skia/src/ports/SkNDKConversions.cpp +FILE: ../../../third_party/skia/src/ports/SkNDKConversions.h FILE: ../../../third_party/skia/src/sksl/SkSLAnalysis.h +FILE: ../../../third_party/skia/src/sksl/SkSLDehydrator.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLDehydrator.h FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.cpp FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.h FILE: ../../../third_party/skia/src/sksl/SkSLSampleUsage.cpp @@ -4008,6 +4024,171 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +LIBRARY: skia +ORIGIN: ../../../third_party/skia/bench/GlyphQuadFillBench.cpp + ../../../third_party/skia/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../third_party/skia/bench/GlyphQuadFillBench.cpp +FILE: ../../../third_party/skia/bench/TessellateBench.cpp +FILE: ../../../third_party/skia/experimental/skrive/include/SkRive.h +FILE: ../../../third_party/skia/experimental/skrive/src/Artboard.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Color.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Component.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Drawable.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Ellipse.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Node.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Paint.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Rectangle.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Shape.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/SkRive.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/BinaryReader.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/JsonReader.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.h +FILE: ../../../third_party/skia/gm/3d.cpp +FILE: ../../../third_party/skia/gm/bc1_transparency.cpp +FILE: ../../../third_party/skia/gm/bicubic.cpp +FILE: ../../../third_party/skia/gm/compressed_textures.cpp +FILE: ../../../third_party/skia/gm/crbug_1073670.cpp +FILE: ../../../third_party/skia/gm/crbug_1086705.cpp +FILE: ../../../third_party/skia/gm/crbug_1113794.cpp +FILE: ../../../third_party/skia/gm/exoticformats.cpp +FILE: ../../../third_party/skia/gm/skbug_9819.cpp +FILE: ../../../third_party/skia/gm/strokerect_anisotropic.cpp +FILE: ../../../third_party/skia/gm/verifiers/gmverifier.cpp +FILE: ../../../third_party/skia/gm/verifiers/gmverifier.h +FILE: ../../../third_party/skia/gm/widebuttcaps.cpp +FILE: ../../../third_party/skia/include/core/SkM44.h +FILE: ../../../third_party/skia/include/effects/SkStrokeAndFillPathEffect.h +FILE: ../../../third_party/skia/include/gpu/GrDirectContext.h +FILE: ../../../third_party/skia/include/private/SkOpts_spi.h +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer.cpp +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer.h +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer_mac.mm +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer_none.cpp +FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer_sfml.cpp +FILE: ../../../third_party/skia/modules/skottie/include/ExternalLayer.h +FILE: ../../../third_party/skia/modules/skottie/src/Adapter.h +FILE: ../../../third_party/skia/modules/skottie/src/Camera.cpp +FILE: ../../../third_party/skia/modules/skottie/src/Camera.h +FILE: ../../../third_party/skia/modules/skottie/src/Path.cpp +FILE: ../../../third_party/skia/modules/skottie/src/Transform.cpp +FILE: ../../../third_party/skia/modules/skottie/src/Transform.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/ScalarKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/ShapeKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/TextKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/Vec2KeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.h +FILE: ../../../third_party/skia/modules/skottie/src/effects/BrightnessContrastEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/CornerPinEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/GlowStyles.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/ShadowStyles.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/AudioLayer.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Ellipse.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/FillStroke.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Gradient.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/MergePaths.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/OffsetPaths.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Polystar.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/PuckerBloat.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Rectangle.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Repeater.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/RoundCorners.cpp +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/ShapeLayer.h +FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/TrimPaths.cpp +FILE: ../../../third_party/skia/modules/skparagraph/gm/simple_gm.cpp +FILE: ../../../third_party/skia/modules/sksg/include/SkSGGeometryEffect.h +FILE: ../../../third_party/skia/modules/sksg/src/SkSGGeometryEffect.cpp +FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_coretext.cpp +FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode.h +FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode_icu.cpp +FILE: ../../../third_party/skia/samplecode/Sample3D.cpp +FILE: ../../../third_party/skia/samplecode/SampleAudio.cpp +FILE: ../../../third_party/skia/samplecode/SampleFitCubicToCircle.cpp +FILE: ../../../third_party/skia/samplecode/SampleSimpleStroker.cpp +FILE: ../../../third_party/skia/src/core/SkColorFilterPriv.h +FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.cpp +FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.h +FILE: ../../../third_party/skia/src/core/SkM44.cpp +FILE: ../../../third_party/skia/src/core/SkMarkerStack.cpp +FILE: ../../../third_party/skia/src/core/SkMarkerStack.h +FILE: ../../../third_party/skia/src/core/SkPathView.h +FILE: ../../../third_party/skia/src/core/SkVerticesPriv.h +FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.cpp +FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.h +FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.h +FILE: ../../../third_party/skia/src/gpu/GrHashMapWithCache.h +FILE: ../../../third_party/skia/src/gpu/GrRecordingContextPriv.cpp +FILE: ../../../third_party/skia/src/gpu/GrSTArenaList.h +FILE: ../../../third_party/skia/src/gpu/ccpr/GrAutoMapVertexBuffer.h +FILE: ../../../third_party/skia/src/gpu/effects/GrArithmeticProcessor.fp +FILE: ../../../third_party/skia/src/gpu/effects/GrDitherEffect.fp +FILE: ../../../third_party/skia/src/gpu/effects/GrHighContrastFilterEffect.fp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.cpp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.h +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.cpp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.h +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.cpp +FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.h +FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathAtlasMgr.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathAtlasMgr.h +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathShapeData.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrSmallPathShapeData.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrMidpointContourParser.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrResolveLevelCounter.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrVectorXform.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrWangsFormula.h +FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.cpp +FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.h +FILE: ../../../third_party/skia/src/opts/SkOpts_skx.cpp +FILE: ../../../third_party/skia/src/ports/SkScalerContext_mac_ct.h +FILE: ../../../third_party/skia/src/ports/SkTypeface_mac_ct.h +FILE: ../../../third_party/skia/src/utils/mac/SkCGBase.h +FILE: ../../../third_party/skia/src/utils/mac/SkCGGeometry.h +FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.cpp +FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.h +---------------------------------------------------------------------------------------------------- +Copyright 2020 Google Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +==================================================================================================== + ==================================================================================================== LIBRARY: skia ORIGIN: ../../../third_party/skia/bench/ReadPixBench.cpp + ../../../third_party/skia/LICENSE @@ -4242,9 +4423,9 @@ FILE: ../../../third_party/skia/docs/examples/Canvas_drawVertices.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_drawVertices_2.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_empty_constructor.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getBaseLayerSize.cpp +FILE: ../../../third_party/skia/docs/examples/Canvas_getContext.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getDeviceClipBounds.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getDeviceClipBounds_2.cpp -FILE: ../../../third_party/skia/docs/examples/Canvas_getGrContext.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getLocalClipBounds.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getLocalClipBounds_2.cpp FILE: ../../../third_party/skia/docs/examples/Canvas_getProps.cpp @@ -5163,10 +5344,10 @@ FILE: ../../../third_party/skia/src/gpu/ccpr/GrStencilAtlasOp.h FILE: ../../../third_party/skia/src/gpu/effects/GrComposeLerpEffect.fp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrComposeLerpEffect.cpp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrComposeLerpEffect.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathTessellateOp.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathTessellateOp.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrStencilPathShader.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrStencilPathShader.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellatePathOp.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellatePathOp.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellationPathRenderer.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellationPathRenderer.h FILE: ../../../third_party/skia/src/pdf/SkPDFGraphicStackState.cpp @@ -5206,156 +5387,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== -==================================================================================================== -LIBRARY: skia -ORIGIN: ../../../third_party/skia/bench/TessellatePathBench.cpp + ../../../third_party/skia/LICENSE -TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/bench/TessellatePathBench.cpp -FILE: ../../../third_party/skia/experimental/skrive/include/SkRive.h -FILE: ../../../third_party/skia/experimental/skrive/src/Artboard.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Color.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Component.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Drawable.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Ellipse.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Node.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Paint.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Rectangle.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/Shape.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/SkRive.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/BinaryReader.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/JsonReader.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.cpp -FILE: ../../../third_party/skia/experimental/skrive/src/reader/StreamReader.h -FILE: ../../../third_party/skia/gm/3d.cpp -FILE: ../../../third_party/skia/gm/bc1_transparency.cpp -FILE: ../../../third_party/skia/gm/bicubic.cpp -FILE: ../../../third_party/skia/gm/compressed_textures.cpp -FILE: ../../../third_party/skia/gm/crbug_1073670.cpp -FILE: ../../../third_party/skia/gm/exoticformats.cpp -FILE: ../../../third_party/skia/gm/skbug_9819.cpp -FILE: ../../../third_party/skia/gm/strokerect_anisotropic.cpp -FILE: ../../../third_party/skia/gm/verifiers/gmverifier.cpp -FILE: ../../../third_party/skia/gm/verifiers/gmverifier.h -FILE: ../../../third_party/skia/gm/widebuttcaps.cpp -FILE: ../../../third_party/skia/include/core/SkM44.h -FILE: ../../../third_party/skia/include/effects/SkStrokeAndFillPathEffect.h -FILE: ../../../third_party/skia/include/gpu/GrDirectContext.h -FILE: ../../../third_party/skia/include/private/SkOpts_spi.h -FILE: ../../../third_party/skia/modules/skottie/include/ExternalLayer.h -FILE: ../../../third_party/skia/modules/skottie/src/Adapter.h -FILE: ../../../third_party/skia/modules/skottie/src/Camera.cpp -FILE: ../../../third_party/skia/modules/skottie/src/Camera.h -FILE: ../../../third_party/skia/modules/skottie/src/Path.cpp -FILE: ../../../third_party/skia/modules/skottie/src/Transform.cpp -FILE: ../../../third_party/skia/modules/skottie/src/Transform.h -FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.h -FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.h -FILE: ../../../third_party/skia/modules/skottie/src/animator/ScalarKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/ShapeKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/TextKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/Vec2KeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.h -FILE: ../../../third_party/skia/modules/skottie/src/effects/BrightnessContrastEffect.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/CornerPinEffect.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/GlowStyles.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/ShadowStyles.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Ellipse.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/FillStroke.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Gradient.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/MergePaths.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/OffsetPaths.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Polystar.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/PuckerBloat.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Rectangle.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Repeater.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/RoundCorners.cpp -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/ShapeLayer.h -FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/TrimPaths.cpp -FILE: ../../../third_party/skia/modules/skparagraph/gm/simple_gm.cpp -FILE: ../../../third_party/skia/modules/sksg/include/SkSGGeometryEffect.h -FILE: ../../../third_party/skia/modules/sksg/src/SkSGGeometryEffect.cpp -FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_coretext.cpp -FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode.h -FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode_icu.cpp -FILE: ../../../third_party/skia/samplecode/Sample3D.cpp -FILE: ../../../third_party/skia/samplecode/SampleFitCubicToCircle.cpp -FILE: ../../../third_party/skia/samplecode/SampleSimpleStroker.cpp -FILE: ../../../third_party/skia/src/core/SkColorFilterPriv.h -FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.cpp -FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.h -FILE: ../../../third_party/skia/src/core/SkM44.cpp -FILE: ../../../third_party/skia/src/core/SkMarkerStack.cpp -FILE: ../../../third_party/skia/src/core/SkMarkerStack.h -FILE: ../../../third_party/skia/src/core/SkVerticesPriv.h -FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.cpp -FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.h -FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.h -FILE: ../../../third_party/skia/src/gpu/GrHashMapWithCache.h -FILE: ../../../third_party/skia/src/gpu/GrRecordingContextPriv.cpp -FILE: ../../../third_party/skia/src/gpu/GrSTArenaList.h -FILE: ../../../third_party/skia/src/gpu/ccpr/GrAutoMapVertexBuffer.h -FILE: ../../../third_party/skia/src/gpu/effects/GrArithmeticProcessor.fp -FILE: ../../../third_party/skia/src/gpu/effects/GrDitherEffect.fp -FILE: ../../../third_party/skia/src/gpu/effects/GrHighContrastFilterEffect.fp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.cpp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrArithmeticProcessor.h -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.cpp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDitherEffect.h -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.cpp -FILE: ../../../third_party/skia/src/gpu/effects/generated/GrHighContrastFilterEffect.h -FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.cpp -FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrMidpointContourParser.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrResolveLevelCounter.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrVectorXform.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrWangsFormula.h -FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.cpp -FILE: ../../../third_party/skia/src/gpu/text/GrSDFTOptions.h -FILE: ../../../third_party/skia/src/opts/SkOpts_skx.cpp -FILE: ../../../third_party/skia/src/ports/SkScalerContext_mac_ct.h -FILE: ../../../third_party/skia/src/ports/SkTypeface_mac_ct.h -FILE: ../../../third_party/skia/src/utils/mac/SkCGBase.h -FILE: ../../../third_party/skia/src/utils/mac/SkCGGeometry.h -FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.cpp -FILE: ../../../third_party/skia/src/utils/mac/SkCTFontSmoothBehavior.h ----------------------------------------------------------------------------------------------------- -Copyright 2020 Google Inc. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - - * Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -==================================================================================================== - ==================================================================================================== LIBRARY: skia ORIGIN: ../../../third_party/skia/docs/examples/50_percent_gray.cpp + ../../../third_party/skia/LICENSE @@ -5534,12 +5565,14 @@ FILE: ../../../third_party/skia/src/gpu/GrFinishCallbacks.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrFillPathShader.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrFillPathShader.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathShader.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeOp.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeOp.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeShader.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellateStrokeShader.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateOp.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateOp.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateShader.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateShader.h FILE: ../../../third_party/skia/src/opts/SkVM_opts.h FILE: ../../../third_party/skia/src/sksl/SkSLAnalysis.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLRehydrator.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLRehydrator.h ---------------------------------------------------------------------------------------------------- Copyright 2020 Google LLC. @@ -5655,8 +5688,11 @@ LIBRARY: skia ORIGIN: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPICreateDDL.cpp + ../../../third_party/skia/LICENSE TYPE: LicenseType.bsd FILE: ../../../third_party/skia/fuzz/FuzzCreateDDL.cpp +FILE: ../../../third_party/skia/fuzz/FuzzPath.cpp +FILE: ../../../third_party/skia/fuzz/FuzzRRect.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPICreateDDL.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPISVGCanvas.cpp +FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKP.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSVG.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSkRuntimeEffect.cpp ---------------------------------------------------------------------------------------------------- @@ -5749,10 +5785,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== LIBRARY: skia -ORIGIN: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPISkDescriptor.cpp + ../../../third_party/skia/LICENSE +ORIGIN: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2GLSL.cpp + ../../../third_party/skia/LICENSE TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/fuzz/FuzzSkDescriptor.cpp -FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzAPISkDescriptor.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2GLSL.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2Metal.cpp FILE: ../../../third_party/skia/fuzz/oss_fuzz/FuzzSKSL2Pipeline.cpp diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index b3d1975a1fe3f..672a13f903388 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: 05b5f53049ea4c29e6228cf204ac94bf +Signature: 9f3361f4c0d2a3218f0a27ac140fb4c0 UNUSED LICENSES: @@ -67,6 +67,33 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +ORIGIN: ../../../third_party/harfbuzz/src/ms-use/COPYING +TYPE: LicenseType.mit +---------------------------------------------------------------------------------------------------- +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE +==================================================================================================== + ==================================================================================================== ORIGIN: ../../../third_party/pkg/when/LICENSE TYPE: LicenseType.bsd @@ -8027,6 +8054,18 @@ FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/netext_3_10k.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/rutext_2_10k.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/sktext_10k.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/zhtext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/datext_latin1_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/entext_ascii_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/netext_3_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/rutext_2_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/sktext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/zhtext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/datext_latin1_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/entext_ascii_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/netext_3_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/rutext_2_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/sktext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/zhtext_10k.dart FILE: ../../../third_party/dart/client/idea/.idea/.name FILE: ../../../third_party/dart/client/idea/.idea/inspectionProfiles/Project_Default.xml FILE: ../../../third_party/dart/client/idea/.idea/vcs.xml @@ -8156,8 +8195,6 @@ FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart/SoundSplayTr FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart/sound_splay_tree.dart FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart2/SoundSplayTreeSieve.dart FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart2/sound_splay_tree.dart -FILE: ../../../third_party/dart/runtime/bin/abi_version.h -FILE: ../../../third_party/dart/runtime/bin/abi_version_in.cc FILE: ../../../third_party/dart/runtime/bin/elf_loader.cc FILE: ../../../third_party/dart/runtime/bin/elf_loader.h FILE: ../../../third_party/dart/runtime/bin/entrypoints_verification_test_extension.cc @@ -8356,13 +8393,15 @@ FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart/TypedDataDupl FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart2/TypedDataDuplicate.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/Utf8Decode.dart FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/Utf8Decode.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/Utf8Encode.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/Utf8Encode.dart FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.cc FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.h FILE: ../../../third_party/dart/runtime/bin/exe_utils.cc FILE: ../../../third_party/dart/runtime/bin/exe_utils.h FILE: ../../../third_party/dart/runtime/bin/platform_macos.h FILE: ../../../third_party/dart/runtime/bin/platform_macos_test.cc -FILE: ../../../third_party/dart/runtime/include/dart_api_dl.cc +FILE: ../../../third_party/dart/runtime/include/dart_api_dl.c FILE: ../../../third_party/dart/runtime/include/dart_api_dl.h FILE: ../../../third_party/dart/runtime/include/dart_version.h FILE: ../../../third_party/dart/runtime/include/internal/dart_api_dl_impl.h @@ -8943,7 +8982,6 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/cli.dart FILE: ../../../third_party/dart/runtime/observatory/lib/debugger.dart FILE: ../../../third_party/dart/runtime/observatory/lib/sample_profile.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/allocation_profile/allocation_profile.dart -FILE: ../../../third_party/dart/runtime/observatory/lib/src/app/analytics.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/cli/command.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/debugger/debugger.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/debugger/debugger_location.dart @@ -12059,20 +12097,21 @@ LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/COPYING TYPE: LicenseType.unknown FILE: ../../../third_party/harfbuzz/.circleci/config.yml +FILE: ../../../third_party/harfbuzz/.codecov.yml FILE: ../../../third_party/harfbuzz/.editorconfig FILE: ../../../third_party/harfbuzz/THANKS FILE: ../../../third_party/harfbuzz/TODO -FILE: ../../../third_party/harfbuzz/appveyor.yml -FILE: ../../../third_party/harfbuzz/azure-pipelines.yml FILE: ../../../third_party/harfbuzz/docs/HarfBuzz.png FILE: ../../../third_party/harfbuzz/docs/HarfBuzz.svg FILE: ../../../third_party/harfbuzz/docs/harfbuzz-docs.xml +FILE: ../../../third_party/harfbuzz/docs/meson.build FILE: ../../../third_party/harfbuzz/docs/usermanual-buffers-language-script-and-direction.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-clusters.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-fonts-and-faces.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-getting-started.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-glyph-information.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-install-harfbuzz.xml +FILE: ../../../third_party/harfbuzz/docs/usermanual-integration.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-object-model.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-opentype-features.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-shaping-concepts.xml @@ -12080,6 +12119,18 @@ FILE: ../../../third_party/harfbuzz/docs/usermanual-utilities.xml FILE: ../../../third_party/harfbuzz/docs/usermanual-what-is-harfbuzz.xml FILE: ../../../third_party/harfbuzz/docs/version.xml.in FILE: ../../../third_party/harfbuzz/harfbuzz.doap +FILE: ../../../third_party/harfbuzz/meson-cc-tests/intel-atomic-primitives-test.c +FILE: ../../../third_party/harfbuzz/meson-cc-tests/solaris-atomic-operations.c +FILE: ../../../third_party/harfbuzz/meson.build +FILE: ../../../third_party/harfbuzz/perf/fonts/Amiri-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/fonts/NotoNastaliqUrdu-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/fonts/NotoSansDevanagari-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/fonts/Roboto-Regular.ttf +FILE: ../../../third_party/harfbuzz/perf/meson.build +FILE: ../../../third_party/harfbuzz/perf/perf-draw.hh +FILE: ../../../third_party/harfbuzz/perf/perf-extents.hh +FILE: ../../../third_party/harfbuzz/perf/perf-shaping.hh +FILE: ../../../third_party/harfbuzz/perf/perf.cc FILE: ../../../third_party/harfbuzz/src/Makefile.sources FILE: ../../../third_party/harfbuzz/src/harfbuzz-config.cmake.in FILE: ../../../third_party/harfbuzz/src/harfbuzz-gobject.pc.in @@ -12087,6 +12138,7 @@ FILE: ../../../third_party/harfbuzz/src/harfbuzz-icu.pc.in FILE: ../../../third_party/harfbuzz/src/harfbuzz-subset.pc.in FILE: ../../../third_party/harfbuzz/src/harfbuzz.cc FILE: ../../../third_party/harfbuzz/src/harfbuzz.pc.in +FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-arabic-joining-list.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-arabic-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-indic-table.cc FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-use-table.cc @@ -12094,13 +12146,28 @@ FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-vowel-constraints.cc FILE: ../../../third_party/harfbuzz/src/hb-ot-tag-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ucd-table.hh FILE: ../../../third_party/harfbuzz/src/hb-unicode-emoji-table.hh +FILE: ../../../third_party/harfbuzz/src/meson.build +FILE: ../../../third_party/harfbuzz/src/update-unicode-tables.make +FILE: ../../../third_party/harfbuzz/subprojects/cairo.wrap +FILE: ../../../third_party/harfbuzz/subprojects/expat.wrap +FILE: ../../../third_party/harfbuzz/subprojects/fontconfig.wrap +FILE: ../../../third_party/harfbuzz/subprojects/freetype2.wrap +FILE: ../../../third_party/harfbuzz/subprojects/glib.wrap +FILE: ../../../third_party/harfbuzz/subprojects/google-benchmark.wrap +FILE: ../../../third_party/harfbuzz/subprojects/libffi.wrap +FILE: ../../../third_party/harfbuzz/subprojects/libpng.wrap +FILE: ../../../third_party/harfbuzz/subprojects/pixman.wrap +FILE: ../../../third_party/harfbuzz/subprojects/proxy-libintl.wrap +FILE: ../../../third_party/harfbuzz/subprojects/ttf-parser.wrap +FILE: ../../../third_party/harfbuzz/subprojects/zlib.wrap ---------------------------------------------------------------------------------------------------- HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. For parts of HarfBuzz that are licensed under different licenses see individual files names COPYING in subdirectories where applicable. -Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019 Google, Inc. -Copyright © 2019 Facebook, Inc. +Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020 Google, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. Copyright © 2012 Mozilla Foundation Copyright © 2011 Codethink Limited Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) @@ -12195,19 +12262,45 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/hb-aat-fdsc-table.hh +ORIGIN: ../../../third_party/harfbuzz/src/failing-alloc.c +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/failing-alloc.c +FILE: ../../../third_party/harfbuzz/src/hb-draw.hh +---------------------------------------------------------------------------------------------------- +Copyright © 2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-aat-layout-ankr-table.hh TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/hb-aat-fdsc-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-ankr-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-bsln-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-feat-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-just-table.hh -FILE: ../../../third_party/harfbuzz/src/hb-aat-layout-lcar-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat-layout.h FILE: ../../../third_party/harfbuzz/src/hb-aat-ltag-table.hh FILE: ../../../third_party/harfbuzz/src/hb-aat.h -FILE: ../../../third_party/harfbuzz/src/hb-ot-color-colr-table.hh -FILE: ../../../third_party/harfbuzz/src/hb-ot-color-sbix-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-color-svg-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-gasp-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-metrics.h @@ -12324,6 +12417,8 @@ FILE: ../../../third_party/harfbuzz/src/hb-number.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-meta-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-meta.cc FILE: ../../../third_party/harfbuzz/src/hb-ot-meta.h +FILE: ../../../third_party/harfbuzz/src/hb-style.cc +FILE: ../../../third_party/harfbuzz/src/hb-style.h FILE: ../../../third_party/harfbuzz/src/test-number.cc FILE: ../../../third_party/harfbuzz/src/test-ot-meta.cc ---------------------------------------------------------------------------------------------------- @@ -12605,7 +12700,6 @@ FILE: ../../../third_party/harfbuzz/src/hb-buffer-deserialize-json.rl FILE: ../../../third_party/harfbuzz/src/hb-buffer-deserialize-text.hh FILE: ../../../third_party/harfbuzz/src/hb-buffer-deserialize-text.rl FILE: ../../../third_party/harfbuzz/src/hb-deprecated.h -FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.h.tmpl FILE: ../../../third_party/harfbuzz/src/hb-ot-layout-jstf-table.hh FILE: ../../../third_party/harfbuzz/src/hb-ot-shape-complex-hangul.cc ---------------------------------------------------------------------------------------------------- @@ -12742,7 +12836,6 @@ FILE: ../../../third_party/harfbuzz/src/hb-shaper-impl.hh FILE: ../../../third_party/harfbuzz/src/hb-shaper-list.hh FILE: ../../../third_party/harfbuzz/src/hb-shaper.cc FILE: ../../../third_party/harfbuzz/src/hb-shaper.hh -FILE: ../../../third_party/harfbuzz/src/hb-warning.cc ---------------------------------------------------------------------------------------------------- Copyright © 2012 Google, Inc. @@ -13026,6 +13119,36 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-draw.cc +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-draw.cc +FILE: ../../../third_party/harfbuzz/src/hb-draw.h +---------------------------------------------------------------------------------------------------- +Copyright © 2019-2020 Ebrahim Byagowi + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-face.cc @@ -13099,10 +13222,7 @@ LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-fallback-shape.cc TYPE: LicenseType.unknown FILE: ../../../third_party/harfbuzz/src/hb-fallback-shape.cc -FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.cc.tmpl FILE: ../../../third_party/harfbuzz/src/hb-gobject-structs.cc -FILE: ../../../third_party/harfbuzz/src/hb-gobject-structs.h -FILE: ../../../third_party/harfbuzz/src/hb-gobject.h FILE: ../../../third_party/harfbuzz/src/hb-uniscribe.h FILE: ../../../third_party/harfbuzz/src/hb-version.h FILE: ../../../third_party/harfbuzz/src/hb-version.h.in @@ -13191,6 +13311,66 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-gobject-enums.cc.tmpl +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.cc.tmpl +FILE: ../../../third_party/harfbuzz/src/hb-gobject-structs.h +FILE: ../../../third_party/harfbuzz/src/hb-gobject.h +---------------------------------------------------------------------------------------------------- +Copyright (C) 2011 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-gobject-enums.h.tmpl +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-gobject-enums.h.tmpl +---------------------------------------------------------------------------------------------------- +Copyright (C) 2013 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-graphite2.cc @@ -13373,6 +13553,37 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-cff1-std-str.hh +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-ot-cff1-std-str.hh +FILE: ../../../third_party/harfbuzz/src/test-bimap.cc +FILE: ../../../third_party/harfbuzz/src/test-ot-glyphname.cc +---------------------------------------------------------------------------------------------------- +Copyright © 2019 Adobe, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-cmap-table.hh @@ -13434,6 +13645,37 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== +==================================================================================================== +LIBRARY: harfbuzz +ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-color-colr-table.hh +TYPE: LicenseType.unknown +FILE: ../../../third_party/harfbuzz/src/hb-ot-color-colr-table.hh +FILE: ../../../third_party/harfbuzz/src/hb-ot-color-sbix-table.hh +---------------------------------------------------------------------------------------------------- +Copyright © 2018 Ebrahim Byagowi +Copyright © 2020 Google, Inc. + + This is part of HarfBuzz, a text shaping library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +==================================================================================================== + ==================================================================================================== LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-color-cpal-table.hh @@ -13879,7 +14121,6 @@ LIBRARY: harfbuzz ORIGIN: ../../../third_party/harfbuzz/src/hb-ot-layout.h TYPE: LicenseType.unknown FILE: ../../../third_party/harfbuzz/src/hb-ot-layout.h -FILE: ../../../third_party/harfbuzz/src/main.cc ---------------------------------------------------------------------------------------------------- Copyright © 2007,2008,2009 Red Hat, Inc. @@ -14529,11 +14770,13 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-bimap.cc +ORIGIN: ../../../third_party/harfbuzz/src/main.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-bimap.cc +FILE: ../../../third_party/harfbuzz/src/main.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2019 Adobe, Inc. +Copyright © 2007,2008,2009 Red Hat, Inc. +Copyright © 2018,2019,2020 Ebrahim Byagowi +Copyright © 2018 Khaled Hosny This is part of HarfBuzz, a text shaping library. @@ -14558,11 +14801,11 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc +ORIGIN: ../../../third_party/harfbuzz/src/test-array.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc +FILE: ../../../third_party/harfbuzz/src/test-array.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2010,2011,2013 Google, Inc. +Copyright © 2020 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -14587,13 +14830,11 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc +ORIGIN: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc -FILE: ../../../third_party/harfbuzz/src/test-gsub-would-substitute.cc -FILE: ../../../third_party/harfbuzz/src/test.cc +FILE: ../../../third_party/harfbuzz/src/test-buffer-serialize.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2010,2011 Google, Inc. +Copyright © 2010,2011,2013 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -14618,12 +14859,13 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ==================================================================================================== LIBRARY: harfbuzz -ORIGIN: ../../../third_party/harfbuzz/src/test-ot-color.cc +ORIGIN: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc TYPE: LicenseType.unknown -FILE: ../../../third_party/harfbuzz/src/test-ot-color.cc +FILE: ../../../third_party/harfbuzz/src/test-gpos-size-params.cc +FILE: ../../../third_party/harfbuzz/src/test-gsub-would-substitute.cc +FILE: ../../../third_party/harfbuzz/src/test.cc ---------------------------------------------------------------------------------------------------- -Copyright © 2018 Ebrahim Byagowi -Copyright © 2018 Khaled Hosny +Copyright © 2010,2011 Google, Inc. This is part of HarfBuzz, a text shaping library. @@ -21819,7 +22061,7 @@ FILE: ../../../third_party/dart/third_party/wasmer/wasmer.rs ---------------------------------------------------------------------------------------------------- MIT License -Copyright (c) 2019 Wasmer, Inc. and its affiliates. +Copyright (c) 2019-present Wasmer, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22856,4 +23098,4 @@ freely, subject to the following restrictions: misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ==================================================================================================== -Total license count: 360 +Total license count: 367 diff --git a/ci/lint.sh b/ci/lint.sh index c3292d8e369fe..e2698f255f7b5 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -1,4 +1,8 @@ #!/bin/bash +# +# 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. set -e @@ -9,15 +13,6 @@ unset CDPATH # link at a time, and then cds into the link destination and find out where it # ends up. # -# The returned filesystem path must be a format usable by Dart's URI parser, -# since the Dart command line tool treats its argument as a file URI, not a -# filename. For instance, multiple consecutive slashes should be reduced to a -# single slash, since double-slashes indicate a URI "authority", and these are -# supposed to be filenames. There is an edge case where this will return -# multiple slashes: when the input resolves to the root directory. However, if -# that were the case, we wouldn't be running this shell, so we don't do anything -# about it. -# # The function is enclosed in a subshell to avoid changing the working directory # of the caller. function follow_links() ( @@ -31,17 +26,21 @@ function follow_links() ( done echo "$file" ) -PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")" -CI_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)" -SRC_DIR="$(cd "$CI_DIR/../.."; pwd -P)" + +SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")") +SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)" +DART_BIN="${SRC_DIR}/third_party/dart/tools/sdks/dart-sdk/bin" +DART="${DART_BIN}/dart" +PUB="${DART_BIN}/pub" COMPILE_COMMANDS="$SRC_DIR/out/compile_commands.json" if [ ! -f "$COMPILE_COMMANDS" ]; then - (cd $SRC_DIR; ./flutter/tools/gn) + (cd "$SRC_DIR"; ./flutter/tools/gn) fi -cd "$CI_DIR" -pub get && dart \ +cd "$SCRIPT_DIR" +"$PUB" get && "$DART" \ + --disable-dart-dev \ bin/lint.dart \ --compile-commands="$COMPILE_COMMANDS" \ --repo="$SRC_DIR/flutter" \ diff --git a/ci/pubspec.yaml b/ci/pubspec.yaml index d345cab6e78f1..eba8dd49bfbbf 100644 --- a/ci/pubspec.yaml +++ b/ci/pubspec.yaml @@ -1,9 +1,14 @@ +# 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. + name: ci_scripts dependencies: args: ^1.6.0 path: ^1.7.0 - process_runner: ^2.0.3 + isolate: ^2.0.3 + process_runner: ^3.0.0 environment: sdk: '>=2.8.0 <3.0.0' diff --git a/ci/test.sh b/ci/test.sh deleted file mode 100755 index c0ea5babca926..0000000000000 --- a/ci/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -cd frontend_server -dart test/server_test.dart diff --git a/common/config.gni b/common/config.gni index c0727a6f8b6f4..f7b122a3f480e 100644 --- a/common/config.gni +++ b/common/config.gni @@ -16,6 +16,9 @@ declare_args() { # Whether to use the Skia text shaper module flutter_enable_skshaper = false + + # Whether to use the legacy embedder when building for Fuchsia. + flutter_enable_legacy_fuchsia_embedder = true } # feature_defines_list --------------------------------------------------------- @@ -56,119 +59,3 @@ if (is_ios || is_mac) { ] flutter_cflags_objcc = flutter_cflags_objc } - -# This template creates a `source_set` in both standard and "fuchsia_legacy" -# configurations. -# -# The "fuchsia_legacy" configuration includes old, non-embedder API sources and -# defines the LEGACY_FUCHSIA_EMBEDDER symbol. This template and the config -# are both transitional and will be removed after the embedder API transition -# is complete. -# TODO(fxb/54041): Remove when no longer neccesary. -# -# `sources`, `defines`, `public_configs`, `configs`, `public_deps`, `deps` work -# as they do in a normal `source_set`. -# -# `legacy_deps` is the list of dependencies which should be mutated by -# appending '_fuchsia_legacy' when creating the 2 `source_set`'s. The template adds -# `legacy_deps` to `public_deps`, whether it mutates them or not. -template("source_set_maybe_fuchsia_legacy") { - public_deps_non_legacy = [] - deps_non_legacy = [] - if (defined(invoker.public_deps)) { - public_deps_non_legacy += invoker.public_deps - } - if (defined(invoker.deps)) { - deps_non_legacy += invoker.deps - } - if (defined(invoker.public_deps_legacy_and_next)) { - foreach(legacy_dep, invoker.public_deps_legacy_and_next) { - public_deps_non_legacy += [ legacy_dep ] - } - } - if (defined(invoker.deps_legacy_and_next)) { - foreach(legacy_dep, invoker.deps_legacy_and_next) { - deps_non_legacy += [ legacy_dep ] - } - } - - source_set(target_name) { - forward_variables_from(invoker, - [ - "testonly", - "sources", - "defines", - "public_configs", - "configs", - ]) - public_deps = public_deps_non_legacy - deps = deps_non_legacy - } - - if (is_fuchsia) { - legagcy_suffix = "_fuchsia_legacy" - - sources_legacy = [] - if (defined(invoker.sources_legacy)) { - sources_legacy += invoker.sources_legacy - } - if (defined(invoker.sources)) { - sources_legacy += invoker.sources - } - - public_configs_legacy = [ "//flutter:fuchsia_legacy" ] - if (defined(invoker.public_configs)) { - public_configs_legacy += invoker.public_configs - } - - public_deps_legacy = [] - deps_legacy = [] - if (defined(invoker.public_deps)) { - public_deps_legacy += invoker.public_deps - } - if (defined(invoker.deps)) { - deps_legacy += invoker.deps - } - if (defined(invoker.public_deps_legacy)) { - public_deps_legacy += invoker.public_deps_legacy - } - if (defined(invoker.deps_legacy)) { - deps_legacy += invoker.deps_legacy - } - if (defined(invoker.public_deps_legacy_and_next)) { - foreach(legacy_dep, invoker.public_deps_legacy_and_next) { - public_deps_legacy += [ legacy_dep + legagcy_suffix ] - } - } - if (defined(invoker.deps_legacy_and_next)) { - foreach(legacy_dep, invoker.deps_legacy_and_next) { - deps_legacy += [ legacy_dep + legagcy_suffix ] - } - } - - source_set(target_name + legagcy_suffix) { - forward_variables_from(invoker, - [ - "testonly", - "defines", - "configs", - ]) - sources = sources_legacy - - public_configs = public_configs_legacy - - public_deps = public_deps_legacy - deps = deps_legacy - } - } else { - if (defined(invoker.sources_legacy)) { - not_needed(invoker, [ "sources_legacy" ]) - } - if (defined(invoker.public_deps_legacy)) { - not_needed(invoker, [ "public_deps_legacy" ]) - } - if (defined(invoker.deps_legacy)) { - not_needed(invoker, [ "deps_legacy" ]) - } - } -} diff --git a/common/settings.h b/common/settings.h index 15166b8d38213..ae52e4a7342ba 100644 --- a/common/settings.h +++ b/common/settings.h @@ -22,10 +22,17 @@ namespace flutter { class FrameTiming { public: - enum Phase { kBuildStart, kBuildFinish, kRasterStart, kRasterFinish, kCount }; - - static constexpr Phase kPhases[kCount] = {kBuildStart, kBuildFinish, - kRasterStart, kRasterFinish}; + enum Phase { + kVsyncStart, + kBuildStart, + kBuildFinish, + kRasterStart, + kRasterFinish, + kCount + }; + + static constexpr Phase kPhases[kCount] = { + kVsyncStart, kBuildStart, kBuildFinish, kRasterStart, kRasterFinish}; fml::TimePoint Get(Phase phase) const { return data_[phase]; } fml::TimePoint Set(Phase phase, fml::TimePoint value) { @@ -102,8 +109,10 @@ struct Settings { bool enable_dart_profiling = false; bool disable_dart_asserts = false; - // Used to signal the embedder whether HTTP connections are disabled. - bool disable_http = false; + // Whether embedder only allows secure connections. + bool may_insecurely_connect_to_all_domains = true; + // JSON-formatted domain network policy. + std::string domain_network_policy; // Used as the script URI in debug messages. Does not affect how the Dart code // is executed. diff --git a/flow/BUILD.gn b/flow/BUILD.gn index 89647c7189784..63f1fe529108b 100644 --- a/flow/BUILD.gn +++ b/flow/BUILD.gn @@ -6,7 +6,7 @@ import("//build/fuchsia/sdk.gni") import("//flutter/common/config.gni") import("//flutter/testing/testing.gni") -source_set_maybe_fuchsia_legacy("flow") { +source_set("flow") { sources = [ "compositor_context.cc", "compositor_context.h", @@ -78,20 +78,23 @@ source_set_maybe_fuchsia_legacy("flow") { "//third_party/skia", ] - sources_legacy = [ - "layers/child_scene_layer.cc", - "layers/child_scene_layer.h", - "scene_update_context.cc", - "scene_update_context.h", - "view_holder.cc", - "view_holder.h", - ] + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { + sources += [ + "layers/child_scene_layer.cc", + "layers/child_scene_layer.h", + "scene_update_context.cc", + "scene_update_context.h", + "view_holder.cc", + "view_holder.h", + ] - public_deps_legacy = [ - "$fuchsia_sdk_root/fidl:fuchsia.ui.app", - "$fuchsia_sdk_root/fidl:fuchsia.ui.gfx", - "$fuchsia_sdk_root/pkg:scenic_cpp", - ] + public_deps = [ + "$fuchsia_sdk_root/fidl:fuchsia.ui.app", + "$fuchsia_sdk_root/fidl:fuchsia.ui.gfx", + "$fuchsia_sdk_root/fidl:fuchsia.ui.views", + "$fuchsia_sdk_root/pkg:scenic_cpp", + ] + } } if (enable_unittests) { @@ -99,7 +102,7 @@ if (enable_unittests) { fixtures = [] } - source_set_maybe_fuchsia_legacy("flow_testing") { + source_set("flow_testing") { testonly = true sources = [ @@ -121,10 +124,10 @@ if (enable_unittests) { "//third_party/googletest:gtest", ] - deps_legacy_and_next = [ ":flow" ] + deps = [ ":flow" ] } - source_set_maybe_fuchsia_legacy("flow_unittests_common") { + executable("flow_unittests") { testonly = true sources = [ @@ -160,7 +163,9 @@ if (enable_unittests) { ] deps = [ + ":flow", ":flow_fixtures", + ":flow_testing", "//flutter/fml", "//flutter/testing:skia", "//flutter/testing:testing_lib", @@ -169,32 +174,10 @@ if (enable_unittests) { "//third_party/skia", ] - sources_legacy = [ "layers/fuchsia_layer_unittests.cc" ] - - deps_legacy = [ "//build/fuchsia/pkg:sys_cpp_testing" ] - - deps_legacy_and_next = [ - ":flow", - ":flow_testing", - ] - } - - if (is_fuchsia) { - executable("flow_unittests") { - testonly = true - - deps = [ ":flow_unittests_common_fuchsia_legacy" ] - } - executable("flow_unittests_next") { - testonly = true - - deps = [ ":flow_unittests_common" ] - } - } else { - executable("flow_unittests") { - testonly = true + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { + sources += [ "layers/fuchsia_layer_unittests.cc" ] - deps = [ ":flow_unittests_common" ] + deps += [ "//build/fuchsia/pkg:sys_cpp_testing" ] } } } diff --git a/flow/compositor_context.cc b/flow/compositor_context.cc index ddf9f1c698893..01e23e057196c 100644 --- a/flow/compositor_context.cc +++ b/flow/compositor_context.cc @@ -9,8 +9,10 @@ namespace flutter { -CompositorContext::CompositorContext(fml::Milliseconds frame_budget) - : raster_time_(frame_budget), ui_time_(frame_budget) {} +CompositorContext::CompositorContext(Delegate& delegate) + : delegate_(delegate), + raster_time_(delegate.GetFrameBudget()), + ui_time_(delegate.GetFrameBudget()) {} CompositorContext::~CompositorContext() = default; @@ -23,8 +25,11 @@ void CompositorContext::BeginFrame(ScopedFrame& frame, } void CompositorContext::EndFrame(ScopedFrame& frame, - bool enable_instrumentation) { - raster_cache_.SweepAfterFrame(); + bool enable_instrumentation, + size_t freed_hint) { + freed_hint += raster_cache_.SweepAfterFrame(); + delegate_.OnCompositorEndFrame(freed_hint); + if (enable_instrumentation) { raster_time_.Stop(); } @@ -64,7 +69,7 @@ CompositorContext::ScopedFrame::ScopedFrame( } CompositorContext::ScopedFrame::~ScopedFrame() { - context_.EndFrame(*this, instrumentation_enabled_); + context_.EndFrame(*this, instrumentation_enabled_, uncached_external_size_); } RasterStatus CompositorContext::ScopedFrame::Raster( diff --git a/flow/compositor_context.h b/flow/compositor_context.h index 47992abda6028..b17dc907420da 100644 --- a/flow/compositor_context.h +++ b/flow/compositor_context.h @@ -37,6 +37,18 @@ enum class RasterStatus { class CompositorContext { public: + class Delegate { + public: + /// Called at the end of a frame with approximately how many bytes mightbe + /// freed if a GC ran now. + /// + /// This method is called from the raster task runner. + virtual void OnCompositorEndFrame(size_t freed_hint) = 0; + + /// Time limit for a smooth frame. See `Engine::GetDisplayRefreshRate`. + virtual fml::Milliseconds GetFrameBudget() = 0; + }; + class ScopedFrame { public: ScopedFrame(CompositorContext& context, @@ -67,6 +79,8 @@ class CompositorContext { virtual RasterStatus Raster(LayerTree& layer_tree, bool ignore_raster_cache); + void add_external_size(size_t size) { uncached_external_size_ += size; } + private: CompositorContext& context_; GrDirectContext* gr_context_; @@ -76,11 +90,12 @@ class CompositorContext { const bool instrumentation_enabled_; const bool surface_supports_readback_; fml::RefPtr raster_thread_merger_; + size_t uncached_external_size_ = 0; FML_DISALLOW_COPY_AND_ASSIGN(ScopedFrame); }; - CompositorContext(fml::Milliseconds frame_budget = fml::kDefaultFrameBudget); + explicit CompositorContext(Delegate& delegate); virtual ~CompositorContext(); @@ -108,6 +123,7 @@ class CompositorContext { Stopwatch& ui_time() { return ui_time_; } private: + Delegate& delegate_; RasterCache raster_cache_; TextureRegistry texture_registry_; Counter frame_count_; @@ -116,7 +132,9 @@ class CompositorContext { void BeginFrame(ScopedFrame& frame, bool enable_instrumentation); - void EndFrame(ScopedFrame& frame, bool enable_instrumentation); + void EndFrame(ScopedFrame& frame, + bool enable_instrumentation, + size_t freed_hint); FML_DISALLOW_COPY_AND_ASSIGN(CompositorContext); }; diff --git a/flow/embedded_views.cc b/flow/embedded_views.cc index 07a484999ff51..9441c8dc9470c 100644 --- a/flow/embedded_views.cc +++ b/flow/embedded_views.cc @@ -60,4 +60,8 @@ const std::vector>::const_iterator MutatorsStack::End() return vector_.end(); }; +bool ExternalViewEmbedder::SupportsDynamicThreadMerging() { + return false; +} + } // namespace flutter diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 455eb91511801..cbfde228786a3 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -266,6 +266,10 @@ class ExternalViewEmbedder { // sets the stage for the next pre-roll. virtual void CancelFrame() = 0; + // Indicates the begining of a frame. + // + // The `raster_thread_merger` will be null if |SupportsDynamicThreadMerging| + // returns false. virtual void BeginFrame( SkISize frame_size, GrDirectContext* context, @@ -306,10 +310,20 @@ class ExternalViewEmbedder { // A new frame on the platform thread starts immediately. If the GPU thread // still has some task running, there could be two frames being rendered // concurrently, which causes undefined behaviors. + // + // The `raster_thread_merger` will be null if |SupportsDynamicThreadMerging| + // returns false. virtual void EndFrame( bool should_resubmit_frame, fml::RefPtr raster_thread_merger) {} + // Whether the embedder should support dynamic thread merging. + // + // Returning `true` results a |RasterThreadMerger| instance to be created. + // * See also |BegineFrame| and |EndFrame| for getting the + // |RasterThreadMerger| instance. + virtual bool SupportsDynamicThreadMerging(); + FML_DISALLOW_COPY_AND_ASSIGN(ExternalViewEmbedder); }; // ExternalViewEmbedder diff --git a/flow/instrumentation.cc b/flow/instrumentation.cc index ea85d06e1cce9..541b8635b6aa7 100644 --- a/flow/instrumentation.cc +++ b/flow/instrumentation.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/instrumentation.h" @@ -52,16 +51,18 @@ double Stopwatch::UnitFrameInterval(double raster_time_ms) const { double Stopwatch::UnitHeight(double raster_time_ms, double max_unit_interval) const { double unitHeight = UnitFrameInterval(raster_time_ms) / max_unit_interval; - if (unitHeight > 1.0) + if (unitHeight > 1.0) { unitHeight = 1.0; + } return unitHeight; } fml::TimeDelta Stopwatch::MaxDelta() const { fml::TimeDelta max_delta; for (size_t i = 0; i < kMaxSamples; i++) { - if (laps_[i] > max_delta) + if (laps_[i] > max_delta) { max_delta = laps_[i]; + } } return max_delta; } @@ -135,7 +136,7 @@ void Stopwatch::InitVisualizeSurface(const SkRect& rect) const { cache_canvas->drawPath(path, paint); } -void Stopwatch::Visualize(SkCanvas& canvas, const SkRect& rect) const { +void Stopwatch::Visualize(SkCanvas* canvas, const SkRect& rect) const { // Initialize visualize cache if it has not yet been initialized. InitVisualizeSurface(rect); @@ -191,8 +192,9 @@ void Stopwatch::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Limit the number of markers displayed. After a certain point, the graph // becomes crowded - if (frame_marker_count > kMaxFrameMarkers) + if (frame_marker_count > kMaxFrameMarkers) { frame_marker_count = 1; + } for (size_t frame_index = 0; frame_index < frame_marker_count; frame_index++) { @@ -224,7 +226,7 @@ void Stopwatch::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Draw the cached surface onto the output canvas. paint.reset(); - visualize_cache_surface_->draw(&canvas, rect.x(), rect.y(), &paint); + visualize_cache_surface_->draw(canvas, rect.x(), rect.y(), &paint); } CounterValues::CounterValues() : current_sample_(kMaxSamples - 1) { @@ -238,7 +240,7 @@ void CounterValues::Add(int64_t value) { values_[current_sample_] = value; } -void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { +void CounterValues::Visualize(SkCanvas* canvas, const SkRect& rect) const { size_t max_bytes = GetMaxValue(); if (max_bytes == 0) { @@ -252,7 +254,7 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Paint the background. paint.setColor(0x99FFFFFF); - canvas.drawRect(rect, paint); + canvas->drawRect(rect, paint); // Establish the graph position. const SkScalar x = rect.x(); @@ -268,10 +270,12 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { for (size_t i = 0; i < kMaxSamples; ++i) { int64_t current_bytes = values_[i]; - double ratio = - (double)(current_bytes - min_bytes) / (max_bytes - min_bytes); - path.lineTo(x + (((double)(i) / (double)kMaxSamples) * width), - y + ((1.0 - ratio) * height)); + double ratio = static_cast(current_bytes - min_bytes) / + static_cast(max_bytes - min_bytes); + path.lineTo( + x + ((static_cast(i) / static_cast(kMaxSamples)) * + width), + y + ((1.0 - ratio) * height)); } path.rLineTo(100, 0); @@ -280,7 +284,7 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { // Draw the graph. paint.setColor(0xAA0000FF); - canvas.drawPath(path, paint); + canvas->drawPath(path, paint); // Paint the vertical marker for the current frame. const double sample_unit_width = (1.0 / kMaxSamples); @@ -294,7 +298,7 @@ void CounterValues::Visualize(SkCanvas& canvas, const SkRect& rect) const { const auto marker_rect = SkRect::MakeLTRB( sample_x, y, sample_x + width * sample_unit_width + sample_margin_width * 2, bottom); - canvas.drawRect(marker_rect, paint); + canvas->drawRect(marker_rect, paint); } int64_t CounterValues::GetCurrentValue() const { diff --git a/flow/instrumentation.h b/flow/instrumentation.h index a4697830cf972..49ca887844cec 100644 --- a/flow/instrumentation.h +++ b/flow/instrumentation.h @@ -30,7 +30,7 @@ class Stopwatch { void InitVisualizeSurface(const SkRect& rect) const; - void Visualize(SkCanvas& canvas, const SkRect& rect) const; + void Visualize(SkCanvas* canvas, const SkRect& rect) const; void Start(); @@ -81,7 +81,7 @@ class CounterValues { void Add(int64_t value); - void Visualize(SkCanvas& canvas, const SkRect& rect) const; + void Visualize(SkCanvas* canvas, const SkRect& rect) const; int64_t GetCurrentValue() const; diff --git a/flow/layers/child_scene_layer.cc b/flow/layers/child_scene_layer.cc index 795946e0855c9..2a51590ff785c 100644 --- a/flow/layers/child_scene_layer.cc +++ b/flow/layers/child_scene_layer.cc @@ -4,8 +4,6 @@ #include "flutter/flow/layers/child_scene_layer.h" -#include "flutter/flow/view_holder.h" - namespace flutter { ChildSceneLayer::ChildSceneLayer(zx_koid_t layer_id, @@ -19,11 +17,9 @@ ChildSceneLayer::ChildSceneLayer(zx_koid_t layer_id, void ChildSceneLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "ChildSceneLayer::Preroll"); - set_needs_system_composite(true); - - CheckForChildLayerBelow(context); context->child_scene_layer_exists_below = true; + CheckForChildLayerBelow(context); // An alpha "hole punch" is required if the frame behind us is not opaque. if (!context->is_opaque) { @@ -49,15 +45,7 @@ void ChildSceneLayer::Paint(PaintContext& context) const { void ChildSceneLayer::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "ChildSceneLayer::UpdateScene"); FML_DCHECK(needs_system_composite()); - - Layer::UpdateScene(context); - - auto* view_holder = ViewHolder::FromId(layer_id_); - FML_DCHECK(view_holder); - - view_holder->UpdateScene(context, offset_, size_, - SkScalarRoundToInt(context.alphaf() * 255), - hit_testable_); + context.UpdateView(layer_id_, offset_, size_, hit_testable_); } } // namespace flutter diff --git a/flow/layers/container_layer.cc b/flow/layers/container_layer.cc index d8bf8ed13a1b4..825826b70835f 100644 --- a/flow/layers/container_layer.cc +++ b/flow/layers/container_layer.cc @@ -4,6 +4,8 @@ #include "flutter/flow/layers/container_layer.h" +#include + namespace flutter { ContainerLayer::ContainerLayer() {} @@ -30,6 +32,9 @@ void ContainerLayer::PrerollChildren(PrerollContext* context, const SkMatrix& child_matrix, SkRect* child_paint_bounds) { #if defined(LEGACY_FUCHSIA_EMBEDDER) + // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), + // Layers that appear above the embedded content will be turned into their own + // Scenic layers. child_layer_exists_below_ = context->child_scene_layer_exists_below; context->child_scene_layer_exists_below = false; #endif @@ -98,63 +103,20 @@ void ContainerLayer::UpdateScene(SceneUpdateContext& context) { } void ContainerLayer::UpdateSceneChildren(SceneUpdateContext& context) { - auto update_scene_layers = [&] { - // Paint all of the layers which need to be drawn into the container. - // These may be flattened down to a containing Scenic Frame. - for (auto& layer : layers_) { - if (layer->needs_system_composite()) { - layer->UpdateScene(context); - } - } - }; - FML_DCHECK(needs_system_composite()); - // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), - // PhysicalShapeLayers that appear above the embedded content will be turned - // into their own Scenic layers. + std::optional frame; if (child_layer_exists_below_) { - float global_scenic_elevation = - context.GetGlobalElevationForNextScenicLayer(); - float local_scenic_elevation = - global_scenic_elevation - context.scenic_elevation(); - float z_translation = -local_scenic_elevation; - - // Retained rendering: speedup by reusing a retained entity node if - // possible. When an entity node is reused, no paint layer is added to the - // frame so we won't call PhysicalShapeLayer::Paint. - LayerRasterCacheKey key(unique_id(), context.Matrix()); - if (context.HasRetainedNode(key)) { - TRACE_EVENT_INSTANT0("flutter", "retained layer cache hit"); - scenic::EntityNode* retained_node = context.GetRetainedNode(key); - FML_DCHECK(context.top_entity()); - FML_DCHECK(retained_node->session() == context.session()); - - // Re-adjust the elevation. - retained_node->SetTranslation(0.f, 0.f, z_translation); - - context.top_entity()->entity_node().AddChild(*retained_node); - return; - } - - TRACE_EVENT_INSTANT0("flutter", "cache miss, creating"); - // If we can't find an existing retained surface, create one. - SceneUpdateContext::Frame frame( + frame.emplace( context, SkRRect::MakeRect(paint_bounds()), SK_ColorTRANSPARENT, - SkScalarRoundToInt(context.alphaf() * 255), - "flutter::PhysicalShapeLayer", z_translation, this); - - frame.AddPaintLayer(this); - - // Node: UpdateSceneChildren needs to be called here so that |frame| is - // still in scope (and therefore alive) while UpdateSceneChildren is being - // called. - float scenic_elevation = context.scenic_elevation(); - context.set_scenic_elevation(scenic_elevation + local_scenic_elevation); - update_scene_layers(); - context.set_scenic_elevation(scenic_elevation); - } else { - update_scene_layers(); + SkScalarRoundToInt(context.alphaf() * 255), "flutter::ContainerLayer"); + frame->AddPaintLayer(this); + } + + for (auto& layer : layers_) { + if (layer->needs_system_composite()) { + layer->UpdateScene(context); + } } } diff --git a/flow/layers/fuchsia_layer_unittests.cc b/flow/layers/fuchsia_layer_unittests.cc index b1e7d2be05500..fcc17c0ad06e4 100644 --- a/flow/layers/fuchsia_layer_unittests.cc +++ b/flow/layers/fuchsia_layer_unittests.cc @@ -238,57 +238,17 @@ class MockSession : public fuchsia::ui::scenic::testing::Session_TestBase { fuchsia::ui::scenic::SessionListenerPtr listener_; }; -class MockSurfaceProducerSurface - : public SceneUpdateContext::SurfaceProducerSurface { +class MockSessionWrapper : public flutter::SessionWrapper { public: - MockSurfaceProducerSurface(scenic::Session* session, const SkISize& size) - : image_(session, 0, 0, {}), size_(size) {} + MockSessionWrapper(fuchsia::ui::scenic::SessionPtr session_ptr) + : session_(std::move(session_ptr)) {} + ~MockSessionWrapper() override = default; - size_t AdvanceAndGetAge() override { return 0; } - - bool FlushSessionAcquireAndReleaseEvents() override { return false; } - - bool IsValid() const override { return false; } - - SkISize GetSize() const override { return size_; } - - void SignalWritesFinished( - const std::function& on_writes_committed) override {} - - scenic::Image* GetImage() override { return &image_; }; - - sk_sp GetSkiaSurface() const override { return nullptr; }; + scenic::Session* get() override { return &session_; } + void Present() override { session_.Flush(); } private: - scenic::Image image_; - SkISize size_; -}; - -class MockSurfaceProducer : public SceneUpdateContext::SurfaceProducer { - public: - MockSurfaceProducer(scenic::Session* session) : session_(session) {} - std::unique_ptr ProduceSurface( - const SkISize& size, - const LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) override { - return std::make_unique(session_, size); - } - - // Query a retained entity node (owned by a retained surface) for retained - // rendering. - bool HasRetainedNode(const LayerRasterCacheKey& key) const override { - return false; - } - - scenic::EntityNode* GetRetainedNode(const LayerRasterCacheKey& key) override { - return nullptr; - } - - void SubmitSurface(std::unique_ptr - surface) override {} - - private: - scenic::Session* session_; + scenic::Session session_; }; struct TestContext { @@ -297,12 +257,11 @@ struct TestContext { fml::RefPtr task_runner; // Session. - MockSession mock_session; fidl::InterfaceRequest listener_request; - std::unique_ptr session; + MockSession mock_session; + std::unique_ptr mock_session_wrapper; // SceneUpdateContext. - std::unique_ptr mock_surface_producer; std::unique_ptr scene_update_context; // PrerollContext. @@ -324,15 +283,13 @@ std::unique_ptr InitTest() { fuchsia::ui::scenic::SessionListenerPtr listener; context->listener_request = listener.NewRequest(); context->mock_session.Bind(session_ptr.NewRequest(), std::move(listener)); - context->session = std::make_unique(std::move(session_ptr)); + context->mock_session_wrapper = + std::make_unique(std::move(session_ptr)); // Init SceneUpdateContext. - context->mock_surface_producer = - std::make_unique(context->session.get()); context->scene_update_context = std::make_unique( - context->session.get(), context->mock_surface_producer.get()); - context->scene_update_context->set_metrics( - fidl::MakeOptional(fuchsia::ui::gfx::Metrics{1.f, 1.f, 1.f})); + "fuchsia_layer_unittest", fuchsia::ui::views::ViewToken(), + scenic::ViewRefPair::New(), *(context->mock_session_wrapper)); // Init PrerollContext. context->preroll_context = std::unique_ptr(new PrerollContext{ @@ -348,7 +305,6 @@ std::unique_ptr InitTest() { context->unused_texture_registry, // texture registry (not // supported) false, // checkerboard_offscreen_layers - 100.f, // maximum depth allowed for rendering 1.f // ratio between logical and physical }); @@ -602,7 +558,7 @@ TEST_F(FuchsiaLayerTest, DISABLED_PhysicalShapeLayersAndChildSceneLayers) { // against the list above. root->UpdateScene(*(test_context->scene_update_context)); - test_context->session->Flush(); + test_context->mock_session_wrapper->Present(); // Run loop until idle, so that the Session receives and processes // its method calls. @@ -784,7 +740,7 @@ TEST_F(FuchsiaLayerTest, DISABLED_OpacityAndTransformLayer) { // commands against the list above. root->UpdateScene(*(test_context->scene_update_context)); - test_context->session->Flush(); + test_context->mock_session_wrapper->Present(); // Run loop until idle, so that the Session receives and processes // its method calls. diff --git a/flow/layers/layer.cc b/flow/layers/layer.cc index 97da04f7f54c8..490f123ec5f6f 100644 --- a/flow/layers/layer.cc +++ b/flow/layers/layer.cc @@ -9,10 +9,11 @@ namespace flutter { -Layer::Layer() +Layer::Layer(size_t external_size) : paint_bounds_(SkRect::MakeEmpty()), unique_id_(NextUniqueID()), - needs_system_composite_(false) {} + needs_system_composite_(false), + external_size_(external_size) {} Layer::~Layer() = default; @@ -58,6 +59,9 @@ Layer::AutoPrerollSaveLayerState::~AutoPrerollSaveLayerState() { #if defined(LEGACY_FUCHSIA_EMBEDDER) void Layer::CheckForChildLayerBelow(PrerollContext* context) { + // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), + // PhysicalShapeLayers that appear above the embedded content will be turned + // into their own Scenic layers. child_layer_exists_below_ = context->child_scene_layer_exists_below; if (child_layer_exists_below_) { set_needs_system_composite(true); @@ -65,42 +69,14 @@ void Layer::CheckForChildLayerBelow(PrerollContext* context) { } void Layer::UpdateScene(SceneUpdateContext& context) { - // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), - // PhysicalShapeLayers that appear above the embedded content will be turned - // into their own Scenic layers. - if (child_layer_exists_below_) { - float global_scenic_elevation = - context.GetGlobalElevationForNextScenicLayer(); - float local_scenic_elevation = - global_scenic_elevation - context.scenic_elevation(); - float z_translation = -local_scenic_elevation; - - // Retained rendering: speedup by reusing a retained entity node if - // possible. When an entity node is reused, no paint layer is added to the - // frame so we won't call PhysicalShapeLayer::Paint. - LayerRasterCacheKey key(unique_id(), context.Matrix()); - if (context.HasRetainedNode(key)) { - TRACE_EVENT_INSTANT0("flutter", "retained layer cache hit"); - scenic::EntityNode* retained_node = context.GetRetainedNode(key); - FML_DCHECK(context.top_entity()); - FML_DCHECK(retained_node->session() == context.session()); - - // Re-adjust the elevation. - retained_node->SetTranslation(0.f, 0.f, z_translation); - - context.top_entity()->entity_node().AddChild(*retained_node); - return; - } - - TRACE_EVENT_INSTANT0("flutter", "cache miss, creating"); - // If we can't find an existing retained surface, create one. - SceneUpdateContext::Frame frame( - context, SkRRect::MakeRect(paint_bounds()), SK_ColorTRANSPARENT, - SkScalarRoundToInt(context.alphaf() * 255), - "flutter::PhysicalShapeLayer", z_translation, this); - - frame.AddPaintLayer(this); - } + FML_DCHECK(needs_system_composite()); + FML_DCHECK(child_layer_exists_below_); + + SceneUpdateContext::Frame frame( + context, SkRRect::MakeRect(paint_bounds()), SK_ColorTRANSPARENT, + SkScalarRoundToInt(context.alphaf() * 255), "flutter::Layer"); + + frame.AddPaintLayer(this); } #endif diff --git a/flow/layers/layer.h b/flow/layers/layer.h index b22a322a73422..5e75b3937ca18 100644 --- a/flow/layers/layer.h +++ b/flow/layers/layer.h @@ -56,14 +56,10 @@ struct PrerollContext { const Stopwatch& ui_time; TextureRegistry& texture_registry; const bool checkerboard_offscreen_layers; - - // These allow us to make use of the scene metrics during Preroll. - float frame_physical_depth; - float frame_device_pixel_ratio; + const float frame_device_pixel_ratio; // These allow us to track properties like elevation, opacity, and the // prescence of a platform view during Preroll. - float total_elevation = 0.0f; bool has_platform_view = false; bool is_opaque = true; #if defined(LEGACY_FUCHSIA_EMBEDDER) @@ -71,13 +67,14 @@ struct PrerollContext { // Informs whether a layer needs to be system composited. bool child_scene_layer_exists_below = false; #endif + size_t uncached_external_size = 0; }; // Represents a single composited layer. Created on the UI thread but then // subquently used on the Rasterizer thread. class Layer { public: - Layer(); + Layer(size_t external_size = 0); virtual ~Layer(); virtual void Preroll(PrerollContext* context, const SkMatrix& matrix); @@ -128,10 +125,7 @@ class Layer { TextureRegistry& texture_registry; const RasterCache* raster_cache; const bool checkerboard_offscreen_layers; - - // These allow us to make use of the scene metrics during Paint. - float frame_physical_depth; - float frame_device_pixel_ratio; + const float frame_device_pixel_ratio; }; // Calls SkCanvas::saveLayer and restores the layer upon destruction. Also @@ -185,6 +179,8 @@ class Layer { uint64_t unique_id() const { return unique_id_; } + size_t external_size() const { return external_size_; } + protected: #if defined(LEGACY_FUCHSIA_EMBEDDER) bool child_layer_exists_below_ = false; @@ -194,6 +190,7 @@ class Layer { SkRect paint_bounds_; uint64_t unique_id_; bool needs_system_composite_; + size_t external_size_ = 0; static uint64_t NextUniqueID(); diff --git a/flow/layers/layer_tree.cc b/flow/layers/layer_tree.cc index c8778630fc1b4..160c8c50e1024 100644 --- a/flow/layers/layer_tree.cc +++ b/flow/layers/layer_tree.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/layers/layer_tree.h" @@ -12,18 +11,19 @@ namespace flutter { -LayerTree::LayerTree(const SkISize& frame_size, - float frame_physical_depth, - float frame_device_pixel_ratio) +LayerTree::LayerTree(const SkISize& frame_size, float device_pixel_ratio) : frame_size_(frame_size), - frame_physical_depth_(frame_physical_depth), - frame_device_pixel_ratio_(frame_device_pixel_ratio), + device_pixel_ratio_(device_pixel_ratio), rasterizer_tracing_threshold_(0), checkerboard_raster_cache_images_(false), - checkerboard_offscreen_layers_(false) {} + checkerboard_offscreen_layers_(false) { + FML_CHECK(device_pixel_ratio_ != 0.0f); +} -void LayerTree::RecordBuildTime(fml::TimePoint build_start, +void LayerTree::RecordBuildTime(fml::TimePoint vsync_start, + fml::TimePoint build_start, fml::TimePoint target_time) { + vsync_start_ = vsync_start; build_start_ = build_start; target_time_ = target_time; build_finish_ = fml::TimePoint::Now(); @@ -55,32 +55,22 @@ bool LayerTree::Preroll(CompositorContext::ScopedFrame& frame, frame.context().ui_time(), frame.context().texture_registry(), checkerboard_offscreen_layers_, - frame_physical_depth_, - frame_device_pixel_ratio_}; + device_pixel_ratio_}; root_layer_->Preroll(&context, frame.root_surface_transformation()); + frame.add_external_size(context.uncached_external_size); return context.surface_needs_readback; } #if defined(LEGACY_FUCHSIA_EMBEDDER) -void LayerTree::UpdateScene(SceneUpdateContext& context, - scenic::ContainerNode& container) { +void LayerTree::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "LayerTree::UpdateScene"); - // Ensure the context is aware of the view metrics. - context.set_dimensions(frame_size_, frame_physical_depth_, - frame_device_pixel_ratio_); - - const auto& metrics = context.metrics(); - FML_DCHECK(metrics->scale_x > 0.0f); - FML_DCHECK(metrics->scale_y > 0.0f); - FML_DCHECK(metrics->scale_z > 0.0f); + // Reset for a new Scene. + context.Reset(); - SceneUpdateContext::Transform transform(context, // context - 1.0f / metrics->scale_x, // X - 1.0f / metrics->scale_y, // Y - 1.0f / metrics->scale_z // Z - ); + const float inv_dpr = 1.0f / device_pixel_ratio_; + SceneUpdateContext::Transform transform(context, inv_dpr, inv_dpr, 1.0f); SceneUpdateContext::Frame frame( context, @@ -93,7 +83,7 @@ void LayerTree::UpdateScene(SceneUpdateContext& context, if (root_layer_->needs_painting()) { frame.AddPaintLayer(root_layer_.get()); } - container.AddChild(transform.entity_node()); + context.root_node().AddChild(transform.entity_node()); } #endif @@ -117,7 +107,7 @@ void LayerTree::Paint(CompositorContext::ScopedFrame& frame, } Layer::PaintContext context = { - (SkCanvas*)&internal_nodes_canvas, + static_cast(&internal_nodes_canvas), frame.canvas(), frame.gr_context(), frame.view_embedder(), @@ -126,11 +116,11 @@ void LayerTree::Paint(CompositorContext::ScopedFrame& frame, frame.context().texture_registry(), ignore_raster_cache ? nullptr : &frame.context().raster_cache(), checkerboard_offscreen_layers_, - frame_physical_depth_, - frame_device_pixel_ratio_}; + device_pixel_ratio_}; - if (root_layer_->needs_painting()) + if (root_layer_->needs_painting()) { root_layer_->Paint(context); + } } sk_sp LayerTree::Flatten(const SkRect& bounds) { @@ -151,19 +141,18 @@ sk_sp LayerTree::Flatten(const SkRect& bounds) { root_surface_transformation.reset(); PrerollContext preroll_context{ - nullptr, // raster_cache (don't consult the cache) - nullptr, // gr_context (used for the raster cache) - nullptr, // external view embedder - unused_stack, // mutator stack - nullptr, // SkColorSpace* dst_color_space - kGiantRect, // SkRect cull_rect - false, // layer reads from surface - unused_stopwatch, // frame time (dont care) - unused_stopwatch, // engine time (dont care) - unused_texture_registry, // texture registry (not supported) - false, // checkerboard_offscreen_layers - frame_physical_depth_, // maximum depth allowed for rendering - frame_device_pixel_ratio_ // ratio between logical and physical + nullptr, // raster_cache (don't consult the cache) + nullptr, // gr_context (used for the raster cache) + nullptr, // external view embedder + unused_stack, // mutator stack + nullptr, // SkColorSpace* dst_color_space + kGiantRect, // SkRect cull_rect + false, // layer reads from surface + unused_stopwatch, // frame time (dont care) + unused_stopwatch, // engine time (dont care) + unused_texture_registry, // texture registry (not supported) + false, // checkerboard_offscreen_layers + device_pixel_ratio_ // ratio between logical and physical }; SkISize canvas_size = canvas->getBaseLayerSize(); @@ -171,17 +160,16 @@ sk_sp LayerTree::Flatten(const SkRect& bounds) { internal_nodes_canvas.addCanvas(canvas); Layer::PaintContext paint_context = { - (SkCanvas*)&internal_nodes_canvas, + static_cast(&internal_nodes_canvas), canvas, // canvas nullptr, nullptr, - unused_stopwatch, // frame time (dont care) - unused_stopwatch, // engine time (dont care) - unused_texture_registry, // texture registry (not supported) - nullptr, // raster cache - false, // checkerboard offscreen layers - frame_physical_depth_, // maximum depth allowed for rendering - frame_device_pixel_ratio_ // ratio between logical and physical + unused_stopwatch, // frame time (dont care) + unused_stopwatch, // engine time (dont care) + unused_texture_registry, // texture registry (not supported) + nullptr, // raster cache + false, // checkerboard offscreen layers + device_pixel_ratio_ // ratio between logical and physical }; // Even if we don't have a root layer, we still need to create an empty diff --git a/flow/layers/layer_tree.h b/flow/layers/layer_tree.h index 733284afe65db..81423c353d458 100644 --- a/flow/layers/layer_tree.h +++ b/flow/layers/layer_tree.h @@ -20,9 +20,7 @@ namespace flutter { class LayerTree { public: - LayerTree(const SkISize& frame_size, - float frame_physical_depth, - float frame_device_pixel_ratio); + LayerTree(const SkISize& frame_size, float device_pixel_ratio); // Perform a preroll pass on the tree and return information about // the tree that affects rendering this frame. @@ -35,8 +33,7 @@ class LayerTree { bool ignore_raster_cache = false); #if defined(LEGACY_FUCHSIA_EMBEDDER) - void UpdateScene(SceneUpdateContext& context, - scenic::ContainerNode& container); + void UpdateScene(SceneUpdateContext& context); #endif void Paint(CompositorContext::ScopedFrame& frame, @@ -51,10 +48,13 @@ class LayerTree { } const SkISize& frame_size() const { return frame_size_; } - float frame_physical_depth() const { return frame_physical_depth_; } - float frame_device_pixel_ratio() const { return frame_device_pixel_ratio_; } + float device_pixel_ratio() const { return device_pixel_ratio_; } - void RecordBuildTime(fml::TimePoint build_start, fml::TimePoint target_time); + void RecordBuildTime(fml::TimePoint vsync_start, + fml::TimePoint build_start, + fml::TimePoint target_time); + fml::TimePoint vsync_start() const { return vsync_start_; } + fml::TimeDelta vsync_overhead() const { return build_start_ - vsync_start_; } fml::TimePoint build_start() const { return build_start_; } fml::TimePoint build_finish() const { return build_finish_; } fml::TimeDelta build_time() const { return build_finish_ - build_start_; } @@ -79,16 +79,14 @@ class LayerTree { checkerboard_offscreen_layers_ = checkerboard; } - double device_pixel_ratio() const { return frame_device_pixel_ratio_; } - private: std::shared_ptr root_layer_; + fml::TimePoint vsync_start_; fml::TimePoint build_start_; fml::TimePoint build_finish_; fml::TimePoint target_time_; SkISize frame_size_ = SkISize::MakeEmpty(); // Physical pixels. - float frame_physical_depth_; - float frame_device_pixel_ratio_ = 1.0f; // Logical / Physical pixels ratio. + const float device_pixel_ratio_; // Logical / Physical pixels ratio. uint32_t rasterizer_tracing_threshold_; bool checkerboard_raster_cache_images_; bool checkerboard_offscreen_layers_; diff --git a/flow/layers/layer_tree_unittests.cc b/flow/layers/layer_tree_unittests.cc index 1215b72f78726..99231f8254edd 100644 --- a/flow/layers/layer_tree_unittests.cc +++ b/flow/layers/layer_tree_unittests.cc @@ -15,11 +15,11 @@ namespace flutter { namespace testing { -class LayerTreeTest : public CanvasTest { +class LayerTreeTest : public CanvasTest, public CompositorContext::Delegate { public: LayerTreeTest() - : layer_tree_(SkISize::Make(64, 64), 100.0f, 1.0f), - compositor_context_(fml::kDefaultFrameBudget), + : layer_tree_(SkISize::Make(64, 64), 1.0f), + compositor_context_(*this), root_transform_(SkMatrix::Translate(1.0f, 1.0f)), scoped_frame_(compositor_context_.AcquireFrame(nullptr, &mock_canvas(), @@ -33,11 +33,24 @@ class LayerTreeTest : public CanvasTest { CompositorContext::ScopedFrame& frame() { return *scoped_frame_.get(); } const SkMatrix& root_transform() { return root_transform_; } + // |CompositorContext::Delegate| + void OnCompositorEndFrame(size_t freed_hint) override { + last_freed_hint_ = freed_hint; + } + + // |CompositorContext::Delegate| + fml::Milliseconds GetFrameBudget() override { + return fml::kDefaultFrameBudget; + } + + size_t last_freed_hint() { return last_freed_hint_; } + private: LayerTree layer_tree_; CompositorContext compositor_context_; SkMatrix root_transform_; std::unique_ptr scoped_frame_; + size_t last_freed_hint_ = 0; }; TEST_F(LayerTreeTest, PaintingEmptyLayerDies) { diff --git a/flow/layers/opacity_layer.h b/flow/layers/opacity_layer.h index 73e508f854bc4..ed5f0283ad356 100644 --- a/flow/layers/opacity_layer.h +++ b/flow/layers/opacity_layer.h @@ -38,7 +38,6 @@ class OpacityLayer : public MergedContainerLayer { private: SkAlpha alpha_; SkPoint offset_; - SkRRect frameRRect_; FML_DISALLOW_COPY_AND_ASSIGN(OpacityLayer); }; diff --git a/flow/layers/performance_overlay_layer.cc b/flow/layers/performance_overlay_layer.cc index 3aac5a54d9a72..05ade5e21af73 100644 --- a/flow/layers/performance_overlay_layer.cc +++ b/flow/layers/performance_overlay_layer.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include #include @@ -14,7 +13,7 @@ namespace flutter { namespace { -void VisualizeStopWatch(SkCanvas& canvas, +void VisualizeStopWatch(SkCanvas* canvas, const Stopwatch& stopwatch, SkScalar x, SkScalar y, @@ -37,7 +36,7 @@ void VisualizeStopWatch(SkCanvas& canvas, stopwatch, label_prefix, font_path); SkPaint paint; paint.setColor(SK_ColorGRAY); - canvas.drawTextBlob(text, x + label_x, y + height + label_y, paint); + canvas->drawTextBlob(text, x + label_x, y + height + label_y, paint); } } @@ -77,8 +76,9 @@ PerformanceOverlayLayer::PerformanceOverlayLayer(uint64_t options, void PerformanceOverlayLayer::Paint(PaintContext& context) const { const int padding = 8; - if (!options_) + if (!options_) { return; + } TRACE_EVENT0("flutter", "PerformanceOverlayLayer::Paint"); SkScalar x = paint_bounds().x() + padding; @@ -88,11 +88,11 @@ void PerformanceOverlayLayer::Paint(PaintContext& context) const { SkAutoCanvasRestore save(context.leaf_nodes_canvas, true); VisualizeStopWatch( - *context.leaf_nodes_canvas, context.raster_time, x, y, width, + context.leaf_nodes_canvas, context.raster_time, x, y, width, height - padding, options_ & kVisualizeRasterizerStatistics, options_ & kDisplayRasterizerStatistics, "Raster", font_path_); - VisualizeStopWatch(*context.leaf_nodes_canvas, context.ui_time, x, y + height, + VisualizeStopWatch(context.leaf_nodes_canvas, context.ui_time, x, y + height, width, height - padding, options_ & kVisualizeEngineStatistics, options_ & kDisplayEngineStatistics, "UI", font_path_); diff --git a/flow/layers/physical_shape_layer.cc b/flow/layers/physical_shape_layer.cc index 4f87fb23605a0..7ba2b7cb734ea 100644 --- a/flow/layers/physical_shape_layer.cc +++ b/flow/layers/physical_shape_layer.cc @@ -21,28 +21,7 @@ PhysicalShapeLayer::PhysicalShapeLayer(SkColor color, shadow_color_(shadow_color), elevation_(elevation), path_(path), - isRect_(false), - clip_behavior_(clip_behavior) { - SkRect rect; - if (path.isRect(&rect)) { - isRect_ = true; - frameRRect_ = SkRRect::MakeRect(rect); - } else if (path.isRRect(&frameRRect_)) { - isRect_ = frameRRect_.isRect(); - } else if (path.isOval(&rect)) { - // isRRect returns false for ovals, so we need to explicitly check isOval - // as well. - frameRRect_ = SkRRect::MakeOval(rect); - } else { - // Scenic currently doesn't provide an easy way to create shapes from - // arbitrary paths. - // For shapes that cannot be represented as a rounded rectangle we - // default to use the bounding rectangle. - // TODO(amirh): fix this once we have a way to create a Scenic shape from - // an SkPath. - frameRRect_ = SkRRect::MakeRect(path.getBounds()); - } -} + clip_behavior_(clip_behavior) {} void PhysicalShapeLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { @@ -50,14 +29,9 @@ void PhysicalShapeLayer::Preroll(PrerollContext* context, Layer::AutoPrerollSaveLayerState save = Layer::AutoPrerollSaveLayerState::Create(context, UsesSaveLayer()); - context->total_elevation += elevation_; - total_elevation_ = context->total_elevation; - SkRect child_paint_bounds; PrerollChildren(context, matrix, &child_paint_bounds); - context->total_elevation -= elevation_; - if (elevation_ == 0) { set_paint_bounds(path_.getBounds()); } else { diff --git a/flow/layers/physical_shape_layer.h b/flow/layers/physical_shape_layer.h index 2c04368e9a81e..ce49af1a003ae 100644 --- a/flow/layers/physical_shape_layer.h +++ b/flow/layers/physical_shape_layer.h @@ -35,16 +35,13 @@ class PhysicalShapeLayer : public ContainerLayer { return clip_behavior_ == Clip::antiAliasWithSaveLayer; } - float total_elevation() const { return total_elevation_; } + float elevation() const { return elevation_; } private: SkColor color_; SkColor shadow_color_; float elevation_ = 0.0f; - float total_elevation_ = 0.0f; SkPath path_; - bool isRect_; - SkRRect frameRRect_; Clip clip_behavior_; }; diff --git a/flow/layers/physical_shape_layer_unittests.cc b/flow/layers/physical_shape_layer_unittests.cc index 7ad0b4e5eddcb..bb5d0acfad757 100644 --- a/flow/layers/physical_shape_layer_unittests.cc +++ b/flow/layers/physical_shape_layer_unittests.cc @@ -131,7 +131,7 @@ TEST_F(PhysicalShapeLayerTest, ElevationSimple) { initial_elevation, 1.0f)); EXPECT_TRUE(layer->needs_painting()); EXPECT_FALSE(layer->needs_system_composite()); - EXPECT_EQ(layer->total_elevation(), initial_elevation); + EXPECT_EQ(layer->elevation(), initial_elevation); // The Fuchsia system compositor handles all elevated PhysicalShapeLayers and // their shadows , so we do not use the direct |Paint()| path there. @@ -162,7 +162,6 @@ TEST_F(PhysicalShapeLayerTest, ElevationComplex) { // | // layers[1] + 2.0f = 3.0f constexpr float initial_elevations[4] = {1.0f, 2.0f, 3.0f, 4.0f}; - constexpr float total_elevations[4] = {1.0f, 3.0f, 4.0f, 8.0f}; SkPath layer_path; layer_path.addRect(0, 0, 80, 80).close(); @@ -187,7 +186,6 @@ TEST_F(PhysicalShapeLayerTest, ElevationComplex) { 1.0f /* pixel_ratio */))); EXPECT_TRUE(layers[i]->needs_painting()); EXPECT_FALSE(layers[i]->needs_system_composite()); - EXPECT_EQ(layers[i]->total_elevation(), total_elevations[i]); } // The Fuchsia system compositor handles all elevated PhysicalShapeLayers and diff --git a/flow/layers/picture_layer.cc b/flow/layers/picture_layer.cc index d5d6a34b573e8..067a59d782398 100644 --- a/flow/layers/picture_layer.cc +++ b/flow/layers/picture_layer.cc @@ -11,8 +11,10 @@ namespace flutter { PictureLayer::PictureLayer(const SkPoint& offset, SkiaGPUObject picture, bool is_complex, - bool will_change) - : offset_(offset), + bool will_change, + size_t external_size) + : Layer(external_size), + offset_(offset), picture_(std::move(picture)), is_complex_(is_complex), will_change_(will_change) {} @@ -26,6 +28,7 @@ void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { SkPicture* sk_picture = picture(); + bool cached = false; if (auto* cache = context->raster_cache) { TRACE_EVENT0("flutter", "PictureLayer::RasterCache (Preroll)"); @@ -34,8 +37,13 @@ void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { #ifndef SUPPORT_FRACTIONAL_TRANSLATION ctm = RasterCache::GetIntegralTransCTM(ctm); #endif - cache->Prepare(context->gr_context, sk_picture, ctm, - context->dst_color_space, is_complex_, will_change_); + cached = cache->Prepare(context->gr_context, sk_picture, ctm, + context->dst_color_space, is_complex_, will_change_, + external_size()); + } + + if (!cached) { + context->uncached_external_size += external_size(); } SkRect bounds = sk_picture->cullRect().makeOffset(offset_.x(), offset_.y()); diff --git a/flow/layers/picture_layer.h b/flow/layers/picture_layer.h index e733e7455ca6c..c86361a9aaaae 100644 --- a/flow/layers/picture_layer.h +++ b/flow/layers/picture_layer.h @@ -18,7 +18,8 @@ class PictureLayer : public Layer { PictureLayer(const SkPoint& offset, SkiaGPUObject picture, bool is_complex, - bool will_change); + bool will_change, + size_t external_size); SkPicture* picture() const { return picture_.get().get(); } diff --git a/flow/layers/picture_layer_unittests.cc b/flow/layers/picture_layer_unittests.cc index dc9e6080c1508..b7bfce854ed82 100644 --- a/flow/layers/picture_layer_unittests.cc +++ b/flow/layers/picture_layer_unittests.cc @@ -24,7 +24,7 @@ using PictureLayerTest = SkiaGPUObjectLayerTest; TEST_F(PictureLayerTest, PaintBeforePrerollInvalidPictureDies) { const SkPoint layer_offset = SkPoint::Make(0.0f, 0.0f); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(), false, false); + layer_offset, SkiaGPUObject(), false, false, 0); EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), "picture_\\.get\\(\\)"); @@ -35,7 +35,8 @@ TEST_F(PictureLayerTest, PaintBeforePreollDies) { const SkRect picture_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f); auto mock_picture = SkPicture::MakePlaceholder(picture_bounds); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false); + layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false, + 0); EXPECT_EQ(layer->paint_bounds(), SkRect::MakeEmpty()); EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), @@ -47,7 +48,8 @@ TEST_F(PictureLayerTest, PaintingEmptyLayerDies) { const SkRect picture_bounds = SkRect::MakeEmpty(); auto mock_picture = SkPicture::MakePlaceholder(picture_bounds); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false); + layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false, + 0); layer->Preroll(preroll_context(), SkMatrix()); EXPECT_EQ(layer->paint_bounds(), SkRect::MakeEmpty()); @@ -62,7 +64,7 @@ TEST_F(PictureLayerTest, PaintingEmptyLayerDies) { TEST_F(PictureLayerTest, InvalidPictureDies) { const SkPoint layer_offset = SkPoint::Make(0.0f, 0.0f); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(), false, false); + layer_offset, SkiaGPUObject(), false, false, 0); // Crashes reading a nullptr. EXPECT_DEATH_IF_SUPPORTED(layer->Preroll(preroll_context(), SkMatrix()), ""); @@ -75,7 +77,10 @@ TEST_F(PictureLayerTest, SimplePicture) { const SkRect picture_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f); auto mock_picture = SkPicture::MakePlaceholder(picture_bounds); auto layer = std::make_shared( - layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false); + layer_offset, SkiaGPUObject(mock_picture, unref_queue()), false, false, + 1000); + + EXPECT_EQ(layer->external_size(), 1000ul); layer->Preroll(preroll_context(), SkMatrix()); EXPECT_EQ(layer->paint_bounds(), diff --git a/flow/layers/platform_view_layer.cc b/flow/layers/platform_view_layer.cc index 0bd6ee7f6dfab..80514b5213e18 100644 --- a/flow/layers/platform_view_layer.cc +++ b/flow/layers/platform_view_layer.cc @@ -48,7 +48,9 @@ void PlatformViewLayer::Paint(PaintContext& context) const { #if defined(LEGACY_FUCHSIA_EMBEDDER) void PlatformViewLayer::UpdateScene(SceneUpdateContext& context) { - context.UpdateScene(view_id_, offset_, size_); + TRACE_EVENT0("flutter", "PlatformViewLayer::UpdateScene"); + FML_DCHECK(needs_system_composite()); + context.UpdateView(view_id_, offset_, size_); } #endif diff --git a/flow/layers/transform_layer.cc b/flow/layers/transform_layer.cc index d01c21950e498..8fe5dd32e1e85 100644 --- a/flow/layers/transform_layer.cc +++ b/flow/layers/transform_layer.cc @@ -4,6 +4,8 @@ #include "flutter/flow/layers/transform_layer.h" +#include + namespace flutter { TransformLayer::TransformLayer(const SkMatrix& transform) @@ -56,12 +58,12 @@ void TransformLayer::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "TransformLayer::UpdateScene"); FML_DCHECK(needs_system_composite()); + std::optional transform; if (!transform_.isIdentity()) { - SceneUpdateContext::Transform transform(context, transform_); - UpdateSceneChildren(context); - } else { - UpdateSceneChildren(context); + transform.emplace(context, transform_); } + + UpdateSceneChildren(context); } #endif diff --git a/flow/matrix_decomposition.cc b/flow/matrix_decomposition.cc index 3d3cb9e969cd9..2207fa35fb878 100644 --- a/flow/matrix_decomposition.cc +++ b/flow/matrix_decomposition.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/matrix_decomposition.h" @@ -18,12 +17,12 @@ MatrixDecomposition::MatrixDecomposition(const SkMatrix& matrix) : MatrixDecomposition(SkM44{matrix}) {} // Use custom normalize to avoid skia precision loss/normalize() privatization. -static inline void SkV3Normalize(SkV3& v) { - double mag = sqrt(v.x * v.x + v.y * v.y + v.z * v.z); +static inline void SkV3Normalize(SkV3* v) { + double mag = sqrt(v->x * v->x + v->y * v->y + v->z * v->z); double scale = 1.0 / mag; - v.x *= scale; - v.y *= scale; - v.z *= scale; + v->x *= scale; + v->y *= scale; + v->z *= scale; } MatrixDecomposition::MatrixDecomposition(SkM44 matrix) : valid_(false) { @@ -71,14 +70,14 @@ MatrixDecomposition::MatrixDecomposition(SkM44 matrix) : valid_(false) { scale_.x = row[0].length(); - SkV3Normalize(row[0]); + SkV3Normalize(&row[0]); shear_.x = row[0].dot(row[1]); row[1] = SkV3Combine(row[1], 1.0, row[0], -shear_.x); scale_.y = row[1].length(); - SkV3Normalize(row[1]); + SkV3Normalize(&row[1]); shear_.x /= scale_.y; @@ -89,7 +88,7 @@ MatrixDecomposition::MatrixDecomposition(SkM44 matrix) : valid_(false) { scale_.z = row[2].length(); - SkV3Normalize(row[2]); + SkV3Normalize(&row[2]); shear_.y /= scale_.z; shear_.z /= scale_.z; diff --git a/flow/matrix_decomposition_unittests.cc b/flow/matrix_decomposition_unittests.cc index f3c6a46c1f985..dc95033e43898 100644 --- a/flow/matrix_decomposition_unittests.cc +++ b/flow/matrix_decomposition_unittests.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/fml/build_config.h" @@ -98,7 +97,8 @@ TEST(MatrixDecomposition, Combination) { TEST(MatrixDecomposition, ScaleFloatError) { constexpr float scale_increment = 0.00001f; - for (float scale = 0.0001f; scale < 2.0f; scale += scale_increment) { + float scale = 0.0001f; + while (scale < 2.0f) { SkM44 matrix; matrix.setScale(scale, scale, 1.0f); @@ -111,11 +111,12 @@ TEST(MatrixDecomposition, ScaleFloatError) { ASSERT_FLOAT_EQ(0, decomposition3.rotation().x); ASSERT_FLOAT_EQ(0, decomposition3.rotation().y); ASSERT_FLOAT_EQ(0, decomposition3.rotation().z); + scale += scale_increment; } SkM44 matrix; - const auto scale = 1.7734375f; - matrix.setScale(scale, scale, 1.f); + const auto scale1 = 1.7734375f; + matrix.setScale(scale1, scale1, 1.f); // Bug upper bound (empirical) const auto scale2 = 1.773437559603f; @@ -136,8 +137,8 @@ TEST(MatrixDecomposition, ScaleFloatError) { flutter::MatrixDecomposition decomposition3(matrix3); ASSERT_TRUE(decomposition3.IsValid()); - ASSERT_FLOAT_EQ(scale, decomposition.scale().x); - ASSERT_FLOAT_EQ(scale, decomposition.scale().y); + ASSERT_FLOAT_EQ(scale1, decomposition.scale().x); + ASSERT_FLOAT_EQ(scale1, decomposition.scale().y); ASSERT_FLOAT_EQ(1.f, decomposition.scale().z); ASSERT_FLOAT_EQ(0, decomposition.rotation().x); ASSERT_FLOAT_EQ(0, decomposition.rotation().y); diff --git a/flow/paint_utils.cc b/flow/paint_utils.cc index b19cd02cfe216..38fc17979a0c3 100644 --- a/flow/paint_utils.cc +++ b/flow/paint_utils.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/paint_utils.h" diff --git a/flow/raster_cache.cc b/flow/raster_cache.cc index c9fcad4a34b49..f077d215a28d4 100644 --- a/flow/raster_cache.cc +++ b/flow/raster_cache.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "flutter/flow/raster_cache.h" @@ -142,6 +141,7 @@ void RasterCache::Prepare(PrerollContext* context, Entry& entry = layer_cache_[cache_key]; entry.access_count++; entry.used_this_frame = true; + entry.external_size = layer->external_size(); if (!entry.image) { entry.image = RasterizeLayer(context, layer, ctm, checkerboard_images_); } @@ -160,16 +160,16 @@ std::unique_ptr RasterCache::RasterizeLayer( canvas_size.height()); internal_nodes_canvas.addCanvas(canvas); Layer::PaintContext paintContext = { - (SkCanvas*)&internal_nodes_canvas, // internal_nodes_canvas - canvas, // leaf_nodes_canvas - context->gr_context, // gr_context - nullptr, // view_embedder + /* internal_nodes_canvas= */ static_cast( + &internal_nodes_canvas), + /* leaf_nodes_canvas= */ canvas, + /* gr_context= */ context->gr_context, + /* view_embedder= */ nullptr, context->raster_time, context->ui_time, context->texture_registry, context->has_platform_view ? nullptr : context->raster_cache, context->checkerboard_offscreen_layers, - context->frame_physical_depth, context->frame_device_pixel_ratio}; if (layer->needs_painting()) { layer->Paint(paintContext); @@ -182,7 +182,8 @@ bool RasterCache::Prepare(GrDirectContext* context, const SkMatrix& transformation_matrix, SkColorSpace* dst_color_space, bool is_complex, - bool will_change) { + bool will_change, + size_t external_size) { // Disabling caching when access_threshold is zero is historic behavior. if (access_threshold_ == 0) { return false; @@ -208,6 +209,7 @@ bool RasterCache::Prepare(GrDirectContext* context, // Creates an entry, if not present prior. Entry& entry = picture_cache_[cache_key]; + entry.external_size = external_size; if (entry.access_count < access_threshold_) { // Frame threshold has not yet been reached. return false; @@ -233,7 +235,7 @@ bool RasterCache::Draw(const SkPicture& picture, SkCanvas& canvas) const { entry.used_this_frame = true; if (entry.image) { - entry.image->draw(canvas); + entry.image->draw(canvas, nullptr); return true; } @@ -261,11 +263,12 @@ bool RasterCache::Draw(const Layer* layer, return false; } -void RasterCache::SweepAfterFrame() { - SweepOneCacheAfterFrame(picture_cache_); - SweepOneCacheAfterFrame(layer_cache_); +size_t RasterCache::SweepAfterFrame() { + size_t removed_size = SweepOneCacheAfterFrame(picture_cache_); + removed_size += SweepOneCacheAfterFrame(layer_cache_); picture_cached_this_frame_ = 0; TraceStatsToTimeline(); + return removed_size; } void RasterCache::Clear() { @@ -299,35 +302,34 @@ void RasterCache::SetCheckboardCacheImages(bool checkerboard) { void RasterCache::TraceStatsToTimeline() const { #if !FLUTTER_RELEASE + constexpr double kMegaBytes = (1 << 20); + FML_TRACE_COUNTER("flutter", "RasterCache", reinterpret_cast(this), + "LayerCount", layer_cache_.size(), "LayerMBytes", + EstimateLayerCacheByteSize() / kMegaBytes, "PictureCount", + picture_cache_.size(), "PictureMBytes", + EstimatePictureCacheByteSize() / kMegaBytes); - size_t layer_cache_count = 0; - size_t layer_cache_bytes = 0; - size_t picture_cache_count = 0; - size_t picture_cache_bytes = 0; +#endif // !FLUTTER_RELEASE +} +size_t RasterCache::EstimateLayerCacheByteSize() const { + size_t layer_cache_bytes = 0; for (const auto& item : layer_cache_) { - layer_cache_count++; if (item.second.image) { layer_cache_bytes += item.second.image->image_bytes(); } } + return layer_cache_bytes; +} +size_t RasterCache::EstimatePictureCacheByteSize() const { + size_t picture_cache_bytes = 0; for (const auto& item : picture_cache_) { - picture_cache_count++; if (item.second.image) { picture_cache_bytes += item.second.image->image_bytes(); } } - - FML_TRACE_COUNTER("flutter", "RasterCache", - reinterpret_cast(this), // - "LayerCount", layer_cache_count, // - "LayerMBytes", layer_cache_bytes * 1e-6, // - "PictureCount", picture_cache_count, // - "PictureMBytes", picture_cache_bytes * 1e-6 // - ); - -#endif // !FLUTTER_RELEASE + return picture_cache_bytes; } } // namespace flutter diff --git a/flow/raster_cache.h b/flow/raster_cache.h index d71b4e2ff9aed..901757974abc1 100644 --- a/flow/raster_cache.h +++ b/flow/raster_cache.h @@ -22,16 +22,14 @@ class RasterCacheResult { virtual ~RasterCacheResult() = default; - virtual void draw(SkCanvas& canvas, const SkPaint* paint = nullptr) const; + virtual void draw(SkCanvas& canvas, const SkPaint* paint) const; virtual SkISize image_dimensions() const { return image_ ? image_->dimensions() : SkISize::Make(0, 0); }; virtual int64_t image_bytes() const { - return image_ ? image_->dimensions().area() * - image_->imageInfo().bytesPerPixel() - : 0; + return image_ ? image_->imageInfo().computeMinByteSize() : 0; }; private: @@ -60,7 +58,7 @@ class RasterCache { * to be stored in the cache. * * @param picture the SkPicture object to be cached. - * @param context the GrContext used for rendering. + * @param context the GrDirectContext used for rendering. * @param ctm the transformation matrix used for rendering. * @param dst_color_space the destination color space that the cached * rendering will be drawn into @@ -139,7 +137,8 @@ class RasterCache { const SkMatrix& transformation_matrix, SkColorSpace* dst_color_space, bool is_complex, - bool will_change); + bool will_change, + size_t external_size = 0); void Prepare(PrerollContext* context, Layer* layer, const SkMatrix& ctm); @@ -158,7 +157,8 @@ class RasterCache { SkCanvas& canvas, SkPaint* paint = nullptr) const; - void SweepAfterFrame(); + /// Returns the amount of external bytes freed by the sweep. + size_t SweepAfterFrame(); void Clear(); @@ -170,21 +170,44 @@ class RasterCache { size_t GetPictureCachedEntriesCount() const; + /** + * @brief Estimate how much memory is used by picture raster cache entries in + * bytes. + * + * Only SkImage's memory usage is counted as other objects are often much + * smaller compared to SkImage. SkImageInfo::computeMinByteSize is used to + * estimate the SkImage memory usage. + */ + size_t EstimatePictureCacheByteSize() const; + + /** + * @brief Estimate how much memory is used by layer raster cache entries in + * bytes. + * + * Only SkImage's memory usage is counted as other objects are often much + * smaller compared to SkImage. SkImageInfo::computeMinByteSize is used to + * estimate the SkImage memory usage. + */ + size_t EstimateLayerCacheByteSize() const; + private: struct Entry { bool used_this_frame = false; size_t access_count = 0; + size_t external_size = 0; std::unique_ptr image; }; template - static void SweepOneCacheAfterFrame(Cache& cache) { + static size_t SweepOneCacheAfterFrame(Cache& cache) { std::vector dead; + size_t removed_size = 0; for (auto it = cache.begin(); it != cache.end(); ++it) { Entry& entry = it->second; if (!entry.used_this_frame) { dead.push_back(it); + removed_size += entry.external_size; } entry.used_this_frame = false; } @@ -192,6 +215,7 @@ class RasterCache { for (auto it : dead) { cache.erase(it); } + return removed_size; } const size_t access_threshold_; diff --git a/flow/rtree_unittests.cc b/flow/rtree_unittests.cc index d5c8466c1ca7d..1a1498efc8af3 100644 --- a/flow/rtree_unittests.cc +++ b/flow/rtree_unittests.cc @@ -1,7 +1,6 @@ // 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. -// FLUTTER_NOLINT #include "rtree.h" @@ -12,7 +11,7 @@ namespace flutter { namespace testing { -TEST(RTree, searchNonOverlappingDrawnRects_NoIntersection) { +TEST(RTree, searchNonOverlappingDrawnRectsNoIntersection) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -32,7 +31,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_NoIntersection) { ASSERT_TRUE(hits.empty()); } -TEST(RTree, searchNonOverlappingDrawnRects_SingleRectIntersection) { +TEST(RTree, searchNonOverlappingDrawnRectsSingleRectIntersection) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -54,7 +53,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_SingleRectIntersection) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(120, 120, 160, 160)); } -TEST(RTree, searchNonOverlappingDrawnRects_IgnoresNonDrawingRecords) { +TEST(RTree, searchNonOverlappingDrawnRectsIgnoresNonDrawingRecords) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -82,7 +81,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_IgnoresNonDrawingRecords) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(120, 120, 180, 180)); } -TEST(RTree, searchNonOverlappingDrawnRects_MultipleRectIntersection) { +TEST(RTree, searchNonOverlappingDrawnRectsMultipleRectIntersection) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -113,7 +112,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_MultipleRectIntersection) { ASSERT_EQ(*std::next(hits.begin(), 1), SkRect::MakeLTRB(300, 100, 400, 200)); } -TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase1) { +TEST(RTree, searchNonOverlappingDrawnRectsJoinRectsWhenIntersectedCase1) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -147,7 +146,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase1) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(100, 100, 175, 175)); } -TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase2) { +TEST(RTree, searchNonOverlappingDrawnRectsJoinRectsWhenIntersectedCase2) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = @@ -188,7 +187,7 @@ TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase2) { ASSERT_EQ(*hits.begin(), SkRect::MakeLTRB(50, 50, 500, 250)); } -TEST(RTree, searchNonOverlappingDrawnRects_JoinRectsWhenIntersectedCase3) { +TEST(RTree, searchNonOverlappingDrawnRectsJoinRectsWhenIntersectedCase3) { auto rtree_factory = RTreeFactory(); auto recorder = std::make_unique(); auto recording_canvas = diff --git a/flow/scene_update_context.cc b/flow/scene_update_context.cc index b0628edf0c417..3edd9c4d9fe26 100644 --- a/flow/scene_update_context.cc +++ b/flow/scene_update_context.cc @@ -4,6 +4,7 @@ #include "flutter/flow/scene_update_context.h" +#include #include #include "flutter/flow/layers/layer.h" @@ -13,10 +14,10 @@ #include "include/core/SkColor.h" namespace flutter { +namespace { -// Helper function to generate clip planes for a scenic::EntityNode. -static void SetEntityNodeClipPlanes(scenic::EntityNode& entity_node, - const SkRect& bounds) { +void SetEntityNodeClipPlanes(scenic::EntityNode& entity_node, + const SkRect& bounds) { const float top = bounds.top(); const float bottom = bounds.bottom(); const float left = bounds.left(); @@ -53,32 +54,78 @@ static void SetEntityNodeClipPlanes(scenic::EntityNode& entity_node, entity_node.SetClipPlanes(std::move(clip_planes)); } -SceneUpdateContext::SceneUpdateContext(scenic::Session* session, - SurfaceProducer* surface_producer) - : session_(session), surface_producer_(surface_producer) { - FML_DCHECK(surface_producer_ != nullptr); +void SetMaterialColor(scenic::Material& material, + SkColor color, + SkAlpha opacity) { + const SkAlpha color_alpha = static_cast( + ((float)SkColorGetA(color) * (float)opacity) / 255.0f); + material.SetColor(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color), + color_alpha); +} + +} // namespace + +SceneUpdateContext::SceneUpdateContext(std::string debug_label, + fuchsia::ui::views::ViewToken view_token, + scenic::ViewRefPair view_ref_pair, + SessionWrapper& session) + : session_(session), + root_view_(session_.get(), + std::move(view_token), + std::move(view_ref_pair.control_ref), + std::move(view_ref_pair.view_ref), + debug_label), + root_node_(session_.get()) { + root_view_.AddChild(root_node_); + root_node_.SetEventMask(fuchsia::ui::gfx::kMetricsEventMask); + + session_.Present(); +} + +std::vector SceneUpdateContext::GetPaintTasks() { + std::vector frame_paint_tasks = std::move(paint_tasks_); + + paint_tasks_.clear(); + + return frame_paint_tasks; +} + +void SceneUpdateContext::EnableWireframe(bool enable) { + session_.get()->Enqueue( + scenic::NewSetEnableDebugViewBoundsCmd(root_view_.id(), enable)); +} + +void SceneUpdateContext::Reset() { + paint_tasks_.clear(); + top_entity_ = nullptr; + top_scale_x_ = 1.f; + top_scale_y_ = 1.f; + top_elevation_ = 0.f; + next_elevation_ = 0.f; + alpha_ = 1.f; + + // We are going to be sending down a fresh node hierarchy every frame. So just + // enqueue a detach op on the imported root node. + session_.get()->Enqueue(scenic::NewDetachChildrenCmd(root_node_.id())); } -void SceneUpdateContext::CreateFrame(scenic::EntityNode entity_node, +void SceneUpdateContext::CreateFrame(scenic::EntityNode& entity_node, const SkRRect& rrect, SkColor color, SkAlpha opacity, const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer) { - FML_DCHECK(!rrect.isEmpty()); + std::vector paint_layers) { + // We don't need a shape if the frame is zero size. + if (rrect.isEmpty()) + return; // Frames always clip their children. SkRect shape_bounds = rrect.getBounds(); SetEntityNodeClipPlanes(entity_node, shape_bounds); - // and possibly for its texture. // TODO(SCN-137): Need to be able to express the radii as vectors. - scenic::ShapeNode shape_node(session()); - scenic::Rectangle shape(session_, // session - rrect.width(), // width - rrect.height() // height - ); + scenic::ShapeNode shape_node(session_.get()); + scenic::Rectangle shape(session_.get(), rrect.width(), rrect.height()); shape_node.SetShape(shape); shape_node.SetTranslation(shape_bounds.width() * 0.5f + shape_bounds.left(), shape_bounds.height() * 0.5f + shape_bounds.top(), @@ -88,7 +135,7 @@ void SceneUpdateContext::CreateFrame(scenic::EntityNode entity_node, if (paint_bounds.isEmpty() || !paint_bounds.intersects(shape_bounds)) paint_layers.clear(); - scenic::Material material(session()); + scenic::Material material(session_.get()); shape_node.SetMaterial(material); entity_node.AddChild(shape_node); @@ -96,147 +143,48 @@ void SceneUpdateContext::CreateFrame(scenic::EntityNode entity_node, if (paint_layers.empty()) { SetMaterialColor(material, color, opacity); } else { - // Apply current metrics and transformation scale factors. - const float scale_x = ScaleX(); - const float scale_y = ScaleY(); - - // Apply a texture to the whole shape. - SetMaterialTextureAndColor(material, color, opacity, scale_x, scale_y, - shape_bounds, std::move(paint_layers), layer, - std::move(entity_node)); - } -} - -void SceneUpdateContext::SetMaterialTextureAndColor( - scenic::Material& material, - SkColor color, - SkAlpha opacity, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node) { - scenic::Image* image = GenerateImageIfNeeded( - color, scale_x, scale_y, paint_bounds, std::move(paint_layers), layer, - std::move(entity_node)); - - if (image != nullptr) { // The final shape's color is material_color * texture_color. The passed in // material color was already used as a background when generating the // texture, so set the model color to |SK_ColorWHITE| in order to allow // using the texture's color unmodified. SetMaterialColor(material, SK_ColorWHITE, opacity); - material.SetTexture(*image); - } else { - // No texture was needed, so apply a solid color to the whole shape. - SetMaterialColor(material, color, opacity); - } -} -void SceneUpdateContext::SetMaterialColor(scenic::Material& material, - SkColor color, - SkAlpha opacity) { - const SkAlpha color_alpha = static_cast( - ((float)SkColorGetA(color) * (float)opacity) / 255.0f); - material.SetColor(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color), - color_alpha); -} - -scenic::Image* SceneUpdateContext::GenerateImageIfNeeded( - SkColor color, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node) { - // Bail if there's nothing to paint. - if (paint_layers.empty()) - return nullptr; - - // Bail if the physical bounds are empty after rounding. - SkISize physical_size = SkISize::Make(paint_bounds.width() * scale_x, - paint_bounds.height() * scale_y); - if (physical_size.isEmpty()) - return nullptr; - - // Acquire a surface from the surface producer and register the paint tasks. - std::unique_ptr surface = - surface_producer_->ProduceSurface( - physical_size, - LayerRasterCacheKey( - // Root frame has a nullptr layer - layer ? layer->unique_id() : 0, Matrix()), - std::make_unique(std::move(entity_node))); - - if (!surface) { - FML_LOG(ERROR) << "Could not acquire a surface from the surface producer " - "of size: " - << physical_size.width() << "x" << physical_size.height(); - return nullptr; - } - - auto image = surface->GetImage(); - - // Enqueue the paint task. - paint_tasks_.push_back({.surface = std::move(surface), - .left = paint_bounds.left(), - .top = paint_bounds.top(), - .scale_x = scale_x, - .scale_y = scale_y, - .background_color = color, - .layers = std::move(paint_layers)}); - return image; -} - -std::vector< - std::unique_ptr> -SceneUpdateContext::ExecutePaintTasks(CompositorContext::ScopedFrame& frame) { - TRACE_EVENT0("flutter", "SceneUpdateContext::ExecutePaintTasks"); - std::vector> surfaces_to_submit; - for (auto& task : paint_tasks_) { - FML_DCHECK(task.surface); - SkCanvas* canvas = task.surface->GetSkiaSurface()->getCanvas(); - Layer::PaintContext context = {canvas, - canvas, - frame.gr_context(), - nullptr, - frame.context().raster_time(), - frame.context().ui_time(), - frame.context().texture_registry(), - &frame.context().raster_cache(), - false, - frame_physical_depth_, - frame_device_pixel_ratio_}; - canvas->restoreToCount(1); - canvas->save(); - canvas->clear(task.background_color); - canvas->scale(task.scale_x, task.scale_y); - canvas->translate(-task.left, -task.top); - for (Layer* layer : task.layers) { - layer->Paint(context); - } - surfaces_to_submit.emplace_back(std::move(task.surface)); + // Enqueue a paint task for these layers, to apply a texture to the whole + // shape. + // + // The task uses the |shape_bounds| as its rendering bounds instead of the + // |paint_bounds|. If the paint_bounds is large than the shape_bounds it + // will be clipped. + paint_tasks_.emplace_back(PaintTask{.paint_bounds = shape_bounds, + .scale_x = top_scale_x_, + .scale_y = top_scale_y_, + .background_color = color, + .material = std::move(material), + .layers = std::move(paint_layers)}); } - paint_tasks_.clear(); - alpha_ = 1.f; - topmost_global_scenic_elevation_ = kScenicZElevationBetweenLayers; - scenic_elevation_ = 0.f; - return surfaces_to_submit; } -void SceneUpdateContext::UpdateScene(int64_t view_id, - const SkPoint& offset, - const SkSize& size) { +void SceneUpdateContext::UpdateView(int64_t view_id, + const SkPoint& offset, + const SkSize& size, + std::optional override_hit_testable) { auto* view_holder = ViewHolder::FromId(view_id); FML_DCHECK(view_holder); - view_holder->SetProperties(size.width(), size.height(), 0, 0, 0, 0, - view_holder->focusable()); - view_holder->UpdateScene(*this, offset, size, - SkScalarRoundToInt(alphaf() * 255), - view_holder->hit_testable()); + if (size.width() > 0.f && size.height() > 0.f) { + view_holder->SetProperties(size.width(), size.height(), 0, 0, 0, 0, + view_holder->focusable()); + } + + bool hit_testable = override_hit_testable.has_value() + ? *override_hit_testable + : view_holder->hit_testable(); + view_holder->UpdateScene(session_.get(), top_entity_->embedder_node(), offset, + size, SkScalarRoundToInt(alphaf() * 255), + hit_testable); + + // Assume embedded views are 10 "layers" wide. + next_elevation_ += 10 * kScenicZElevationBetweenLayers; } void SceneUpdateContext::CreateView(int64_t view_id, @@ -253,6 +201,16 @@ void SceneUpdateContext::CreateView(int64_t view_id, view_holder->set_focusable(focusable); } +void SceneUpdateContext::UpdateView(int64_t view_id, + bool hit_testable, + bool focusable) { + auto* view_holder = ViewHolder::FromId(view_id); + FML_DCHECK(view_holder); + + view_holder->set_hit_testable(hit_testable); + view_holder->set_focusable(focusable); +} + void SceneUpdateContext::DestroyView(int64_t view_id) { ViewHolder::Destroy(view_id); } @@ -260,13 +218,17 @@ void SceneUpdateContext::DestroyView(int64_t view_id) { SceneUpdateContext::Entity::Entity(SceneUpdateContext& context) : context_(context), previous_entity_(context.top_entity_), - entity_node_(context.session()) { - if (previous_entity_) - previous_entity_->embedder_node().AddChild(entity_node_); + entity_node_(context.session_.get()) { context.top_entity_ = this; } SceneUpdateContext::Entity::~Entity() { + if (previous_entity_) { + previous_entity_->embedder_node().AddChild(entity_node_); + } else { + context_.root_node_.AddChild(entity_node_); + } + FML_DCHECK(context_.top_entity_ == this); context_.top_entity_ = previous_entity_; } @@ -329,19 +291,25 @@ SceneUpdateContext::Frame::Frame(SceneUpdateContext& context, const SkRRect& rrect, SkColor color, SkAlpha opacity, - std::string label, - float z_translation, - Layer* layer) + std::string label) : Entity(context), + previous_elevation_(context.top_elevation_), rrect_(rrect), color_(color), opacity_(opacity), - opacity_node_(context.session()), - paint_bounds_(SkRect::MakeEmpty()), - layer_(layer) { + opacity_node_(context.session_.get()), + paint_bounds_(SkRect::MakeEmpty()) { + // Increment elevation trackers before calculating any local elevation. + // |UpdateView| can modify context.next_elevation_, which is why it is + // neccesary to track this addtional state. + context.top_elevation_ += kScenicZElevationBetweenLayers; + context.next_elevation_ += kScenicZElevationBetweenLayers; + + float local_elevation = context.next_elevation_ - previous_elevation_; + entity_node().SetTranslation(0.f, 0.f, -local_elevation); entity_node().SetLabel(label); - entity_node().SetTranslation(0.f, 0.f, z_translation); entity_node().AddChild(opacity_node_); + // Scenic currently lacks an API to enable rendering of alpha channel; alpha // channels are only rendered if there is a OpacityNode higher in the tree // with opacity != 1. For now, clamp to a infinitesimally smaller value than @@ -350,20 +318,11 @@ SceneUpdateContext::Frame::Frame(SceneUpdateContext& context, } SceneUpdateContext::Frame::~Frame() { - // We don't need a shape if the frame is zero size. - if (rrect_.isEmpty()) - return; - - // isEmpty should account for this, but we are adding these experimental - // checks to validate if this is the root cause for b/144933519. - if (std::isnan(rrect_.width()) || std::isnan(rrect_.height())) { - FML_LOG(ERROR) << "Invalid RoundedRectangle"; - return; - } + context().top_elevation_ = previous_elevation_; // Add a part which represents the frame's geometry for clipping purposes - context().CreateFrame(std::move(entity_node()), rrect_, color_, opacity_, - paint_bounds_, std::move(paint_layers_), layer_); + context().CreateFrame(entity_node(), rrect_, color_, opacity_, paint_bounds_, + std::move(paint_layers_)); } void SceneUpdateContext::Frame::AddPaintLayer(Layer* layer) { diff --git a/flow/scene_update_context.h b/flow/scene_update_context.h index 3b46fb2247f3c..f15c59fa7f6ea 100644 --- a/flow/scene_update_context.h +++ b/flow/scene_update_context.h @@ -5,18 +5,19 @@ #ifndef FLUTTER_FLOW_SCENE_UPDATE_CONTEXT_H_ #define FLUTTER_FLOW_SCENE_UPDATE_CONTEXT_H_ +#include +#include +#include +#include + #include #include #include #include -#include "flutter/flow/compositor_context.h" #include "flutter/flow/embedded_views.h" -#include "flutter/flow/raster_cache_key.h" -#include "flutter/fml/compiler_specific.h" #include "flutter/fml/logging.h" #include "flutter/fml/macros.h" -#include "lib/ui/scenic/cpp/resources.h" #include "third_party/skia/include/core/SkRect.h" #include "third_party/skia/include/core/SkSurface.h" @@ -33,50 +34,16 @@ constexpr float kOneMinusEpsilon = 1 - FLT_EPSILON; // How much layers are separated in Scenic z elevation. constexpr float kScenicZElevationBetweenLayers = 10.f; -class SceneUpdateContext : public flutter::ExternalViewEmbedder { +class SessionWrapper { public: - class SurfaceProducerSurface { - public: - virtual ~SurfaceProducerSurface() = default; - - virtual size_t AdvanceAndGetAge() = 0; - - virtual bool FlushSessionAcquireAndReleaseEvents() = 0; - - virtual bool IsValid() const = 0; + virtual ~SessionWrapper() {} - virtual SkISize GetSize() const = 0; - - virtual void SignalWritesFinished( - const std::function& on_writes_committed) = 0; - - virtual scenic::Image* GetImage() = 0; - - virtual sk_sp GetSkiaSurface() const = 0; - }; - - class SurfaceProducer { - public: - virtual ~SurfaceProducer() = default; - - // The produced surface owns the entity_node and has a layer_key for - // retained rendering. The surface will only be retained if the layer_key - // has a non-null layer pointer (layer_key.id()). - virtual std::unique_ptr ProduceSurface( - const SkISize& size, - const LayerRasterCacheKey& layer_key, - std::unique_ptr entity_node) = 0; - - // Query a retained entity node (owned by a retained surface) for retained - // rendering. - virtual bool HasRetainedNode(const LayerRasterCacheKey& key) const = 0; - virtual scenic::EntityNode* GetRetainedNode( - const LayerRasterCacheKey& key) = 0; - - virtual void SubmitSurface( - std::unique_ptr surface) = 0; - }; + virtual scenic::Session* get() = 0; + virtual void Present() = 0; +}; +class SceneUpdateContext : public flutter::ExternalViewEmbedder { + public: class Entity { public: Entity(SceneUpdateContext& context); @@ -116,9 +83,7 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { const SkRRect& rrect, SkColor color, SkAlpha opacity, - std::string label, - float z_translation = 0.0f, - Layer* layer = nullptr); + std::string label); virtual ~Frame(); scenic::ContainerNode& embedder_node() override { return opacity_node_; } @@ -126,6 +91,8 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { void AddPaintLayer(Layer* layer); private: + const float previous_elevation_; + const SkRRect rrect_; SkColor const color_; SkAlpha const opacity_; @@ -133,7 +100,6 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { scenic::OpacityNodeHACK opacity_node_; std::vector paint_layers_; SkRect paint_bounds_; - Layer* layer_; }; class Clip : public Entity { @@ -142,68 +108,35 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { ~Clip() = default; }; - SceneUpdateContext(scenic::Session* session, - SurfaceProducer* surface_producer); - ~SceneUpdateContext() = default; - - scenic::Session* session() { return session_; } + struct PaintTask { + SkRect paint_bounds; + SkScalar scale_x; + SkScalar scale_y; + SkColor background_color; + scenic::Material material; + std::vector layers; + }; - Entity* top_entity() { return top_entity_; } + SceneUpdateContext(std::string debug_label, + fuchsia::ui::views::ViewToken view_token, + scenic::ViewRefPair view_ref_pair, + SessionWrapper& session); + ~SceneUpdateContext() = default; - bool has_metrics() const { return !!metrics_; } - void set_metrics(fuchsia::ui::gfx::MetricsPtr metrics) { - metrics_ = std::move(metrics); - } - const fuchsia::ui::gfx::MetricsPtr& metrics() const { return metrics_; } - - void set_dimensions(const SkISize& frame_physical_size, - float frame_physical_depth, - float frame_device_pixel_ratio) { - frame_physical_size_ = frame_physical_size; - frame_physical_depth_ = frame_physical_depth; - frame_device_pixel_ratio_ = frame_device_pixel_ratio; - } - const SkISize& frame_size() const { return frame_physical_size_; } - float frame_physical_depth() const { return frame_physical_depth_; } - float frame_device_pixel_ratio() const { return frame_device_pixel_ratio_; } - - // TODO(chinmaygarde): This method must submit the surfaces as soon as paint - // tasks are done. However, given that there is no support currently for - // Vulkan semaphores, we need to submit all the surfaces after an explicit - // CPU wait. Once Vulkan semaphores are available, this method must return - // void and the implementation must submit surfaces on its own as soon as the - // specific canvas operations are done. - [[nodiscard]] std::vector> - ExecutePaintTasks(CompositorContext::ScopedFrame& frame); - - float ScaleX() const { return metrics_->scale_x * top_scale_x_; } - float ScaleY() const { return metrics_->scale_y * top_scale_y_; } - - // The transformation matrix of the current context. It's used to construct - // the LayerRasterCacheKey for a given layer. - SkMatrix Matrix() const { return SkMatrix::MakeScale(ScaleX(), ScaleY()); } - - bool HasRetainedNode(const LayerRasterCacheKey& key) const { - return surface_producer_->HasRetainedNode(key); - } - scenic::EntityNode* GetRetainedNode(const LayerRasterCacheKey& key) { - return surface_producer_->GetRetainedNode(key); - } + scenic::ContainerNode& root_node() { return root_node_; } // The cumulative alpha value based on all the parent OpacityLayers. void set_alphaf(float alpha) { alpha_ = alpha; } float alphaf() { return alpha_; } - // The global scenic elevation at a given point in the traversal. - float scenic_elevation() { return scenic_elevation_; } + // Returns all `PaintTask`s generated for the current frame. + std::vector GetPaintTasks(); - void set_scenic_elevation(float elevation) { scenic_elevation_ = elevation; } + // Enable/disable wireframe rendering around the root view bounds. + void EnableWireframe(bool enable); - float GetGlobalElevationForNextScenicLayer() { - float elevation = topmost_global_scenic_elevation_; - topmost_global_scenic_elevation_ += kScenicZElevationBetweenLayers; - return elevation; - } + // Reset state for a new frame. + void Reset(); // |ExternalViewEmbedder| SkCanvas* GetRootCanvas() override { return nullptr; } @@ -234,73 +167,35 @@ class SceneUpdateContext : public flutter::ExternalViewEmbedder { } void CreateView(int64_t view_id, bool hit_testable, bool focusable); - + void UpdateView(int64_t view_id, bool hit_testable, bool focusable); void DestroyView(int64_t view_id); - - void UpdateScene(int64_t view_id, const SkPoint& offset, const SkSize& size); + void UpdateView(int64_t view_id, + const SkPoint& offset, + const SkSize& size, + std::optional override_hit_testable = std::nullopt); private: - struct PaintTask { - std::unique_ptr surface; - SkScalar left; - SkScalar top; - SkScalar scale_x; - SkScalar scale_y; - SkColor background_color; - std::vector layers; - }; - - // Setup the entity_node as a frame that materialize all the paint_layers. In - // most cases, this creates a VulkanSurface (SurfaceProducerSurface) by - // calling SetShapeTextureOrColor and GenerageImageIfNeeded. Such surface will - // own the associated entity_node. If the layer pointer isn't nullptr, the - // surface (and thus the entity_node) will be retained for that layer to - // improve the performance. - void CreateFrame(scenic::EntityNode entity_node, + void CreateFrame(scenic::EntityNode& entity_node, const SkRRect& rrect, SkColor color, SkAlpha opacity, const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer); - void SetMaterialTextureAndColor(scenic::Material& material, - SkColor color, - SkAlpha opacity, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node); - void SetMaterialColor(scenic::Material& material, - SkColor color, - SkAlpha opacity); - scenic::Image* GenerateImageIfNeeded(SkColor color, - SkScalar scale_x, - SkScalar scale_y, - const SkRect& paint_bounds, - std::vector paint_layers, - Layer* layer, - scenic::EntityNode entity_node); + std::vector paint_layers); - Entity* top_entity_ = nullptr; - float top_scale_x_ = 1.f; - float top_scale_y_ = 1.f; + SessionWrapper& session_; - scenic::Session* const session_; - SurfaceProducer* const surface_producer_; + scenic::View root_view_; + scenic::EntityNode root_node_; - fuchsia::ui::gfx::MetricsPtr metrics_; - SkISize frame_physical_size_; - float frame_physical_depth_ = 0.0f; - float frame_device_pixel_ratio_ = - 1.0f; // Ratio between logical and physical pixels. + std::vector paint_tasks_; - float alpha_ = 1.0f; - float scenic_elevation_ = 0.f; - float topmost_global_scenic_elevation_ = kScenicZElevationBetweenLayers; + Entity* top_entity_ = nullptr; + float top_scale_x_ = 1.f; + float top_scale_y_ = 1.f; + float top_elevation_ = 0.f; - std::vector paint_tasks_; + float next_elevation_ = 0.f; + float alpha_ = 1.f; FML_DISALLOW_COPY_AND_ASSIGN(SceneUpdateContext); }; diff --git a/flow/skia_gpu_object.cc b/flow/skia_gpu_object.cc index aadb4e3b72f71..7415916d77954 100644 --- a/flow/skia_gpu_object.cc +++ b/flow/skia_gpu_object.cc @@ -11,7 +11,7 @@ namespace flutter { SkiaUnrefQueue::SkiaUnrefQueue(fml::RefPtr task_runner, fml::TimeDelta delay, - fml::WeakPtr context) + fml::WeakPtr context) : task_runner_(std::move(task_runner)), drain_delay_(delay), drain_pending_(false), diff --git a/flow/skia_gpu_object.h b/flow/skia_gpu_object.h index ef7cb596f1a36..662d560fc9eff 100644 --- a/flow/skia_gpu_object.h +++ b/flow/skia_gpu_object.h @@ -35,14 +35,14 @@ class SkiaUnrefQueue : public fml::RefCountedThreadSafe { std::mutex mutex_; std::deque objects_; bool drain_pending_; - fml::WeakPtr context_; + fml::WeakPtr context_; // The `GrDirectContext* context` is only used for signaling Skia to // performDeferredCleanup. It can be nullptr when such signaling is not needed // (e.g., in unit tests). SkiaUnrefQueue(fml::RefPtr task_runner, fml::TimeDelta delay, - fml::WeakPtr context = {}); + fml::WeakPtr context = {}); ~SkiaUnrefQueue(); diff --git a/flow/testing/layer_test.h b/flow/testing/layer_test.h index c63057fca1942..d2df8b404ca9b 100644 --- a/flow/testing/layer_test.h +++ b/flow/testing/layer_test.h @@ -47,11 +47,9 @@ class LayerTestBase : public CanvasTestBase { kGiantRect, /* cull_rect */ false, /* layer reads from surface */ raster_time_, ui_time_, texture_registry_, - false, /* checkerboard_offscreen_layers */ - 100.0f, /* frame_physical_depth */ - 1.0f, /* frame_device_pixel_ratio */ - 0.0f, /* total_elevation */ - false, /* has_platform_view */ + false, /* checkerboard_offscreen_layers */ + 1.0f, /* frame_device_pixel_ratio */ + false, /* has_platform_view */ }), paint_context_({ TestT::mock_canvas().internal_canvas(), /* internal_nodes_canvas */ @@ -61,7 +59,6 @@ class LayerTestBase : public CanvasTestBase { raster_time_, ui_time_, texture_registry_, nullptr, /* raster_cache */ false, /* checkerboard_offscreen_layers */ - 100.0f, /* frame_physical_depth */ 1.0f, /* frame_device_pixel_ratio */ }) { use_null_raster_cache(); diff --git a/flow/testing/mock_layer.cc b/flow/testing/mock_layer.cc index 5fe1b98088af1..b32bdb23abc09 100644 --- a/flow/testing/mock_layer.cc +++ b/flow/testing/mock_layer.cc @@ -22,7 +22,6 @@ void MockLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { parent_mutators_ = context->mutators_stack; parent_matrix_ = matrix; parent_cull_rect_ = context->cull_rect; - parent_elevation_ = context->total_elevation; parent_has_platform_view_ = context->has_platform_view; context->has_platform_view = fake_has_platform_view_; diff --git a/flow/testing/mock_layer.h b/flow/testing/mock_layer.h index 835c3ee9621cc..b92583f581209 100644 --- a/flow/testing/mock_layer.h +++ b/flow/testing/mock_layer.h @@ -28,7 +28,6 @@ class MockLayer : public Layer { const MutatorsStack& parent_mutators() { return parent_mutators_; } const SkMatrix& parent_matrix() { return parent_matrix_; } const SkRect& parent_cull_rect() { return parent_cull_rect_; } - float parent_elevation() { return parent_elevation_; } bool parent_has_platform_view() { return parent_has_platform_view_; } private: @@ -37,7 +36,6 @@ class MockLayer : public Layer { SkRect parent_cull_rect_ = SkRect::MakeEmpty(); SkPath fake_paint_path_; SkPaint fake_paint_; - float parent_elevation_ = 0; bool parent_has_platform_view_ = false; bool fake_has_platform_view_ = false; bool fake_needs_system_composite_ = false; diff --git a/flow/testing/mock_layer_unittests.cc b/flow/testing/mock_layer_unittests.cc index 0e6e37978c2e1..ebb837ca8b8dd 100644 --- a/flow/testing/mock_layer_unittests.cc +++ b/flow/testing/mock_layer_unittests.cc @@ -39,13 +39,11 @@ TEST_F(MockLayerTest, SimpleParams) { const SkMatrix start_matrix = SkMatrix::Translate(1.0f, 2.0f); const SkMatrix scale_matrix = SkMatrix::Scale(0.5f, 0.5f); const SkRect cull_rect = SkRect::MakeWH(5.0f, 5.0f); - const float parent_elevation = 5.0f; const bool parent_has_platform_view = true; auto layer = std::make_shared(path, paint); preroll_context()->mutators_stack.PushTransform(scale_matrix); preroll_context()->cull_rect = cull_rect; - preroll_context()->total_elevation = parent_elevation; preroll_context()->has_platform_view = parent_has_platform_view; layer->Preroll(preroll_context(), start_matrix); EXPECT_EQ(preroll_context()->has_platform_view, false); @@ -55,7 +53,6 @@ TEST_F(MockLayerTest, SimpleParams) { EXPECT_EQ(layer->parent_mutators(), std::vector{Mutator(scale_matrix)}); EXPECT_EQ(layer->parent_matrix(), start_matrix); EXPECT_EQ(layer->parent_cull_rect(), cull_rect); - EXPECT_EQ(layer->parent_elevation(), parent_elevation); EXPECT_EQ(layer->parent_has_platform_view(), parent_has_platform_view); layer->Paint(paint_context()); diff --git a/flow/view_holder.cc b/flow/view_holder.cc index 7fd00500bb02c..c2011825c9474 100644 --- a/flow/view_holder.cc +++ b/flow/view_holder.cc @@ -4,6 +4,8 @@ #include "flutter/flow/view_holder.h" +#include + #include "flutter/fml/thread_local.h" namespace { @@ -98,18 +100,17 @@ ViewHolder::ViewHolder(fml::RefPtr ui_task_runner, FML_DCHECK(pending_view_holder_token_.value); } -void ViewHolder::UpdateScene(SceneUpdateContext& context, +void ViewHolder::UpdateScene(scenic::Session* session, + scenic::ContainerNode& container_node, const SkPoint& offset, const SkSize& size, SkAlpha opacity, bool hit_testable) { if (pending_view_holder_token_.value) { - entity_node_ = std::make_unique(context.session()); - opacity_node_ = - std::make_unique(context.session()); + entity_node_ = std::make_unique(session); + opacity_node_ = std::make_unique(session); view_holder_ = std::make_unique( - context.session(), std::move(pending_view_holder_token_), - "Flutter SceneHost"); + session, std::move(pending_view_holder_token_), "Flutter SceneHost"); opacity_node_->AddChild(*entity_node_); opacity_node_->SetLabel("flutter::ViewHolder"); entity_node_->Attach(*view_holder_); @@ -125,7 +126,7 @@ void ViewHolder::UpdateScene(SceneUpdateContext& context, FML_DCHECK(opacity_node_); FML_DCHECK(view_holder_); - context.top_entity()->embedder_node().AddChild(*opacity_node_); + container_node.AddChild(*opacity_node_); opacity_node_->SetOpacity(opacity / 255.0f); entity_node_->SetTranslation(offset.x(), offset.y(), -0.1f); entity_node_->SetHitTestBehavior( diff --git a/flow/view_holder.h b/flow/view_holder.h index bb8ff83d776ab..f25b205c7823c 100644 --- a/flow/view_holder.h +++ b/flow/view_holder.h @@ -9,17 +9,17 @@ #include #include #include +#include #include -#include "third_party/skia/include/core/SkMatrix.h" -#include "third_party/skia/include/core/SkPoint.h" -#include "third_party/skia/include/core/SkSize.h" #include -#include "flutter/flow/scene_update_context.h" #include "flutter/fml/macros.h" #include "flutter/fml/memory/ref_counted.h" #include "flutter/fml/task_runner.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPoint.h" +#include "third_party/skia/include/core/SkSize.h" namespace flutter { @@ -54,7 +54,8 @@ class ViewHolder { // Creates or updates the contained ViewHolder resource using the specified // |SceneUpdateContext|. - void UpdateScene(SceneUpdateContext& context, + void UpdateScene(scenic::Session* session, + scenic::ContainerNode& container_node, const SkPoint& offset, const SkSize& size, SkAlpha opacity, diff --git a/fml/file.cc b/fml/file.cc index 3d9e4397ee6b0..b96bfe6341453 100644 --- a/fml/file.cc +++ b/fml/file.cc @@ -44,14 +44,21 @@ fml::UniqueFD CreateDirectory(const fml::UniqueFD& base_directory, return CreateDirectory(base_directory, components, permission, 0); } -ScopedTemporaryDirectory::ScopedTemporaryDirectory() { - path_ = CreateTemporaryDirectory(); +ScopedTemporaryDirectory::ScopedTemporaryDirectory() + : path_(CreateTemporaryDirectory()) { if (path_ != "") { dir_fd_ = OpenDirectory(path_.c_str(), false, FilePermission::kRead); } } ScopedTemporaryDirectory::~ScopedTemporaryDirectory() { + // POSIX requires the directory to be empty before UnlinkDirectory. + if (path_ != "") { + if (!RemoveFilesInDirectory(dir_fd_)) { + FML_LOG(ERROR) << "Could not clean directory: " << path_; + } + } + // Windows has to close UniqueFD first before UnlinkDirectory dir_fd_.reset(); if (path_ != "") { diff --git a/fml/logging.cc b/fml/logging.cc index 8c4796db0a504..d4273b9381da7 100644 --- a/fml/logging.cc +++ b/fml/logging.cc @@ -39,9 +39,8 @@ const char* StripPath(const char* path) { auto* p = strrchr(path, '/'); if (p) { return p + 1; - } else { - return path; } + return path; } } // namespace diff --git a/fml/memory/ref_counted_unittest.cc b/fml/memory/ref_counted_unittest.cc index 959117c488e38..4cefb8b5ff5f9 100644 --- a/fml/memory/ref_counted_unittest.cc +++ b/fml/memory/ref_counted_unittest.cc @@ -227,6 +227,8 @@ TEST(RefCountedTest, NullAssignmentToNull) { // No-op null assignment using move constructor. r1 = std::move(r2); EXPECT_TRUE(r1.get() == nullptr); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r2.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_FALSE(r1); EXPECT_FALSE(r2); @@ -272,6 +274,8 @@ TEST(RefCountedTest, NonNullAssignmentToNull) { RefPtr r2; // Move assignment (to null ref pointer). r2 = std::move(r1); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r1.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_EQ(created, r2.get()); EXPECT_FALSE(r1); @@ -336,6 +340,8 @@ TEST(RefCountedTest, NullAssignmentToNonNull) { // Null assignment using move constructor. r1 = std::move(r2); EXPECT_TRUE(r1.get() == nullptr); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r2.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_FALSE(r1); EXPECT_FALSE(r2); @@ -389,6 +395,8 @@ TEST(RefCountedTest, NonNullAssignmentToNonNull) { RefPtr r2(MakeRefCounted(nullptr, &was_destroyed2)); // Move assignment (to non-null ref pointer). r2 = std::move(r1); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_TRUE(r1.get() == nullptr); // NOLINT(clang-analyzer-cplusplus.Move) EXPECT_FALSE(r2.get() == nullptr); EXPECT_FALSE(r1); @@ -436,7 +444,13 @@ TEST(RefCountedTest, SelfAssignment) { { MyClass* created = nullptr; was_destroyed = false; - RefPtr r(MakeRefCounted(&created, &was_destroyed)); + // This line is marked NOLINT because the clang linter does not reason about + // the value of the reference count. In particular, the self-assignment + // below is handled in the copy constructor by a refcount increment then + // decrement. The linter sees only that the decrement might destroy the + // object. + RefPtr r(MakeRefCounted( // NOLINT + &created, &was_destroyed)); // Copy. ALLOW_SELF_ASSIGN_OVERLOADED(r = r); EXPECT_EQ(created, r.get()); diff --git a/fml/memory/weak_ptr_unittest.cc b/fml/memory/weak_ptr_unittest.cc index a1db3ae1d1988..e055ad9095409 100644 --- a/fml/memory/weak_ptr_unittest.cc +++ b/fml/memory/weak_ptr_unittest.cc @@ -38,6 +38,8 @@ TEST(WeakPtrTest, MoveConstruction) { WeakPtrFactory factory(&data); WeakPtr ptr = factory.GetWeakPtr(); WeakPtr ptr2(std::move(ptr)); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_EQ(nullptr, ptr.get()); // NOLINT EXPECT_EQ(&data, ptr2.get()); } @@ -60,6 +62,8 @@ TEST(WeakPtrTest, MoveAssignment) { WeakPtr ptr2; EXPECT_EQ(nullptr, ptr2.get()); ptr2 = std::move(ptr); + // The clang linter flags the method called on the moved-from reference, but + // this is testing the move implementation, so it is marked NOLINT. EXPECT_EQ(nullptr, ptr.get()); // NOLINT EXPECT_EQ(&data, ptr2.get()); } diff --git a/fml/message_loop_task_queues.cc b/fml/message_loop_task_queues.cc index 8df08fa1b244d..1d5a9091083f3 100644 --- a/fml/message_loop_task_queues.cc +++ b/fml/message_loop_task_queues.cc @@ -246,7 +246,7 @@ bool MessageLoopTaskQueues::Unmerge(TaskQueueId owner) { bool MessageLoopTaskQueues::Owns(TaskQueueId owner, TaskQueueId subsumed) const { std::lock_guard guard(queue_mutex_); - return subsumed == queue_entries_.at(owner)->owner_of || owner == subsumed; + return subsumed == queue_entries_.at(owner)->owner_of; } // Subsumed queues will never have pending tasks. diff --git a/fml/message_loop_task_queues_unittests.cc b/fml/message_loop_task_queues_unittests.cc index a1c2df0a47f6c..1086bb28b8013 100644 --- a/fml/message_loop_task_queues_unittests.cc +++ b/fml/message_loop_task_queues_unittests.cc @@ -173,6 +173,13 @@ TEST(MessageLoopTaskQueue, NotifyObserversWhileCreatingQueues) { before_second_observer.Signal(); notify_observers.join(); } + +TEST(MessageLoopTaskQueue, QueueDoNotOwnItself) { + auto task_queue = fml::MessageLoopTaskQueues::GetInstance(); + auto queue_id = task_queue->CreateTaskQueue(); + ASSERT_FALSE(task_queue->Owns(queue_id, queue_id)); +} + // TODO(chunhtai): This unit-test is flaky and sometimes fails asynchronizely // after the test has finished. // https://github.com/flutter/flutter/issues/43858 diff --git a/fml/raster_thread_merger.cc b/fml/raster_thread_merger.cc index b29c303371b8a..62b696db70aa4 100644 --- a/fml/raster_thread_merger.cc +++ b/fml/raster_thread_merger.cc @@ -17,23 +17,41 @@ RasterThreadMerger::RasterThreadMerger(fml::TaskQueueId platform_queue_id, gpu_queue_id_(gpu_queue_id), task_queues_(fml::MessageLoopTaskQueues::GetInstance()), lease_term_(kLeaseNotSet) { - is_merged_ = task_queues_->Owns(platform_queue_id_, gpu_queue_id_); + FML_CHECK(!task_queues_->Owns(platform_queue_id_, gpu_queue_id_)); } void RasterThreadMerger::MergeWithLease(size_t lease_term) { + if (TaskQueuesAreSame()) { + return; + } + FML_DCHECK(lease_term > 0) << "lease_term should be positive."; - if (!is_merged_) { - is_merged_ = task_queues_->Merge(platform_queue_id_, gpu_queue_id_); + std::scoped_lock lock(lease_term_mutex_); + if (!IsMergedUnSafe()) { + bool success = task_queues_->Merge(platform_queue_id_, gpu_queue_id_); + FML_CHECK(success) << "Unable to merge the raster and platform threads."; lease_term_ = lease_term; } + merged_condition_.notify_one(); +} + +void RasterThreadMerger::UnMergeNow() { + if (TaskQueuesAreSame()) { + return; + } + + std::scoped_lock lock(lease_term_mutex_); + lease_term_ = 0; + bool success = task_queues_->Unmerge(platform_queue_id_); + FML_CHECK(success) << "Unable to un-merge the raster and platform threads."; } bool RasterThreadMerger::IsOnPlatformThread() const { return MessageLoop::GetCurrentTaskQueueId() == platform_queue_id_; } -bool RasterThreadMerger::IsOnRasterizingThread() const { - if (is_merged_) { +bool RasterThreadMerger::IsOnRasterizingThread() { + if (IsMergedUnSafe()) { return IsOnPlatformThread(); } else { return !IsOnPlatformThread(); @@ -41,24 +59,45 @@ bool RasterThreadMerger::IsOnRasterizingThread() const { } void RasterThreadMerger::ExtendLeaseTo(size_t lease_term) { - FML_DCHECK(lease_term > 0) << "lease_term should be positive."; + if (TaskQueuesAreSame()) { + return; + } + std::scoped_lock lock(lease_term_mutex_); + FML_DCHECK(IsMergedUnSafe()) << "lease_term should be positive."; if (lease_term_ != kLeaseNotSet && static_cast(lease_term) > lease_term_) { lease_term_ = lease_term; } } -bool RasterThreadMerger::IsMerged() const { - return is_merged_; +bool RasterThreadMerger::IsMerged() { + std::scoped_lock lock(lease_term_mutex_); + return IsMergedUnSafe(); } -RasterThreadStatus RasterThreadMerger::DecrementLease() { - if (!is_merged_) { - return RasterThreadStatus::kRemainsUnmerged; +bool RasterThreadMerger::IsMergedUnSafe() { + return lease_term_ > 0 || TaskQueuesAreSame(); +} + +bool RasterThreadMerger::TaskQueuesAreSame() { + return platform_queue_id_ == gpu_queue_id_; +} + +void RasterThreadMerger::WaitUntilMerged() { + if (TaskQueuesAreSame()) { + return; } + FML_CHECK(IsOnPlatformThread()); + std::unique_lock lock(lease_term_mutex_); + merged_condition_.wait(lock, [&] { return IsMergedUnSafe(); }); +} - // we haven't been set to merge. - if (lease_term_ == kLeaseNotSet) { +RasterThreadStatus RasterThreadMerger::DecrementLease() { + if (TaskQueuesAreSame()) { + return RasterThreadStatus::kRemainsMerged; + } + std::unique_lock lock(lease_term_mutex_); + if (!IsMergedUnSafe()) { return RasterThreadStatus::kRemainsUnmerged; } @@ -66,9 +105,9 @@ RasterThreadStatus RasterThreadMerger::DecrementLease() { << "lease_term should always be positive when merged."; lease_term_--; if (lease_term_ == 0) { - bool success = task_queues_->Unmerge(platform_queue_id_); - FML_CHECK(success) << "Unable to un-merge the raster and platform threads."; - is_merged_ = false; + // |UnMergeNow| is going to acquire the lock again. + lock.unlock(); + UnMergeNow(); return RasterThreadStatus::kUnmergedNow; } diff --git a/fml/raster_thread_merger.h b/fml/raster_thread_merger.h index 7c0318ff77d26..b01cf76a11436 100644 --- a/fml/raster_thread_merger.h +++ b/fml/raster_thread_merger.h @@ -5,6 +5,8 @@ #ifndef FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_ #define FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_ +#include +#include #include "flutter/fml/macros.h" #include "flutter/fml/memory/ref_counted.h" #include "flutter/fml/message_loop_task_queues.h" @@ -28,15 +30,37 @@ class RasterThreadMerger // When the caller merges with a lease term of say 2. The threads // are going to remain merged until 2 invocations of |DecreaseLease|, // unless an |ExtendLeaseTo| gets called. + // + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged this method becomes no-op. void MergeWithLease(size_t lease_term); + // Un-merges the threads now, and resets the lease term to 0. + // + // Must be executed on the raster task runner. + // + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged, we never unmerge them and + // this method becomes no-op. + void UnMergeNow(); + + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged this method becomes no-op. void ExtendLeaseTo(size_t lease_term); // Returns |RasterThreadStatus::kUnmergedNow| if this call resulted in // splitting the raster and platform threads. Reduces the lease term by 1. + // + // If the task queues are the same, we consider them statically merged. + // When task queues are statically merged this method becomes no-op. RasterThreadStatus DecrementLease(); - bool IsMerged() const; + bool IsMerged(); + + // Waits until the threads are merged. + // + // Must run on the platform task runner. + void WaitUntilMerged(); RasterThreadMerger(fml::TaskQueueId platform_queue_id, fml::TaskQueueId gpu_queue_id); @@ -44,7 +68,7 @@ class RasterThreadMerger // Returns true if the current thread owns rasterizing. // When the threads are merged, platform thread owns rasterizing. // When un-merged, raster thread owns rasterizing. - bool IsOnRasterizingThread() const; + bool IsOnRasterizingThread(); // Returns true if the current thread is the platform thread. bool IsOnPlatformThread() const; @@ -55,7 +79,13 @@ class RasterThreadMerger fml::TaskQueueId gpu_queue_id_; fml::RefPtr task_queues_; std::atomic_int lease_term_; - bool is_merged_; + std::condition_variable merged_condition_; + std::mutex lease_term_mutex_; + + bool IsMergedUnSafe(); + // The platform_queue_id and gpu_queue_id are exactly the same. + // We consider the threads are always merged and cannot be unmerged. + bool TaskQueuesAreSame(); FML_FRIEND_REF_COUNTED_THREAD_SAFE(RasterThreadMerger); FML_FRIEND_MAKE_REF_COUNTED(RasterThreadMerger); diff --git a/fml/raster_thread_merger_unittests.cc b/fml/raster_thread_merger_unittests.cc index a182723a79191..f3df1c1bb2259 100644 --- a/fml/raster_thread_merger_unittests.cc +++ b/fml/raster_thread_merger_unittests.cc @@ -208,3 +208,96 @@ TEST(RasterThreadMerger, LeaseExtension) { thread1.join(); thread2.join(); } + +TEST(RasterThreadMerger, WaitUntilMerged) { + fml::RefPtr raster_thread_merger; + + fml::AutoResetWaitableEvent create_thread_merger_latch; + fml::MessageLoop* loop_platform = nullptr; + fml::AutoResetWaitableEvent latch_platform; + fml::AutoResetWaitableEvent term_platform; + fml::AutoResetWaitableEvent latch_merged; + std::thread thread_platform([&]() { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + loop_platform = &fml::MessageLoop::GetCurrent(); + latch_platform.Signal(); + create_thread_merger_latch.Wait(); + raster_thread_merger->WaitUntilMerged(); + latch_merged.Signal(); + term_platform.Wait(); + }); + + const int kNumFramesMerged = 5; + fml::MessageLoop* loop_raster = nullptr; + fml::AutoResetWaitableEvent term_raster; + std::thread thread_raster([&]() { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + loop_raster = &fml::MessageLoop::GetCurrent(); + latch_platform.Wait(); + fml::TaskQueueId qid_platform = + loop_platform->GetTaskRunner()->GetTaskQueueId(); + fml::TaskQueueId qid_raster = + loop_raster->GetTaskRunner()->GetTaskQueueId(); + raster_thread_merger = + fml::MakeRefCounted(qid_platform, qid_raster); + ASSERT_FALSE(raster_thread_merger->IsMerged()); + create_thread_merger_latch.Signal(); + raster_thread_merger->MergeWithLease(kNumFramesMerged); + term_raster.Wait(); + }); + + latch_merged.Wait(); + ASSERT_TRUE(raster_thread_merger->IsMerged()); + + for (int i = 0; i < kNumFramesMerged; i++) { + ASSERT_TRUE(raster_thread_merger->IsMerged()); + raster_thread_merger->DecrementLease(); + } + + ASSERT_FALSE(raster_thread_merger->IsMerged()); + + term_platform.Signal(); + term_raster.Signal(); + thread_platform.join(); + thread_raster.join(); +} + +TEST(RasterThreadMerger, HandleTaskQueuesAreTheSame) { + fml::MessageLoop* loop1 = nullptr; + fml::AutoResetWaitableEvent latch1; + fml::AutoResetWaitableEvent term1; + std::thread thread1([&loop1, &latch1, &term1]() { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + loop1 = &fml::MessageLoop::GetCurrent(); + latch1.Signal(); + term1.Wait(); + }); + + latch1.Wait(); + + fml::TaskQueueId qid1 = loop1->GetTaskRunner()->GetTaskQueueId(); + fml::TaskQueueId qid2 = qid1; + const auto raster_thread_merger_ = + fml::MakeRefCounted(qid1, qid2); + // Statically merged. + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + + // Test decrement lease and unmerge are both no-ops. + // The task queues should be always merged. + const int kNumFramesMerged = 5; + raster_thread_merger_->MergeWithLease(kNumFramesMerged); + + for (int i = 0; i < kNumFramesMerged; i++) { + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + raster_thread_merger_->DecrementLease(); + } + + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + + // Wait until merged should also return immediately. + raster_thread_merger_->WaitUntilMerged(); + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + + term1.Signal(); + thread1.join(); +} diff --git a/lib/io/dart_io.cc b/lib/io/dart_io.cc index 6e5e538d74da0..d656e8b538943 100644 --- a/lib/io/dart_io.cc +++ b/lib/io/dart_io.cc @@ -16,19 +16,27 @@ using tonic::ToDart; namespace flutter { -void DartIO::InitForIsolate(bool disable_http) { - Dart_Handle result = Dart_SetNativeResolver( - Dart_LookupLibrary(ToDart("dart:io")), dart::bin::LookupIONative, - dart::bin::LookupIONativeSymbol); +void DartIO::InitForIsolate(bool may_insecurely_connect_to_all_domains, + std::string domain_network_policy) { + Dart_Handle io_lib = Dart_LookupLibrary(ToDart("dart:io")); + Dart_Handle result = Dart_SetNativeResolver(io_lib, dart::bin::LookupIONative, + dart::bin::LookupIONativeSymbol); FML_CHECK(!LogIfError(result)); - // The SDK expects this field to represent "allow http" so we switch the - // value. - Dart_Handle allow_http_value = disable_http ? Dart_False() : Dart_True(); - Dart_Handle set_field_result = - Dart_SetField(Dart_LookupLibrary(ToDart("dart:_http")), - ToDart("_embedderAllowsHttp"), allow_http_value); - FML_CHECK(!LogIfError(set_field_result)); + Dart_Handle embedder_config_type = + Dart_GetType(io_lib, ToDart("_EmbedderConfig"), 0, nullptr); + FML_CHECK(!LogIfError(embedder_config_type)); + + Dart_Handle allow_insecure_connections_result = Dart_SetField( + embedder_config_type, ToDart("_mayInsecurelyConnectToAllDomains"), + ToDart(may_insecurely_connect_to_all_domains)); + FML_CHECK(!LogIfError(allow_insecure_connections_result)); + + Dart_Handle dart_args[1]; + dart_args[0] = ToDart(domain_network_policy); + Dart_Handle set_domain_network_policy_result = Dart_Invoke( + embedder_config_type, ToDart("_setDomainPolicies"), 1, dart_args); + FML_CHECK(!LogIfError(set_domain_network_policy_result)); } } // namespace flutter diff --git a/lib/io/dart_io.h b/lib/io/dart_io.h index 27ce7aa65baeb..34bc8a54aaed9 100644 --- a/lib/io/dart_io.h +++ b/lib/io/dart_io.h @@ -6,6 +6,7 @@ #define FLUTTER_LIB_IO_DART_IO_H_ #include +#include #include "flutter/fml/macros.h" @@ -13,7 +14,8 @@ namespace flutter { class DartIO { public: - static void InitForIsolate(bool disable_http); + static void InitForIsolate(bool may_insecurely_connect_to_all_domains, + std::string domain_network_policy); private: FML_DISALLOW_IMPLICIT_CONSTRUCTORS(DartIO); diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index 103699d640692..decca25d81f90 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -6,7 +6,7 @@ import("//build/fuchsia/sdk.gni") import("//flutter/common/config.gni") import("//flutter/testing/testing.gni") -source_set_maybe_fuchsia_legacy("ui") { +source_set("ui") { sources = [ "compositing/scene.cc", "compositing/scene.h", @@ -93,6 +93,8 @@ source_set_maybe_fuchsia_legacy("ui") { "text/text_box.h", "ui_dart_state.cc", "ui_dart_state.h", + "window/platform_configuration.cc", + "window/platform_configuration.h", "window/platform_message.cc", "window/platform_message.h", "window/platform_message_response.cc", @@ -118,6 +120,7 @@ source_set_maybe_fuchsia_legacy("ui") { deps = [ "//flutter/assets", "//flutter/common", + "//flutter/flow", "//flutter/fml", "//flutter/runtime:test_font", "//flutter/third_party/tonic", @@ -130,18 +133,18 @@ source_set_maybe_fuchsia_legacy("ui") { defines = [ "FLUTTER_ENABLE_SKSHAPER" ] } - sources_legacy = [ - "compositing/scene_host.cc", - "compositing/scene_host.h", - ] - - deps_legacy = [ - "$fuchsia_sdk_root/pkg:async-cpp", - "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", - "//flutter/shell/platform/fuchsia/dart-pkg/zircon", - ] + if (is_fuchsia && flutter_enable_legacy_fuchsia_embedder) { + sources += [ + "compositing/scene_host.cc", + "compositing/scene_host.h", + ] - deps_legacy_and_next = [ "//flutter/flow:flow" ] + deps += [ + "$fuchsia_sdk_root/pkg:async-cpp", + "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", + "//flutter/shell/platform/fuchsia/dart-pkg/zircon", + ] + } } if (enable_unittests) { @@ -172,7 +175,7 @@ if (enable_unittests) { ] } - source_set_maybe_fuchsia_legacy("ui_unittests_common") { + executable("ui_unittests") { testonly = true public_configs = [ "//flutter:export_dynamic_symbols" ] @@ -180,48 +183,27 @@ if (enable_unittests) { sources = [ "painting/image_encoding_unittests.cc", "painting/vertices_unittests.cc", + "window/platform_configuration_unittests.cc", "window/pointer_data_packet_converter_unittests.cc", ] deps = [ + ":ui", ":ui_unittests_fixtures", "//flutter/common", + "//flutter/shell/common:shell_test_fixture_sources", "//flutter/testing", + "//flutter/testing:dart", + "//flutter/testing:fixture_test", "//flutter/third_party/tonic", "//third_party/dart/runtime/bin:elf_loader", ] - # TODO(): This test is hard-coded to use a TestGLSurface so it cannot run on fuchsia. + # TODO(https://github.com/flutter/flutter/issues/63837): This test is hard-coded to use a TestGLSurface so it cannot run on fuchsia. if (!is_fuchsia) { sources += [ "painting/image_decoder_unittests.cc" ] deps += [ "//flutter/testing:opengl" ] } - - deps_legacy_and_next = [ - ":ui", - "//flutter/shell/common:shell_test_fixture_sources", - "//flutter/testing:dart", - "//flutter/testing:fixture_test", - ] - } - - if (is_fuchsia) { - executable("ui_unittests") { - testonly = true - - deps = [ ":ui_unittests_common_fuchsia_legacy" ] - } - executable("ui_unittests_next") { - testonly = true - - deps = [ ":ui_unittests_common" ] - } - } else { - executable("ui_unittests") { - testonly = true - - deps = [ ":ui_unittests_common" ] - } } } diff --git a/lib/ui/channel_buffers.dart b/lib/ui/channel_buffers.dart index ba67c41269b9c..a32a557e1f121 100644 --- a/lib/ui/channel_buffers.dart +++ b/lib/ui/channel_buffers.dart @@ -25,7 +25,7 @@ class _StoredMessage { /// A fixed-size circular queue. class _RingBuffer { - /// The underlying data for the RingBuffer. ListQueue's dynamically resize, + /// The underlying data for the RingBuffer. ListQueues dynamically resize, /// [_RingBuffer]s do not. final collection.ListQueue _queue; diff --git a/lib/ui/compositing.dart b/lib/ui/compositing.dart index 4f8332f6efdfd..39b0d63fc2b36 100644 --- a/lib/ui/compositing.dart +++ b/lib/ui/compositing.dart @@ -699,12 +699,12 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// texture just before resizing the Android view and un-freezes it when it is /// certain that a frame with the new size is ready. void addTexture( - int/*!*/ textureId, { - Offset/*!*/ offset = Offset.zero, - double/*!*/ width = 0.0, - double/*!*/ height = 0.0, - bool/*!*/ freeze = false, - FilterQuality/*!*/ filterQuality = FilterQuality.low, + int textureId, { + Offset offset = Offset.zero, + double width = 0.0, + double height = 0.0, + bool freeze = false, + FilterQuality filterQuality = FilterQuality.low, }) { assert(offset != null, 'Offset argument was null'); // ignore: unnecessary_null_comparison _addTexture(offset.dx, offset.dy, width, height, textureId, freeze, filterQuality.index); diff --git a/lib/ui/compositing/scene.cc b/lib/ui/compositing/scene.cc index f5403ecae9a61..96eac5885d083 100644 --- a/lib/ui/compositing/scene.cc +++ b/lib/ui/compositing/scene.cc @@ -8,6 +8,7 @@ #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/picture.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/window.h" #include "third_party/skia/include/core/SkImageInfo.h" #include "third_party/skia/include/core/SkSurface.h" @@ -41,12 +42,14 @@ Scene::Scene(std::shared_ptr rootLayer, uint32_t rasterizerTracingThreshold, bool checkerboardRasterCacheImages, bool checkerboardOffscreenLayers) { - auto viewport_metrics = UIDartState::Current()->window()->viewport_metrics(); + auto viewport_metrics = UIDartState::Current() + ->platform_configuration() + ->window() + ->viewport_metrics(); layer_tree_ = std::make_unique( SkISize::Make(viewport_metrics.physical_width, viewport_metrics.physical_height), - static_cast(viewport_metrics.physical_depth), static_cast(viewport_metrics.device_pixel_ratio)); layer_tree_->set_root_layer(std::move(rootLayer)); layer_tree_->set_rasterizer_tracing_threshold(rasterizerTracingThreshold); diff --git a/lib/ui/compositing/scene_builder.cc b/lib/ui/compositing/scene_builder.cc index 1c0fa9bd5597f..b60a5f1c67caa 100644 --- a/lib/ui/compositing/scene_builder.cc +++ b/lib/ui/compositing/scene_builder.cc @@ -220,7 +220,7 @@ void SceneBuilder::addPicture(double dx, pictureRect.offset(offset.x(), offset.y()); auto layer = std::make_unique( offset, UIDartState::CreateGPUObject(picture->picture()), !!(hints & 1), - !!(hints & 2)); + !!(hints & 2), picture->GetAllocationSize()); AddLayer(std::move(layer)); } diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc index a02bf3523d612..f9f89d6f22d67 100644 --- a/lib/ui/dart_ui.cc +++ b/lib/ui/dart_ui.cc @@ -30,7 +30,7 @@ #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/text/paragraph.h" #include "flutter/lib/ui/text/paragraph_builder.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "third_party/tonic/converter/dart_converter.h" #include "third_party/tonic/logging/dart_error.h" @@ -85,7 +85,7 @@ void DartUI::InitForGlobal() { SemanticsUpdate::RegisterNatives(g_natives); SemanticsUpdateBuilder::RegisterNatives(g_natives); Vertices::RegisterNatives(g_natives); - Window::RegisterNatives(g_natives); + PlatformConfiguration::RegisterNatives(g_natives); #if defined(LEGACY_FUCHSIA_EMBEDDER) SceneHost::RegisterNatives(g_natives); #endif diff --git a/lib/ui/fixtures/ui_test.dart b/lib/ui/fixtures/ui_test.dart index 717f9b7fd335f..478c919066562 100644 --- a/lib/ui/fixtures/ui_test.dart +++ b/lib/ui/fixtures/ui_test.dart @@ -3,6 +3,7 @@ // 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:typed_data'; import 'dart:ui'; @@ -34,6 +35,7 @@ void createVertices() { ); _validateVertices(vertices); } + void _validateVertices(Vertices vertices) native 'ValidateVertices'; @pragma('vm:entry-point') @@ -42,8 +44,10 @@ void frameCallback(FrameInfo info) { } @pragma('vm:entry-point') -void messageCallback(dynamic data) { -} +void messageCallback(dynamic data) {} + +@pragma('vm:entry-point') +void validateConfiguration() native 'ValidateConfiguration'; // Draw a circle on a Canvas that has a PictureRecorder. Take the image from @@ -70,3 +74,59 @@ Future encodeImageProducesExternalUint8List() async { void _encodeImage(Image i, int format, void Function(Uint8List result)) native 'EncodeImage'; void _validateExternal(Uint8List result) native 'ValidateExternal'; + +@pragma('vm:entry-point') +Future pumpImage() async { + final FrameCallback renderBlank = (Duration duration) { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawRect(Rect.largest, Paint()); + final Picture picture = recorder.endRecording(); + + final SceneBuilder builder = SceneBuilder(); + builder.addPicture(Offset.zero, picture); + + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + window.onBeginFrame = (Duration duration) { + window.onDrawFrame = _onBeginFrameDone; + }; + window.scheduleFrame(); + }; + + final FrameCallback renderImage = (Duration duration) { + const int width = 8000; + const int height = 8000; + final Completer completer = Completer(); + decodeImageFromPixels( + Uint8List.fromList(List.filled(width * height * 4, 0xFF)), + width, + height, + PixelFormat.rgba8888, + (Image image) => completer.complete(image), + ); + completer.future.then((Image image) { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawImage(image, Offset.zero, Paint()); + final Picture picture = recorder.endRecording(); + + final SceneBuilder builder = SceneBuilder(); + builder.addPicture(Offset.zero, picture); + + _captureImageAndPicture(image, picture); + + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + window.onBeginFrame = renderBlank; + window.scheduleFrame(); + }); + }; + + window.onBeginFrame = renderImage; + window.scheduleFrame(); +} +void _captureImageAndPicture(Image image, Picture picture) native 'CaptureImageAndPicture'; +Future _onBeginFrameDone() native 'OnBeginFrameDone'; diff --git a/lib/ui/geometry.dart b/lib/ui/geometry.dart index a7404996cd674..23829b3efaaca 100644 --- a/lib/ui/geometry.dart +++ b/lib/ui/geometry.dart @@ -656,7 +656,7 @@ class Rect { /// Constructs a rectangle from its center point, width, and height. /// /// The `center` argument is assumed to be an offset from the origin. - Rect.fromCenter({ required Offset center/*!*/, required double width, required double height }) : this.fromLTRB( + Rect.fromCenter({ required Offset center, required double width, required double height }) : this.fromLTRB( center.dx - width / 2, center.dy - height / 2, center.dx + width / 2, diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 39bab12406fb8..a911d79f63a8d 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -14,7 +14,6 @@ void _updateWindowMetrics( double devicePixelRatio, double width, double height, - double depth, double viewPaddingTop, double viewPaddingRight, double viewPaddingBottom, @@ -31,7 +30,6 @@ void _updateWindowMetrics( window .._devicePixelRatio = devicePixelRatio .._physicalSize = Size(width, height) - .._physicalDepth = depth .._viewPadding = WindowPadding._( top: viewPaddingTop, right: viewPaddingRight, @@ -200,7 +198,7 @@ void _reportTimings(List timings) { assert(timings.length % FramePhase.values.length == 0); final List frameTimings = []; for (int i = 0; i < timings.length; i += FramePhase.values.length) { - frameTimings.add(FrameTiming(timings.sublist(i, i + FramePhase.values.length))); + frameTimings.add(FrameTiming._(timings.sublist(i, i + FramePhase.values.length))); } _invoke1(window.onReportTimings, window._onReportTimingsZone, frameTimings); } @@ -238,7 +236,7 @@ void _runMainZoned(Function startMainIsolateFunction, }, null); } -void _reportUnhandledException(String error, String stackTrace) native 'Window_reportUnhandledException'; +void _reportUnhandledException(String error, String stackTrace) native 'PlatformConfiguration_reportUnhandledException'; /// Invokes [callback] inside the given [zone]. void _invoke(void callback()?, Zone zone) { diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 454c8c34c4a4d..449d11c171445 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -3191,8 +3191,8 @@ class Gradient extends Shader { List colors, [ List? colorStops, TileMode tileMode = TileMode.clamp, - double startAngle/*?*/ = 0.0, - double endAngle/*!*/ = math.pi * 2, + double startAngle = 0.0, + double endAngle = math.pi * 2, Float64List? matrix4, ]) : assert(_offsetIsValid(center)), assert(colors != null), // ignore: unnecessary_null_comparison @@ -4026,13 +4026,128 @@ class Canvas extends NativeFieldWrapperClass2 { List? paintObjects, ByteData paintData) native 'Canvas_drawVertices'; - /// Draws part of an image - the [atlas] - onto the canvas. + /// Draws many parts of an image - the [atlas] - onto the canvas. + /// + /// This method allows for optimization when you want to draw many parts of an + /// image onto the canvas, such as when using sprites or zooming. It is more efficient + /// than using multiple calls to [drawImageRect] and provides more functionality + /// to individually transform each image part by a separate rotation or scale and + /// blend or modulate those parts with a solid color. + /// + /// The method takes a list of [Rect] objects that each define a piece of the + /// [atlas] image to be drawn independently. Each [Rect] is associated with an + /// [RSTransform] entry in the [transforms] list which defines the location, + /// rotation, and (uniform) scale with which to draw that portion of the image. + /// Each [Rect] can also be associated with an optional [Color] which will be + /// composed with the associated image part using the [blendMode] before blending + /// the result onto the canvas. The full operation can be broken down as: + /// + /// - Blend each rectangular portion of the image specified by an entry in the + /// [rects] argument with its associated entry in the [colors] list using the + /// [blendMode] argument (if a color is specified). In this part of the operation, + /// the image part will be considered the source of the operation and the associated + /// color will be considered the destination. + /// - Blend the result from the first step onto the canvas using the translation, + /// rotation, and scale properties expressed in the associated entry in the + /// [transforms] list using the properties of the [Paint] object. + /// + /// If the first stage of the operation which blends each part of the image with + /// a color is needed, then both the [colors] and [blendMode] arguments must + /// not be null and there must be an entry in the [colors] list for each + /// image part. If that stage is not needed, then the [colors] argument can + /// be either null or an empty list and the [blendMode] argument may also be null. + /// + /// The optional [cullRect] argument can provide an estimate of the bounds of the + /// coordinates rendered by all components of the atlas to be compared against + /// the clip to quickly reject the operation if it does not intersect. + /// + /// An example usage to render many sprites from a single sprite atlas with no + /// rotations or scales: /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// } /// - /// All parameters must not be null. + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, [ + /// for (Sprite sprite in allSprites) + /// RSTransform.fromComponents( + /// rotation: 0.0, + /// scale: 1.0, + /// // Center of the sprite relative to its rect + /// anchorX: 5.0, + /// anchorY: 5.0, + /// // Location at which to draw the center of the sprite + /// translateX: sprite.centerX, + /// translateY: sprite.centerY, + /// ), + /// ], [ + /// for (Sprite sprite in allSprites) + /// Rect.fromLTWH(sprite.index * 10.0, 0.0, 10.0, 10.0), + /// ], null, null, null, paint); + /// } + /// + /// ... + /// } + /// ``` + /// + /// Another example usage which renders sprites with an optional opacity and rotation: + /// + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// int alpha; + /// double rotation; + /// } + /// + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, [ + /// for (Sprite sprite in allSprites) + /// RSTransform.fromComponents( + /// rotation: sprite.rotation, + /// scale: 1.0, + /// // Center of the sprite relative to its rect + /// anchorX: 5.0, + /// anchorY: 5.0, + /// // Location at which to draw the center of the sprite + /// translateX: sprite.centerX, + /// translateY: sprite.centerY, + /// ), + /// ], [ + /// for (Sprite sprite in allSprites) + /// Rect.fromLTWH(sprite.index * 10.0, 0.0, 10.0, 10.0), + /// ], [ + /// for (Sprite sprite in allSprites) + /// Color.white.withAlpha(sprite.alpha), + /// ], BlendMode.srcIn, null, paint); + /// } + /// + /// ... + /// } + /// ``` + /// + /// The length of the [transforms] and [rects] lists must be equal and + /// if the [colors] argument is not null then it must either be empty or + /// have the same length as the other two lists. /// /// See also: /// @@ -4041,22 +4156,21 @@ class Canvas extends NativeFieldWrapperClass2 { void drawAtlas(Image atlas, List transforms, List rects, - List colors, - BlendMode blendMode, + List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(transforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || colors.isEmpty || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; if (transforms.length != rectCount) throw ArgumentError('"transforms" and "rects" lengths must match.'); - if (colors.isNotEmpty && colors.length != rectCount) + if (colors != null && colors.isNotEmpty && colors.length != rectCount) throw ArgumentError('If non-null, "colors" length must match that of "transforms" and "rects".'); final Float32List rstTransformBuffer = Float32List(rectCount * 4); @@ -4080,20 +4194,27 @@ class Canvas extends NativeFieldWrapperClass2 { rectBuffer[index3] = rect.bottom; } - final Int32List? colorBuffer = colors.isEmpty ? null : _encodeColorList(colors); + final Int32List? colorBuffer = (colors == null || colors.isEmpty) ? null : _encodeColorList(colors); final Float32List? cullRectBuffer = cullRect?._value32; _drawAtlas( paint._objects, paint._data, atlas, rstTransformBuffer, rectBuffer, - colorBuffer, blendMode.index, cullRectBuffer + colorBuffer, (blendMode ?? BlendMode.src).index, cullRectBuffer ); } - /// Draws part of an image - the [atlas] - onto the canvas. + /// Draws many parts of an image - the [atlas] - onto the canvas. + /// + /// This method allows for optimization when you want to draw many parts of an + /// image onto the canvas, such as when using sprites or zooming. It is more efficient + /// than using multiple calls to [drawImageRect] and provides more functionality + /// to individually transform each image part by a separate rotation or scale and + /// blend or modulate those parts with a solid color. It is also more efficient + /// than [drawAtlas] as the data in the arguments is already packed in a format + /// that can be directly used by the rendering code. /// - /// This method allows for optimization when you only want to draw part of an - /// image on the canvas, such as when using sprites or zooming. It is more - /// efficient than using clips or masks directly. + /// A full description of how this method uses its arguments to draw onto the + /// canvas can be found in the description of the [drawAtlas] method. /// /// The [rstTransforms] argument is interpreted as a list of four-tuples, with /// each tuple being ([RSTransform.scos], [RSTransform.ssin], @@ -4103,7 +4224,121 @@ class Canvas extends NativeFieldWrapperClass2 { /// tuple being ([Rect.left], [Rect.top], [Rect.right], [Rect.bottom]). /// /// The [colors] argument, which can be null, is interpreted as a list of - /// 32-bit colors, with the same packing as [Color.value]. + /// 32-bit colors, with the same packing as [Color.value]. If the [colors] + /// argument is not null then the [blendMode] argument must also not be null. + /// + /// An example usage to render many sprites from a single sprite atlas with no rotations + /// or scales: + /// + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// } + /// + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// // For best advantage, these lists should be cached and only specific + /// // entries updated when the sprite information changes. This code is + /// // illustrative of how to set up the data and not a recommendation for + /// // optimal usage. + /// Float32List rectList = Float32List(allSprites.length * 4); + /// Float32List transformList = Float32List(allSprites.length * 4); + /// for (int i = 0; i < allSprites.length; i++) { + /// final double rectX = sprite.spriteIndex * 10.0; + /// rectList[i * 4 + 0] = rectX; + /// rectList[i * 4 + 1] = 0.0; + /// rectList[i * 4 + 2] = rectX + 10.0; + /// rectList[i * 4 + 3] = 10.0; + /// + /// // This example sets the RSTransform values directly for a common case of no + /// // rotations or scales and just a translation to position the atlas entry. For + /// // more complicated transforms one could use the RSTransform class to compute + /// // the necessary values or do the same math directly. + /// transformList[i * 4 + 0] = 1.0; + /// transformList[i * 4 + 1] = 0.0; + /// transformList[i * 4 + 2] = sprite.centerX - 5.0; + /// transformList[i * 4 + 2] = sprite.centerY - 5.0; + /// } + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, transformList, rectList, null, null, null, paint); + /// } + /// + /// ... + /// } + /// ``` + /// + /// Another example usage which renders sprites with an optional opacity and rotation: + /// + /// ```dart + /// class Sprite { + /// int index; + /// double centerX; + /// double centerY; + /// int alpha; + /// double rotation; + /// } + /// + /// class MyPainter extends CustomPainter { + /// // assume spriteAtlas contains N 10x10 sprites side by side in a (N*10)x10 image + /// ui.Image spriteAtlas; + /// List allSprites; + /// + /// @override + /// void paint(Canvas canvas, Size size) { + /// // For best advantage, these lists should be cached and only specific + /// // entries updated when the sprite information changes. This code is + /// // illustrative of how to set up the data and not a recommendation for + /// // optimal usage. + /// Float32List rectList = Float32List(allSprites.length * 4); + /// Float32List transformList = Float32List(allSprites.length * 4); + /// Int32List colorList = Int32List(allSprites.length); + /// for (int i = 0; i < allSprites.length; i++) { + /// final double rectX = sprite.spriteIndex * 10.0; + /// rectList[i * 4 + 0] = rectX; + /// rectList[i * 4 + 1] = 0.0; + /// rectList[i * 4 + 2] = rectX + 10.0; + /// rectList[i * 4 + 3] = 10.0; + /// + /// // This example uses an RSTransform object to compute the necessary values for + /// // the transform using a factory helper method because the sprites contain + /// // rotation values which are not trivial to work with. But if the math for the + /// // values falls out from other calculations on the sprites then the values could + /// // possibly be generated directly from the sprite update code. + /// final RSTransform transform = RSTransform.fromComponents( + /// rotation: sprite.rotation, + /// scale: 1.0, + /// // Center of the sprite relative to its rect + /// anchorX: 5.0, + /// anchorY: 5.0, + /// // Location at which to draw the center of the sprite + /// translateX: sprite.centerX, + /// translateY: sprite.centerY, + /// ); + /// transformList[i * 4 + 0] = transform.scos; + /// transformList[i * 4 + 1] = transform.ssin; + /// transformList[i * 4 + 2] = transform.tx; + /// transformList[i * 4 + 2] = transform.ty; + /// + /// // This example computes the color value directly, but one could also compute + /// // an actual Color object and use its Color.value getter for the same result. + /// // Since we are using BlendMode.srcIn, only the alpha component matters for + /// // these colors which makes this a simple shift operation. + /// colorList[i] = sprite.alpha << 24; + /// } + /// Paint paint = Paint(); + /// canvas.drawAtlas(spriteAtlas, transformList, rectList, colorList, BlendMode.srcIn, null, paint); + /// } + /// + /// ... + /// } + /// ``` /// /// See also: /// @@ -4112,16 +4347,15 @@ class Canvas extends NativeFieldWrapperClass2 { void drawRawAtlas(Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - BlendMode blendMode, + Int32List? colors, + BlendMode? blendMode, Rect? cullRect, Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(rstTransforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; @@ -4129,12 +4363,12 @@ class Canvas extends NativeFieldWrapperClass2 { throw ArgumentError('"rstTransforms" and "rects" lengths must match.'); if (rectCount % 4 != 0) throw ArgumentError('"rstTransforms" and "rects" lengths must be a multiple of four.'); - if (colors.length * 4 != rectCount) + if (colors != null && colors.length * 4 != rectCount) throw ArgumentError('If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); _drawAtlas( paint._objects, paint._data, atlas, rstTransforms, rects, - colors, blendMode.index, cullRect?._value32 + colors, (blendMode ?? BlendMode.src).index, cullRect?._value32 ); } diff --git a/lib/ui/painting/canvas.cc b/lib/ui/painting/canvas.cc index be28e8c964bbc..c68f0d6001ad6 100644 --- a/lib/ui/painting/canvas.cc +++ b/lib/ui/painting/canvas.cc @@ -11,6 +11,7 @@ #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/matrix.h" #include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/window.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkCanvas.h" @@ -326,7 +327,6 @@ void Canvas::drawImage(const CanvasImage* image, ToDart("Canvas.drawImage called with non-genuine Image.")); return; } - external_allocation_size_ += image->GetAllocationSize(); canvas_->drawImage(image->image(), x, y, paint.paint()); } @@ -351,7 +351,6 @@ void Canvas::drawImageRect(const CanvasImage* image, } SkRect src = SkRect::MakeLTRB(src_left, src_top, src_right, src_bottom); SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom); - external_allocation_size_ += image->GetAllocationSize(); canvas_->drawImageRect(image->image(), src, dst, paint.paint(), SkCanvas::kFast_SrcRectConstraint); } @@ -380,7 +379,6 @@ void Canvas::drawImageNine(const CanvasImage* image, SkIRect icenter; center.round(&icenter); SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom); - external_allocation_size_ += image->GetAllocationSize(); canvas_->drawImageNine(image->image(), icenter, dst, paint.paint()); } @@ -474,18 +472,21 @@ void Canvas::drawShadow(const CanvasPath* path, ToDart("Canvas.drawShader called with non-genuine Path.")); return; } - SkScalar dpr = - UIDartState::Current()->window()->viewport_metrics().device_pixel_ratio; + SkScalar dpr = UIDartState::Current() + ->platform_configuration() + ->window() + ->viewport_metrics() + .device_pixel_ratio; external_allocation_size_ += path->path().approximateBytesUsed(); flutter::PhysicalShapeLayer::DrawShadow(canvas_, path->path(), color, elevation, transparentOccluder, dpr); } void Canvas::Invalidate() { + canvas_ = nullptr; if (dart_wrapper()) { ClearDartWrapper(); } - canvas_ = nullptr; } } // namespace flutter diff --git a/lib/ui/painting/image.cc b/lib/ui/painting/image.cc index 126205530fa4a..7da7c0ad029ec 100644 --- a/lib/ui/painting/image.cc +++ b/lib/ui/painting/image.cc @@ -37,8 +37,8 @@ Dart_Handle CanvasImage::toByteData(int format, Dart_Handle callback) { } void CanvasImage::dispose() { - ClearDartWrapper(); image_.reset(); + ClearDartWrapper(); } size_t CanvasImage::GetAllocationSize() const { diff --git a/lib/ui/painting/multi_frame_codec.cc b/lib/ui/painting/multi_frame_codec.cc index 4965bd924b215..c40561836011a 100644 --- a/lib/ui/painting/multi_frame_codec.cc +++ b/lib/ui/painting/multi_frame_codec.cc @@ -75,7 +75,7 @@ static bool CopyToBitmap(SkBitmap* dst, } sk_sp MultiFrameCodec::State::GetNextFrameImage( - fml::WeakPtr resourceContext) { + fml::WeakPtr resourceContext) { SkBitmap bitmap = SkBitmap(); SkImageInfo info = generator_->getInfo().makeColorType(kN32_SkColorType); if (info.alphaType() == kUnpremul_SkAlphaType) { @@ -136,7 +136,7 @@ sk_sp MultiFrameCodec::State::GetNextFrameImage( void MultiFrameCodec::State::GetNextFrameAndInvokeCallback( std::unique_ptr callback, fml::RefPtr ui_task_runner, - fml::WeakPtr resourceContext, + fml::WeakPtr resourceContext, fml::RefPtr unref_queue, size_t trace_id) { fml::RefPtr frameInfo = NULL; diff --git a/lib/ui/painting/multi_frame_codec.h b/lib/ui/painting/multi_frame_codec.h index 5843876046e31..428d67c0bfeda 100644 --- a/lib/ui/painting/multi_frame_codec.h +++ b/lib/ui/painting/multi_frame_codec.h @@ -53,12 +53,13 @@ class MultiFrameCodec : public Codec { // The index of the last decoded required frame. int lastRequiredFrameIndex_ = -1; - sk_sp GetNextFrameImage(fml::WeakPtr resourceContext); + sk_sp GetNextFrameImage( + fml::WeakPtr resourceContext); void GetNextFrameAndInvokeCallback( std::unique_ptr callback, fml::RefPtr ui_task_runner, - fml::WeakPtr resourceContext, + fml::WeakPtr resourceContext, fml::RefPtr unref_queue, size_t trace_id); }; diff --git a/lib/ui/painting/picture.cc b/lib/ui/painting/picture.cc index 48dd11226cf26..1285a6b0921cb 100644 --- a/lib/ui/painting/picture.cc +++ b/lib/ui/painting/picture.cc @@ -56,8 +56,8 @@ Dart_Handle Picture::toImage(uint32_t width, } void Picture::dispose() { - ClearDartWrapper(); picture_.reset(); + ClearDartWrapper(); } size_t Picture::GetAllocationSize() const { diff --git a/lib/ui/text/font_collection.cc b/lib/ui/text/font_collection.cc index 38d931b402ea7..7405941da41ed 100644 --- a/lib/ui/text/font_collection.cc +++ b/lib/ui/text/font_collection.cc @@ -8,7 +8,7 @@ #include "flutter/lib/ui/text/asset_manager_font_provider.h" #include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/runtime/test_font_data.h" #include "rapidjson/document.h" #include "rapidjson/rapidjson.h" @@ -30,8 +30,10 @@ namespace { void LoadFontFromList(tonic::Uint8List& font_data, // NOLINT Dart_Handle callback, std::string family_name) { - FontCollection& font_collection = - UIDartState::Current()->window()->client()->GetFontCollection(); + FontCollection& font_collection = UIDartState::Current() + ->platform_configuration() + ->client() + ->GetFontCollection(); font_collection.LoadFontFromList(font_data.data(), font_data.num_elements(), family_name); font_data.Release(); diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 396ce50f5cfca..679fc887ac89a 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -10,7 +10,7 @@ #include "flutter/fml/task_runner.h" #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/third_party/txt/src/txt/font_style.h" #include "flutter/third_party/txt/src/txt/font_weight.h" #include "flutter/third_party/txt/src/txt/paragraph_style.h" @@ -288,8 +288,10 @@ ParagraphBuilder::ParagraphBuilder( style.locale = locale; } - FontCollection& font_collection = - UIDartState::Current()->window()->client()->GetFontCollection(); + FontCollection& font_collection = UIDartState::Current() + ->platform_configuration() + ->client() + ->GetFontCollection(); #if FLUTTER_ENABLE_SKSHAPER #define FLUTTER_PARAGRAPH_BUILDER txt::ParagraphBuilder::CreateSkiaBuilder diff --git a/lib/ui/ui_dart_state.cc b/lib/ui/ui_dart_state.cc index 1bd00e35ae972..b43a442f849e0 100644 --- a/lib/ui/ui_dart_state.cc +++ b/lib/ui/ui_dart_state.cc @@ -5,7 +5,7 @@ #include "flutter/lib/ui/ui_dart_state.h" #include "flutter/fml/message_loop.h" -#include "flutter/lib/ui/window/window.h" +#include "flutter/lib/ui/window/platform_configuration.h" #include "third_party/tonic/converter/dart_converter.h" #include "third_party/tonic/dart_message_handler.h" @@ -73,8 +73,9 @@ void UIDartState::ThrowIfUIOperationsProhibited() { void UIDartState::SetDebugName(const std::string debug_name) { debug_name_ = debug_name; - if (window_) { - window_->client()->UpdateIsolateDescription(debug_name_, main_port_); + if (platform_configuration_) { + platform_configuration_->client()->UpdateIsolateDescription(debug_name_, + main_port_); } } @@ -82,10 +83,12 @@ UIDartState* UIDartState::Current() { return static_cast(DartState::Current()); } -void UIDartState::SetWindow(std::unique_ptr window) { - window_ = std::move(window); - if (window_) { - window_->client()->UpdateIsolateDescription(debug_name_, main_port_); +void UIDartState::SetPlatformConfiguration( + std::unique_ptr platform_configuration) { + platform_configuration_ = std::move(platform_configuration); + if (platform_configuration_) { + platform_configuration_->client()->UpdateIsolateDescription(debug_name_, + main_port_); } } @@ -133,7 +136,7 @@ fml::WeakPtr UIDartState::GetSnapshotDelegate() const { return snapshot_delegate_; } -fml::WeakPtr UIDartState::GetResourceContext() const { +fml::WeakPtr UIDartState::GetResourceContext() const { if (!io_manager_) { return {}; } diff --git a/lib/ui/ui_dart_state.h b/lib/ui/ui_dart_state.h index fd21146d24dd4..71755931a30d1 100644 --- a/lib/ui/ui_dart_state.h +++ b/lib/ui/ui_dart_state.h @@ -27,7 +27,7 @@ namespace flutter { class FontSelector; -class Window; +class PlatformConfiguration; class UIDartState : public tonic::DartState { public: @@ -44,7 +44,9 @@ class UIDartState : public tonic::DartState { const std::string& logger_prefix() const { return logger_prefix_; } - Window* window() const { return window_.get(); } + PlatformConfiguration* platform_configuration() const { + return platform_configuration_.get(); + } const TaskRunners& GetTaskRunners() const; @@ -58,7 +60,7 @@ class UIDartState : public tonic::DartState { fml::WeakPtr GetSnapshotDelegate() const; - fml::WeakPtr GetResourceContext() const; + fml::WeakPtr GetResourceContext() const; fml::WeakPtr GetImageDecoder() const; @@ -97,7 +99,8 @@ class UIDartState : public tonic::DartState { ~UIDartState() override; - void SetWindow(std::unique_ptr window); + void SetPlatformConfiguration( + std::unique_ptr platform_configuration); const std::string& GetAdvisoryScriptURI() const; @@ -119,7 +122,7 @@ class UIDartState : public tonic::DartState { Dart_Port main_port_ = ILLEGAL_PORT; const bool is_root_isolate_; std::string debug_name_; - std::unique_ptr window_; + std::unique_ptr platform_configuration_; tonic::DartMicrotaskQueue microtask_queue_; UnhandledExceptionCallback unhandled_exception_callback_; const std::shared_ptr isolate_name_server_; diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 5d99e42781fd6..a39c5407ad45e 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -47,6 +47,11 @@ typedef _SetNeedsReportTimingsFunc = void Function(bool value); /// /// [FrameTiming] records a timestamp of each phase for performance analysis. enum FramePhase { + /// The timestamp of the vsync signal given by the operating system. + /// + /// See also [FrameTiming.vsyncOverhead]. + vsyncStart, + /// When the UI thread starts building a frame. /// /// See also [FrameTiming.buildDuration]. @@ -82,6 +87,26 @@ enum FramePhase { /// Therefore it's recommended to only monitor and analyze performance metrics /// in profile and release modes. class FrameTiming { + /// Construct [FrameTiming] with raw timestamps in microseconds. + /// + /// This constructor is used for unit test only. Real [FrameTiming]s should + /// be retrieved from [Window.onReportTimings]. + factory FrameTiming({ + required int vsyncStart, + required int buildStart, + required int buildFinish, + required int rasterStart, + required int rasterFinish, + }) { + return FrameTiming._([ + vsyncStart, + buildStart, + buildFinish, + rasterStart, + rasterFinish + ]); + } + /// Construct [FrameTiming] with raw timestamps in microseconds. /// /// List [timestamps] must have the same number of elements as @@ -89,7 +114,7 @@ class FrameTiming { /// /// This constructor is usually only called by the Flutter engine, or a test. /// To get the [FrameTiming] of your app, see [Window.onReportTimings]. - FrameTiming(List timestamps) + FrameTiming._(List timestamps) : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; /// This is a raw timestamp in microseconds from some epoch. The epoch in all @@ -121,14 +146,18 @@ class FrameTiming { /// {@macro dart.ui.FrameTiming.fps_milliseconds} Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart); - /// The timespan between build start and raster finish. + /// The duration between receiving the vsync signal and starting building the + /// frame. + Duration get vsyncOverhead => _rawDuration(FramePhase.buildStart) - _rawDuration(FramePhase.vsyncStart); + + /// The timespan between vsync start and raster finish. /// /// To achieve the lowest latency on an X fps display, this should not exceed /// 1000/X milliseconds. /// {@macro dart.ui.FrameTiming.fps_milliseconds} /// - /// See also [buildDuration] and [rasterDuration]. - Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.buildStart); + /// See also [vsyncOverhead], [buildDuration] and [rasterDuration]. + Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.vsyncStart); final List _timestamps; // in microseconds @@ -136,7 +165,7 @@ class FrameTiming { @override String toString() { - return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, totalSpan: ${_formatMS(totalSpan)})'; + return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, vsyncOverhead: ${_formatMS(vsyncOverhead)}, totalSpan: ${_formatMS(totalSpan)})'; } } @@ -627,20 +656,6 @@ class Window { Size get physicalSize => _physicalSize; Size _physicalSize = Size.zero; - /// The physical depth is the maximum elevation that the Window allows. - /// - /// Physical layers drawn at or above this elevation will have their elevation - /// clamped to this value. This can happen if the physical layer itself has - /// an elevation larger than available depth, or if some ancestor of the layer - /// causes it to have a cumulative elevation that is larger than the available - /// depth. - /// - /// The default value is [double.maxFinite], which is used for platforms that - /// do not specify a maximum elevation. This property is currently on expected - /// to be set to a non-default value on Fuchsia. - double get physicalDepth => _physicalDepth; - double _physicalDepth = double.maxFinite; - /// The number of physical pixels on each side of the display rectangle into /// which the application can render, but over which the operating system /// will likely place system UI, such as the keyboard, that fully obscures @@ -823,7 +838,7 @@ class Window { } return null; } - List _computePlatformResolvedLocale(List supportedLocalesData) native 'Window_computePlatformResolvedLocale'; + List _computePlatformResolvedLocale(List supportedLocalesData) native 'PlatformConfiguration_computePlatformResolvedLocale'; /// A callback that is invoked whenever [locale] changes value. /// @@ -1001,7 +1016,7 @@ class Window { } late _SetNeedsReportTimingsFunc _setNeedsReportTimings; - void _nativeSetNeedsReportTimings(bool value) native 'Window_setNeedsReportTimings'; + void _nativeSetNeedsReportTimings(bool value) native 'PlatformConfiguration_setNeedsReportTimings'; /// A callback that is invoked when pointer data is available. /// @@ -1027,23 +1042,19 @@ class Window { /// /// ## Android /// - /// On Android, calling - /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `createFlutterView` method in your `FlutterActivity` - /// subclass is a suitable time to set the value. The application's - /// `AndroidManifest.xml` file must also be updated to have a suitable - /// [``](https://developer.android.com/guide/topics/manifest/intent-filter-element.html). + /// On Android, the initial route can be set on the [initialRoute](/javadoc/io/flutter/embedding/android/FlutterActivity.NewEngineIntentBuilder.html#initialRoute-java.lang.String-) + /// method of the [FlutterActivity](/javadoc/io/flutter/embedding/android/FlutterActivity.html)'s + /// intent builder. + /// + /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen#initial-route-with-a-cached-engine. /// /// ## iOS /// - /// On iOS, calling - /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `application:didFinishLaunchingWithOptions:` method is a - /// suitable time to set this value. + /// On iOS, the initial route can be set on the `initialRoute` + /// parameter of the [FlutterViewController](/objcdoc/Classes/FlutterViewController.html)'s + /// initializer. + /// + /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/ios/add-flutter-screen#route. /// /// See also: /// @@ -1051,7 +1062,7 @@ class Window { /// * [SystemChannels.navigation], which handles subsequent navigation /// requests from the embedder. String get defaultRouteName => _defaultRouteName(); - String _defaultRouteName() native 'Window_defaultRouteName'; + String _defaultRouteName() native 'PlatformConfiguration_defaultRouteName'; /// Requests that, at the next appropriate opportunity, the [onBeginFrame] /// and [onDrawFrame] callbacks be invoked. @@ -1060,7 +1071,7 @@ class Window { /// /// * [SchedulerBinding], the Flutter framework class which manages the /// scheduling of frames. - void scheduleFrame() native 'Window_scheduleFrame'; + void scheduleFrame() native 'PlatformConfiguration_scheduleFrame'; /// Updates the application's rendering on the GPU with the newly provided /// [Scene]. This function must be called within the scope of the @@ -1086,7 +1097,7 @@ class Window { /// scheduling of frames. /// * [RendererBinding], the Flutter framework class which manages layout and /// painting. - void render(Scene scene) native 'Window_render'; + void render(Scene scene) native 'PlatformConfiguration_render'; /// Whether the user has requested that [updateSemantics] be called when /// the semantic contents of window changes. @@ -1126,7 +1137,7 @@ class Window { /// Additional accessibility features that may be enabled by the platform. AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; - // The zero value matches the default value in `window_data.h`. + // The zero value matches the default value in `platform_data.h`. AccessibilityFeatures _accessibilityFeatures = const AccessibilityFeatures._(0); /// A callback that is invoked when the value of [accessibilityFeatures] changes. @@ -1148,7 +1159,7 @@ class Window { /// /// In either case, this function disposes the given update, which means the /// semantics update cannot be used further. - void updateSemantics(SemanticsUpdate update) native 'Window_updateSemantics'; + void updateSemantics(SemanticsUpdate update) native 'PlatformConfiguration_updateSemantics'; /// Set the debug name associated with this window's root isolate. /// @@ -1158,7 +1169,7 @@ class Window { /// This can be combined with flutter tools `--isolate-filter` flag to debug /// specific root isolates. For example: `flutter attach --isolate-filter=[name]`. /// Note that this does not rename any child isolates of the root. - void setIsolateDebugName(String name) native 'Window_setIsolateDebugName'; + void setIsolateDebugName(String name) native 'PlatformConfiguration_setIsolateDebugName'; /// Sends a message to a platform-specific plugin. /// @@ -1179,7 +1190,7 @@ class Window { } String? _sendPlatformMessage(String name, PlatformMessageResponseCallback? callback, - ByteData? data) native 'Window_sendPlatformMessage'; + ByteData? data) native 'PlatformConfiguration_sendPlatformMessage'; /// Called whenever this window receives a message from a platform-specific /// plugin. @@ -1204,7 +1215,7 @@ class Window { /// Called by [_dispatchPlatformMessage]. void _respondToPlatformMessage(int responseId, ByteData? data) - native 'Window_respondToPlatformMessage'; + native 'PlatformConfiguration_respondToPlatformMessage'; /// Wraps the given [callback] in another callback that ensures that the /// original callback is called in the zone it was registered in. @@ -1230,7 +1241,7 @@ class Window { /// /// For asynchronous communication between the embedder and isolate, a /// platform channel may be used. - ByteData? getPersistentIsolateData() native 'Window_getPersistentIsolateData'; + ByteData? getPersistentIsolateData() native 'PlatformConfiguration_getPersistentIsolateData'; } /// Additional accessibility features that may be enabled by the platform. diff --git a/lib/ui/window/platform_configuration.cc b/lib/ui/window/platform_configuration.cc new file mode 100644 index 0000000000000..3ce180e3bce71 --- /dev/null +++ b/lib/ui/window/platform_configuration.cc @@ -0,0 +1,437 @@ +// 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. + +#include "flutter/lib/ui/window/platform_configuration.h" + +#include "flutter/lib/ui/compositing/scene.h" +#include "flutter/lib/ui/ui_dart_state.h" +#include "flutter/lib/ui/window/platform_message_response_dart.h" +#include "flutter/lib/ui/window/window.h" +#include "third_party/tonic/converter/dart_converter.h" +#include "third_party/tonic/dart_args.h" +#include "third_party/tonic/dart_library_natives.h" +#include "third_party/tonic/dart_microtask_queue.h" +#include "third_party/tonic/logging/dart_invoke.h" +#include "third_party/tonic/typed_data/dart_byte_data.h" + +namespace flutter { +namespace { + +void DefaultRouteName(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + std::string routeName = UIDartState::Current() + ->platform_configuration() + ->client() + ->DefaultRouteName(); + Dart_SetReturnValue(args, tonic::StdStringToDart(routeName)); +} + +void ScheduleFrame(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + UIDartState::Current()->platform_configuration()->client()->ScheduleFrame(); +} + +void Render(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + Scene* scene = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + UIDartState::Current()->platform_configuration()->client()->Render(scene); +} + +void UpdateSemantics(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + SemanticsUpdate* update = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + UIDartState::Current()->platform_configuration()->client()->UpdateSemantics( + update); +} + +void SetIsolateDebugName(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + const std::string name = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + UIDartState::Current()->SetDebugName(name); +} + +void SetNeedsReportTimings(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle exception = nullptr; + bool value = tonic::DartConverter::FromArguments(args, 1, exception); + UIDartState::Current() + ->platform_configuration() + ->client() + ->SetNeedsReportTimings(value); +} + +void ReportUnhandledException(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + + Dart_Handle exception = nullptr; + + auto error_name = + tonic::DartConverter::FromArguments(args, 0, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + + auto stack_trace = + tonic::DartConverter::FromArguments(args, 1, exception); + if (exception) { + Dart_ThrowException(exception); + return; + } + + UIDartState::Current()->ReportUnhandledException(std::move(error_name), + std::move(stack_trace)); +} + +Dart_Handle SendPlatformMessage(Dart_Handle window, + const std::string& name, + Dart_Handle callback, + Dart_Handle data_handle) { + UIDartState* dart_state = UIDartState::Current(); + + if (!dart_state->platform_configuration()) { + return tonic::ToDart( + "Platform messages can only be sent from the main isolate"); + } + + fml::RefPtr response; + if (!Dart_IsNull(callback)) { + response = fml::MakeRefCounted( + tonic::DartPersistentValue(dart_state, callback), + dart_state->GetTaskRunners().GetUITaskRunner()); + } + if (Dart_IsNull(data_handle)) { + dart_state->platform_configuration()->client()->HandlePlatformMessage( + fml::MakeRefCounted(name, response)); + } else { + tonic::DartByteData data(data_handle); + const uint8_t* buffer = static_cast(data.data()); + dart_state->platform_configuration()->client()->HandlePlatformMessage( + fml::MakeRefCounted( + name, std::vector(buffer, buffer + data.length_in_bytes()), + response)); + } + + return Dart_Null(); +} + +void _SendPlatformMessage(Dart_NativeArguments args) { + tonic::DartCallStatic(&SendPlatformMessage, args); +} + +void RespondToPlatformMessage(Dart_Handle window, + int response_id, + const tonic::DartByteData& data) { + if (Dart_IsNull(data.dart_handle())) { + UIDartState::Current() + ->platform_configuration() + ->CompletePlatformMessageEmptyResponse(response_id); + } else { + // TODO(engine): Avoid this copy. + const uint8_t* buffer = static_cast(data.data()); + UIDartState::Current() + ->platform_configuration() + ->CompletePlatformMessageResponse( + response_id, + std::vector(buffer, buffer + data.length_in_bytes())); + } +} + +void _RespondToPlatformMessage(Dart_NativeArguments args) { + tonic::DartCallStatic(&RespondToPlatformMessage, args); +} + +void GetPersistentIsolateData(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + + auto persistent_isolate_data = UIDartState::Current() + ->platform_configuration() + ->client() + ->GetPersistentIsolateData(); + + if (!persistent_isolate_data) { + Dart_SetReturnValue(args, Dart_Null()); + return; + } + + Dart_SetReturnValue( + args, tonic::DartByteData::Create(persistent_isolate_data->GetMapping(), + persistent_isolate_data->GetSize())); +} + +Dart_Handle ToByteData(const std::vector& buffer) { + return tonic::DartByteData::Create(buffer.data(), buffer.size()); +} + +} // namespace + +PlatformConfigurationClient::~PlatformConfigurationClient() {} + +PlatformConfiguration::PlatformConfiguration( + PlatformConfigurationClient* client) + : client_(client) {} + +PlatformConfiguration::~PlatformConfiguration() {} + +void PlatformConfiguration::DidCreateIsolate() { + library_.Set(tonic::DartState::Current(), + Dart_LookupLibrary(tonic::ToDart("dart:ui"))); + window_.reset(new Window({1.0, 0.0, 0.0})); +} + +void PlatformConfiguration::UpdateLocales( + const std::vector& locales) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_updateLocales", + { + tonic::ToDart>(locales), + })); +} + +void PlatformConfiguration::UpdateUserSettingsData(const std::string& data) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), + "_updateUserSettingsData", + { + tonic::StdStringToDart(data), + })); +} + +void PlatformConfiguration::UpdateLifecycleState(const std::string& data) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + tonic::LogIfError(tonic::DartInvokeField(library_.value(), + "_updateLifecycleState", + { + tonic::StdStringToDart(data), + })); +} + +void PlatformConfiguration::UpdateSemanticsEnabled(bool enabled) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + UIDartState::ThrowIfUIOperationsProhibited(); + + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_updateSemanticsEnabled", {tonic::ToDart(enabled)})); +} + +void PlatformConfiguration::UpdateAccessibilityFeatures(int32_t values) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), + "_updateAccessibilityFeatures", + {tonic::ToDart(values)})); +} + +void PlatformConfiguration::DispatchPlatformMessage( + fml::RefPtr message) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + FML_DLOG(WARNING) + << "Dropping platform message for lack of DartState on channel: " + << message->channel(); + return; + } + tonic::DartState::Scope scope(dart_state); + Dart_Handle data_handle = + (message->hasData()) ? ToByteData(message->data()) : Dart_Null(); + if (Dart_IsError(data_handle)) { + FML_DLOG(WARNING) + << "Dropping platform message because of a Dart error on channel: " + << message->channel(); + return; + } + + int response_id = 0; + if (auto response = message->response()) { + response_id = next_response_id_++; + pending_responses_[response_id] = response; + } + + tonic::LogIfError( + tonic::DartInvokeField(library_.value(), "_dispatchPlatformMessage", + {tonic::ToDart(message->channel()), data_handle, + tonic::ToDart(response_id)})); +} + +void PlatformConfiguration::DispatchSemanticsAction(int32_t id, + SemanticsAction action, + std::vector args) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + Dart_Handle args_handle = (args.empty()) ? Dart_Null() : ToByteData(args); + + if (Dart_IsError(args_handle)) { + return; + } + + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_dispatchSemanticsAction", + {tonic::ToDart(id), tonic::ToDart(static_cast(action)), + args_handle})); +} + +void PlatformConfiguration::BeginFrame(fml::TimePoint frameTime) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + int64_t microseconds = (frameTime - fml::TimePoint()).ToMicroseconds(); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_beginFrame", + { + Dart_NewInteger(microseconds), + })); + + UIDartState::Current()->FlushMicrotasksNow(); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_drawFrame", {})); +} + +void PlatformConfiguration::ReportTimings(std::vector timings) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { + return; + } + tonic::DartState::Scope scope(dart_state); + + Dart_Handle data_handle = + Dart_NewTypedData(Dart_TypedData_kInt64, timings.size()); + + Dart_TypedData_Type type; + void* data = nullptr; + intptr_t num_acquired = 0; + FML_CHECK(!Dart_IsError( + Dart_TypedDataAcquireData(data_handle, &type, &data, &num_acquired))); + FML_DCHECK(num_acquired == static_cast(timings.size())); + + memcpy(data, timings.data(), sizeof(int64_t) * timings.size()); + FML_CHECK(Dart_TypedDataReleaseData(data_handle)); + + tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_reportTimings", + { + data_handle, + })); +} + +void PlatformConfiguration::CompletePlatformMessageEmptyResponse( + int response_id) { + if (!response_id) { + return; + } + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) { + return; + } + auto response = std::move(it->second); + pending_responses_.erase(it); + response->CompleteEmpty(); +} + +void PlatformConfiguration::CompletePlatformMessageResponse( + int response_id, + std::vector data) { + if (!response_id) { + return; + } + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) { + return; + } + auto response = std::move(it->second); + pending_responses_.erase(it); + response->Complete(std::make_unique(std::move(data))); +} + +Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) { + std::vector supportedLocales = + tonic::DartConverter>::FromDart( + supportedLocalesHandle); + + std::vector results = + *UIDartState::Current() + ->platform_configuration() + ->client() + ->ComputePlatformResolvedLocale(supportedLocales); + + return tonic::DartConverter>::ToDart(results); +} + +static void _ComputePlatformResolvedLocale(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle result = + ComputePlatformResolvedLocale(Dart_GetNativeArgument(args, 1)); + Dart_SetReturnValue(args, result); +} + +void PlatformConfiguration::RegisterNatives( + tonic::DartLibraryNatives* natives) { + natives->Register({ + {"PlatformConfiguration_defaultRouteName", DefaultRouteName, 1, true}, + {"PlatformConfiguration_scheduleFrame", ScheduleFrame, 1, true}, + {"PlatformConfiguration_sendPlatformMessage", _SendPlatformMessage, 4, + true}, + {"PlatformConfiguration_respondToPlatformMessage", + _RespondToPlatformMessage, 3, true}, + {"PlatformConfiguration_render", Render, 2, true}, + {"PlatformConfiguration_updateSemantics", UpdateSemantics, 2, true}, + {"PlatformConfiguration_setIsolateDebugName", SetIsolateDebugName, 2, + true}, + {"PlatformConfiguration_reportUnhandledException", + ReportUnhandledException, 2, true}, + {"PlatformConfiguration_setNeedsReportTimings", SetNeedsReportTimings, 2, + true}, + {"PlatformConfiguration_getPersistentIsolateData", + GetPersistentIsolateData, 1, true}, + {"PlatformConfiguration_computePlatformResolvedLocale", + _ComputePlatformResolvedLocale, 2, true}, + }); +} + +} // namespace flutter diff --git a/lib/ui/window/platform_configuration.h b/lib/ui/window/platform_configuration.h new file mode 100644 index 0000000000000..4652c7c5dcdbb --- /dev/null +++ b/lib/ui/window/platform_configuration.h @@ -0,0 +1,421 @@ +// 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. + +#ifndef FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ +#define FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ + +#include +#include +#include +#include +#include + +#include "flutter/fml/time/time_point.h" +#include "flutter/lib/ui/semantics/semantics_update.h" +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/lib/ui/window/pointer_data_packet.h" +#include "flutter/lib/ui/window/viewport_metrics.h" +#include "flutter/lib/ui/window/window.h" +#include "third_party/tonic/dart_persistent_value.h" + +namespace tonic { +class DartLibraryNatives; + +// So tonic::ToDart> returns List instead of +// List. +template <> +struct DartListFactory { + static Dart_Handle NewList(intptr_t length) { + return Dart_NewListOf(Dart_CoreType_Int, length); + } +}; + +} // namespace tonic + +namespace flutter { +class FontCollection; +class PlatformMessage; +class Scene; + +//-------------------------------------------------------------------------- +/// @brief An enum for defining the different kinds of accessibility features +/// that can be enabled by the platform. +/// +/// Must match the `AccessibilityFeatureFlag` enum in framework. +enum class AccessibilityFeatureFlag : int32_t { + kAccessibleNavigation = 1 << 0, + kInvertColors = 1 << 1, + kDisableAnimations = 1 << 2, + kBoldText = 1 << 3, + kReduceMotion = 1 << 4, + kHighContrast = 1 << 5, +}; + +//-------------------------------------------------------------------------- +/// @brief A client interface that the `RuntimeController` uses to define +/// handlers for `PlatformConfiguration` requests. +/// +/// @see `PlatformConfiguration` +/// +class PlatformConfigurationClient { + public: + //-------------------------------------------------------------------------- + /// @brief The route or path that the embedder requested when the + /// application was launched. + /// + /// This will be the string "`/`" if no particular route was + /// requested. + /// + virtual std::string DefaultRouteName() = 0; + + //-------------------------------------------------------------------------- + /// @brief Requests that, at the next appropriate opportunity, a new + /// frame be scheduled for rendering. + /// + virtual void ScheduleFrame() = 0; + + //-------------------------------------------------------------------------- + /// @brief Updates the client's rendering on the GPU with the newly + /// provided Scene. + /// + virtual void Render(Scene* scene) = 0; + + //-------------------------------------------------------------------------- + /// @brief Receives a updated semantics tree from the Framework. + /// + /// @param[in] update The updated semantic tree to apply. + /// + virtual void UpdateSemantics(SemanticsUpdate* update) = 0; + + //-------------------------------------------------------------------------- + /// @brief When the Flutter application has a message to send to the + /// underlying platform, the message needs to be forwarded to + /// the platform on the appropriate thread (via the platform + /// task runner). The PlatformConfiguration delegates this task + /// to the engine via this method. + /// + /// @see `PlatformView::HandlePlatformMessage` + /// + /// @param[in] message The message from the Flutter application to send to + /// the underlying platform. + /// + virtual void HandlePlatformMessage(fml::RefPtr message) = 0; + + //-------------------------------------------------------------------------- + /// @brief Returns the current collection of fonts available on the + /// platform. + /// + /// This function reads an XML file and makes font families and + /// collections of them. MinikinFontForTest is used for FontFamily + /// creation. + virtual FontCollection& GetFontCollection() = 0; + + //-------------------------------------------------------------------------- + /// @brief Notifies this client of the name of the root isolate and its + /// port when that isolate is launched, restarted (in the + /// cold-restart scenario) or the application itself updates the + /// name of the root isolate (via `Window.setIsolateDebugName` + /// in `window.dart`). The name of the isolate is meaningless to + /// the engine but is used in instrumentation and tooling. + /// Currently, this information is to update the service + /// protocol list of available root isolates running in the VM + /// and their names so that the appropriate isolate can be + /// selected in the tools for debugging and instrumentation. + /// + /// @param[in] isolate_name The isolate name + /// @param[in] isolate_port The isolate port + /// + virtual void UpdateIsolateDescription(const std::string isolate_name, + int64_t isolate_port) = 0; + + //-------------------------------------------------------------------------- + /// @brief Notifies this client that the application has an opinion about + /// whether its frame timings need to be reported backed to it. + /// Due to the asynchronous nature of rendering in Flutter, it is + /// not possible for the application to determine the total time + /// it took to render a specific frame. While the layer-tree is + /// constructed on the UI thread, it needs to be rendering on the + /// raster thread. Dart code cannot execute on this thread. So any + /// instrumentation about the frame times gathered on this thread + /// needs to be aggregated and sent back to the UI thread for + /// processing in Dart. + /// + /// When the application indicates that frame times need to be + /// reported, it collects this information till a specified number + /// of data points are gathered. Then this information is sent + /// back to Dart code via `Engine::ReportTimings`. + /// + /// This option is engine counterpart of the + /// `Window._setNeedsReportTimings` in `window.dart`. + /// + /// @param[in] needs_reporting If reporting information should be collected + /// and send back to Dart. + /// + virtual void SetNeedsReportTimings(bool value) = 0; + + //-------------------------------------------------------------------------- + /// @brief The embedder can specify data that the isolate can request + /// synchronously on launch. This accessor fetches that data. + /// + /// This data is persistent for the duration of the Flutter + /// application and is available even after isolate restarts. + /// Because of this lifecycle, the size of this data must be kept + /// to a minimum. + /// + /// For asynchronous communication between the embedder and + /// isolate, a platform channel may be used. + /// + /// @return A map of the isolate data that the framework can request upon + /// launch. + /// + virtual std::shared_ptr GetPersistentIsolateData() = 0; + + //-------------------------------------------------------------------------- + /// @brief Directly invokes platform-specific APIs to compute the + /// locale the platform would have natively resolved to. + /// + /// @param[in] supported_locale_data The vector of strings that represents + /// the locales supported by the app. + /// Each locale consists of three + /// strings: languageCode, countryCode, + /// and scriptCode in that order. + /// + /// @return A vector of 3 strings languageCode, countryCode, and + /// scriptCode that represents the locale selected by the + /// platform. Empty strings mean the value was unassigned. Empty + /// vector represents a null locale. + /// + virtual std::unique_ptr> + ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) = 0; + + protected: + virtual ~PlatformConfigurationClient(); +}; + +//---------------------------------------------------------------------------- +/// @brief A class for holding and distributing platform-level information +/// to and from the Dart code in Flutter's framework. +/// +/// It handles communication between the engine and the framework, +/// and owns the main window. +/// +/// It communicates with the RuntimeController through the use of a +/// PlatformConfigurationClient interface, which the +/// RuntimeController defines. +/// +class PlatformConfiguration final { + public: + //---------------------------------------------------------------------------- + /// @brief Creates a new PlatformConfiguration, typically created by the + /// RuntimeController. + /// + /// @param[in] client The `PlatformConfigurationClient` to be injected into + /// the PlatformConfiguration. This client is used to + /// forward requests to the RuntimeController. + /// + explicit PlatformConfiguration(PlatformConfigurationClient* client); + + // PlatformConfiguration is not copyable. + PlatformConfiguration(const PlatformConfiguration&) = delete; + PlatformConfiguration& operator=(const PlatformConfiguration&) = delete; + + ~PlatformConfiguration(); + + //---------------------------------------------------------------------------- + /// @brief Access to the platform configuration client (which typically + /// is implemented by the RuntimeController). + /// + /// @return Returns the client used to construct this + /// PlatformConfiguration. + /// + PlatformConfigurationClient* client() const { return client_; } + + //---------------------------------------------------------------------------- + /// @brief Called by the RuntimeController once it has created the root + /// isolate, so that the PlatformController can get a handle to + /// the 'dart:ui' library. + /// + /// It uses the handle to call the hooks in hooks.dart. + /// + void DidCreateIsolate(); + + //---------------------------------------------------------------------------- + /// @brief Update the specified locale data in the framework. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] locale_data The locale data. This should consist of groups of + /// 4 strings, each group representing a single locale. + /// + void UpdateLocales(const std::vector& locales); + + //---------------------------------------------------------------------------- + /// @brief Update the user settings data in the framework. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] data The user settings data. + /// + void UpdateUserSettingsData(const std::string& data); + + //---------------------------------------------------------------------------- + /// @brief Updates the lifecycle state data in the framework. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] data The lifecycle state data. + /// + void UpdateLifecycleState(const std::string& data); + + //---------------------------------------------------------------------------- + /// @brief Notifies the PlatformConfiguration that the embedder has + /// expressed an opinion about whether the accessibility tree + /// should be generated or not. This call originates in the + /// platform view and is forwarded to the PlatformConfiguration + /// here by the engine. + /// + /// @param[in] enabled Whether the accessibility tree is enabled or + /// disabled. + /// + void UpdateSemanticsEnabled(bool enabled); + + //---------------------------------------------------------------------------- + /// @brief Forward the preference of accessibility features that must be + /// enabled in the semantics tree to the framwork. + /// + /// @param[in] flags The accessibility features that must be generated in + /// the semantics tree. + /// + void UpdateAccessibilityFeatures(int32_t flags); + + //---------------------------------------------------------------------------- + /// @brief Notifies the PlatformConfiguration that the client has sent + /// it a message. This call originates in the platform view and + /// has been forwarded through the engine to here. + /// + /// @param[in] message The message sent from the embedder to the Dart + /// application. + /// + void DispatchPlatformMessage(fml::RefPtr message); + + //---------------------------------------------------------------------------- + /// @brief Notifies the framework that the embedder encountered an + /// accessibility related action on the specified node. This call + /// originates on the platform view and has been forwarded to the + /// platform configuration here by the engine. + /// + /// @param[in] id The identifier of the accessibility node. + /// @param[in] action The accessibility related action performed on the + /// node of the specified ID. + /// @param[in] args Optional data that applies to the specified action. + /// + void DispatchSemanticsAction(int32_t id, + SemanticsAction action, + std::vector args); + + //---------------------------------------------------------------------------- + /// @brief Notifies the framework that it is time to begin working on a + /// new + /// frame previously scheduled via a call to + /// `PlatformConfigurationClient::ScheduleFrame`. This call + /// originates in the animator. + /// + /// The frame time given as the argument indicates the point at + /// which the current frame interval began. It is very slightly + /// (because of scheduling overhead) in the past. If a new layer + /// tree is not produced and given to the GPU task runner within + /// one frame interval from this point, the Flutter application + /// will jank. + /// + /// This method calls the `::_beginFrame` method in `hooks.dart`. + /// + /// @param[in] frame_time The point at which the current frame interval + /// began. May be used by animation interpolators, + /// physics simulations, etc.. + /// + void BeginFrame(fml::TimePoint frame_time); + + //---------------------------------------------------------------------------- + /// @brief Dart code cannot fully measure the time it takes for a + /// specific frame to be rendered. This is because Dart code only + /// runs on the UI task runner. That is only a small part of the + /// overall frame workload. The GPU task runner frame workload is + /// executed on a thread where Dart code cannot run (and hence + /// instrument). Besides, due to the pipelined nature of rendering + /// in Flutter, there may be multiple frame workloads being + /// processed at any given time. However, for non-Timeline based + /// profiling, it is useful for trace collection and processing to + /// happen in Dart. To do this, the GPU task runner frame + /// workloads need to be instrumented separately. After a set + /// number of these profiles have been gathered, they need to be + /// reported back to Dart code. The engine reports this extra + /// instrumentation information back to the framework by invoking + /// this method at predefined intervals. + /// + /// @see `FrameTiming` + /// + /// @param[in] timings Collection of `FrameTiming::kCount` * `n` timestamps + /// for `n` frames whose timings have not been reported + /// yet. A collection of integers is reported here for + /// easier conversions to Dart objects. The timestamps + /// are measured against the system monotonic clock + /// measured in microseconds. + /// + void ReportTimings(std::vector timings); + + //---------------------------------------------------------------------------- + /// @brief Registers the native handlers for Dart functions that this + /// class handles. + /// + /// @param[in] natives The natives registry that the functions will be + /// registered with. + /// + static void RegisterNatives(tonic::DartLibraryNatives* natives); + + //---------------------------------------------------------------------------- + /// @brief Retrieves the Window managed by the PlatformConfiguration. + /// + /// @return a pointer to the Window. + /// + Window* window() const { return window_.get(); } + + //---------------------------------------------------------------------------- + /// @brief Responds to a previous platform message to the engine from the + /// framework. + /// + /// @param[in] response_id The unique id that identifies the original platform + /// message to respond to. + /// @param[in] data The data to send back in the response. + /// + void CompletePlatformMessageResponse(int response_id, + std::vector data); + + //---------------------------------------------------------------------------- + /// @brief Responds to a previous platform message to the engine from the + /// framework with an empty response. + /// + /// @param[in] response_id The unique id that identifies the original platform + /// message to respond to. + /// + void CompletePlatformMessageEmptyResponse(int response_id); + + private: + PlatformConfigurationClient* client_; + tonic::DartPersistentValue library_; + + std::unique_ptr window_; + + // We use id 0 to mean that no response is expected. + int next_response_id_ = 1; + std::unordered_map> + pending_responses_; +}; + +} // namespace flutter + +#endif // FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ diff --git a/lib/ui/window/platform_configuration_unittests.cc b/lib/ui/window/platform_configuration_unittests.cc new file mode 100644 index 0000000000000..903b6d748e2dc --- /dev/null +++ b/lib/ui/window/platform_configuration_unittests.cc @@ -0,0 +1,139 @@ +// 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. + +#define FML_USED_ON_EMBEDDER + +#include + +#include "flutter/lib/ui/window/platform_configuration.h" + +#include "flutter/common/task_runners.h" +#include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/lib/ui/painting/vertices.h" +#include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/shell_test.h" +#include "flutter/shell/common/thread_host.h" +#include "flutter/testing/testing.h" + +namespace flutter { +namespace testing { + +class DummyPlatformConfigurationClient : public PlatformConfigurationClient { + public: + DummyPlatformConfigurationClient() { + std::vector data; + isolate_data_.reset(new ::fml::DataMapping(data)); + } + std::string DefaultRouteName() override { return "TestRoute"; } + void ScheduleFrame() override {} + void Render(Scene* scene) override {} + void UpdateSemantics(SemanticsUpdate* update) override {} + void HandlePlatformMessage(fml::RefPtr message) override {} + FontCollection& GetFontCollection() override { return font_collection_; } + void UpdateIsolateDescription(const std::string isolate_name, + int64_t isolate_port) override {} + void SetNeedsReportTimings(bool value) override {} + std::shared_ptr GetPersistentIsolateData() override { + return isolate_data_; + } + std::unique_ptr> ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) override { + return nullptr; + }; + + private: + FontCollection font_collection_; + std::shared_ptr isolate_data_; +}; + +TEST_F(ShellTest, PlatformConfigurationInitialization) { + auto message_latch = std::make_shared(); + + auto nativeValidateConfiguration = [message_latch]( + Dart_NativeArguments args) { + PlatformConfiguration* configuration = + UIDartState::Current()->platform_configuration(); + ASSERT_NE(configuration->window(), nullptr); + ASSERT_EQ(configuration->window()->viewport_metrics().device_pixel_ratio, + 1.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_width, 0.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_height, 0.0); + + message_latch->Signal(); + }; + + Settings settings = CreateSettingsForFixture(); + TaskRunners task_runners("test", // label + GetCurrentTaskRunner(), // platform + CreateNewThread(), // raster + CreateNewThread(), // ui + CreateNewThread() // io + ); + + AddNativeCallback("ValidateConfiguration", + CREATE_NATIVE_ENTRY(nativeValidateConfiguration)); + + std::unique_ptr shell = + CreateShell(std::move(settings), std::move(task_runners)); + + ASSERT_TRUE(shell->IsSetup()); + auto run_configuration = RunConfiguration::InferFromSettings(settings); + run_configuration.SetEntrypoint("validateConfiguration"); + + shell->RunEngine(std::move(run_configuration), [&](auto result) { + ASSERT_EQ(result, Engine::RunStatus::Success); + }); + + message_latch->Wait(); + DestroyShell(std::move(shell), std::move(task_runners)); +} + +TEST_F(ShellTest, PlatformConfigurationWindowMetricsUpdate) { + auto message_latch = std::make_shared(); + + auto nativeValidateConfiguration = [message_latch]( + Dart_NativeArguments args) { + PlatformConfiguration* configuration = + UIDartState::Current()->platform_configuration(); + + ASSERT_NE(configuration->window(), nullptr); + configuration->window()->UpdateWindowMetrics( + ViewportMetrics{2.0, 10.0, 20.0}); + ASSERT_EQ(configuration->window()->viewport_metrics().device_pixel_ratio, + 2.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_width, 10.0); + ASSERT_EQ(configuration->window()->viewport_metrics().physical_height, + 20.0); + + message_latch->Signal(); + }; + + Settings settings = CreateSettingsForFixture(); + TaskRunners task_runners("test", // label + GetCurrentTaskRunner(), // platform + CreateNewThread(), // raster + CreateNewThread(), // ui + CreateNewThread() // io + ); + + AddNativeCallback("ValidateConfiguration", + CREATE_NATIVE_ENTRY(nativeValidateConfiguration)); + + std::unique_ptr shell = + CreateShell(std::move(settings), std::move(task_runners)); + + ASSERT_TRUE(shell->IsSetup()); + auto run_configuration = RunConfiguration::InferFromSettings(settings); + run_configuration.SetEntrypoint("validateConfiguration"); + + shell->RunEngine(std::move(run_configuration), [&](auto result) { + ASSERT_EQ(result, Engine::RunStatus::Success); + }); + + message_latch->Wait(); + DestroyShell(std::move(shell), std::move(task_runners)); +} + +} // namespace testing +} // namespace flutter diff --git a/lib/ui/window/viewport_metrics.cc b/lib/ui/window/viewport_metrics.cc index 0b6dab6d4c1e0..f642bd116966f 100644 --- a/lib/ui/window/viewport_metrics.cc +++ b/lib/ui/window/viewport_metrics.cc @@ -8,6 +8,8 @@ namespace flutter { +ViewportMetrics::ViewportMetrics() = default; + ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, double p_physical_height, @@ -48,32 +50,10 @@ ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, - double p_physical_height, - double p_physical_depth, - double p_physical_padding_top, - double p_physical_padding_right, - double p_physical_padding_bottom, - double p_physical_padding_left, - double p_physical_view_inset_front, - double p_physical_view_inset_back, - double p_physical_view_inset_top, - double p_physical_view_inset_right, - double p_physical_view_inset_bottom, - double p_physical_view_inset_left) + double p_physical_height) : device_pixel_ratio(p_device_pixel_ratio), physical_width(p_physical_width), - physical_height(p_physical_height), - physical_depth(p_physical_depth), - physical_padding_top(p_physical_padding_top), - physical_padding_right(p_physical_padding_right), - physical_padding_bottom(p_physical_padding_bottom), - physical_padding_left(p_physical_padding_left), - physical_view_inset_top(p_physical_view_inset_top), - physical_view_inset_right(p_physical_view_inset_right), - physical_view_inset_bottom(p_physical_view_inset_bottom), - physical_view_inset_left(p_physical_view_inset_left), - physical_view_inset_front(p_physical_view_inset_front), - physical_view_inset_back(p_physical_view_inset_back) { + physical_height(p_physical_height) { // Ensure we don't have nonsensical dimensions. FML_DCHECK(physical_width >= 0); FML_DCHECK(physical_height >= 0); diff --git a/lib/ui/window/viewport_metrics.h b/lib/ui/window/viewport_metrics.h index f60adbfcee110..01081e3f345f1 100644 --- a/lib/ui/window/viewport_metrics.h +++ b/lib/ui/window/viewport_metrics.h @@ -5,21 +5,10 @@ #ifndef FLUTTER_LIB_UI_WINDOW_VIEWPORT_METRICS_H_ #define FLUTTER_LIB_UI_WINDOW_VIEWPORT_METRICS_H_ -#include - namespace flutter { -// This is the value of double.maxFinite from dart:core. -// Platforms that do not explicitly set a depth will use this value, which -// avoids the need to special case logic that wants to check the max depth on -// the Dart side. -static const double kUnsetDepth = 1.7976931348623157e+308; - struct ViewportMetrics { - ViewportMetrics() = default; - ViewportMetrics(const ViewportMetrics& other) = default; - - // Create a 2D ViewportMetrics instance. + ViewportMetrics(); ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, double p_physical_height, @@ -36,26 +25,15 @@ struct ViewportMetrics { double p_physical_system_gesture_inset_bottom, double p_physical_system_gesture_inset_left); - // Create a ViewportMetrics instance that contains z information. + // Create a ViewportMetrics instance that doesn't include depth, padding, or + // insets. ViewportMetrics(double p_device_pixel_ratio, double p_physical_width, - double p_physical_height, - double p_physical_depth, - double p_physical_padding_top, - double p_physical_padding_right, - double p_physical_padding_bottom, - double p_physical_padding_left, - double p_physical_view_inset_front, - double p_physical_view_inset_back, - double p_physical_view_inset_top, - double p_physical_view_inset_right, - double p_physical_view_inset_bottom, - double p_physical_view_inset_left); + double p_physical_height); double device_pixel_ratio = 1.0; double physical_width = 0; double physical_height = 0; - double physical_depth = kUnsetDepth; double physical_padding_top = 0; double physical_padding_right = 0; double physical_padding_bottom = 0; @@ -64,8 +42,6 @@ struct ViewportMetrics { double physical_view_inset_right = 0; double physical_view_inset_bottom = 0; double physical_view_inset_left = 0; - double physical_view_inset_front = kUnsetDepth; - double physical_view_inset_back = kUnsetDepth; double physical_system_gesture_inset_top = 0; double physical_system_gesture_inset_right = 0; double physical_system_gesture_inset_bottom = 0; @@ -75,7 +51,6 @@ struct ViewportMetrics { struct LogicalSize { double width = 0.0; double height = 0.0; - double depth = kUnsetDepth; }; struct LogicalInset { @@ -83,14 +58,11 @@ struct LogicalInset { double top = 0.0; double right = 0.0; double bottom = 0.0; - double front = kUnsetDepth; - double back = kUnsetDepth; }; struct LogicalMetrics { LogicalSize size; double scale = 1.0; - double scale_z = 1.0; LogicalInset padding; LogicalInset view_inset; }; diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 9dc964ad6ef5c..1779998945554 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -4,183 +4,35 @@ #include "flutter/lib/ui/window/window.h" -#include "flutter/lib/ui/compositing/scene.h" -#include "flutter/lib/ui/ui_dart_state.h" -#include "flutter/lib/ui/window/platform_message_response_dart.h" #include "third_party/tonic/converter/dart_converter.h" #include "third_party/tonic/dart_args.h" -#include "third_party/tonic/dart_library_natives.h" -#include "third_party/tonic/dart_microtask_queue.h" #include "third_party/tonic/logging/dart_invoke.h" #include "third_party/tonic/typed_data/dart_byte_data.h" namespace flutter { -namespace { -void DefaultRouteName(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - std::string routeName = - UIDartState::Current()->window()->client()->DefaultRouteName(); - Dart_SetReturnValue(args, tonic::StdStringToDart(routeName)); -} - -void ScheduleFrame(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - UIDartState::Current()->window()->client()->ScheduleFrame(); -} - -void Render(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - Scene* scene = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } - UIDartState::Current()->window()->client()->Render(scene); -} - -void UpdateSemantics(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - SemanticsUpdate* update = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } - UIDartState::Current()->window()->client()->UpdateSemantics(update); -} - -void SetIsolateDebugName(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - const std::string name = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } - UIDartState::Current()->SetDebugName(name); -} - -void SetNeedsReportTimings(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle exception = nullptr; - bool value = tonic::DartConverter::FromArguments(args, 1, exception); - UIDartState::Current()->window()->client()->SetNeedsReportTimings(value); +Window::Window(ViewportMetrics metrics) : viewport_metrics_(metrics) { + library_.Set(tonic::DartState::Current(), + Dart_LookupLibrary(tonic::ToDart("dart:ui"))); } -void ReportUnhandledException(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - - Dart_Handle exception = nullptr; - - auto error_name = - tonic::DartConverter::FromArguments(args, 0, exception); - if (exception) { - Dart_ThrowException(exception); - return; - } +Window::~Window() {} - auto stack_trace = - tonic::DartConverter::FromArguments(args, 1, exception); - if (exception) { - Dart_ThrowException(exception); +void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) { return; } + tonic::DartState::Scope scope(dart_state); - UIDartState::Current()->ReportUnhandledException(std::move(error_name), - std::move(stack_trace)); -} - -Dart_Handle SendPlatformMessage(Dart_Handle window, - const std::string& name, - Dart_Handle callback, - Dart_Handle data_handle) { - UIDartState* dart_state = UIDartState::Current(); - - if (!dart_state->window()) { - return tonic::ToDart( - "Platform messages can only be sent from the main isolate"); - } - - fml::RefPtr response; - if (!Dart_IsNull(callback)) { - response = fml::MakeRefCounted( - tonic::DartPersistentValue(dart_state, callback), - dart_state->GetTaskRunners().GetUITaskRunner()); - } - if (Dart_IsNull(data_handle)) { - dart_state->window()->client()->HandlePlatformMessage( - fml::MakeRefCounted(name, response)); - } else { - tonic::DartByteData data(data_handle); - const uint8_t* buffer = static_cast(data.data()); - dart_state->window()->client()->HandlePlatformMessage( - fml::MakeRefCounted( - name, std::vector(buffer, buffer + data.length_in_bytes()), - response)); - } - - return Dart_Null(); -} - -void _SendPlatformMessage(Dart_NativeArguments args) { - tonic::DartCallStatic(&SendPlatformMessage, args); -} - -void RespondToPlatformMessage(Dart_Handle window, - int response_id, - const tonic::DartByteData& data) { - if (Dart_IsNull(data.dart_handle())) { - UIDartState::Current()->window()->CompletePlatformMessageEmptyResponse( - response_id); - } else { - // TODO(engine): Avoid this copy. - const uint8_t* buffer = static_cast(data.data()); - UIDartState::Current()->window()->CompletePlatformMessageResponse( - response_id, - std::vector(buffer, buffer + data.length_in_bytes())); - } -} - -void _RespondToPlatformMessage(Dart_NativeArguments args) { - tonic::DartCallStatic(&RespondToPlatformMessage, args); -} - -void GetPersistentIsolateData(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - - auto persistent_isolate_data = - UIDartState::Current()->window()->client()->GetPersistentIsolateData(); - - if (!persistent_isolate_data) { - Dart_SetReturnValue(args, Dart_Null()); + const std::vector& buffer = packet.data(); + Dart_Handle data_handle = + tonic::DartByteData::Create(buffer.data(), buffer.size()); + if (Dart_IsError(data_handle)) { return; } - - Dart_SetReturnValue( - args, tonic::DartByteData::Create(persistent_isolate_data->GetMapping(), - persistent_isolate_data->GetSize())); -} - -Dart_Handle ToByteData(const std::vector& buffer) { - return tonic::DartByteData::Create(buffer.data(), buffer.size()); -} - -} // namespace - -WindowClient::~WindowClient() {} - -Window::Window(WindowClient* client) : client_(client) {} - -Window::~Window() {} - -void Window::DidCreateIsolate() { - library_.Set(tonic::DartState::Current(), - Dart_LookupLibrary(tonic::ToDart("dart:ui"))); + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_dispatchPointerDataPacket", {data_handle})); } void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { @@ -197,7 +49,6 @@ void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { tonic::ToDart(metrics.device_pixel_ratio), tonic::ToDart(metrics.physical_width), tonic::ToDart(metrics.physical_height), - tonic::ToDart(metrics.physical_depth), tonic::ToDart(metrics.physical_padding_top), tonic::ToDart(metrics.physical_padding_right), tonic::ToDart(metrics.physical_padding_bottom), @@ -213,244 +64,4 @@ void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { })); } -void Window::UpdateLocales(const std::vector& locales) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_updateLocales", - { - tonic::ToDart>(locales), - })); -} - -void Window::UpdateUserSettingsData(const std::string& data) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), - "_updateUserSettingsData", - { - tonic::StdStringToDart(data), - })); -} - -void Window::UpdateLifecycleState(const std::string& data) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - tonic::LogIfError(tonic::DartInvokeField(library_.value(), - "_updateLifecycleState", - { - tonic::StdStringToDart(data), - })); -} - -void Window::UpdateSemanticsEnabled(bool enabled) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - UIDartState::ThrowIfUIOperationsProhibited(); - - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_updateSemanticsEnabled", {tonic::ToDart(enabled)})); -} - -void Window::UpdateAccessibilityFeatures(int32_t values) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), - "_updateAccessibilityFeatures", - {tonic::ToDart(values)})); -} - -void Window::DispatchPlatformMessage(fml::RefPtr message) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - FML_DLOG(WARNING) - << "Dropping platform message for lack of DartState on channel: " - << message->channel(); - return; - } - tonic::DartState::Scope scope(dart_state); - Dart_Handle data_handle = - (message->hasData()) ? ToByteData(message->data()) : Dart_Null(); - if (Dart_IsError(data_handle)) { - FML_DLOG(WARNING) - << "Dropping platform message because of a Dart error on channel: " - << message->channel(); - return; - } - - int response_id = 0; - if (auto response = message->response()) { - response_id = next_response_id_++; - pending_responses_[response_id] = response; - } - - tonic::LogIfError( - tonic::DartInvokeField(library_.value(), "_dispatchPlatformMessage", - {tonic::ToDart(message->channel()), data_handle, - tonic::ToDart(response_id)})); -} - -void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - Dart_Handle data_handle = ToByteData(packet.data()); - if (Dart_IsError(data_handle)) { - return; - } - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_dispatchPointerDataPacket", {data_handle})); -} - -void Window::DispatchSemanticsAction(int32_t id, - SemanticsAction action, - std::vector args) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - Dart_Handle args_handle = (args.empty()) ? Dart_Null() : ToByteData(args); - - if (Dart_IsError(args_handle)) { - return; - } - - tonic::LogIfError(tonic::DartInvokeField( - library_.value(), "_dispatchSemanticsAction", - {tonic::ToDart(id), tonic::ToDart(static_cast(action)), - args_handle})); -} - -void Window::BeginFrame(fml::TimePoint frameTime) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - int64_t microseconds = (frameTime - fml::TimePoint()).ToMicroseconds(); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_beginFrame", - { - Dart_NewInteger(microseconds), - })); - - UIDartState::Current()->FlushMicrotasksNow(); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_drawFrame", {})); -} - -void Window::ReportTimings(std::vector timings) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) { - return; - } - tonic::DartState::Scope scope(dart_state); - - Dart_Handle data_handle = - Dart_NewTypedData(Dart_TypedData_kInt64, timings.size()); - - Dart_TypedData_Type type; - void* data = nullptr; - intptr_t num_acquired = 0; - FML_CHECK(!Dart_IsError( - Dart_TypedDataAcquireData(data_handle, &type, &data, &num_acquired))); - FML_DCHECK(num_acquired == static_cast(timings.size())); - - memcpy(data, timings.data(), sizeof(int64_t) * timings.size()); - FML_CHECK(Dart_TypedDataReleaseData(data_handle)); - - tonic::LogIfError(tonic::DartInvokeField(library_.value(), "_reportTimings", - { - data_handle, - })); -} - -void Window::CompletePlatformMessageEmptyResponse(int response_id) { - if (!response_id) { - return; - } - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) { - return; - } - auto response = std::move(it->second); - pending_responses_.erase(it); - response->CompleteEmpty(); -} - -void Window::CompletePlatformMessageResponse(int response_id, - std::vector data) { - if (!response_id) { - return; - } - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) { - return; - } - auto response = std::move(it->second); - pending_responses_.erase(it); - response->Complete(std::make_unique(std::move(data))); -} - -Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) { - std::vector supportedLocales = - tonic::DartConverter>::FromDart( - supportedLocalesHandle); - - std::vector results = - *UIDartState::Current() - ->window() - ->client() - ->ComputePlatformResolvedLocale(supportedLocales); - - return tonic::DartConverter>::ToDart(results); -} - -static void _ComputePlatformResolvedLocale(Dart_NativeArguments args) { - UIDartState::ThrowIfUIOperationsProhibited(); - Dart_Handle result = - ComputePlatformResolvedLocale(Dart_GetNativeArgument(args, 1)); - Dart_SetReturnValue(args, result); -} - -void Window::RegisterNatives(tonic::DartLibraryNatives* natives) { - natives->Register({ - {"Window_defaultRouteName", DefaultRouteName, 1, true}, - {"Window_scheduleFrame", ScheduleFrame, 1, true}, - {"Window_sendPlatformMessage", _SendPlatformMessage, 4, true}, - {"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true}, - {"Window_render", Render, 2, true}, - {"Window_updateSemantics", UpdateSemantics, 2, true}, - {"Window_setIsolateDebugName", SetIsolateDebugName, 2, true}, - {"Window_reportUnhandledException", ReportUnhandledException, 2, true}, - {"Window_setNeedsReportTimings", SetNeedsReportTimings, 2, true}, - {"Window_getPersistentIsolateData", GetPersistentIsolateData, 1, true}, - {"Window_computePlatformResolvedLocale", _ComputePlatformResolvedLocale, - 2, true}, - }); -} - } // namespace flutter diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index 1e68b09d8095a..172cf6b8c2ae5 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -9,102 +9,27 @@ #include #include -#include "flutter/fml/time/time_point.h" -#include "flutter/lib/ui/semantics/semantics_update.h" #include "flutter/lib/ui/window/platform_message.h" #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/viewport_metrics.h" #include "third_party/skia/include/gpu/GrDirectContext.h" #include "third_party/tonic/dart_persistent_value.h" -namespace tonic { -class DartLibraryNatives; - -// So tonice::ToDart> returns List instead of -// List. -template <> -struct DartListFactory { - static Dart_Handle NewList(intptr_t length) { - return Dart_NewListOf(Dart_CoreType_Int, length); - } -}; - -} // namespace tonic - namespace flutter { -class FontCollection; -class Scene; - -// Must match the AccessibilityFeatureFlag enum in window.dart. -enum class AccessibilityFeatureFlag : int32_t { - kAccessibleNavigation = 1 << 0, - kInvertColors = 1 << 1, - kDisableAnimations = 1 << 2, - kBoldText = 1 << 3, - kReduceMotion = 1 << 4, - kHighContrast = 1 << 5, -}; - -class WindowClient { - public: - virtual std::string DefaultRouteName() = 0; - virtual void ScheduleFrame() = 0; - virtual void Render(Scene* scene) = 0; - virtual void UpdateSemantics(SemanticsUpdate* update) = 0; - virtual void HandlePlatformMessage(fml::RefPtr message) = 0; - virtual FontCollection& GetFontCollection() = 0; - virtual void UpdateIsolateDescription(const std::string isolate_name, - int64_t isolate_port) = 0; - virtual void SetNeedsReportTimings(bool value) = 0; - virtual std::shared_ptr GetPersistentIsolateData() = 0; - virtual std::unique_ptr> - ComputePlatformResolvedLocale( - const std::vector& supported_locale_data) = 0; - - protected: - virtual ~WindowClient(); -}; - class Window final { public: - explicit Window(WindowClient* client); + explicit Window(ViewportMetrics metrics); ~Window(); - WindowClient* client() const { return client_; } + const ViewportMetrics& viewport_metrics() const { return viewport_metrics_; } - const ViewportMetrics& viewport_metrics() { return viewport_metrics_; } - - void DidCreateIsolate(); - void UpdateWindowMetrics(const ViewportMetrics& metrics); - void UpdateLocales(const std::vector& locales); - void UpdateUserSettingsData(const std::string& data); - void UpdateLifecycleState(const std::string& data); - void UpdateSemanticsEnabled(bool enabled); - void UpdateAccessibilityFeatures(int32_t flags); - void DispatchPlatformMessage(fml::RefPtr message); void DispatchPointerDataPacket(const PointerDataPacket& packet); - void DispatchSemanticsAction(int32_t id, - SemanticsAction action, - std::vector args); - void BeginFrame(fml::TimePoint frameTime); - void ReportTimings(std::vector timings); - - void CompletePlatformMessageResponse(int response_id, - std::vector data); - void CompletePlatformMessageEmptyResponse(int response_id); - - static void RegisterNatives(tonic::DartLibraryNatives* natives); + void UpdateWindowMetrics(const ViewportMetrics& metrics); private: - WindowClient* client_; tonic::DartPersistentValue library_; ViewportMetrics viewport_metrics_; - - // We use id 0 to mean that no response is expected. - int next_response_id_ = 1; - std::unordered_map> - pending_responses_; }; } // namespace flutter diff --git a/lib/web_ui/CODE_CONVENTIONS.md b/lib/web_ui/CODE_CONVENTIONS.md new file mode 100644 index 0000000000000..de91812be7b7f --- /dev/null +++ b/lib/web_ui/CODE_CONVENTIONS.md @@ -0,0 +1,62 @@ +# Web-specific coding conventions and terminology + +Here you will find various naming and structural conventions used in the Web +engine code. This is not a code style guide. For code style refer to +[Flutter's style guide][1]. This document does not apply outside the `web_ui` +directory. + +## CanvasKit Renderer + +All code specific to the CanvasKit renderer lives in `lib/src/engine/canvaskit`. + +CanvasKit bindings should use the exact names defined in CanvasKit's JavaScript +API, even if it violates Flutter's style guide, such as function names that +start with a capital letter (e.g. "MakeSkVertices"). This makes it easier to find +the relevant code in Skia's source code. CanvasKit bindings should all go in +the `canvaskit_api.dart` file. + +Files and directories should use all-lower-case "canvaskit", without +capitalization or punctuation (such as "canvasKit", "canvas-kit", "canvas_kit"). +This is consistent with Skia's conventions. + +Variable, function, method, and class names should use camel case, i.e. +"canvasKit", "CanvasKit". + +In documentation (doc comments, flutter.dev website, markdown files, +blog posts, etc) refer to Flutter's usage of CanvasKit as "CanvasKit renderer" +(to avoid confusion with CanvasKit as the standalone library, which can be used +without Flutter). + +Classes that wrap CanvasKit classes should replace the `Sk` class prefix with +`Ck` (which stands for "CanvasKit"), e.g. `CkPaint` wraps `SkPaint`, `CkImage` +wraps `SkImage`. + +## HTML Renderer + +All code specific to the HTML renderer lives in `lib/src/engine/html`. + +In documentation (doc comments, flutter.dev website, markdown files, +blog posts, etc) refer to Flutter's HTML implementation as "HTML renderer". We +include SVG, CSS, and Canvas 2D under the "HTML" umbrella. + +The implementation of the layer system uses the term "surface" to refer to +layers. We rely on persisting the DOM information across frames to gain +efficiency. Each concrete implementation of the `Surface` class should start +with the prefix `Persisted`, e.g. `PersistedOpacity`, `PersistedPicture`. + +## Semantics + +The semantics (accessibility) code is shared between CanvasKit and HTML. All +semantics code lives in `lib/src/engine/semantics`. + +## Text editing + +Text editing code is shared between CanvasKit and HTML, and it lives in +`lib/src/engine/text_editing`. + +## Common utilities + +Small common utilities do not need dedicated directories. It is OK to put all +such utilities in `lib/src/engine` (see, for example, `alarm_clock.dart`). + +[1]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo diff --git a/lib/web_ui/dev/README.md b/lib/web_ui/dev/README.md index 5903f07e9327a..73e181a9f8bb7 100644 --- a/lib/web_ui/dev/README.md +++ b/lib/web_ui/dev/README.md @@ -41,9 +41,7 @@ felt build [-w] -j 100 If you are a Google employee, you can use an internal instance of Goma to parallelize your builds. Because Goma compiles code on remote servers, this option is effective even on low-powered laptops. -By default, when compiling Dart code to JavaScript, we use 4 `dart2js` workers. -If you need to increase or reduce the number of workers, set the `BUILD_MAX_WORKERS_PER_TASK` -environment variable to the desired number. +By default, when compiling Dart code to JavaScript, we use 8 `dart2js` workers. ## Running web engine tests @@ -142,6 +140,7 @@ flutter/goldens updating the screenshots. Then update this file pointing to the new revision. ## Developing the `felt` tool + If you are making changes in the `felt` tool itself, you need to be aware of Dart snapshots. We create a Dart snapshot of the `felt` tool to make the startup faster. To make sure you are running the `felt` tool with your changes included, you would need to stop using the snapshot. This can be achived through the environment variable `FELT_USE_SNAPSHOT`: @@ -157,3 +156,22 @@ FELT_USE_SNAPSHOT=0 felt ``` _**Note**: if `FELT_USE_SNAPSHOT` is omitted or has any value other than "false" or "0", the snapshot mode will be enabled._ + +## Upgrade Browser Version + +Since the engine code and infra recipes do not live in the same repository there are few steps to follow in order to upgrade a browser's version. For now these instructins are most relevant to Chrome. + +1. Dowload the binaries for the new browser/driver for each operaing system (macOS, linux, windows). +2. Create CIPD packages for these packages. (More documentation is available for Googlers. go/cipd-flutter-web) +3. Add the new browser version to the recipe. Do not remove the old one. This recipe will apply to all PRs as soon as it is merged. However, not all PRs will have the up to date code for a while. +4. Update the version in this repo. Do this by changing the related fields in `browser_lock.yaml` file. +5. After a few days don't forget to remove the old version from the LUCI recipe. + +Note that for LUCI builders, both unit and integration tests are using the same browser. + +Some useful links: + +1. For Chrome downloads [link](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html) +2. Browser and driver CIPD [packages](https://chrome-infra-packages.appspot.com/p/flutter_internal) (Note: Access rights are restricted for these packages.) +3. LUCI web [recipe](https://flutter.googlesource.com/recipes/+/refs/heads/master/recipes/web_engine.py) +4. More general reading on CIPD packages [link](https://chromium.googlesource.com/chromium/src.git/+/master/docs/cipd.md#What-is-CIPD) diff --git a/lib/web_ui/dev/browser_lock.yaml b/lib/web_ui/dev/browser_lock.yaml index 57fbf7a6816fe..11b2baea1d55f 100644 --- a/lib/web_ui/dev/browser_lock.yaml +++ b/lib/web_ui/dev/browser_lock.yaml @@ -1,11 +1,21 @@ +## Driver version in use. +## For an integration test to run, the browser's major version and the driver's +## major version should be equal. Please make sure the major version of +## the binary for `chrome` is the same with `required_driver_version`. +## (Major version meaning: For a browser that has version 13.0.5, the major +## version is 13.) +## Please refer to README's `Upgrade Browser Version` section for more details +## on how to update the version number. +required_driver_version: + ## Make sure the major version of the binary in `browser_lock.yaml` is the + ## same for Chrome. + chrome: '84' chrome: # It seems Chrome can't always release from the same build for all operating # systems, so we specify per-OS build number. - # Note: 741412 binary is for Chrome Version 82. Driver for Chrome version 83 - # is not working with chrome.binary option. - Linux: 741412 - Mac: 735194 - Win: 735105 + Linux: 768968 # Major version 84 + Mac: 768985 # Major version 84 + Win: 768975 # Major version 84 firefox: version: '72.0' edge: diff --git a/lib/web_ui/dev/chrome_installer.dart b/lib/web_ui/dev/chrome_installer.dart index 6f46c2a57d903..3766488f497dd 100644 --- a/lib/web_ui/dev/chrome_installer.dart +++ b/lib/web_ui/dev/chrome_installer.dart @@ -3,8 +3,11 @@ // found in the LICENSE file. // @dart = 2.6 +import 'dart:async'; import 'dart:io' as io; +import 'package:archive/archive.dart'; +import 'package:archive/archive_io.dart'; import 'package:args/args.dart'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; @@ -186,7 +189,7 @@ class ChromeInstaller { } else if (versionDir.existsSync() && isLuci) { print('INFO: Chrome version directory in LUCI: ' '${versionDir.path}'); - } else if(!versionDir.existsSync() && isLuci) { + } else if (!versionDir.existsSync() && isLuci) { // Chrome should have been deployed as a CIPD package on LUCI. // Throw if it does not exists. throw StateError('Failed to locate Chrome on LUCI on path:' @@ -196,6 +199,8 @@ class ChromeInstaller { versionDir.createSync(recursive: true); } + print('INFO: Starting Chrome download.'); + final String url = PlatformBinding.instance.getChromeDownloadUrl(version); final StreamedResponse download = await client.send(Request( 'GET', @@ -206,17 +211,50 @@ class ChromeInstaller { io.File(path.join(versionDir.path, 'chrome.zip')); await download.stream.pipe(downloadedFile.openWrite()); - final io.ProcessResult unzipResult = await io.Process.run('unzip', [ - downloadedFile.path, - '-d', - versionDir.path, - ]); - - if (unzipResult.exitCode != 0) { - throw BrowserInstallerException( - 'Failed to unzip the downloaded Chrome archive ${downloadedFile.path}.\n' - 'With the version path ${versionDir.path}\n' - 'The unzip process exited with code ${unzipResult.exitCode}.'); + /// Windows LUCI bots does not have a `unzip`. Instead we are + /// using `archive` pub package. + /// + /// We didn't use `archieve` on Mac/Linux since the new files have + /// permission issues. For now we are not able change file permissions + /// from dart. + /// See: https://github.com/dart-lang/sdk/issues/15078. + if (io.Platform.isWindows) { + final Stopwatch stopwatch = Stopwatch()..start(); + + // Read the Zip file from disk. + final bytes = downloadedFile.readAsBytesSync(); + + final Archive archive = ZipDecoder().decodeBytes(bytes); + + // Extract the contents of the Zip archive to disk. + for (final ArchiveFile file in archive) { + final String filename = file.name; + if (file.isFile) { + final data = file.content as List; + io.File(path.join(versionDir.path, filename)) + ..createSync(recursive: true) + ..writeAsBytesSync(data); + } else { + io.Directory(path.join(versionDir.path, filename)) + ..create(recursive: true); + } + } + + stopwatch.stop(); + print('INFO: The unzip took ${stopwatch.elapsedMilliseconds ~/ 1000} seconds.'); + } else { + final io.ProcessResult unzipResult = + await io.Process.run('unzip', [ + downloadedFile.path, + '-d', + versionDir.path, + ]); + if (unzipResult.exitCode != 0) { + throw BrowserInstallerException( + 'Failed to unzip the downloaded Chrome archive ${downloadedFile.path}.\n' + 'With the version path ${versionDir.path}\n' + 'The unzip process exited with code ${unzipResult.exitCode}.'); + } } downloadedFile.deleteSync(); diff --git a/lib/web_ui/dev/common.dart b/lib/web_ui/dev/common.dart index 588fe67eedf6f..511794a1e92f3 100644 --- a/lib/web_ui/dev/common.dart +++ b/lib/web_ui/dev/common.dart @@ -59,11 +59,11 @@ class _WindowsBinding implements PlatformBinding { @override String getChromeDownloadUrl(String version) => - 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Win%2F${version}%2Fchrome-win32.zip?alt=media'; + 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Win%2F${version}%2Fchrome-win.zip?alt=media'; @override String getChromeExecutablePath(io.Directory versionDir) => - path.join(versionDir.path, 'chrome-win32', 'chrome'); + path.join(versionDir.path, 'chrome-win', 'chrome.exe'); @override String getFirefoxDownloadUrl(String version) => diff --git a/lib/web_ui/dev/driver_manager.dart b/lib/web_ui/dev/driver_manager.dart index de15c467c5510..595c965d20c80 100644 --- a/lib/web_ui/dev/driver_manager.dart +++ b/lib/web_ui/dev/driver_manager.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.6 import 'dart:io' as io; import 'package:meta/meta.dart'; @@ -20,19 +21,44 @@ import 'utils.dart'; /// /// This manager can be used for both macOS and Linux. class ChromeDriverManager extends DriverManager { - ChromeDriverManager(String browser) : super(browser); + /// Directory which contains the Chrome's major version. + /// + /// On LUCI we are using the CIPD packages to control Chrome binaries we use. + /// There will be multiple CIPD packages loaded at the same time. Yaml file + /// `driver_version.yaml` contains the version number we want to use. + /// + /// Initialized to the current first to avoid the `Non-nullable` error. + // TODO: https://github.com/flutter/flutter/issues/53179. Local integration + // tests are still using the system Chrome. + io.Directory _browserDriverDirWithVersion; + + ChromeDriverManager(String browser) : super(browser) { + final io.File lockFile = io.File(pathlib.join( + environment.webUiRootDir.path, 'dev', 'browser_lock.yaml')); + final YamlMap _configuration = + loadYaml(lockFile.readAsStringSync()) as YamlMap; + final String requiredChromeDriverVersion = + _configuration['required_driver_version']['chrome'] as String; + print('INFO: Major version for Chrome Driver $requiredChromeDriverVersion'); + _browserDriverDirWithVersion = io.Directory(pathlib.join( + environment.webUiDartToolDir.path, + 'drivers', + browser, + requiredChromeDriverVersion, + '${browser}driver-${io.Platform.operatingSystem.toString()}')); + } @override Future _installDriver() async { - if (_browserDriverDir.existsSync()) { - _browserDriverDir.deleteSync(recursive: true); + if (_browserDriverDirWithVersion.existsSync()) { + _browserDriverDirWithVersion.deleteSync(recursive: true); } - _browserDriverDir.createSync(recursive: true); + _browserDriverDirWithVersion.createSync(recursive: true); temporaryDirectories.add(_drivers); final io.Directory temp = io.Directory.current; - io.Directory.current = _browserDriverDir; + io.Directory.current = _browserDriverDirWithVersion; try { // TODO(nurhan): https://github.com/flutter/flutter/issues/53179 @@ -50,17 +76,17 @@ class ChromeDriverManager extends DriverManager { /// Driver should already exist on LUCI as a CIPD package. @override Future _verifyDriverForLUCI() { - if (!_browserDriverDir.existsSync()) { + if (!_browserDriverDirWithVersion.existsSync()) { throw StateError('Failed to locate Chrome driver on LUCI on path:' - '${_browserDriverDir.path}'); + '${_browserDriverDirWithVersion.path}'); } return Future.value(); } @override - Future _startDriver(String driverPath) async { + Future _startDriver() async { await startProcess('./chromedriver/chromedriver', ['--port=4444'], - workingDirectory: driverPath); + workingDirectory: _browserDriverDirWithVersion.path); print('INFO: Driver started'); } } @@ -106,9 +132,9 @@ class FirefoxDriverManager extends DriverManager { } @override - Future _startDriver(String driverPath) async { + Future _startDriver() async { await startProcess('./firefoxdriver/geckodriver', ['--port=4444'], - workingDirectory: driverPath); + workingDirectory: _browserDriverDir.path); print('INFO: Driver started'); } @@ -144,7 +170,7 @@ class SafariDriverManager extends DriverManager { } @override - Future _startDriver(String driverPath) async { + Future _startDriver() async { final SafariDriverRunner safariDriverRunner = SafariDriverRunner(); final io.Process process = @@ -184,7 +210,7 @@ abstract class DriverManager { } else { await _verifyDriverForLUCI(); } - await _startDriver(_browserDriverDir.path); + await _startDriver(); } /// Always re-install since driver can change frequently. @@ -198,7 +224,7 @@ abstract class DriverManager { Future _verifyDriverForLUCI(); @protected - Future _startDriver(String driverPath); + Future _startDriver(); static DriverManager chooseDriver(String browser) { if (browser == 'chrome') { diff --git a/lib/web_ui/dev/driver_version.yaml b/lib/web_ui/dev/driver_version.yaml index 174e69448ac7c..f2c3baf9391f8 100644 --- a/lib/web_ui/dev/driver_version.yaml +++ b/lib/web_ui/dev/driver_version.yaml @@ -1,4 +1,3 @@ - ## Map for driver versions to use for each browser version. ## See: https://chromedriver.chromium.org/downloads chrome: diff --git a/lib/web_ui/dev/felt_windows.bat b/lib/web_ui/dev/felt_windows.bat index bacd1b0d50975..2ae0e2ab8b125 100644 --- a/lib/web_ui/dev/felt_windows.bat +++ b/lib/web_ui/dev/felt_windows.bat @@ -57,15 +57,15 @@ IF %orTempValue%==0 ( :: TODO(nurhan): The batch script does not support snanphot option. :: Support snapshot option. CALL :installdeps -IF %1==test (%DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* --browser=edge) ELSE ( %DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* ) +IF %1==test (%DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* --browser=chrome) ELSE ( %DART_SDK_DIR%\bin\dart "%DEV_DIR%\felt.dart" %* ) -EXIT /B 0 +EXIT /B %ERRORLEVEL% :installdeps ECHO "Running \`pub get\` in 'engine/src/flutter/web_sdk/web_engine_tester'" cd "%FLUTTER_DIR%web_sdk\web_engine_tester" CALL %PUB_DIR% get ECHO "Running \`pub get\` in 'engine/src/flutter/lib/web_ui'" -cd %WEB_UI_DIR% +cd %WEB_UI_DIR% CALL %PUB_DIR% get EXIT /B 0 diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 2d5ea68c65afa..7daac2c7ae55f 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 4fb2ce1ea4b3a0bd48b01d7b3724be87244964d6 +revision: f26d68c3596eece3d40112e9dff01dc55d9bae97 diff --git a/lib/web_ui/dev/macos_info.dart b/lib/web_ui/dev/macos_info.dart index 40742c1e84943..b406aa6a32e88 100644 --- a/lib/web_ui/dev/macos_info.dart +++ b/lib/web_ui/dev/macos_info.dart @@ -1,6 +1,8 @@ // 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. + +// @dart = 2.6 import 'dart:convert'; import 'utils.dart'; diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index 20e16be900217..341d9b245ee37 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -34,7 +34,6 @@ import 'package:test_core/src/runner/environment.dart'; // ignore: implementatio import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports -import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' as wip; @@ -407,15 +406,6 @@ Golden file $filename did not match the image generated by the test. throw ArgumentError('$browser is not a browser.'); } - var htmlPath = p.withoutExtension(path) + '.html'; - if (File(htmlPath).existsSync() && - !File(htmlPath).readAsStringSync().contains('packages/test/dart.js')) { - throw LoadException( - path, - '"${htmlPath}" must contain .'); - } - if (_closed) { return null; } @@ -811,15 +801,18 @@ class BrowserManager { controller = deserializeSuite(path, currentPlatform(_runtime), suiteConfig, await _environment, suiteChannel, message); - final String mapPath = p.join( - env.environment.webUiRootDir.path, - 'build', - '$path.browser_test.dart.js.map', - ); - PackageConfig packageConfig = await loadPackageConfigUri( - await Isolate.packageConfig); - Map packageMap = - {for (var p in packageConfig.packages) p.name: p.packageUriRoot}; + final String sourceMapFileName = + '${p.basename(path)}.browser_test.dart.js.map'; + final String pathToTest = p.dirname(path); + + final String mapPath = p.join(env.environment.webUiRootDir.path, + 'build', pathToTest, sourceMapFileName); + + PackageConfig packageConfig = + await loadPackageConfigUri(await Isolate.packageConfig); + Map packageMap = { + for (var p in packageConfig.packages) p.name: p.packageUriRoot + }; final JSStackTraceMapper mapper = JSStackTraceMapper( await File(mapPath).readAsString(), mapUrl: p.toUri(mapPath), diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index e9f57b32f0153..14a34d5eea4df 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -9,6 +9,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; +import 'package:pool/pool.dart'; import 'package:test_core/src/runner/hack_register_platform.dart' as hack; // ignore: implementation_imports import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports @@ -99,6 +100,9 @@ class TestCommand extends Command with ArgUtils { TestTypesRequested testTypesRequested = null; + /// How many dart2js build tasks are running at the same time. + final Pool _pool = Pool(8); + /// Check the flags to see what type of tests are requested. TestTypesRequested findTestType() { if (boolArg('unit-tests-only') && boolArg('integration-tests-only')) { @@ -130,6 +134,7 @@ class TestCommand extends Command with ArgUtils { /// Collect information on the bot. final MacOSInfo macOsInfo = new MacOSInfo(); await macOsInfo.printInformation(); + /// Tests may fail on the CI, therefore exit test_runner. if (isLuci) { return true; @@ -142,9 +147,7 @@ class TestCommand extends Command with ArgUtils { case TestTypesRequested.integration: return runIntegrationTests(); case TestTypesRequested.all: - // TODO(nurhan): https://github.com/flutter/flutter/issues/53322 - // TODO(nurhan): Expand browser matrix for felt integration tests. - if (runAllTests && (isChrome || isSafariOnMacOS || isFirefox)) { + if (runAllTests && isIntegrationTestsAvailable) { bool unitTestResult = await runUnitTests(); bool integrationTestResult = await runIntegrationTests(); if (integrationTestResult != unitTestResult) { @@ -240,12 +243,35 @@ class TestCommand extends Command with ArgUtils { } if (htmlTargets.isNotEmpty) { - await _buildTests(targets: htmlTargets, forCanvasKit: false); + await _buildTestsInParallel(targets: htmlTargets, forCanvasKit: false); } + // Currently iOS Safari tests are running on simulator, which does not + // support canvaskit backend. if (canvasKitTargets.isNotEmpty) { - await _buildTests(targets: canvasKitTargets, forCanvasKit: true); + await _buildTestsInParallel( + targets: canvasKitTargets, forCanvasKit: true); } + + // Copy image files from test/ to build/test/. + // A side effect is this file copies all the images even when only one + // target test is asked to run. + final List contents = + environment.webUiTestDir.listSync(recursive: true); + contents.whereType().forEach((final io.File entity) { + final String directoryPath = path.relative(path.dirname(entity.path), + from: environment.webUiRootDir.path); + final io.Directory directory = io.Directory( + path.join(environment.webUiBuildDir.path, directoryPath)); + if (!directory.existsSync()) { + directory.createSync(recursive: true); + } + final String pathRelativeToWebUi = path.relative(entity.absolute.path, + from: environment.webUiRootDir.path); + entity.copySync( + path.join(environment.webUiBuildDir.path, pathRelativeToWebUi)); + }); + stopwatch.stop(); print('The build took ${stopwatch.elapsedMilliseconds ~/ 1000} seconds.'); } @@ -279,8 +305,40 @@ class TestCommand extends Command with ArgUtils { bool get isFirefox => browser == 'firefox'; /// Whether [browser] is set to "safari". - bool get isSafariOnMacOS => browser == 'safari' - && io.Platform.isMacOS; + bool get isSafariOnMacOS => browser == 'safari' && io.Platform.isMacOS; + + /// Due to lack of resources Chrome integration tests only run on Linux on + /// LUCI. + /// + /// They run on all platforms for local. + bool get isChromeIntegrationTestAvailable => + (isChrome && isLuci && io.Platform.isLinux) || (isChrome && !isLuci); + + /// Due to efficiancy constraints, Firefox integration tests only run on + /// Linux on LUCI. + /// + /// For now Firefox integration tests only run on Linux and Mac on local. + /// + // TODO: https://github.com/flutter/flutter/issues/63832 + bool get isFirefoxIntegrationTestAvailable => + (isFirefox && isLuci && io.Platform.isLinux) || + (isFirefox && !isLuci && !io.Platform.isWindows); + + /// Latest versions of Safari Desktop are only available on macOS. + /// + /// Integration testing on LUCI is not supported at the moment. + // TODO: https://github.com/flutter/flutter/issues/63710 + bool get isSafariIntegrationTestAvailable => isSafariOnMacOS && !isLuci; + + /// Due to various factors integration tests might be missing on a given + /// platform and given environment. + /// See: [isChromeIntegrationTestAvailable] + /// See: [isSafariIntegrationTestAvailable] + /// See: [isFirefoxIntegrationTestAvailable] + bool get isIntegrationTestsAvailable => + isChromeIntegrationTestAvailable || + isFirefoxIntegrationTestAvailable || + isSafariIntegrationTestAvailable; /// Use system flutter instead of cloning the repository. /// @@ -308,8 +366,10 @@ class TestCommand extends Command with ArgUtils { 'test', )); - // Screenshot tests and smoke tests only run in Chrome. - if (isChrome) { + // Screenshot tests and smoke tests only run on: "Chrome locally" or + // "Chrome on a Linux bot". We can remove the Linux bot restriction after: + // TODO: https://github.com/flutter/flutter/issues/63710 + if ((isChrome && isLuci && io.Platform.isLinux) || (isChrome && !isLuci)) { // Separate screenshot tests from unit-tests. Screenshot tests must run // one at a time. Otherwise, they will end up screenshotting each other. // This is not an issue for unit-tests. @@ -448,69 +508,69 @@ class TestCommand extends Command with ArgUtils { timestampFile.writeAsStringSync(timestamp); } + Future _buildTestsInParallel( + {List targets, bool forCanvasKit = false}) async { + final List buildInputs = targets + .map((FilePath f) => TestBuildInput(f, forCanvasKit: forCanvasKit)) + .toList(); + + final results = _pool.forEach( + buildInputs, + _buildTest, + ); + await for (final bool isSuccess in results) { + if (!isSuccess) { + throw ToolException('Failed to compile tests.'); + } + } + } + /// Builds the specific test [targets]. /// /// [targets] must not be null. /// - /// When building for CanvasKit we have to use a separate `build.canvaskit.yaml` - /// config file. Otherwise, `build.html.yaml` is used. Because `build_runner` - /// overwrites the output directories, we redirect the CanvasKit output to a - /// separate directory, then copy the files back to `build/test`. - Future _buildTests({List targets, bool forCanvasKit}) async { - print( - 'Building ${targets.length} targets for ${forCanvasKit ? 'CanvasKit' : 'HTML'}'); - final String canvasKitOutputRelativePath = - path.join('.dart_tool', 'canvaskit_tests'); + /// Uses `dart2js` for building the test. + /// + /// When building for CanvasKit we have to use extra argument + /// `DFLUTTER_WEB_USE_SKIA=true`. + Future _buildTest(TestBuildInput input) async { + final targetFileName = + '${input.path.relativeToWebUi}.browser_test.dart.js'; + final String targetPath = path.join('build', targetFileName); + + final io.Directory directoryToTarget = io.Directory(path.join( + environment.webUiBuildDir.path, + path.dirname(input.path.relativeToWebUi))); + + if (!directoryToTarget.existsSync()) { + directoryToTarget.createSync(recursive: true); + } + List arguments = [ - 'run', - 'build_runner', - 'build', + '--no-minify', + '--disable-inlining', + '--enable-asserts', '--enable-experiment=non-nullable', - 'test', + '--no-sound-null-safety', + if (input.forCanvasKit) '-DFLUTTER_WEB_USE_SKIA=true', + '-O2', '-o', - forCanvasKit ? canvasKitOutputRelativePath : 'build', - '--config', - // CanvasKit uses `build.canvaskit.yaml`, which HTML Uses `build.html.yaml`. - forCanvasKit ? 'canvaskit' : 'html', - for (FilePath path in targets) ...[ - '--build-filter=${path.relativeToWebUi}.js', - '--build-filter=${path.relativeToWebUi}.browser_test.dart.js', - ], + targetPath, // target path. + '${input.path.relativeToWebUi}', // current path. ]; final int exitCode = await runProcess( - environment.pubExecutable, + environment.dart2jsExecutable, arguments, workingDirectory: environment.webUiRootDir.path, - environment: { - // This determines the number of concurrent dart2js processes. - // - // By default build_runner uses 4 workers. - // - // In a testing on a 32-core 132GB workstation increasing this number to - // 32 sped up the build from ~4min to ~1.5min. - if (io.Platform.environment.containsKey('BUILD_MAX_WORKERS_PER_TASK')) - 'BUILD_MAX_WORKERS_PER_TASK': - io.Platform.environment['BUILD_MAX_WORKERS_PER_TASK'], - }, ); if (exitCode != 0) { - throw ToolException( - 'Failed to compile tests. Compiler exited with exit code $exitCode'); - } - - if (forCanvasKit) { - final io.Directory canvasKitTemporaryOutputDirectory = io.Directory( - path.join(environment.webUiRootDir.path, canvasKitOutputRelativePath, - 'test', 'canvaskit')); - final io.Directory canvasKitOutputDirectory = io.Directory( - path.join(environment.webUiBuildDir.path, 'test', 'canvaskit')); - if (await canvasKitOutputDirectory.exists()) { - await canvasKitOutputDirectory.delete(recursive: true); - } - await canvasKitTemporaryOutputDirectory - .rename(canvasKitOutputDirectory.path); + io.stderr.writeln('ERROR: Failed to compile test ${input.path}. ' + 'Dart2js exited with exit code $exitCode'); + return false; + } else { + return true; } } @@ -582,3 +642,16 @@ void _copyTestFontsIntoWebUi() { sourceTtf.copySync(destinationTtfPath); } } + +/// Used as an input message to the PoolResources that are building a test. +class TestBuildInput { + /// Test to build. + final FilePath path; + + /// Whether these tests should be build for CanvasKit. + /// + /// `-DFLUTTER_WEB_USE_SKIA=true` is passed to dart2js for CanvasKit. + final bool forCanvasKit; + + TestBuildInput(this.path, {this.forCanvasKit = false}); +} diff --git a/lib/web_ui/lib/assets/houdini_painter.js b/lib/web_ui/lib/assets/houdini_painter.js deleted file mode 100644 index 74ec4a29e25cb..0000000000000 --- a/lib/web_ui/lib/assets/houdini_painter.js +++ /dev/null @@ -1,1069 +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. - -// TODO(yjbanov): Consider the following optimizations: -// - Switch from JSON to typed arrays. See: -// https://github.com/w3c/css-houdini-drafts/issues/136 -// - When there is no DOM-rendered content, then clipping in the canvas is more -// efficient than DOM-rendered clipping. -// - When DOM-rendered clip is the only option, then clipping _again_ in the -// canvas is superfluous. -// - When transform is a 2D transform and there is no DOM-rendered content, then -// canvas transform is more efficient than DOM-rendered transform. -// - If a transform must be DOM-rendered, then clipping in the canvas _again_ is -// superfluous. - -/** - * Applies paint commands to CSS Paint API (a.k.a. Houdini). - * - * This painter is driven by houdini_canvas.dart. This painter and the - * HoudiniCanvas class must be kept in sync with each other. - */ -class FlutterPainter { - /** - * Properties used by this painter. - * - * @return {string[]} list of CSS properties this painter depends on. - */ - static get inputProperties() { - return ['--flt']; - } - - /** - * Implements the painter interface. - */ - paint(ctx, geom, properties) { - let fltProp = properties.get('--flt').toString(); - if (!fltProp) { - // Nothing to paint. - return; - } - const commands = JSON.parse(fltProp); - for (let i = 0; i < commands.length; i++) { - let command = commands[i]; - // TODO(yjbanov): we should probably move command identifiers into an enum - switch (command[0]) { - case 1: - this._save(ctx, geom, command); - break; - case 2: - this._restore(ctx, geom, command); - break; - case 3: - this._translate(ctx, geom, command); - break; - case 4: - this._scale(ctx, geom, command); - break; - case 5: - this._rotate(ctx, geom, command); - break; - // Skip case 6: implemented in the DOM for now. - case 7: - this._skew(ctx, geom, command); - break; - case 8: - this._clipRect(ctx, geom, command); - break; - case 9: - this._clipRRect(ctx, geom, command); - break; - case 10: - this._clipPath(ctx, geom, command); - break; - case 11: - this._drawColor(ctx, geom, command); - break; - case 12: - this._drawLine(ctx, geom, command); - break; - case 13: - this._drawPaint(ctx, geom, command); - break; - case 14: - this._drawRect(ctx, geom, command); - break; - case 15: - this._drawRRect(ctx, geom, command); - break; - case 16: - this._drawDRRect(ctx, geom, command); - break; - case 17: - this._drawOval(ctx, geom, command); - break; - case 18: - this._drawCircle(ctx, geom, command); - break; - case 19: - this._drawPath(ctx, geom, command); - break; - case 20: - this._drawShadow(ctx, geom, command); - break; - default: - throw new Error(`Unsupported command ID: ${command[0]}`); - } - } - } - - _applyPaint(ctx, paint) { - let blendMode = _stringForBlendMode(paint.blendMode); - ctx.globalCompositeOperation = blendMode ? blendMode : 'source-over'; - ctx.lineWidth = paint.strokeWidth ? paint.strokeWidth : 1.0; - - let strokeCap = _stringForStrokeCap(paint.strokeCap); - ctx.lineCap = strokeCap ? strokeCap : 'butt'; - - if (paint.shader != null) { - let paintStyle = paint.shader.createPaintStyle(ctx); - ctx.fillStyle = paintStyle; - ctx.strokeStyle = paintStyle; - } else if (paint.color != null) { - let colorString = paint.color; - ctx.fillStyle = colorString; - ctx.strokeStyle = colorString; - } - if (paint.maskFilter != null) { - ctx.filter = `blur(${paint.maskFilter[1]}px)`; - } - } - - _strokeOrFill(ctx, paint, resetPaint) { - switch (paint.style) { - case PaintingStyle.stroke: - ctx.stroke(); - break; - case PaintingStyle.fill: - default: - ctx.fill(); - break; - } - if (resetPaint) { - this._resetPaint(ctx); - } - } - - _resetPaint(ctx) { - ctx.globalCompositeOperation = 'source-over'; - ctx.lineWidth = 1.0; - ctx.lineCap = 'butt'; - ctx.filter = 'none'; - ctx.fillStyle = null; - ctx.strokeStyle = null; - } - - _save(ctx, geom, command) { - ctx.save(); - } - - _restore(ctx, geom, command) { - ctx.restore(); - } - - _translate(ctx, geom, command) { - ctx.translate(command[1], command[2]); - } - - _scale(ctx, geom, command) { - ctx.translate(command[1], command[2]); - } - - _rotate(ctx, geom, command) { - ctx.rotate(command[1]); - } - - _skew(ctx, geom, command) { - ctx.translate(command[1], command[2]); - } - - _drawRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let rect = scanner.scanRect(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.rect(rect.left, rect.top, rect.width(), rect.height()); - this._strokeOrFill(ctx, paint, true); - } - - _drawRRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let rrect = scanner.scanRRect(); - let paint = scanner.scanPaint(); - - this._applyPaint(ctx, paint); - this._drawRRectPath(ctx, rrect, true); - this._strokeOrFill(ctx, paint, true); - } - - _drawDRRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let outer = scanner.scanRRect(); - let inner = scanner.scanRRect(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - this._drawRRectPath(ctx, outer, true); - this._drawRRectPathReverse(ctx, inner, false); - this._strokeOrFill(ctx, paint, true); - } - - _drawRRectPath(ctx, rrect, startNewPath) { - // TODO(mdebbar): there's a bug in this code, it doesn't correctly handle - // the case when the radius is greater than the width of the - // rect. When we fix that in BitmapCanvas, we need to fix it - // here too. - // To draw the rounded rectangle, perform the following 8 steps: - // 1. draw the line for the top - // 2. draw the arc for the top-right corner - // 3. draw the line for the right side - // 4. draw the arc for the bottom-right corner - // 5. draw the line for the bottom of the rectangle - // 6. draw the arc for the bottom-left corner - // 7. draw the line for the left side - // 8. draw the arc for the top-left corner - // - // After drawing, the current point will be the left side of the top of the - // rounded rectangle (after the corner). - // TODO(het): Confirm that this is the end point in Flutter for RRect - - if (startNewPath) { - ctx.beginPath(); - } - - ctx.moveTo(rrect.left + rrect.trRadiusX, rrect.top); - - // Top side and top-right corner - ctx.lineTo(rrect.right - rrect.trRadiusX, rrect.top); - ctx.ellipse( - rrect.right - rrect.trRadiusX, - rrect.top + rrect.trRadiusY, - rrect.trRadiusX, - rrect.trRadiusY, - 0, - 1.5 * Math.PI, - 2.0 * Math.PI, - false, - ); - - // Right side and bottom-right corner - ctx.lineTo(rrect.right, rrect.bottom - rrect.brRadiusY); - ctx.ellipse( - rrect.right - rrect.brRadiusX, - rrect.bottom - rrect.brRadiusY, - rrect.brRadiusX, - rrect.brRadiusY, - 0, - 0, - 0.5 * Math.PI, - false, - ); - - // Bottom side and bottom-left corner - ctx.lineTo(rrect.left + rrect.blRadiusX, rrect.bottom); - ctx.ellipse( - rrect.left + rrect.blRadiusX, - rrect.bottom - rrect.blRadiusY, - rrect.blRadiusX, - rrect.blRadiusY, - 0, - 0.5 * Math.PI, - Math.PI, - false, - ); - - // Left side and top-left corner - ctx.lineTo(rrect.left, rrect.top + rrect.tlRadiusY); - ctx.ellipse( - rrect.left + rrect.tlRadiusX, - rrect.top + rrect.tlRadiusY, - rrect.tlRadiusX, - rrect.tlRadiusY, - 0, - Math.PI, - 1.5 * Math.PI, - false, - ); - } - - _drawRRectPathReverse(ctx, rrect, startNewPath) { - // Draw the rounded rectangle, counterclockwise. - ctx.moveTo(rrect.right - rrect.trRadiusX, rrect.top); - - if (startNewPath) { - ctx.beginPath(); - } - - // Top side and top-left corner - ctx.lineTo(rrect.left + rrect.tlRadiusX, rrect.top); - ctx.ellipse( - rrect.left + rrect.tlRadiusX, - rrect.top + rrect.tlRadiusY, - rrect.tlRadiusX, - rrect.tlRadiusY, - 0, - 1.5 * Math.PI, - Math.PI, - true, - ); - - // Left side and bottom-left corner - ctx.lineTo(rrect.left, rrect.bottom - rrect.blRadiusY); - ctx.ellipse( - rrect.left + rrect.blRadiusX, - rrect.bottom - rrect.blRadiusY, - rrect.blRadiusX, - rrect.blRadiusY, - 0, - Math.PI, - 0.5 * Math.PI, - true, - ); - - // Bottom side and bottom-right corner - ctx.lineTo(rrect.right - rrect.brRadiusX, rrect.bottom); - ctx.ellipse( - rrect.right - rrect.brRadiusX, - rrect.bottom - rrect.brRadiusY, - rrect.brRadiusX, - rrect.brRadiusY, - 0, - 0.5 * Math.PI, - 0, - true, - ); - - // Right side and top-right corner - ctx.lineTo(rrect.right, rrect.top + rrect.trRadiusY); - ctx.ellipse( - rrect.right - rrect.trRadiusX, - rrect.top + rrect.trRadiusY, - rrect.trRadiusX, - rrect.trRadiusY, - 0, - 0, - 1.5 * Math.PI, - true, - ); - } - - _clipRect(ctx, geom, command) { - let scanner = _scanCommand(command); - let rect = scanner.scanRect(); - ctx.beginPath(); - ctx.rect(rect.left, rect.top, rect.width(), rect.height()); - ctx.clip(); - } - - _clipRRect(ctx, geom, command) { - let path = new Path([]); - let commands = [new RRectCommand(command[1])]; - path.subpaths.push(new Subpath(commands)); - this._runPath(ctx, path); - ctx.clip(); - } - - _clipPath(ctx, geom, command) { - let scanner = _scanCommand(command); - let path = scanner.scanPath(); - this._runPath(ctx, path); - ctx.clip(); - } - - _drawCircle(ctx, geom, command) { - let scanner = _scanCommand(command); - let dx = scanner.scanNumber(); - let dy = scanner.scanNumber(); - let radius = scanner.scanNumber(); - let paint = scanner.scanPaint(); - - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.ellipse(dx, dy, radius, radius, 0, 0, 2.0 * Math.PI, false); - this._strokeOrFill(ctx, paint, true); - } - - _drawOval(ctx, geom, command) { - let scanner = _scanCommand(command); - let rect = scanner.scanRect(); - let paint = scanner.scanPaint(); - - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.ellipse( - (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, - rect.width / 2, rect.height / 2, 0, 0, 2.0 * Math.PI, false); - this._strokeOrFill(ctx, paint, true); - } - - _drawPath(ctx, geom, command) { - let scanner = _scanCommand(command); - let path = scanner.scanPath(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - this._runPath(ctx, path); - this._strokeOrFill(ctx, paint, true); - } - - _drawShadow(ctx, geom, command) { - // TODO: this is mostly a stub; implement properly. - let scanner = _scanCommand(command); - let path = scanner.scanPath(); - let color = scanner.scanArray(); - let elevation = scanner.scanNumber(); - let transparentOccluder = scanner.scanBool(); - - let shadows = _computeShadowsForElevation(elevation, color); - for (let i = 0; i < shadows.length; i++) { - let shadow = shadows[i]; - - let paint = new Paint( - null, // blendMode - PaintingStyle.fill, // style - 1.0, // strokeWidth - null, // strokeCap - true, // isAntialias - shadow.color, // color - null, // shader - [BlurStyle.normal, shadow.blur], // maskFilter - null, // filterQuality - null // colorFilter - ); - - ctx.save(); - ctx.translate(shadow.offsetX, shadow.offsetY); - this._applyPaint(ctx, paint); - this._runPath(ctx, path, true); - this._strokeOrFill(ctx, paint, false); - ctx.restore(); - } - this._resetPaint(ctx); - } - - _runPath(ctx, path) { - ctx.beginPath(); - for (let i = 0; i < path.subpaths.length; i++) { - let subpath = path.subpaths[i]; - for (let j = 0; j < subpath.commands.length; j++) { - let command = subpath.commands[j]; - switch (command.type()) { - case PathCommandType.bezierCurveTo: - ctx.bezierCurveTo( - command.x1, command.y1, command.x2, command.y2, command.x3, - command.y3); - break; - case PathCommandType.close: - ctx.closePath(); - break; - case PathCommandType.ellipse: - ctx.ellipse( - command.x, command.y, command.radiusX, command.radiusY, - command.rotation, command.startAngle, command.endAngle, - command.anticlockwise); - break; - case PathCommandType.lineTo: - ctx.lineTo(command.x, command.y); - break; - case PathCommandType.moveTo: - ctx.moveTo(command.x, command.y); - break; - case PathCommandType.rrect: - this._drawRRectPath(ctx, command.rrect, false); - break; - case PathCommandType.rect: - ctx.rect(command.x, command.y, command.width, command.height); - break; - case PathCommandType.quadraticCurveTo: - ctx.quadraticCurveTo( - command.x1, command.y1, command.x2, command.y2); - break; - default: - throw new Error(`Unknown path command ${command.type()}`); - } - } - } - } - - _drawColor(ctx, geom, command) { - ctx.globalCompositeOperation = _stringForBlendMode(command[2]); - ctx.fillStyle = command[1]; - - // Fill a virtually infinite rect with the color. - // - // We can't use (0, 0, width, height) because the current transform can - // cause it to not fill the entire clip. - ctx.fillRect(-10000, -10000, 20000, 20000); - this._resetPaint(ctx); - } - - _drawLine(ctx, geom, command) { - let scanner = _scanCommand(command); - let p1dx = scanner.scanNumber(); - let p1dy = scanner.scanNumber(); - let p2dx = scanner.scanNumber(); - let p2dy = scanner.scanNumber(); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - ctx.beginPath(); - ctx.moveTo(p1dx, p1dy); - ctx.lineTo(p2dx, p2dy); - ctx.stroke(); - this._resetPaint(ctx); - } - - _drawPaint(ctx, geom, command) { - let scanner = _scanCommand(command); - let paint = scanner.scanPaint(); - this._applyPaint(ctx, paint); - ctx.beginPath(); - - // Fill a virtually infinite rect with the color. - // - // We can't use (0, 0, width, height) because the current transform can - // cause it to not fill the entire clip. - ctx.fillRect(-10000, -10000, 20000, 20000); - this._resetPaint(ctx); - } -} - -function _scanCommand(command) { - return new CommandScanner(command); -} - -const PaintingStyle = { - fill: 0, - stroke: 1, -}; - -/// A singleton used to parse serialized commands. -class CommandScanner { - constructor(command) { - // Skip the first element, which is always the command ID. - this.index = 1; - this.command = command; - } - - scanRect() { - let rect = this.command[this.index++]; - return new Rect(rect[0], rect[1], rect[2], rect[3]); - } - - scanRRect() { - let rrect = this.command[this.index++]; - return new RRect( - rrect[0], rrect[1], rrect[2], rrect[3], rrect[4], rrect[5], rrect[6], - rrect[7], rrect[8], rrect[9], rrect[10], rrect[11]); - } - - scanPaint() { - let paint = this.command[this.index++]; - return new Paint( - paint[0], paint[1], paint[2], paint[3], paint[4], paint[5], paint[6], - paint[7], paint[8], paint[9]); - } - - scanNumber() { - return this.command[this.index++]; - } - - scanString() { - return this.command[this.index++]; - } - - scanBool() { - return this.command[this.index++]; - } - - scanPath() { - let subpaths = this.command[this.index++]; - return new Path(subpaths); - } - - scanArray() { - return this.command[this.index++]; - } -} - -class Rect { - constructor(left, top, right, bottom) { - this.left = left; - this.top = top; - this.right = right; - this.bottom = bottom; - } - - width() { - return this.right - this.left; - } - - height() { - return this.bottom - this.top; - } -} - -class RRect { - constructor( - left, top, right, bottom, tlRadiusX, tlRadiusY, trRadiusX, trRadiusY, - brRadiusX, brRadiusY, blRadiusX, blRadiusY) { - this.left = left; - this.top = top; - this.right = right; - this.bottom = bottom; - this.tlRadiusX = tlRadiusX; - this.tlRadiusY = tlRadiusY; - this.trRadiusX = trRadiusX; - this.trRadiusY = trRadiusY; - this.brRadiusX = brRadiusX; - this.brRadiusY = brRadiusY; - this.blRadiusX = blRadiusX; - this.blRadiusY = blRadiusY; - } - - tallMiddleRect() { - let leftRadius = Math.max(this.blRadiusX, this.tlRadiusX); - let rightRadius = Math.max(this.trRadiusX, this.brRadiusX); - return new Rect( - this.left + leftRadius, this.top, this.right - rightRadius, - this.bottom); - } - - middleRect() { - let leftRadius = Math.max(this.blRadiusX, this.tlRadiusX); - let topRadius = Math.max(this.tlRadiusY, this.trRadiusY); - let rightRadius = Math.max(this.trRadiusX, this.brRadiusX); - let bottomRadius = Math.max(this.brRadiusY, this.blRadiusY); - return new Rect( - this.left + leftRadius, this.top + topRadius, this.right - rightRadius, - this.bottom - bottomRadius); - } - - wideMiddleRect() { - let topRadius = Math.max(this.tlRadiusY, this.trRadiusY); - let bottomRadius = Math.max(this.brRadiusY, this.blRadiusY); - return new Rect( - this.left, this.top + topRadius, this.right, - this.bottom - bottomRadius); - } -} - -class Paint { - constructor( - blendMode, style, strokeWidth, strokeCap, isAntialias, color, shader, - maskFilter, filterQuality, colorFilter) { - this.blendMode = blendMode; - this.style = style; - this.strokeWidth = strokeWidth; - this.strokeCap = strokeCap; - this.isAntialias = isAntialias; - this.color = color; - this.shader = _deserializeShader(shader); // TODO: deserialize - this.maskFilter = maskFilter; - this.filterQuality = filterQuality; - this.colorFilter = colorFilter; // TODO: deserialize - } -} - -function _deserializeShader(data) { - if (!data) { - return null; - } - - switch (data[0]) { - case 1: - return new GradientLinear(data); - default: - throw new Error(`Shader type not supported: ${data}`); - } -} - -class GradientLinear { - constructor(data) { - this.fromX = data[1]; - this.fromY = data[2]; - this.toX = data[3]; - this.toY = data[4]; - this.colors = data[5]; - this.colorStops = data[6]; - this.tileMode = data[7]; - } - - createPaintStyle(ctx) { - let gradient = - ctx.createLinearGradient(this.fromX, this.fromY, this.toX, this.toY); - if (this.colorStops == null) { - gradient.addColorStop(0, this.colors[0]); - gradient.addColorStop(1, this.colors[1]); - return gradient; - } - for (let i = 0; i < this.colors.length; i++) { - gradient.addColorStop(this.colorStops[i], this.colors[i]); - } - return gradient; - } -} - -const BlendMode = { - clear: 0, - src: 1, - dst: 2, - srcOver: 3, - dstOver: 4, - srcIn: 5, - dstIn: 6, - srcOut: 7, - dstOut: 8, - srcATop: 9, - dstATop: 10, - xor: 11, - plus: 12, - modulate: 13, - screen: 14, - overlay: 15, - darken: 16, - lighten: 17, - colorDodge: 18, - colorBurn: 19, - hardLight: 20, - softLight: 21, - difference: 22, - exclusion: 23, - multiply: 24, - hue: 25, - saturation: 26, - color: 27, - luminosity: 28, -}; - -function _stringForBlendMode(blendMode) { - if (blendMode == null) return null; - switch (blendMode) { - case BlendMode.srcOver: - return 'source-over'; - case BlendMode.srcIn: - return 'source-in'; - case BlendMode.srcOut: - return 'source-out'; - case BlendMode.srcATop: - return 'source-atop'; - case BlendMode.dstOver: - return 'destination-over'; - case BlendMode.dstIn: - return 'destination-in'; - case BlendMode.dstOut: - return 'destination-out'; - case BlendMode.dstATop: - return 'destination-atop'; - case BlendMode.plus: - return 'lighten'; - case BlendMode.src: - return 'copy'; - case BlendMode.xor: - return 'xor'; - case BlendMode.multiply: - // Falling back to multiply, ignoring alpha channel. - // TODO(flutter_web): only used for debug, find better fallback for web. - case BlendMode.modulate: - return 'multiply'; - case BlendMode.screen: - return 'screen'; - case BlendMode.overlay: - return 'overlay'; - case BlendMode.darken: - return 'darken'; - case BlendMode.lighten: - return 'lighten'; - case BlendMode.colorDodge: - return 'color-dodge'; - case BlendMode.colorBurn: - return 'color-burn'; - case BlendMode.hardLight: - return 'hard-light'; - case BlendMode.softLight: - return 'soft-light'; - case BlendMode.difference: - return 'difference'; - case BlendMode.exclusion: - return 'exclusion'; - case BlendMode.hue: - return 'hue'; - case BlendMode.saturation: - return 'saturation'; - case BlendMode.color: - return 'color'; - case BlendMode.luminosity: - return 'luminosity'; - default: - throw new Error( - 'Flutter web does not support the blend mode: $blendMode'); - } -} - -const StrokeCap = { - butt: 0, - round: 1, - square: 2, -}; - -function _stringForStrokeCap(strokeCap) { - if (strokeCap == null) return null; - switch (strokeCap) { - case StrokeCap.butt: - return 'butt'; - case StrokeCap.round: - return 'round'; - case StrokeCap.square: - default: - return 'square'; - } -} - -class Path { - constructor(serializedSubpaths) { - this.subpaths = []; - for (let i = 0; i < serializedSubpaths.length; i++) { - let subpath = serializedSubpaths[i]; - let pathCommands = []; - for (let j = 0; j < subpath.length; j++) { - let pathCommand = subpath[j]; - switch (pathCommand[0]) { - case 1: - pathCommands.push(new MoveTo(pathCommand)); - break; - case 2: - pathCommands.push(new LineTo(pathCommand)); - break; - case 3: - pathCommands.push(new Ellipse(pathCommand)); - break; - case 4: - pathCommands.push(new QuadraticCurveTo(pathCommand)); - break; - case 5: - pathCommands.push(new BezierCurveTo(pathCommand)); - break; - case 6: - pathCommands.push(new RectCommand(pathCommand)); - break; - case 7: - pathCommands.push(new RRectCommand(pathCommand)); - break; - case 8: - pathCommands.push(new CloseCommand()); - break; - default: - throw new Error(`Unsupported path command: ${pathCommand}`); - } - } - - this.subpaths.push(new Subpath(pathCommands)); - } - } -} - -class Subpath { - constructor(commands) { - this.commands = commands; - } -} - -class MoveTo { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - } - - type() { - return PathCommandType.moveTo; - } -} - -class LineTo { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - } - - type() { - return PathCommandType.lineTo; - } -} - -class Ellipse { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - this.radiusX = data[3]; - this.radiusY = data[4]; - this.rotation = data[5]; - this.startAngle = data[6]; - this.endAngle = data[7]; - this.anticlockwise = data[8]; - } - - type() { - return PathCommandType.ellipse; - } -} - -class QuadraticCurveTo { - constructor(data) { - this.x1 = data[1]; - this.y1 = data[2]; - this.x2 = data[3]; - this.y2 = data[4]; - } - - type() { - return PathCommandType.quadraticCurveTo; - } -} - -class BezierCurveTo { - constructor(data) { - this.x1 = data[1]; - this.y1 = data[2]; - this.x2 = data[3]; - this.y2 = data[4]; - this.x3 = data[5]; - this.y3 = data[6]; - } - - type() { - return PathCommandType.bezierCurveTo; - } -} - -class RectCommand { - constructor(data) { - this.x = data[1]; - this.y = data[2]; - this.width = data[3]; - this.height = data[4]; - } - - type() { - return PathCommandType.rect; - } -} - -class RRectCommand { - constructor(data) { - let scanner = _scanCommand(data); - this.rrect = scanner.scanRRect(); - } - - type() { - return PathCommandType.rrect; - } -} - -class CloseCommand { - type() { - return PathCommandType.close; - } -} - -class CanvasShadow { - constructor(offsetX, offsetY, blur, spread, color) { - this.offsetX = offsetX; - this.offsetY = offsetY; - this.blur = blur; - this.spread = spread; - this.color = color; - } -} - -const _noShadows = []; - -function _computeShadowsForElevation(elevation, color) { - if (elevation <= 0.0) { - return _noShadows; - } else if (elevation <= 1.0) { - return _computeShadowElevation(2, color); - } else if (elevation <= 2.0) { - return _computeShadowElevation(4, color); - } else if (elevation <= 3.0) { - return _computeShadowElevation(6, color); - } else if (elevation <= 4.0) { - return _computeShadowElevation(8, color); - } else if (elevation <= 5.0) { - return _computeShadowElevation(16, color); - } else { - return _computeShadowElevation(24, color); - } -} - -function _computeShadowElevation(dp, color) { - // TODO(yjbanov): multiple shadows are very expensive. Find a more efficient - // method to render them. - let red = color[1]; - let green = color[2]; - let blue = color[3]; - - // let penumbraColor = `rgba(${red}, ${green}, ${blue}, 0.14)`; - // let ambientShadowColor = `rgba(${red}, ${green}, ${blue}, 0.12)`; - let umbraColor = `rgba(${red}, ${green}, ${blue}, 0.2)`; - - let result = []; - if (dp === 2) { - // result.push(new CanvasShadow(0.0, 2.0, 1.0, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 3.0, 0.5, -2.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 1.0, 2.5, 0.0, umbraColor)); - } else if (dp === 3) { - // result.push(new CanvasShadow(0.0, 1.5, 4.0, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 3.0, 1.5, -2.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 1.0, 4.0, 0.0, umbraColor)); - } else if (dp === 4) { - // result.push(new CanvasShadow(0.0, 4.0, 2.5, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 1.0, 5.0, 0.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 2.0, 2.0, -1.0, umbraColor)); - } else if (dp === 6) { - // result.push(new CanvasShadow(0.0, 6.0, 5.0, 0.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 1.0, 9.0, 0.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 3.0, 2.5, -1.0, umbraColor)); - } else if (dp === 8) { - // result.push(new CanvasShadow(0.0, 4.0, 10.0, 1.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 3.0, 7.0, 2.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 5.0, 2.5, -3.0, umbraColor)); - } else if (dp === 12) { - // result.push(new CanvasShadow(0.0, 12.0, 8.5, 2.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 5.0, 11.0, 4.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 7.0, 4.0, -4.0, umbraColor)); - } else if (dp === 16) { - // result.push(new CanvasShadow(0.0, 16.0, 12.0, 2.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 6.0, 15.0, 5.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 0.0, 5.0, -5.0, umbraColor)); - } else { - // result.push(new CanvasShadow(0.0, 24.0, 18.0, 3.0, penumbraColor)); - // result.push(new CanvasShadow(0.0, 9.0, 23.0, 8.0, ambientShadowColor)); - result.push(new CanvasShadow(0.0, 11.0, 7.5, -7.0, umbraColor)); - } - return result; -} - -const PathCommandType = { - moveTo: 0, - lineTo: 1, - ellipse: 2, - close: 3, - quadraticCurveTo: 4, - bezierCurveTo: 5, - rect: 6, - rrect: 7, -}; - -const TileMode = { - clamp: 0, - repeated: 1, -}; - -const BlurStyle = { - normal: 0, - solid: 1, - outer: 2, - inner: 3, -}; - -/// This makes the painter available as "background-image: paint(flt)". -registerPaint('flt', FlutterPainter); diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 165a672cebb57..232b00e4c6028 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -27,43 +27,70 @@ part 'engine/assets.dart'; part 'engine/bitmap_canvas.dart'; part 'engine/browser_detection.dart'; part 'engine/browser_location.dart'; +part 'engine/canvaskit/canvas.dart'; +part 'engine/canvaskit/canvaskit_canvas.dart'; +part 'engine/canvaskit/canvaskit_api.dart'; +part 'engine/canvaskit/color_filter.dart'; +part 'engine/canvaskit/embedded_views.dart'; +part 'engine/canvaskit/fonts.dart'; +part 'engine/canvaskit/image.dart'; +part 'engine/canvaskit/image_filter.dart'; +part 'engine/canvaskit/initialization.dart'; +part 'engine/canvaskit/layer.dart'; +part 'engine/canvaskit/layer_scene_builder.dart'; +part 'engine/canvaskit/layer_tree.dart'; +part 'engine/canvaskit/mask_filter.dart'; +part 'engine/canvaskit/n_way_canvas.dart'; +part 'engine/canvaskit/path.dart'; +part 'engine/canvaskit/painting.dart'; +part 'engine/canvaskit/path_metrics.dart'; +part 'engine/canvaskit/picture.dart'; +part 'engine/canvaskit/picture_recorder.dart'; +part 'engine/canvaskit/platform_message.dart'; +part 'engine/canvaskit/raster_cache.dart'; +part 'engine/canvaskit/rasterizer.dart'; +part 'engine/canvaskit/shader.dart'; +part 'engine/canvaskit/skia_object_cache.dart'; +part 'engine/canvaskit/surface.dart'; +part 'engine/canvaskit/text.dart'; +part 'engine/canvaskit/util.dart'; +part 'engine/canvaskit/vertices.dart'; +part 'engine/canvaskit/viewport_metrics.dart'; part 'engine/canvas_pool.dart'; part 'engine/clipboard.dart'; part 'engine/color_filter.dart'; -part 'engine/compositor/canvas.dart'; -part 'engine/compositor/canvas_kit_canvas.dart'; -part 'engine/compositor/canvaskit_api.dart'; -part 'engine/compositor/color_filter.dart'; -part 'engine/compositor/embedded_views.dart'; -part 'engine/compositor/fonts.dart'; -part 'engine/compositor/image.dart'; -part 'engine/compositor/image_filter.dart'; -part 'engine/compositor/initialization.dart'; -part 'engine/compositor/layer.dart'; -part 'engine/compositor/layer_scene_builder.dart'; -part 'engine/compositor/layer_tree.dart'; -part 'engine/compositor/mask_filter.dart'; -part 'engine/compositor/n_way_canvas.dart'; -part 'engine/compositor/path.dart'; -part 'engine/compositor/painting.dart'; -part 'engine/compositor/path_metrics.dart'; -part 'engine/compositor/picture.dart'; -part 'engine/compositor/picture_recorder.dart'; -part 'engine/compositor/platform_message.dart'; -part 'engine/compositor/raster_cache.dart'; -part 'engine/compositor/rasterizer.dart'; -part 'engine/compositor/skia_object_cache.dart'; -part 'engine/compositor/surface.dart'; -part 'engine/compositor/text.dart'; -part 'engine/compositor/util.dart'; -part 'engine/compositor/vertices.dart'; -part 'engine/compositor/viewport_metrics.dart'; part 'engine/dom_canvas.dart'; part 'engine/dom_renderer.dart'; part 'engine/engine_canvas.dart'; part 'engine/frame_reference.dart'; part 'engine/history.dart'; -part 'engine/houdini_canvas.dart'; +part 'engine/html/backdrop_filter.dart'; +part 'engine/html/canvas.dart'; +part 'engine/html/clip.dart'; +part 'engine/html/debug_canvas_reuse_overlay.dart'; +part 'engine/html/image_filter.dart'; +part 'engine/html/offset.dart'; +part 'engine/html/opacity.dart'; +part 'engine/html/painting.dart'; +part 'engine/html/path/conic.dart'; +part 'engine/html/path/cubic.dart'; +part 'engine/html/path/path.dart'; +part 'engine/html/path/path_metrics.dart'; +part 'engine/html/path/path_ref.dart'; +part 'engine/html/path/path_to_svg.dart'; +part 'engine/html/path/path_utils.dart'; +part 'engine/html/path/path_windings.dart'; +part 'engine/html/path/tangent.dart'; +part 'engine/html/picture.dart'; +part 'engine/html/platform_view.dart'; +part 'engine/html/recording_canvas.dart'; +part 'engine/html/render_vertices.dart'; +part 'engine/html/scene.dart'; +part 'engine/html/scene_builder.dart'; +part 'engine/html/shader.dart'; +part 'engine/html/surface.dart'; +part 'engine/html/surface_stats.dart'; +part 'engine/html/transform.dart'; part 'engine/html_image_codec.dart'; part 'engine/keyboard.dart'; part 'engine/mouse_cursor.dart'; @@ -90,34 +117,7 @@ part 'engine/services/buffers.dart'; part 'engine/services/message_codec.dart'; part 'engine/services/message_codecs.dart'; part 'engine/services/serialization.dart'; -part 'engine/shader.dart'; part 'engine/shadow.dart'; -part 'engine/surface/backdrop_filter.dart'; -part 'engine/surface/canvas.dart'; -part 'engine/surface/clip.dart'; -part 'engine/surface/debug_canvas_reuse_overlay.dart'; -part 'engine/surface/image_filter.dart'; -part 'engine/surface/offset.dart'; -part 'engine/surface/opacity.dart'; -part 'engine/surface/painting.dart'; -part 'engine/surface/path/conic.dart'; -part 'engine/surface/path/cubic.dart'; -part 'engine/surface/path/path.dart'; -part 'engine/surface/path/path_metrics.dart'; -part 'engine/surface/path/path_ref.dart'; -part 'engine/surface/path/path_to_svg.dart'; -part 'engine/surface/path/path_utils.dart'; -part 'engine/surface/path/path_windings.dart'; -part 'engine/surface/path/tangent.dart'; -part 'engine/surface/picture.dart'; -part 'engine/surface/platform_view.dart'; -part 'engine/surface/recording_canvas.dart'; -part 'engine/surface/render_vertices.dart'; -part 'engine/surface/scene.dart'; -part 'engine/surface/scene_builder.dart'; -part 'engine/surface/surface.dart'; -part 'engine/surface/surface_stats.dart'; -part 'engine/surface/transform.dart'; part 'engine/test_embedding.dart'; part 'engine/text/font_collection.dart'; part 'engine/text/line_break_properties.dart'; diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index bd3fc5390b8ea..1954fa5231363 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -193,9 +193,9 @@ class BitmapCanvas extends EngineCanvas { /// /// See also: /// - /// * [PersistedStandardPicture._applyBitmapPaint] which uses this method to + /// * [PersistedPicture._applyBitmapPaint] which uses this method to /// decide whether to reuse this canvas or not. - /// * [PersistedStandardPicture._recycleCanvas] which also uses this method + /// * [PersistedPicture._recycleCanvas] which also uses this method /// for the same reason. bool isReusable() { return _devicePixelRatio == EngineWindow.browserDevicePixelRatio; @@ -965,7 +965,9 @@ List _clipContent(List<_SaveClipEntry> clipStack, ..height = '${roundRect.bottom - clipOffsetY}px'; setElementTransform(curElement, newClipTransform.storage); } else if (entry.path != null) { - curElement.style.transform = matrix4ToCssTransform(newClipTransform); + curElement.style + ..transform = matrix4ToCssTransform(newClipTransform) + ..transformOrigin = '0 0 0'; String svgClipPath = createSvgClipDef(curElement as html.HtmlElement, entry.path!); final html.Element clipElement = html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer()); diff --git a/lib/web_ui/lib/src/engine/browser_detection.dart b/lib/web_ui/lib/src/engine/browser_detection.dart index 0a4f3cfac291e..1ada5e37ed512 100644 --- a/lib/web_ui/lib/src/engine/browser_detection.dart +++ b/lib/web_ui/lib/src/engine/browser_detection.dart @@ -159,3 +159,32 @@ bool get isDesktop => _desktopOperatingSystems.contains(operatingSystem); /// See [_desktopOperatingSystems]. /// See [isDesktop]. bool get isMobile => !isDesktop; + +int? _cachedWebGLVersion; + +/// The highest WebGL version supported by the current browser, or -1 if WebGL +/// is not supported. +int get webGLVersion => _cachedWebGLVersion ?? (_cachedWebGLVersion = _detectWebGLVersion()); + +/// Detects the highest WebGL version supported by the current browser, or +/// -1 if WebGL is not supported. +/// +/// Chrome reports that `WebGL2RenderingContext` is available even when WebGL 2 is +/// disabled due hardware-specific issues. This happens, for example, on Chrome on +/// Moto E5. Therefore checking for the presence of `WebGL2RenderingContext` or +/// using the current [browserEngine] is insufficient. +/// +/// Our CanvasKit backend is affected due to: https://github.com/emscripten-core/emscripten/issues/11819 +int _detectWebGLVersion() { + final html.CanvasElement canvas = html.CanvasElement( + width: 1, + height: 1, + ); + if (canvas.getContext('webgl2') != null) { + return 2; + } + if (canvas.getContext('webgl') != null) { + return 1; + } + return -1; +} diff --git a/lib/web_ui/lib/src/engine/compositor/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart similarity index 96% rename from lib/web_ui/lib/src/engine/compositor/canvas.dart rename to lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 690d5b29b355c..3426253473469 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -5,7 +5,10 @@ // @dart = 2.10 part of engine; -/// A Dart wrapper around Skia's SKCanvas. +/// A Dart wrapper around Skia's [SkCanvas]. +/// +/// This is intentionally not memory-managing the underlying [SkCanvas]. See +/// the docs on [SkCanvas], which explain the reason. class CkCanvas { final SkCanvas skCanvas; @@ -203,7 +206,7 @@ class CkCanvas { ui.Vertices vertices, ui.BlendMode blendMode, CkPaint paint) { CkVertices skVertices = vertices as CkVertices; skCanvas.drawVertices( - skVertices.skVertices, + skVertices.skiaObject, toSkBlendMode(blendMode), paint.skiaObject, ); diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart similarity index 83% rename from lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart rename to lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index ab51ab4134a44..8a67398f07181 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -4,7 +4,7 @@ /// Bindings for CanvasKit JavaScript API. /// -/// Prefer keeping the originl CanvasKit names so it is easier to locate +/// Prefer keeping the original CanvasKit names so it is easier to locate /// the API behind these bindings in the Skia source code. // @dart = 2.10 @@ -17,10 +17,11 @@ late CanvasKit canvasKit; /// static APIs. /// /// See, e.g. [SkPaint]. -@JS('window.flutter_canvas_kit') +@JS('window.flutterCanvasKit') external set windowFlutterCanvasKit(CanvasKit value); @JS() +@anonymous class CanvasKit { external SkBlendModeEnum get BlendMode; external SkPaintStyleEnum get PaintStyle; @@ -30,6 +31,8 @@ class CanvasKit { external SkBlurStyleEnum get BlurStyle; external SkTileModeEnum get TileMode; external SkFillTypeEnum get FillType; + external SkAlphaTypeEnum get AlphaType; + external SkColorTypeEnum get ColorType; external SkPathOpEnum get PathOp; external SkClipOpEnum get ClipOp; external SkPointModeEnum get PointMode; @@ -43,7 +46,8 @@ class CanvasKit { external SkFontSlantEnum get FontSlant; external SkAnimatedImage MakeAnimatedImageFromEncoded(Uint8List imageData); external SkShaderNamespace get SkShader; - external SkMaskFilter MakeBlurMaskFilter(SkBlurStyle blurStyle, double sigma, bool respectCTM); + external SkMaskFilter MakeBlurMaskFilter( + SkBlurStyle blurStyle, double sigma, bool respectCTM); external SkColorFilterNamespace get SkColorFilter; external SkImageFilterNamespace get SkImageFilter; external SkPath MakePathFromOp(SkPath path1, SkPath path2, SkPathOp pathOp); @@ -57,8 +61,16 @@ class CanvasKit { Uint16List? indices, ); external SkParagraphBuilderNamespace get ParagraphBuilder; - external SkParagraphStyle ParagraphStyle(SkParagraphStyleProperties properties); + external SkParagraphStyle ParagraphStyle( + SkParagraphStyleProperties properties); external SkTextStyle TextStyle(SkTextStyleProperties properties); + external SkSurface MakeSurface( + int width, + int height, + ); + external Uint8List getSkDataBytes( + SkData skData, + ); // Text decoration enum is embedded in the CanvasKit object itself. external int get NoDecoration; @@ -68,12 +80,14 @@ class CanvasKit { // End of text decoration enum. external SkFontMgrNamespace get SkFontMgr; - external int GetWebGLContext(html.CanvasElement canvas, SkWebGLContextOptions options); + external TypefaceFontProviderNamespace get TypefaceFontProvider; + external int GetWebGLContext( + html.CanvasElement canvas, SkWebGLContextOptions options); external SkGrContext MakeGrContext(int glContext); external SkSurface MakeOnScreenGLSurface( SkGrContext grContext, - double width, - double height, + int width, + int height, SkColorSpace colorSpace, ); external SkSurface MakeSWCanvasSurface(html.CanvasElement canvas); @@ -100,7 +114,7 @@ class CanvasKitInitPromise { external void then(CanvasKitInitCallback callback); } -@JS('window.flutter_canvas_kit.SkColorSpace.SRGB') +@JS('window.flutterCanvasKit.SkColorSpace.SRGB') external SkColorSpace get SkColorSpaceSRGB; @JS() @@ -111,6 +125,8 @@ class SkColorSpace {} class SkWebGLContextOptions { external factory SkWebGLContextOptions({ required int anitalias, + // WebGL version: 1 or 2. + required int majorVersion, }); } @@ -121,9 +137,11 @@ class SkSurface { external int width(); external int height(); external void dispose(); + external SkImage makeImageSnapshot(); } @JS() +@anonymous class SkGrContext { external void setResourceCacheLimitBytes(int limit); external void releaseResourcesAndAbandonContext(); @@ -569,7 +587,6 @@ SkStrokeJoin toSkStrokeJoin(ui.StrokeJoin strokeJoin) { return _skStrokeJoins[strokeJoin.index]; } - @JS() class SkFilterQualityEnum { external SkFilterQuality get None; @@ -594,7 +611,6 @@ SkFilterQuality toSkFilterQuality(ui.FilterQuality filterQuality) { return _skFilterQualitys[filterQuality.index]; } - @JS() class SkTileModeEnum { external SkTileMode get Clamp; @@ -618,14 +634,50 @@ SkTileMode toSkTileMode(ui.TileMode mode) { } @JS() +class SkAlphaTypeEnum { + external SkAlphaType get Opaque; + external SkAlphaType get Premul; + external SkAlphaType get Unpremul; +} + +@JS() +class SkAlphaType { + external int get value; +} + +@JS() +class SkColorTypeEnum { + external SkColorType get Alpha_8; + external SkColorType get RGB_565; + external SkColorType get ARGB_4444; + external SkColorType get RGBA_8888; + external SkColorType get RGB_888x; + external SkColorType get BGRA_8888; + external SkColorType get RGBA_1010102; + external SkColorType get RGB_101010x; + external SkColorType get Gray_8; + external SkColorType get RGBA_F16; + external SkColorType get RGBA_F32; +} + +@JS() +class SkColorType { + external int get value; +} + +@JS() +@anonymous class SkAnimatedImage { external int getFrameCount(); + /// Returns duration in milliseconds. external int getRepetitionCount(); external int decodeNextFrame(); external SkImage getCurrentFrame(); external int width(); external int height(); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); /// Deletes the C++ object. /// @@ -634,11 +686,18 @@ class SkAnimatedImage { } @JS() +@anonymous class SkImage { external void delete(); external int width(); external int height(); - external SkShader makeShader(SkTileMode tileModeX, SkTileMode tileModeY); + external SkShader makeShader( + SkTileMode tileModeX, + SkTileMode tileModeY, + Float32List? matrix, // 3x3 matrix + ); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); } @JS() @@ -672,18 +731,31 @@ class SkShaderNamespace { Float32List? matrix, // 3x3 matrix int flags, ); + + external SkShader MakeSweepGradient( + double cx, + double cy, + List colors, + Float32List colorStops, + SkTileMode tileMode, + Float32List? matrix, // 3x3 matrix + int flags, + double startAngle, + double endAngle, + ); } @JS() +@anonymous class SkShader { - + external void delete(); } // This needs to be bound to top-level because SkPaint is initialized // with `new`. Also in Dart you can't write this: // // external SkPaint SkPaint(); -@JS('window.flutter_canvas_kit.SkPaint') +@JS('window.flutterCanvasKit.SkPaint') class SkPaint { // TODO(yjbanov): implement invertColors, see paint.cc external SkPaint(); @@ -704,6 +776,7 @@ class SkPaint { } @JS() +@anonymous class SkMaskFilter { external void delete(); } @@ -719,6 +792,7 @@ class SkColorFilterNamespace { } @JS() +@anonymous class SkColorFilter { external void delete(); } @@ -740,6 +814,7 @@ class SkImageFilterNamespace { } @JS() +@anonymous class SkImageFilter { external void delete(); } @@ -816,7 +891,7 @@ external _NativeFloat32ArrayType get _nativeFloat32ArrayType; @JS() class _NativeFloat32ArrayType {} -@JS('window.flutter_canvas_kit.Malloc') +@JS('window.flutterCanvasKit.Malloc') external SkFloat32List _mallocFloat32List( _NativeFloat32ArrayType float32ListType, int size, @@ -835,7 +910,7 @@ SkFloat32List mallocFloat32List(int size) { /// The [list] is no longer usable after calling this function. /// /// Use this function to free lists owned by the engine. -@JS('window.flutter_canvas_kit.Free') +@JS('window.flutterCanvasKit.Free') external void freeFloat32List(SkFloat32List list); /// Wraps a [Float32List] backed by WASM memory. @@ -873,6 +948,7 @@ Float32List _populateSkColor(SkFloat32List skColor, ui.Color color) { Float32List toSharedSkColor1(ui.Color color) { return _populateSkColor(_sharedSkColor1, color); } + final SkFloat32List _sharedSkColor1 = mallocFloat32List(4); /// Unpacks the [color] into CanvasKit-compatible representation stored @@ -883,6 +959,7 @@ final SkFloat32List _sharedSkColor1 = mallocFloat32List(4); Float32List toSharedSkColor2(ui.Color color) { return _populateSkColor(_sharedSkColor2, color); } + final SkFloat32List _sharedSkColor2 = mallocFloat32List(4); /// Unpacks the [color] into CanvasKit-compatible representation stored @@ -893,6 +970,7 @@ final SkFloat32List _sharedSkColor2 = mallocFloat32List(4); Float32List toSharedSkColor3(ui.Color color) { return _populateSkColor(_sharedSkColor3, color); } + final SkFloat32List _sharedSkColor3 = mallocFloat32List(4); Uint32List toSkIntColorList(List colors) { @@ -928,7 +1006,7 @@ List encodeRawColorList(Int32List rawColors) { return toSkFloatColorList(colors); } -@JS('window.flutter_canvas_kit.SkPath') +@JS('window.flutterCanvasKit.SkPath') class SkPath { external SkPath([SkPath? other]); external void setFillType(SkFillType fillType); @@ -967,12 +1045,21 @@ class SkPath { external void addRect( SkRect rect, ); - external void arcTo( + external void arcToOval( SkRect oval, double startAngleDegrees, double sweepAngleDegrees, bool forceMoveTo, ); + external void arcToRotated( + double radiusX, + double radiusY, + double rotation, + bool useSmallArc, + bool counterClockWise, + double x, + double y, + ); external void close(); external void conicTo( double x1, @@ -1051,22 +1138,7 @@ class SkPath { ); } -/// A different view on [SkPath] used to overload [SkPath.arcTo]. -// TODO(yjbanov): this is a hack to get around https://github.com/flutter/flutter/issues/61305 -@JS() -class SkPathArcToPointOverload { - external void arcTo( - double radiusX, - double radiusY, - double rotation, - bool useSmallArc, - bool counterClockWise, - double x, - double y, - ); -} - -@JS('window.flutter_canvas_kit.SkContourMeasureIter') +@JS('window.flutterCanvasKit.SkContourMeasureIter') class SkContourMeasureIter { external SkContourMeasureIter(SkPath path, bool forceClosed, int startIndex); external SkContourMeasure? next(); @@ -1220,7 +1292,7 @@ Uint16List toUint16List(List ints) { return result; } -@JS('window.flutter_canvas_kit.SkPictureRecorder') +@JS('window.flutterCanvasKit.SkPictureRecorder') class SkPictureRecorder { external SkPictureRecorder(); external SkCanvas beginRecording(SkRect bounds); @@ -1228,6 +1300,11 @@ class SkPictureRecorder { external void delete(); } +/// We do not use the `delete` method (which may be removed in the future anyway). +/// +/// By Skia coding convention raw pointers should always be treated as +/// "borrowed", i.e. their memory is managed by other objects. In the case of +/// [SkCanvas] it is managed by [SkPictureRecorder]. @JS() class SkCanvas { external void clear(Float32List color); @@ -1382,6 +1459,7 @@ class SkCanvasSaveLayerWithFilterOverload { } @JS() +@anonymous class SkPicture { external void delete(); } @@ -1392,20 +1470,27 @@ class SkParagraphBuilderNamespace { SkParagraphStyle paragraphStyle, SkFontMgr? fontManager, ); + + external SkParagraphBuilder MakeFromFontProvider( + SkParagraphStyle paragraphStyle, + TypefaceFontProvider? fontManager, + ); } @JS() +@anonymous class SkParagraphBuilder { external void addText(String text); external void pushStyle(SkTextStyle textStyle); + external void pushPaintStyle( + SkTextStyle textStyle, SkPaint foreground, SkPaint background); external void pop(); external SkParagraph build(); external void delete(); } @JS() -class SkParagraphStyle { -} +class SkParagraphStyle {} @JS() @anonymous @@ -1433,9 +1518,7 @@ class SkParagraphStyleProperties { } @JS() -class SkTextStyle { - -} +class SkTextStyle {} @JS() @anonymous @@ -1476,12 +1559,20 @@ class SkFontStyle { } @JS() +@anonymous class SkFontMgr { external String? getFamilyName(int fontId); external void delete(); } +@JS('window.flutterCanvasKit.TypefaceFontProvider') +class TypefaceFontProvider extends SkFontMgr { + external TypefaceFontProvider(); + external void registerFont(Uint8List font, String family); +} + @JS() +@anonymous class SkParagraph { external double getAlphabeticBaseline(); external bool didExceedMaxLines(); @@ -1519,7 +1610,10 @@ class SkTextRange { } @JS() -class SkVertices { } +@anonymous +class SkVertices { + external void delete(); +} @JS() @anonymous @@ -1537,3 +1631,111 @@ class SkFontMgrNamespace { // TODO(yjbanov): can this be made non-null? It returns null in our unit-tests right now. external SkFontMgr? FromData(List fonts); } + +@JS() +class TypefaceFontProviderNamespace { + external TypefaceFontProvider Make(); +} + +Timer? _skObjectCollector; +List _skObjectDeleteQueue = []; + +final SkObjectFinalizationRegistry skObjectFinalizationRegistry = SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) { + _skObjectDeleteQueue.add(deletable); + _skObjectCollector ??= _scheduleSkObjectCollection(); +})); + +/// Schedules an asap timer to delete garbage-collected Skia objects. +/// +/// We use a timer for the following reasons: +/// +/// - Deleting the object immediately may lead to dangling pointer as the Skia +/// object may still be used by a function in the current frame. For example, +/// a `CkPaint` + `SkPaint` pair may be created by the framework, passed to +/// the engine, and the `CkPaint` dropped immediately. Because GC can kick in +/// any time, including in the middle of the event, we may delete `SkPaint` +/// prematurely. +/// - A microtask, while solves the problem above, would prevent the event from +/// yielding to the graphics system to render the frame on the screen if there +/// is a large number of objects to delete, causing jank. +Timer _scheduleSkObjectCollection() => Timer(Duration.zero, () { + html.window.performance.mark('SkObject collection-start'); + final int length = _skObjectDeleteQueue.length; + for (int i = 0; i < length; i++) { + _skObjectDeleteQueue[i].delete(); + } + _skObjectDeleteQueue = []; + + // Null out the timer so we can schedule a new one next time objects are + // scheduled for deletion. + _skObjectCollector = null; + html.window.performance.mark('SkObject collection-end'); + html.window.performance.measure('SkObject collection', 'SkObject collection-start', 'SkObject collection-end'); +}); + +typedef SkObjectFinalizer = void Function(T key); + +/// Any Skia object that has a `delete` method. +@JS() +@anonymous +class SkDeletable { + /// Deletes the C++ side object. + external void delete(); +} + +/// Attaches a weakly referenced object to another object and calls a finalizer +/// with the latter when weakly referenced object is garbage collected. +/// +/// We use this to delete Skia objects when their "Ck" wrapper is garbage +/// collected. +/// +/// Example sequence of events: +/// +/// 1. A (CkPaint, SkPaint) pair created. +/// 2. The paint is used to paint some picture. +/// 3. CkPaint is dropped by the app. +/// 4. GC decides to perform a GC cycle and collects CkPaint. +/// 5. The finalizer function is called with the SkPaint as the sole argument. +/// 6. We call `delete` on SkPaint. +@JS('window.FinalizationRegistry') +class SkObjectFinalizationRegistry { + external SkObjectFinalizationRegistry(SkObjectFinalizer finalizer); + external void register(Object ckObject, Object skObject); +} + +@JS('window.FinalizationRegistry') +external Object? get _finalizationRegistryConstructor; + +/// Whether the current browser supports `FinalizationRegistry`. +bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; + +@JS() +class SkData { + external int size(); + external bool isEmpty(); + external Uint8List bytes(); +} + +@JS() +@anonymous +class SkImageInfo { + external factory SkImageInfo({ + required int width, + required int height, + SkAlphaType alphaType, + SkColorSpace colorSpace, + SkColorType colorType, + }); + external SkAlphaType get alphaType; + external SkColorSpace get colorSpace; + external SkColorType get colorType; + external int get height; + external bool get isEmpty; + external bool get isOpaque; + external SkRect get bounds; + external int get width; + external SkImageInfo makeAlphaType(SkAlphaType alphaType); + external SkImageInfo makeColorSpace(SkColorSpace colorSpace); + external SkImageInfo makeColorType(SkColorType colorType); + external SkImageInfo makeWH(int width, int height); +} diff --git a/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart similarity index 95% rename from lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart rename to lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart index 4af4b9e935061..92c60e8ac2fbc 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart @@ -350,23 +350,22 @@ class CanvasKitCanvas implements ui.Canvas { ui.Image atlas, List transforms, List rects, - List colors, - ui.BlendMode blendMode, + List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(transforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || colors.isEmpty || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; if (transforms.length != rectCount) { throw ArgumentError('"transforms" and "rects" lengths must match.'); } - if (colors.isNotEmpty && colors.length != rectCount) { + if (colors != null && colors.isNotEmpty && colors.length != rectCount) { throw ArgumentError( 'If non-null, "colors" length must match that of "transforms" and "rects".'); } @@ -393,10 +392,10 @@ class CanvasKitCanvas implements ui.Canvas { } final List? colorBuffer = - colors.isEmpty ? null : toSkFloatColorList(colors); + (colors == null || colors.isEmpty) ? null : toSkFloatColorList(colors); _drawAtlas( - paint, atlas, rstTransformBuffer, rectBuffer, colorBuffer, blendMode); + paint, atlas, rstTransformBuffer, rectBuffer, colorBuffer, blendMode ?? ui.BlendMode.src); } @override @@ -404,16 +403,15 @@ class CanvasKitCanvas implements ui.Canvas { ui.Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - ui.BlendMode blendMode, + Int32List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint) { // ignore: unnecessary_null_comparison assert(atlas != null); // atlas is checked on the engine side assert(rstTransforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; @@ -422,12 +420,13 @@ class CanvasKitCanvas implements ui.Canvas { if (rectCount % 4 != 0) throw ArgumentError( '"rstTransforms" and "rects" lengths must be a multiple of four.'); - if (colors.length * 4 != rectCount) + if (colors != null && colors.length * 4 != rectCount) throw ArgumentError( 'If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); - _drawAtlas(paint, atlas, rstTransforms, rects, encodeRawColorList(colors), - blendMode); + final List? colorBuffer = colors == null ? null : encodeRawColorList(colors); + + _drawAtlas(paint, atlas, rstTransforms, rects, colorBuffer, blendMode ?? ui.BlendMode.src); } // TODO(hterkelsen): Pass a cull_rect once CanvasKit supports that. diff --git a/lib/web_ui/lib/src/engine/compositor/color_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart similarity index 96% rename from lib/web_ui/lib/src/engine/compositor/color_filter.dart rename to lib/web_ui/lib/src/engine/canvaskit/color_filter.dart index 341c5f5c02b56..2c219b4b19957 100644 --- a/lib/web_ui/lib/src/engine/compositor/color_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart @@ -6,7 +6,7 @@ part of engine; /// A [ui.ColorFilter] backed by Skia's [CkColorFilter]. -class CkColorFilter extends ResurrectableSkiaObject { +class CkColorFilter extends ManagedSkiaObject { final EngineColorFilter _engineFilter; CkColorFilter.mode(EngineColorFilter filter) : _engineFilter = filter; diff --git a/lib/web_ui/lib/src/engine/compositor/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart similarity index 99% rename from lib/web_ui/lib/src/engine/compositor/embedded_views.dart rename to lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 0a29ef7ef0495..5f8c4a9b8e73c 100644 --- a/lib/web_ui/lib/src/engine/compositor/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -49,7 +49,7 @@ class HtmlViewEmbedder { Map _clipCount = {}; /// The size of the frame, in physical pixels. - ui.Size _frameSize = _computeFrameSize(); + ui.Size _frameSize = ui.window.physicalSize; void set frameSize(ui.Size size) { if (_frameSize == size) { diff --git a/lib/web_ui/lib/src/engine/compositor/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart similarity index 69% rename from lib/web_ui/lib/src/engine/compositor/fonts.dart rename to lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 7d40185e35b37..a26a3192ae316 100644 --- a/lib/web_ui/lib/src/engine/compositor/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -20,28 +20,18 @@ class SkiaFontCollection { >[]; /// Fonts which have been registered and loaded. - final List<_RegisteredFont?> _registeredFonts = <_RegisteredFont?>[]; - - /// A mapping from the name a font was registered with, to the family name - /// embedded in the font's bytes (the font's "actual" name). - /// - /// For example, a font may be registered in Flutter assets with the name - /// "MaterialIcons", but if you read the family name out of the font's bytes - /// it is actually "Material Icons". Skia works with the actual names of the - /// fonts, so when we create a Skia Paragraph with Flutter font families, we - /// must convert them to their actual family name when we pass them to Skia. - final Map fontFamilyOverrides = {}; + final List<_RegisteredFont> _registeredFonts = <_RegisteredFont>[]; final Set registeredFamilies = {}; Future ensureFontsLoaded() async { await _loadFonts(); - _computeFontFamilyOverrides(); - final List fontBuffers = - _registeredFonts.map((f) => f!.bytes).toList(); + fontProvider = canvasKit.TypefaceFontProvider.Make(); - skFontMgr = canvasKit.SkFontMgr.FromData(fontBuffers); + for (var font in _registeredFonts) { + fontProvider!.registerFont(font.bytes, font.flutterFamily); + } } /// Loads all of the unloaded fonts in [_unloadedFonts] and adds them @@ -50,28 +40,14 @@ class SkiaFontCollection { if (_unloadedFonts.isEmpty) { return; } - - final List<_RegisteredFont?> loadedFonts = await Future.wait(_unloadedFonts); - _registeredFonts.addAll(loadedFonts.where((x) => x != null)); - _unloadedFonts.clear(); - } - - void _computeFontFamilyOverrides() { - fontFamilyOverrides.clear(); - - for (_RegisteredFont? font in _registeredFonts) { - if (fontFamilyOverrides.containsKey(font!.flutterFamily)) { - if (fontFamilyOverrides[font.flutterFamily] != font.actualFamily) { - html.window.console.warn('Fonts in family ${font.flutterFamily} ' - 'have different actual family names.'); - html.window.console.warn( - 'Current actual family: ${fontFamilyOverrides[font.flutterFamily]}'); - html.window.console.warn('New actual family: ${font.actualFamily}'); - } - } else { - fontFamilyOverrides[font.flutterFamily] = font.actualFamily; + final List<_RegisteredFont?> loadedFonts = + await Future.wait(_unloadedFonts); + for (_RegisteredFont? font in loadedFonts) { + if (font != null) { + _registeredFonts.add(font); } } + _unloadedFonts.clear(); } Future loadFontFromList(Uint8List list, {String? fontFamily}) async { @@ -118,8 +94,9 @@ class SkiaFontCollection { 'There was a problem trying to load FontManifest.json'); } - for (Map fontFamily in fontManifest.cast>()) { - final String? family = fontFamily['family']; + for (Map fontFamily + in fontManifest.cast>()) { + final String family = fontFamily['family']!; final List fontAssets = fontFamily['fonts']; registeredFamilies.add(family); @@ -141,10 +118,12 @@ class SkiaFontCollection { } } - Future<_RegisteredFont?> _registerFont(String url, String? family) async { + Future<_RegisteredFont?> _registerFont(String url, String family) async { ByteBuffer buffer; try { - buffer = await html.window.fetch(url).then(_getArrayBuffer as FutureOr Function(dynamic)); + buffer = await html.window + .fetch(url) + .then(_getArrayBuffer as FutureOr Function(dynamic)); } catch (e) { html.window.console.warn('Failed to load font $family at $url'); html.window.console.warn(e); @@ -160,7 +139,7 @@ class SkiaFontCollection { actualFamily = family; } - return _RegisteredFont(bytes, family!, actualFamily!); + return _RegisteredFont(bytes, family, actualFamily); } String? _readActualFamilyName(Uint8List bytes) { @@ -178,6 +157,7 @@ class SkiaFontCollection { } SkFontMgr? skFontMgr; + TypefaceFontProvider? fontProvider; } /// Represents a font that has been registered. diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart new file mode 100644 index 0000000000000..8bcb210d961ef --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -0,0 +1,185 @@ +// 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. + +// @dart = 2.10 +part of engine; + +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia. +void skiaInstantiateImageCodec(Uint8List list, Callback callback, + [int? width, int? height, int? format, int? rowBytes]) { + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(list); + final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); + final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); + callback(codec); +} + +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after requesting from URI. +void skiaInstantiateWebImageCodec(String src, Callback callback, + WebOnlyImageCodecChunkCallback? chunkCallback) { + chunkCallback?.call(0, 100); + //TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported. + html.HttpRequest.request( + src, + responseType: "arraybuffer", + ).then((html.HttpRequest response) { + chunkCallback?.call(100, 100); + final Uint8List list = + new Uint8List.view((response.response as ByteBuffer)); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(list); + final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); + final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); + callback(codec); + }); +} + +/// A wrapper for `SkAnimatedImage`. +class CkAnimatedImage implements ui.Image { + final SkAnimatedImage _skAnimatedImage; + + // Use a box because `SkImage` may be deleted either due to this object + // being garbage-collected, or by an explicit call to [delete]. + late final SkiaObjectBox box; + + CkAnimatedImage(this._skAnimatedImage) { + box = SkiaObjectBox(this, _skAnimatedImage as SkDeletable); + } + + @override + void dispose() { + box.delete(); + } + + int get frameCount => _skAnimatedImage.getFrameCount(); + + /// Decodes the next frame and returns the frame duration. + Duration decodeNextFrame() { + final int durationMillis = _skAnimatedImage.decodeNextFrame(); + return Duration(milliseconds: durationMillis); + } + + int get repetitionCount => _skAnimatedImage.getRepetitionCount(); + + CkImage get currentFrameAsImage { + return CkImage(_skAnimatedImage.getCurrentFrame()); + } + + @override + int get width => _skAnimatedImage.width(); + + @override + int get height => _skAnimatedImage.height(); + + @override + Future toByteData( + {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); + } +} + +/// A [ui.Image] backed by an `SkImage` from Skia. +class CkImage implements ui.Image { + final SkImage skImage; + + // Use a box because `SkImage` may be deleted either due to this object + // being garbage-collected, or by an explicit call to [delete]. + late final SkiaObjectBox box; + + CkImage(this.skImage) { + box = SkiaObjectBox(this, skImage as SkDeletable); + } + + @override + void dispose() { + box.delete(); + } + + @override + int get width => skImage.width(); + + @override + int get height => skImage.height(); + + @override + Future toByteData( + {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = skImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = skImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); + } +} + +/// A [Codec] that wraps an `SkAnimatedImage`. +class CkAnimatedImageCodec implements ui.Codec { + CkAnimatedImage animatedImage; + + CkAnimatedImageCodec(this.animatedImage); + + @override + void dispose() { + animatedImage.dispose(); + } + + @override + int get frameCount => animatedImage.frameCount; + + @override + int get repetitionCount => animatedImage.repetitionCount; + + @override + Future getNextFrame() { + final Duration duration = animatedImage.decodeNextFrame(); + final CkImage image = animatedImage.currentFrameAsImage; + return Future.value(AnimatedImageFrameInfo(duration, image)); + } +} + +/// Data for a single frame of an animated image. +class AnimatedImageFrameInfo implements ui.FrameInfo { + final Duration _duration; + final CkImage _image; + + AnimatedImageFrameInfo(this._duration, this._image); + + @override + Duration get duration => _duration; + + @override + ui.Image get image => _image; +} diff --git a/lib/web_ui/lib/src/engine/compositor/image_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart similarity index 92% rename from lib/web_ui/lib/src/engine/compositor/image_filter.dart rename to lib/web_ui/lib/src/engine/canvaskit/image_filter.dart index 55288800db721..02ebb4bccad4e 100644 --- a/lib/web_ui/lib/src/engine/compositor/image_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart @@ -8,7 +8,7 @@ part of engine; /// The CanvasKit implementation of [ui.ImageFilter]. /// /// Currently only supports `blur`. -class CkImageFilter extends ResurrectableSkiaObject implements ui.ImageFilter { +class CkImageFilter extends ManagedSkiaObject implements ui.ImageFilter { CkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) : _sigmaX = sigmaX, _sigmaY = sigmaY; diff --git a/lib/web_ui/lib/src/engine/compositor/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart similarity index 97% rename from lib/web_ui/lib/src/engine/compositor/initialization.dart rename to lib/web_ui/lib/src/engine/canvaskit/initialization.dart index fa6207b455871..c1d91ad525925 100644 --- a/lib/web_ui/lib/src/engine/compositor/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -20,7 +20,7 @@ const bool canvasKitForceCpuOnly = /// NPM, update this URL to `https://unpkg.com/canvaskit-wasm@0.34.0/bin/`. const String canvasKitBaseUrl = String.fromEnvironment( 'FLUTTER_WEB_CANVASKIT_URL', - defaultValue: 'https://unpkg.com/canvaskit-wasm@0.17.2/bin/', + defaultValue: 'https://unpkg.com/canvaskit-wasm@0.17.3/bin/', ); /// Initialize CanvasKit. diff --git a/lib/web_ui/lib/src/engine/compositor/layer.dart b/lib/web_ui/lib/src/engine/canvaskit/layer.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/layer.dart rename to lib/web_ui/lib/src/engine/canvaskit/layer.dart diff --git a/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart rename to lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart diff --git a/lib/web_ui/lib/src/engine/compositor/layer_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart similarity index 92% rename from lib/web_ui/lib/src/engine/compositor/layer_tree.dart rename to lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart index afde94d1c26ed..3b40be4728b39 100644 --- a/lib/web_ui/lib/src/engine/compositor/layer_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart @@ -11,7 +11,7 @@ class LayerTree { Layer? rootLayer; /// The size (in physical pixels) of the frame to paint this layer tree into. - final ui.Size frameSize = _computeFrameSize(); + final ui.Size frameSize = ui.window.physicalSize; /// The devicePixelRatio of the frame to paint this layer tree into. double? devicePixelRatio; @@ -54,14 +54,6 @@ class LayerTree { } } -ui.Size _computeFrameSize() { - final ui.Size physicalSize = ui.window.physicalSize; - return ui.Size( - physicalSize.width.truncate().toDouble(), - physicalSize.height.truncate().toDouble(), - ); -} - /// A single frame to be rendered. class Frame { /// The canvas to render this frame to. diff --git a/lib/web_ui/lib/src/engine/compositor/mask_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart similarity index 91% rename from lib/web_ui/lib/src/engine/compositor/mask_filter.dart rename to lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart index 51be8820dc61f..8c120d1520035 100644 --- a/lib/web_ui/lib/src/engine/compositor/mask_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart @@ -6,7 +6,7 @@ part of engine; /// The CanvasKit implementation of [ui.MaskFilter]. -class CkMaskFilter extends ResurrectableSkiaObject { +class CkMaskFilter extends ManagedSkiaObject { CkMaskFilter.blur(ui.BlurStyle blurStyle, double sigma) : _blurStyle = blurStyle, _sigma = sigma; diff --git a/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart rename to lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart diff --git a/lib/web_ui/lib/src/engine/compositor/painting.dart b/lib/web_ui/lib/src/engine/canvaskit/painting.dart similarity index 94% rename from lib/web_ui/lib/src/engine/compositor/painting.dart rename to lib/web_ui/lib/src/engine/canvaskit/painting.dart index f8c21b2b7dd02..550ff12351e40 100644 --- a/lib/web_ui/lib/src/engine/compositor/painting.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/painting.dart @@ -9,7 +9,7 @@ part of engine; /// /// This class is backed by a Skia object that must be explicitly /// deleted to avoid a memory leak. This is done by extending [SkiaObject]. -class CkPaint extends ResurrectableSkiaObject implements ui.Paint { +class CkPaint extends ManagedSkiaObject implements ui.Paint { CkPaint(); static const ui.Color _defaultPaintColor = ui.Color(0xFF000000); @@ -117,17 +117,17 @@ class CkPaint extends ResurrectableSkiaObject implements ui.Paint { bool _invertColors = false; @override - ui.Shader? get shader => _shader as ui.Shader?; + ui.Shader? get shader => _shader; @override set shader(ui.Shader? value) { if (_shader == value) { return; } - _shader = value as EngineShader?; - skiaObject.setShader(_shader?.createSkiaShader()); + _shader = value as CkShader?; + skiaObject.setShader(_shader?.skiaObject); } - EngineShader? _shader; + CkShader? _shader; @override ui.MaskFilter? get maskFilter => _maskFilter; @@ -222,7 +222,7 @@ class CkPaint extends ResurrectableSkiaObject implements ui.Paint { paint.setStrokeWidth(_strokeWidth); paint.setAntiAlias(_isAntiAlias); paint.setColorInt(_color.value); - paint.setShader(_shader?.createSkiaShader()); + paint.setShader(_shader?.skiaObject); paint.setMaskFilter(_ckMaskFilter?.skiaObject); paint.setColorFilter(_ckColorFilter?.skiaObject); paint.setImageFilter(_imageFilter?.skiaObject); diff --git a/lib/web_ui/lib/src/engine/compositor/path.dart b/lib/web_ui/lib/src/engine/canvaskit/path.dart similarity index 99% rename from lib/web_ui/lib/src/engine/compositor/path.dart rename to lib/web_ui/lib/src/engine/canvaskit/path.dart index 321c6a1246f39..723517eb5ce5e 100644 --- a/lib/web_ui/lib/src/engine/compositor/path.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/path.dart @@ -116,7 +116,7 @@ class CkPath implements ui.Path { void arcTo( ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { const double toDegrees = 180.0 / math.pi; - _skPath.arcTo( + _skPath.arcToOval( toSkRect(rect), startAngle * toDegrees, sweepAngle * toDegrees, @@ -130,7 +130,7 @@ class CkPath implements ui.Path { double rotation = 0.0, bool largeArc = false, bool clockwise = true}) { - (_skPath as SkPathArcToPointOverload).arcTo( + _skPath.arcToRotated( radius.x, radius.y, rotation, diff --git a/lib/web_ui/lib/src/engine/compositor/path_metrics.dart b/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/path_metrics.dart rename to lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart diff --git a/lib/web_ui/lib/src/engine/compositor/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart similarity index 65% rename from lib/web_ui/lib/src/engine/compositor/picture.dart rename to lib/web_ui/lib/src/engine/canvaskit/picture.dart index 49d6fea0c3731..2a0d292ebd45b 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -21,9 +21,13 @@ class CkPicture implements ui.Picture { } @override - Future toImage(int width, int height) { - throw UnsupportedError( - 'Picture.toImage not yet implemented for CanvasKit and HTML'); + Future toImage(int width, int height) async { + final SkSurface skSurface = canvasKit.MakeSurface(width, height); + final SkCanvas skCanvas = skSurface.getCanvas(); + skCanvas.drawPicture(skiaObject.skiaObject); + final SkImage skImage = skSurface.makeImageSnapshot(); + skSurface.dispose(); + return CkImage(skImage); } } @@ -32,6 +36,6 @@ class SkPictureSkiaObject extends OneShotSkiaObject { @override void delete() { - rawSkiaObject?.delete(); + rawSkiaObject.delete(); } } diff --git a/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/picture_recorder.dart rename to lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart diff --git a/lib/web_ui/lib/src/engine/compositor/platform_message.dart b/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/platform_message.dart rename to lib/web_ui/lib/src/engine/canvaskit/platform_message.dart diff --git a/lib/web_ui/lib/src/engine/compositor/raster_cache.dart b/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/raster_cache.dart rename to lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart diff --git a/lib/web_ui/lib/src/engine/compositor/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/rasterizer.dart rename to lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart diff --git a/lib/web_ui/lib/src/engine/canvaskit/shader.dart b/lib/web_ui/lib/src/engine/canvaskit/shader.dart new file mode 100644 index 0000000000000..6b0074b609d4c --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/shader.dart @@ -0,0 +1,186 @@ +// 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. + +// @dart = 2.10 +part of engine; + +abstract class CkShader extends ManagedSkiaObject implements ui.Shader { + @override + void delete() { + rawSkiaObject?.delete(); + } +} + +class CkGradientSweep extends CkShader implements ui.Gradient { + CkGradientSweep(this.center, this.colors, this.colorStops, this.tileMode, + this.startAngle, this.endAngle, this.matrix4) + : assert(_offsetIsValid(center)), + assert(colors != null), // ignore: unnecessary_null_comparison + assert(tileMode != null), // ignore: unnecessary_null_comparison + assert(startAngle != null), // ignore: unnecessary_null_comparison + assert(endAngle != null), // ignore: unnecessary_null_comparison + assert(startAngle < endAngle), + assert(matrix4 == null || _matrix4IsValid(matrix4)) { + _validateColorStops(colors, colorStops); + } + + final ui.Offset center; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final double startAngle; + final double endAngle; + final Float32List? matrix4; + + @override + SkShader createDefault() { + return canvasKit.SkShader.MakeSweepGradient( + center.dx, + center.dy, + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, + 0, + startAngle, + endAngle, + ); + } + + @override + SkShader resurrect() { + return createDefault(); + } +} + +class CkGradientLinear extends CkShader implements ui.Gradient { + CkGradientLinear( + this.from, + this.to, + this.colors, + this.colorStops, + this.tileMode, + Float64List? matrix, + ) : assert(_offsetIsValid(from)), + assert(_offsetIsValid(to)), + assert(colors != null), // ignore: unnecessary_null_comparison + assert(tileMode != null), // ignore: unnecessary_null_comparison + this.matrix4 = matrix == null ? null : _FastMatrix64(matrix) { + if (assertionsEnabled) { + _validateColorStops(colors, colorStops); + } + } + + final ui.Offset from; + final ui.Offset to; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final _FastMatrix64? matrix4; + + @override + SkShader createDefault() { + assert(experimentalUseSkia); + + return canvasKit.SkShader.MakeLinearGradient( + toSkPoint(from), + toSkPoint(to), + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + ); + } + + @override + SkShader resurrect() => createDefault(); +} + +class CkGradientRadial extends CkShader implements ui.Gradient { + CkGradientRadial(this.center, this.radius, this.colors, this.colorStops, + this.tileMode, this.matrix4); + + final ui.Offset center; + final double radius; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final Float32List? matrix4; + + @override + SkShader createDefault() { + assert(experimentalUseSkia); + + return canvasKit.SkShader.MakeRadialGradient( + toSkPoint(center), + radius, + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, + 0, + ); + } + + @override + SkShader resurrect() => createDefault(); +} + +class CkGradientConical extends CkShader implements ui.Gradient { + CkGradientConical(this.focal, this.focalRadius, this.center, this.radius, + this.colors, this.colorStops, this.tileMode, this.matrix4); + + final ui.Offset focal; + final double focalRadius; + final ui.Offset center; + final double radius; + final List colors; + final List? colorStops; + final ui.TileMode tileMode; + final Float32List? matrix4; + + @override + SkShader createDefault() { + assert(experimentalUseSkia); + return canvasKit.SkShader.MakeTwoPointConicalGradient( + toSkPoint(focal), + focalRadius, + toSkPoint(center), + radius, + toSkFloatColorList(colors), + toSkColorStops(colorStops), + toSkTileMode(tileMode), + matrix4 != null ? toSkMatrixFromFloat32(matrix4!) : null, + 0, + ); + } + + @override + SkShader resurrect() => createDefault(); +} + +class CkImageShader extends CkShader implements ui.ImageShader { + CkImageShader( + ui.Image image, this.tileModeX, this.tileModeY, this.matrix4) + : _skImage = image as CkImage; + + final ui.TileMode tileModeX; + final ui.TileMode tileModeY; + final Float64List matrix4; + final CkImage _skImage; + + @override + SkShader createDefault() => _skImage.skImage.makeShader( + toSkTileMode(tileModeX), + toSkTileMode(tileModeY), + toSkMatrixFromFloat64(matrix4), + ); + + @override + SkShader resurrect() => createDefault(); + + @override + void delete() { + rawSkiaObject?.delete(); + } +} diff --git a/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart similarity index 69% rename from lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart rename to lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart index b94908be6e8df..160ee172f2f7d 100644 --- a/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart @@ -86,7 +86,7 @@ class SkiaObjectCache { /// WebAssembly heap. /// /// These objects are automatically deleted when no longer used. -abstract class SkiaObject { +abstract class SkiaObject { /// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap. T get skiaObject; @@ -99,16 +99,19 @@ abstract class SkiaObject { void didDelete(); } -/// A [SkiaObject] that can resurrect its C++ counterpart. +/// A [SkiaObject] that manages the lifecycle of its C++ counterpart. /// -/// Because there is no feedback from JavaScript's GC (no destructors or -/// finalizers), we pessimistically delete the underlying C++ object before -/// the Dart object is garbage-collected. The current algorithm deletes objects -/// at the end of every frame. This allows reusing the C++ objects within the -/// frame. In the future we may add smarter strategies that will allow us to -/// reuse C++ objects across frames. +/// In browsers that support weak references we use feedback from the garbage +/// collector to determine when it is safe to release the C++ object. /// -/// The lifecycle of a C++ object is as follows: +/// In browsers that do not support weak references we pessimistically delete +/// the underlying C++ object before the Dart object is garbage-collected. The +/// current algorithm deletes objects at the end of every frame. This allows +/// reusing the C++ objects within the frame. If the object is used again after +/// is was deleted, we [resurrect] it based on the data available on the +/// JavaScript side. +/// +/// The lifecycle of a resurrectable C++ object is as follows: /// /// - Create default: when instantiating a C++ object for a Dart object for the /// first time, the C++ object is populated with default data (the defaults are @@ -120,13 +123,22 @@ abstract class SkiaObject { /// [resurrect] method. /// - Final delete: if a Dart object is never reused, it is GC'd after its /// underlying C++ object is deleted. This is implemented by [SkiaObjects]. -abstract class ResurrectableSkiaObject extends SkiaObject { - ResurrectableSkiaObject() { - rawSkiaObject = createDefault(); - if (isResurrectionExpensive) { - SkiaObjects.manageExpensive(this); +abstract class ManagedSkiaObject extends SkiaObject { + ManagedSkiaObject() { + final T defaultObject = createDefault(); + rawSkiaObject = defaultObject; + if (browserSupportsFinalizationRegistry) { + // If FinalizationRegistry is supported we will only ever need the + // default object, as we know precisely when to delete it. + skObjectFinalizationRegistry.register(this, defaultObject); } else { - SkiaObjects.manageResurrectable(this); + // If FinalizationRegistry is _not_ supported we may need to delete + // and resurrect the object multiple times before deleting it forever. + if (isResurrectionExpensive) { + SkiaObjects.manageExpensive(this); + } else { + SkiaObjects.manageResurrectable(this); + } } } @@ -134,6 +146,7 @@ abstract class ResurrectableSkiaObject extends SkiaObject { T get skiaObject => rawSkiaObject ?? _doResurrect(); T _doResurrect() { + assert(!browserSupportsFinalizationRegistry); final T skiaObject = resurrect(); rawSkiaObject = skiaObject; if (isResurrectionExpensive) { @@ -146,6 +159,7 @@ abstract class ResurrectableSkiaObject extends SkiaObject { @override void didDelete() { + assert(!browserSupportsFinalizationRegistry); rawSkiaObject = null; } @@ -182,7 +196,11 @@ abstract class ResurrectableSkiaObject extends SkiaObject { // use. This issue discusses ways to address this: // https://github.com/flutter/flutter/issues/60401 /// A [SkiaObject] which is deleted once and cannot be used again. -abstract class OneShotSkiaObject extends SkiaObject { +/// +/// In browsers that support weak references we use feedback from the garbage +/// collector to determine when it is safe to release the C++ object. Otherwise, +/// we use an LRU cache (see [SkiaObjects.manageOneShot]). +abstract class OneShotSkiaObject extends SkiaObject { /// Returns the current skia object as is without attempting to /// resurrect it. /// @@ -191,34 +209,85 @@ abstract class OneShotSkiaObject extends SkiaObject { /// /// Use this field instead of the [skiaObject] getter when implementing /// the [delete] method. - T? rawSkiaObject; + T rawSkiaObject; + + bool _isDeleted = false; - OneShotSkiaObject(this.rawSkiaObject) { - SkiaObjects.manageOneShot(this); + OneShotSkiaObject(T skObject) : this.rawSkiaObject = skObject { + if (browserSupportsFinalizationRegistry) { + skObjectFinalizationRegistry.register(this, skObject); + } else { + SkiaObjects.manageOneShot(this); + } } @override T get skiaObject { - if (rawSkiaObject == null) { + if (browserSupportsFinalizationRegistry) { + return rawSkiaObject; + } + if (_isDeleted) { throw StateError('Attempting to use a Skia object that has been freed.'); } SkiaObjects.oneShotCache.markUsed(this); - return rawSkiaObject!; + return rawSkiaObject; } @override void didDelete() { - rawSkiaObject = null; + _isDeleted = true; + } +} + +/// Manages the lifecycle of a Skia object owned by a wrapper object. +/// +/// When the wrapper is garbage collected, deletes the corresponding +/// [skObject] (only in browsers that support weak references). +/// +/// The [delete] method can be used to eagerly delete the [skObject] +/// before the wrapper is garbage collected. +/// +/// The [delete] method may be called any number of times. The box +/// will only delete the object once. +class SkiaObjectBox { + SkiaObjectBox(Object wrapper, this.skObject) { + if (browserSupportsFinalizationRegistry) { + boxRegistry.register(wrapper, this); + } + } + + /// The Skia object whose lifecycle is being managed. + final SkDeletable skObject; + + /// Whether this object has been deleted. + bool get isDeleted => _isDeleted; + bool _isDeleted = false; + + /// Deletes Skia objects when their wrappers are garbage collected. + static final SkObjectFinalizationRegistry boxRegistry = + SkObjectFinalizationRegistry( + js.allowInterop((SkiaObjectBox box) { + box.delete(); + })); + + /// Deletes the [skObject]. + /// + /// Does nothing if the object has already been deleted. + void delete() { + if (_isDeleted) { + return; + } + _isDeleted = true; + _skObjectDeleteQueue.add(skObject); + _skObjectCollector ??= _scheduleSkObjectCollection(); } } /// Singleton that manages the lifecycles of [SkiaObject] instances. class SkiaObjects { - // TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects - // beyond a single frame. @visibleForTesting - static final List resurrectableObjects = - []; + static final List resurrectableObjects = + []; @visibleForTesting static int maximumCacheSize = 8192; @@ -247,7 +316,7 @@ class SkiaObjects { /// Starts managing the lifecycle of resurrectable [object]. /// /// These can safely be deleted at any time. - static void manageResurrectable(ResurrectableSkiaObject object) { + static void manageResurrectable(ManagedSkiaObject object) { registerCleanupCallback(); resurrectableObjects.add(object); } @@ -265,7 +334,7 @@ class SkiaObjects { /// /// Since it's expensive to resurrect, we shouldn't just delete it after every /// frame. Instead, add it to a cache and only delete it when the cache fills. - static void manageExpensive(ResurrectableSkiaObject object) { + static void manageExpensive(ManagedSkiaObject object) { registerCleanupCallback(); expensiveCache.add(object); } diff --git a/lib/web_ui/lib/src/engine/compositor/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart similarity index 80% rename from lib/web_ui/lib/src/engine/compositor/surface.dart rename to lib/web_ui/lib/src/engine/canvaskit/surface.dart index bacbe5e1c09da..17aac05770ecd 100644 --- a/lib/web_ui/lib/src/engine/compositor/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -89,22 +89,20 @@ class Surface { _addedToScene = true; } + ui.Size? _currentSize; + void _createOrUpdateSurfaces(ui.Size size) { if (size.isEmpty) { throw CanvasKitError('Cannot create surfaces of empty size.'); } - final CkSurface? currentSurface = _surface; - if (currentSurface != null) { - final bool isSameSize = size.width == currentSurface.width() && - size.height == currentSurface.height(); - if (isSameSize) { - // The existing surface is still reusable. - return; - } + if (size == _currentSize) { + // The existing surface is still reusable. + return; } - currentSurface?.dispose(); + _currentSize = size; + _surface?.dispose(); _surface = null; htmlElement?.remove(); htmlElement = null; @@ -113,17 +111,31 @@ class Surface { _surface = _wrapHtmlCanvas(size); } - CkSurface _wrapHtmlCanvas(ui.Size size) { - final ui.Size logicalSize = size / ui.window.devicePixelRatio; + CkSurface _wrapHtmlCanvas(ui.Size physicalSize) { + // If `physicalSize` is not precise, use a slightly bigger canvas. This way + // we ensure that the rendred picture covers the entire browser window. + final int pixelWidth = physicalSize.width.ceil(); + final int pixelHeight = physicalSize.height.ceil(); final html.CanvasElement htmlCanvas = html.CanvasElement( - width: size.width.ceil(), height: size.height.ceil()); + width: pixelWidth, + height: pixelHeight, + ); + + // The logical size of the canvas is not based on the size of the window + // but on the size of the canvas, which, due to `ceil()` above, may not be + // the same as the window. We do not round/floor/ceil the logical size as + // CSS pixels can contain more than one physical pixel and therefore to + // match the size of the window precisely we use the most precise floating + // point value we can get. + final double logicalWidth = pixelWidth / ui.window.devicePixelRatio; + final double logicalHeight = pixelHeight / ui.window.devicePixelRatio; htmlCanvas.style ..position = 'absolute' - ..width = '${logicalSize.width.ceil()}px' - ..height = '${logicalSize.height.ceil()}px'; + ..width = '${logicalWidth}px' + ..height = '${logicalHeight}px'; htmlElement = htmlCanvas; - if (canvasKitForceCpuOnly) { + if (webGLVersion == -1 || canvasKitForceCpuOnly) { return _makeSoftwareCanvasSurface(htmlCanvas); } else { // Try WebGL first. @@ -133,6 +145,7 @@ class Surface { // Default to no anti-aliasing. Paint commands can be explicitly // anti-aliased by setting their `Paint` object's `antialias` property. anitalias: 0, + majorVersion: webGLVersion, ), ); @@ -152,8 +165,8 @@ class Surface { SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface( _grContext!, - size.width, - size.height, + pixelWidth, + pixelHeight, SkColorSpaceSRGB, ); diff --git a/lib/web_ui/lib/src/engine/compositor/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart similarity index 90% rename from lib/web_ui/lib/src/engine/compositor/text.dart rename to lib/web_ui/lib/src/engine/canvaskit/text.dart index 1b99d2c9944ee..1079e814ae9b3 100644 --- a/lib/web_ui/lib/src/engine/compositor/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -20,17 +20,17 @@ class CkParagraphStyle implements ui.ParagraphStyle { String? ellipsis, ui.Locale? locale, }) : skParagraphStyle = toSkParagraphStyle( - textAlign, - textDirection, - maxLines, - fontFamily, - fontSize, - height, - textHeightBehavior, - fontWeight, - fontStyle, - ellipsis, - ) { + textAlign, + textDirection, + maxLines, + fontFamily, + fontSize, + height, + textHeightBehavior, + fontWeight, + fontStyle, + ellipsis, + ) { assert(skParagraphStyle != null); _textDirection = textDirection ?? ui.TextDirection.ltr; _fontFamily = fontFamily; @@ -59,9 +59,6 @@ class CkParagraphStyle implements ui.ParagraphStyle { !skiaFontCollection.registeredFamilies.contains(fontFamily)) { fontFamily = 'Roboto'; } - if (skiaFontCollection.fontFamilyOverrides.containsKey(fontFamily)) { - fontFamily = skiaFontCollection.fontFamilyOverrides[fontFamily]!; - } skTextStyle.fontFamilies = [fontFamily]; return skTextStyle; @@ -114,6 +111,8 @@ class CkParagraphStyle implements ui.ParagraphStyle { class CkTextStyle implements ui.TextStyle { SkTextStyle skTextStyle; + CkPaint? background; + CkPaint? foreground; factory CkTextStyle({ ui.Color? color, @@ -173,9 +172,6 @@ class CkTextStyle implements ui.TextStyle { fontFamily = 'Roboto'; } - if (skiaFontCollection.fontFamilyOverrides.containsKey(fontFamily)) { - fontFamily = skiaFontCollection.fontFamilyOverrides[fontFamily]!; - } List fontFamilies = [fontFamily]; if (fontFamilyFallback != null && !fontFamilyFallback.every((font) => fontFamily == font)) { @@ -202,14 +198,14 @@ class CkTextStyle implements ui.TextStyle { // - locale // - shadows // - fontFeatures - return CkTextStyle._(canvasKit.TextStyle(properties)); + return CkTextStyle._( + canvasKit.TextStyle(properties), foreground, background); } - CkTextStyle._(this.skTextStyle); + CkTextStyle._(this.skTextStyle, this.foreground, this.background); } -SkFontStyle toSkFontStyle( - ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) { +SkFontStyle toSkFontStyle(ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) { final style = SkFontStyle(); if (fontWeight != null) { style.weight = toSkFontWeight(fontWeight); @@ -220,7 +216,8 @@ SkFontStyle toSkFontStyle( return style; } -class CkParagraph extends ResurrectableSkiaObject implements ui.Paragraph { +class CkParagraph extends ManagedSkiaObject + implements ui.Paragraph { CkParagraph( this._initialParagraph, this._paragraphStyle, this._paragraphCommands); @@ -285,8 +282,7 @@ class CkParagraph extends ResurrectableSkiaObject implements ui.Par bool get isResurrectionExpensive => true; @override - double get alphabeticBaseline => - skiaObject.getAlphabeticBaseline(); + double get alphabeticBaseline => skiaObject.getAlphabeticBaseline(); @override bool get didExceedMaxLines => skiaObject.didExceedMaxLines(); @@ -295,8 +291,7 @@ class CkParagraph extends ResurrectableSkiaObject implements ui.Par double get height => skiaObject.getHeight(); @override - double get ideographicBaseline => - skiaObject.getIdeographicBaseline(); + double get ideographicBaseline => skiaObject.getIdeographicBaseline(); @override double get longestLine => skiaObject.getLongestLine(); @@ -414,9 +409,9 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { CkParagraphBuilder(ui.ParagraphStyle style) : _commands = <_ParagraphCommand>[], _style = style as CkParagraphStyle, - _paragraphBuilder = canvasKit.ParagraphBuilder.Make( + _paragraphBuilder = canvasKit.ParagraphBuilder.MakeFromFontProvider( style.skParagraphStyle, - skiaFontCollection.skFontMgr, + skiaFontCollection.fontProvider, ); // TODO(hterkelsen): Implement placeholders. @@ -468,7 +463,14 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { void pushStyle(ui.TextStyle style) { final CkTextStyle skStyle = style as CkTextStyle; _commands.add(_ParagraphCommand.pushStyle(skStyle)); - _paragraphBuilder.pushStyle(skStyle.skTextStyle); + if (skStyle.foreground != null || skStyle.background != null) { + final SkPaint foreground = skStyle.foreground?.skiaObject ?? SkPaint(); + final SkPaint background = skStyle.background?.skiaObject ?? SkPaint(); + _paragraphBuilder.pushPaintStyle( + skStyle.skTextStyle, foreground, background); + } else { + _paragraphBuilder.pushStyle(skStyle.skTextStyle); + } } } diff --git a/lib/web_ui/lib/src/engine/compositor/util.dart b/lib/web_ui/lib/src/engine/canvaskit/util.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/util.dart rename to lib/web_ui/lib/src/engine/canvaskit/util.dart diff --git a/lib/web_ui/lib/src/engine/compositor/vertices.dart b/lib/web_ui/lib/src/engine/canvaskit/vertices.dart similarity index 65% rename from lib/web_ui/lib/src/engine/compositor/vertices.dart rename to lib/web_ui/lib/src/engine/canvaskit/vertices.dart index 2c05c91d02465..ec13910db9c39 100644 --- a/lib/web_ui/lib/src/engine/compositor/vertices.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/vertices.dart @@ -5,17 +5,16 @@ // @dart = 2.10 part of engine; -class CkVertices implements ui.Vertices { - late SkVertices skVertices; - - CkVertices( +class CkVertices extends ManagedSkiaObject implements ui.Vertices { + factory CkVertices( ui.VertexMode mode, List positions, { List? textureCoordinates, List? colors, List? indices, - }) : assert(mode != null), // ignore: unnecessary_null_comparison - assert(positions != null) { // ignore: unnecessary_null_comparison + }) { + assert(mode != null); // ignore: unnecessary_null_comparison + assert(positions != null); // ignore: unnecessary_null_comparison if (textureCoordinates != null && textureCoordinates.length != positions.length) throw ArgumentError( @@ -27,7 +26,7 @@ class CkVertices implements ui.Vertices { throw ArgumentError( '"indices" values must be valid indices in the positions list.'); - skVertices = canvasKit.MakeSkVertices( + return CkVertices._( toSkVertexMode(mode), toSkPoints2d(positions), textureCoordinates != null ? toSkPoints2d(textureCoordinates) : null, @@ -36,14 +35,15 @@ class CkVertices implements ui.Vertices { ); } - CkVertices.raw( + factory CkVertices.raw( ui.VertexMode mode, Float32List positions, { Float32List? textureCoordinates, Int32List? colors, Uint16List? indices, - }) : assert(mode != null), // ignore: unnecessary_null_comparison - assert(positions != null) { // ignore: unnecessary_null_comparison + }) { + assert(mode != null); // ignore: unnecessary_null_comparison + assert(positions != null); // ignore: unnecessary_null_comparison if (textureCoordinates != null && textureCoordinates.length != positions.length) throw ArgumentError( @@ -55,7 +55,7 @@ class CkVertices implements ui.Vertices { throw ArgumentError( '"indices" values must be valid indices in the positions list.'); - skVertices = canvasKit.MakeSkVertices( + return CkVertices._( toSkVertexMode(mode), rawPointsToSkPoints2d(positions), textureCoordinates != null ? rawPointsToSkPoints2d(textureCoordinates) : null, @@ -63,4 +63,39 @@ class CkVertices implements ui.Vertices { indices, ); } + + CkVertices._( + this._mode, + this._positions, + this._textureCoordinates, + this._colors, + this._indices, + ); + + final SkVertexMode _mode; + final List _positions; + final List? _textureCoordinates; + final List? _colors; + final Uint16List? _indices; + + @override + SkVertices createDefault() { + return canvasKit.MakeSkVertices( + _mode, + _positions, + _textureCoordinates, + _colors, + _indices, + ); + } + + @override + SkVertices resurrect() { + return createDefault(); + } + + @override + void delete() { + rawSkiaObject?.delete(); + } } diff --git a/lib/web_ui/lib/src/engine/compositor/viewport_metrics.dart b/lib/web_ui/lib/src/engine/canvaskit/viewport_metrics.dart similarity index 100% rename from lib/web_ui/lib/src/engine/compositor/viewport_metrics.dart rename to lib/web_ui/lib/src/engine/canvaskit/viewport_metrics.dart diff --git a/lib/web_ui/lib/src/engine/color_filter.dart b/lib/web_ui/lib/src/engine/color_filter.dart index 0181695ca2d5a..230cdf9a2ae25 100644 --- a/lib/web_ui/lib/src/engine/color_filter.dart +++ b/lib/web_ui/lib/src/engine/color_filter.dart @@ -169,8 +169,4 @@ class EngineColorFilter implements ui.ColorFilter { return 'Unknown ColorFilter type. This is an error. If you\'re seeing this, please file an issue at https://github.com/flutter/flutter/issues/new.'; } } - - List webOnlySerializeToCssPaint() { - throw UnsupportedError('ColorFilter for CSS paint not yet supported'); - } } diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart deleted file mode 100644 index 61ed3c13c866d..0000000000000 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ /dev/null @@ -1,117 +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. - -// @dart = 2.10 -part of engine; - -/// Instantiates a [ui.Codec] backed by an `SkImage` from Skia. -void skiaInstantiateImageCodec(Uint8List list, Callback callback, - [int? width, int? height, int? format, int? rowBytes]) { - final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(list); - final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); - final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); - callback(codec); -} - -/// A wrapper for `SkAnimatedImage`. -class CkAnimatedImage implements ui.Image { - final SkAnimatedImage _skAnimatedImage; - - CkAnimatedImage(this._skAnimatedImage); - - @override - void dispose() { - _skAnimatedImage.delete(); - } - - int get frameCount => _skAnimatedImage.getFrameCount(); - - /// Decodes the next frame and returns the frame duration. - Duration decodeNextFrame() { - final int durationMillis = _skAnimatedImage.decodeNextFrame(); - return Duration(milliseconds: durationMillis); - } - - int get repetitionCount => _skAnimatedImage.getRepetitionCount(); - - CkImage get currentFrameAsImage { - return CkImage(_skAnimatedImage.getCurrentFrame()); - } - - @override - int get width => _skAnimatedImage.width(); - - @override - int get height => _skAnimatedImage.height(); - - @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; - } -} - -/// A [ui.Image] backed by an `SkImage` from Skia. -class CkImage implements ui.Image { - final SkImage skImage; - - CkImage(this.skImage); - - @override - void dispose() { - skImage.delete(); - } - - @override - int get width => skImage.width(); - - @override - int get height => skImage.height(); - - @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; - } -} - -/// A [Codec] that wraps an `SkAnimatedImage`. -class CkAnimatedImageCodec implements ui.Codec { - CkAnimatedImage? animatedImage; - - CkAnimatedImageCodec(this.animatedImage); - - @override - void dispose() { - animatedImage!.dispose(); - animatedImage = null; - } - - @override - int get frameCount => animatedImage!.frameCount; - - @override - int get repetitionCount => animatedImage!.repetitionCount; - - @override - Future getNextFrame() { - final Duration duration = animatedImage!.decodeNextFrame(); - final CkImage image = animatedImage!.currentFrameAsImage; - return Future.value(AnimatedImageFrameInfo(duration, image)); - } -} - -/// Data for a single frame of an animated image. -class AnimatedImageFrameInfo implements ui.FrameInfo { - final Duration _duration; - final CkImage _image; - - AnimatedImageFrameInfo(this._duration, this._image); - - @override - Duration get duration => _duration; - - @override - ui.Image get image => _image; -} diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index 1e38b05360297..5b0f7d31d8167 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -282,3 +282,127 @@ html.Element _drawParagraphElement( } return paragraphElement; } + +class _SaveElementStackEntry { + _SaveElementStackEntry({ + required this.savedElement, + required this.transform, + }); + + final html.Element savedElement; + final Matrix4 transform; +} + +/// Provides save stack tracking functionality to implementations of +/// [EngineCanvas]. +mixin SaveElementStackTracking on EngineCanvas { + static final Vector3 _unitZ = Vector3(0.0, 0.0, 1.0); + + final List<_SaveElementStackEntry> _saveStack = <_SaveElementStackEntry>[]; + + /// The element at the top of the element stack, or [rootElement] if the stack + /// is empty. + html.Element get currentElement => + _elementStack.isEmpty ? rootElement : _elementStack.last; + + /// The stack that maintains the DOM elements used to express certain paint + /// operations, such as clips. + final List _elementStack = []; + + /// Pushes the [element] onto the element stack for the purposes of applying + /// a paint effect using a DOM element, e.g. for clipping. + /// + /// The [restore] method automatically pops the element off the stack. + void pushElement(html.Element element) { + _elementStack.add(element); + } + + /// Empties the save stack and the element stack, and resets the transform + /// and clip parameters. + /// + /// Classes that override this method must call `super.clear()`. + @override + void clear() { + _saveStack.clear(); + _elementStack.clear(); + _currentTransform = Matrix4.identity(); + } + + /// The current transformation matrix. + Matrix4 get currentTransform => _currentTransform; + Matrix4 _currentTransform = Matrix4.identity(); + + /// Saves current clip and transform on the save stack. + /// + /// Classes that override this method must call `super.save()`. + @override + void save() { + _saveStack.add(_SaveElementStackEntry( + savedElement: currentElement, + transform: _currentTransform.clone(), + )); + } + + /// Restores current clip and transform from the save stack. + /// + /// Classes that override this method must call `super.restore()`. + @override + void restore() { + if (_saveStack.isEmpty) { + return; + } + final _SaveElementStackEntry entry = _saveStack.removeLast(); + _currentTransform = entry.transform; + + // Pop out of any clips. + while (currentElement != entry.savedElement) { + _elementStack.removeLast(); + } + } + + /// Multiplies the [currentTransform] matrix by a translation. + /// + /// Classes that override this method must call `super.translate()`. + @override + void translate(double dx, double dy) { + _currentTransform.translate(dx, dy); + } + + /// Scales the [currentTransform] matrix. + /// + /// Classes that override this method must call `super.scale()`. + @override + void scale(double sx, double sy) { + _currentTransform.scale(sx, sy); + } + + /// Rotates the [currentTransform] matrix. + /// + /// Classes that override this method must call `super.rotate()`. + @override + void rotate(double radians) { + _currentTransform.rotate(_unitZ, radians); + } + + /// Skews the [currentTransform] matrix. + /// + /// Classes that override this method must call `super.skew()`. + @override + void skew(double sx, double sy) { + // DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians, + // but in our case they are transform matrix values. + final Matrix4 skewMatrix = Matrix4.identity(); + final Float32List storage = skewMatrix.storage; + storage[1] = sy; + storage[4] = sx; + _currentTransform.multiply(skewMatrix); + } + + /// Multiplies the [currentTransform] matrix by another matrix. + /// + /// Classes that override this method must call `super.transform()`. + @override + void transform(Float32List matrix4) { + _currentTransform.multiply(Matrix4.fromFloat32List(matrix4)); + } +} diff --git a/lib/web_ui/lib/src/engine/houdini_canvas.dart b/lib/web_ui/lib/src/engine/houdini_canvas.dart deleted file mode 100644 index 1f83140a69983..0000000000000 --- a/lib/web_ui/lib/src/engine/houdini_canvas.dart +++ /dev/null @@ -1,370 +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. - -// TODO(yjbanov): optimization opportunities (see also houdini_painter.js) -// - collapse non-drawing paint operations -// - avoid producing DOM-based clips if there is no text -// - evaluate using stylesheets for static CSS properties -// - evaluate reusing houdini canvases - -// @dart = 2.10 -part of engine; - -/// A canvas that renders to a combination of HTML DOM and CSS Custom Paint API. -/// -/// This canvas produces paint commands for houdini_painter.js to apply. This -/// class must be kept in sync with houdini_painter.js. -class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking { - @override - final html.Element rootElement = html.Element.tag('flt-houdini'); - - /// The rectangle positioned relative to the parent layer's coordinate system - /// where this canvas paints. - /// - /// Painting outside the bounds of this rectangle is cropped. - final ui.Rect? bounds; - - HoudiniCanvas(this.bounds) { - // TODO(yjbanov): would it be faster to specify static values in a - // stylesheet and let the browser apply them? - rootElement.style - ..position = 'absolute' - ..top = '0' - ..left = '0' - ..width = '${bounds!.size.width}px' - ..height = '${bounds!.size.height}px' - ..backgroundImage = 'paint(flt)'; - } - - /// Prepare to reuse this canvas by clearing it's current contents. - @override - void clear() { - super.clear(); - _serializedCommands = >[]; - // TODO(yjbanov): we should measure if reusing old elements is beneficial. - domRenderer.clearDom(rootElement); - } - - /// Paint commands serialized for sending to the CSS custom painter. - List> _serializedCommands = >[]; - - void apply(PaintCommand command) { - // Some commands are applied purely in HTML DOM and do not need to be - // serialized. - if (command is! PaintDrawParagraph && - command is! PaintDrawImageRect && - command is! PaintTransform) { - command.serializeToCssPaint(_serializedCommands); - } - command.apply(this); - } - - /// Sends the paint commands to the CSS custom painter for painting. - void commit() { - if (_serializedCommands.isNotEmpty) { - rootElement.style.setProperty('--flt', json.encode(_serializedCommands)); - } else { - rootElement.style.removeProperty('--flt'); - } - } - - @override - void clipRect(ui.Rect rect) { - final html.Element clip = html.Element.tag('flt-clip-rect'); - final String cssTransform = matrix4ToCssTransform( - transformWithOffset(currentTransform, ui.Offset(rect.left, rect.top))); - clip.style - ..overflow = 'hidden' - ..position = 'absolute' - ..transform = cssTransform - ..width = '${rect.width}px' - ..height = '${rect.height}px'; - - // The clipping element will translate the coordinate system as well, which - // is not what a clip should do. To offset that we translate in the opposite - // direction. - super.translate(-rect.left, -rect.top); - - currentElement.append(clip); - pushElement(clip); - } - - @override - void clipRRect(ui.RRect rrect) { - final ui.Rect outer = rrect.outerRect; - if (rrect.isRect) { - clipRect(outer); - return; - } - - final html.Element clip = html.Element.tag('flt-clip-rrect'); - final html.CssStyleDeclaration style = clip.style; - style - ..overflow = 'hidden' - ..position = 'absolute' - ..transform = 'translate(${outer.left}px, ${outer.right}px)' - ..width = '${outer.width}px' - ..height = '${outer.height}px'; - - if (rrect.tlRadiusY == rrect.tlRadiusX) { - style.borderTopLeftRadius = '${rrect.tlRadiusX}px'; - } else { - style.borderTopLeftRadius = '${rrect.tlRadiusX}px ${rrect.tlRadiusY}px'; - } - - if (rrect.trRadiusY == rrect.trRadiusX) { - style.borderTopRightRadius = '${rrect.trRadiusX}px'; - } else { - style.borderTopRightRadius = '${rrect.trRadiusX}px ${rrect.trRadiusY}px'; - } - - if (rrect.brRadiusY == rrect.brRadiusX) { - style.borderBottomRightRadius = '${rrect.brRadiusX}px'; - } else { - style.borderBottomRightRadius = - '${rrect.brRadiusX}px ${rrect.brRadiusY}px'; - } - - if (rrect.blRadiusY == rrect.blRadiusX) { - style.borderBottomLeftRadius = '${rrect.blRadiusX}px'; - } else { - style.borderBottomLeftRadius = - '${rrect.blRadiusX}px ${rrect.blRadiusY}px'; - } - - // The clipping element will translate the coordinate system as well, which - // is not what a clip should do. To offset that we translate in the opposite - // direction. - super.translate(-rrect.left, -rrect.top); - - currentElement.append(clip); - pushElement(clip); - } - - @override - void clipPath(ui.Path path) { - // TODO(yjbanov): implement. - } - - @override - void drawColor(ui.Color color, ui.BlendMode blendMode) { - // Drawn using CSS Paint. - } - - @override - void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawPaint(SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawRect(ui.Rect rect, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawRRect(ui.RRect rrect, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawOval(ui.Rect rect, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawPath(ui.Path path, SurfacePaintData paint) { - // Drawn using CSS Paint. - } - - @override - void drawShadow(ui.Path path, ui.Color color, double elevation, - bool transparentOccluder) { - // Drawn using CSS Paint. - } - - @override - void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { - // TODO(yjbanov): implement. - } - - @override - void drawImageRect( - ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) { - // TODO(yjbanov): implement src rectangle - final HtmlImage htmlImage = image as HtmlImage; - final html.Element imageBox = html.Element.tag('flt-img'); - final String cssTransform = matrix4ToCssTransform( - transformWithOffset(currentTransform, ui.Offset(dst.left, dst.top))); - imageBox.style - ..position = 'absolute' - ..transformOrigin = '0 0 0' - ..width = '${dst.width.toInt()}px' - ..height = '${dst.height.toInt()}px' - ..transform = cssTransform - ..backgroundImage = 'url(${htmlImage.imgElement.src})' - ..backgroundRepeat = 'norepeat' - ..backgroundSize = '${dst.width}px ${dst.height}px'; - currentElement.append(imageBox); - } - - @override - void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { - final html.Element paragraphElement = - _drawParagraphElement(paragraph as EngineParagraph, offset, transform: currentTransform); - currentElement.append(paragraphElement); - } - - @override - void drawVertices( - ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { - // TODO(flutter_web): implement. - } - - @override - void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) { - // TODO(flutter_web): implement. - } - - @override - void endOfPaint() {} -} - -class _SaveElementStackEntry { - _SaveElementStackEntry({ - required this.savedElement, - required this.transform, - }); - - final html.Element savedElement; - final Matrix4 transform; -} - -/// Provides save stack tracking functionality to implementations of -/// [EngineCanvas]. -mixin SaveElementStackTracking on EngineCanvas { - static final Vector3 _unitZ = Vector3(0.0, 0.0, 1.0); - - final List<_SaveElementStackEntry> _saveStack = <_SaveElementStackEntry>[]; - - /// The element at the top of the element stack, or [rootElement] if the stack - /// is empty. - html.Element get currentElement => - _elementStack.isEmpty ? rootElement : _elementStack.last; - - /// The stack that maintains the DOM elements used to express certain paint - /// operations, such as clips. - final List _elementStack = []; - - /// Pushes the [element] onto the element stack for the purposes of applying - /// a paint effect using a DOM element, e.g. for clipping. - /// - /// The [restore] method automatically pops the element off the stack. - void pushElement(html.Element element) { - _elementStack.add(element); - } - - /// Empties the save stack and the element stack, and resets the transform - /// and clip parameters. - /// - /// Classes that override this method must call `super.clear()`. - @override - void clear() { - _saveStack.clear(); - _elementStack.clear(); - _currentTransform = Matrix4.identity(); - } - - /// The current transformation matrix. - Matrix4 get currentTransform => _currentTransform; - Matrix4 _currentTransform = Matrix4.identity(); - - /// Saves current clip and transform on the save stack. - /// - /// Classes that override this method must call `super.save()`. - @override - void save() { - _saveStack.add(_SaveElementStackEntry( - savedElement: currentElement, - transform: _currentTransform.clone(), - )); - } - - /// Restores current clip and transform from the save stack. - /// - /// Classes that override this method must call `super.restore()`. - @override - void restore() { - if (_saveStack.isEmpty) { - return; - } - final _SaveElementStackEntry entry = _saveStack.removeLast(); - _currentTransform = entry.transform; - - // Pop out of any clips. - while (currentElement != entry.savedElement) { - _elementStack.removeLast(); - } - } - - /// Multiplies the [currentTransform] matrix by a translation. - /// - /// Classes that override this method must call `super.translate()`. - @override - void translate(double dx, double dy) { - _currentTransform.translate(dx, dy); - } - - /// Scales the [currentTransform] matrix. - /// - /// Classes that override this method must call `super.scale()`. - @override - void scale(double sx, double sy) { - _currentTransform.scale(sx, sy); - } - - /// Rotates the [currentTransform] matrix. - /// - /// Classes that override this method must call `super.rotate()`. - @override - void rotate(double radians) { - _currentTransform.rotate(_unitZ, radians); - } - - /// Skews the [currentTransform] matrix. - /// - /// Classes that override this method must call `super.skew()`. - @override - void skew(double sx, double sy) { - // DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians, - // but in our case they are transform matrix values. - final Matrix4 skewMatrix = Matrix4.identity(); - final Float32List storage = skewMatrix.storage; - storage[1] = sy; - storage[4] = sx; - _currentTransform.multiply(skewMatrix); - } - - /// Multiplies the [currentTransform] matrix by another matrix. - /// - /// Classes that override this method must call `super.transform()`. - @override - void transform(Float32List matrix4) { - _currentTransform.multiply(Matrix4.fromFloat32List(matrix4)); - } -} diff --git a/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart b/lib/web_ui/lib/src/engine/html/backdrop_filter.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/backdrop_filter.dart rename to lib/web_ui/lib/src/engine/html/backdrop_filter.dart diff --git a/lib/web_ui/lib/src/engine/surface/canvas.dart b/lib/web_ui/lib/src/engine/html/canvas.dart similarity index 97% rename from lib/web_ui/lib/src/engine/surface/canvas.dart rename to lib/web_ui/lib/src/engine/html/canvas.dart index 4f3fc554ab034..b5b942dc84d16 100644 --- a/lib/web_ui/lib/src/engine/surface/canvas.dart +++ b/lib/web_ui/lib/src/engine/html/canvas.dart @@ -490,8 +490,8 @@ class SurfaceCanvas implements ui.Canvas { ui.Image atlas, List transforms, List rects, - List colors, - ui.BlendMode blendMode, + List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint, ) { @@ -499,15 +499,14 @@ class SurfaceCanvas implements ui.Canvas { assert(atlas != null); // atlas is checked on the engine side assert(transforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || colors.isEmpty || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; if (transforms.length != rectCount) { throw ArgumentError('"transforms" and "rects" lengths must match.'); } - if (colors.isNotEmpty && colors.length != rectCount) { + if (colors != null && colors.isNotEmpty && colors.length != rectCount) { throw ArgumentError( 'If non-null, "colors" length must match that of "transforms" and "rects".'); } @@ -521,8 +520,8 @@ class SurfaceCanvas implements ui.Canvas { ui.Image atlas, Float32List rstTransforms, Float32List rects, - Int32List colors, - ui.BlendMode blendMode, + Int32List? colors, + ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint, ) { @@ -530,8 +529,7 @@ class SurfaceCanvas implements ui.Canvas { assert(atlas != null); // atlas is checked on the engine side assert(rstTransforms != null); // ignore: unnecessary_null_comparison assert(rects != null); // ignore: unnecessary_null_comparison - assert(colors != null); // ignore: unnecessary_null_comparison - assert(blendMode != null); // ignore: unnecessary_null_comparison + assert(colors == null || blendMode != null); assert(paint != null); // ignore: unnecessary_null_comparison final int rectCount = rects.length; @@ -542,7 +540,7 @@ class SurfaceCanvas implements ui.Canvas { throw ArgumentError( '"rstTransforms" and "rects" lengths must be a multiple of four.'); } - if (colors.length * 4 != rectCount) { + if (colors != null && colors.length * 4 != rectCount) { throw ArgumentError( 'If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); } diff --git a/lib/web_ui/lib/src/engine/surface/clip.dart b/lib/web_ui/lib/src/engine/html/clip.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/clip.dart rename to lib/web_ui/lib/src/engine/html/clip.dart index 5b7a871d77ec6..d2fd64fed746f 100644 --- a/lib/web_ui/lib/src/engine/surface/clip.dart +++ b/lib/web_ui/lib/src/engine/html/clip.dart @@ -325,6 +325,8 @@ class PersistedPhysicalShape extends PersistedContainerSurface } if (oldSurface.path != path) { oldSurface._clipElement?.remove(); + // Reset style on prior element since we may have switched between + // rect/rrect and arbitrary path. domRenderer.setElementStyle(rootElement!, 'clip-path', ''); domRenderer.setElementStyle(rootElement!, '-webkit-clip-path', ''); _applyShape(); diff --git a/lib/web_ui/lib/src/engine/surface/debug_canvas_reuse_overlay.dart b/lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/debug_canvas_reuse_overlay.dart rename to lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart diff --git a/lib/web_ui/lib/src/engine/surface/image_filter.dart b/lib/web_ui/lib/src/engine/html/image_filter.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/image_filter.dart rename to lib/web_ui/lib/src/engine/html/image_filter.dart diff --git a/lib/web_ui/lib/src/engine/surface/offset.dart b/lib/web_ui/lib/src/engine/html/offset.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/offset.dart rename to lib/web_ui/lib/src/engine/html/offset.dart diff --git a/lib/web_ui/lib/src/engine/surface/opacity.dart b/lib/web_ui/lib/src/engine/html/opacity.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/opacity.dart rename to lib/web_ui/lib/src/engine/html/opacity.dart diff --git a/lib/web_ui/lib/src/engine/surface/painting.dart b/lib/web_ui/lib/src/engine/html/painting.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/painting.dart rename to lib/web_ui/lib/src/engine/html/painting.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/conic.dart b/lib/web_ui/lib/src/engine/html/path/conic.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/conic.dart rename to lib/web_ui/lib/src/engine/html/path/conic.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/cubic.dart b/lib/web_ui/lib/src/engine/html/path/cubic.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/cubic.dart rename to lib/web_ui/lib/src/engine/html/path/cubic.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/path.dart b/lib/web_ui/lib/src/engine/html/path/path.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/path/path.dart rename to lib/web_ui/lib/src/engine/html/path/path.dart index 36b8e77be672a..33ece6bba1fc3 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path.dart +++ b/lib/web_ui/lib/src/engine/html/path/path.dart @@ -1542,12 +1542,6 @@ class SurfacePath implements ui.Path { ui.Rect? get webOnlyPathAsCircle => pathRef.isOval == -1 ? null : pathRef.getBounds(); - /// Serializes this path to a value that's sent to a CSS custom painter for - /// painting. - List webOnlySerializeToCssPaint() { - throw UnimplementedError(); - } - /// Returns if Path is empty. /// Empty Path may have FillType but has no points, verbs or weights. /// Constructor, reset and rewind makes SkPath empty. diff --git a/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart b/lib/web_ui/lib/src/engine/html/path/path_metrics.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/path_metrics.dart rename to lib/web_ui/lib/src/engine/html/path/path_metrics.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/path_ref.dart b/lib/web_ui/lib/src/engine/html/path/path_ref.dart similarity index 99% rename from lib/web_ui/lib/src/engine/surface/path/path_ref.dart rename to lib/web_ui/lib/src/engine/html/path/path_ref.dart index 078f3d6de0561..2b9c9f094ca09 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_ref.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_ref.dart @@ -367,7 +367,7 @@ class PathRef { } else { _conicWeights!.setAll(0, ref._conicWeights!); } - assert(verbCount == 0 || _fVerbs[0] != 0); + assert(verbCount == 0 || _fVerbs[0] == ref._fVerbs[0]); fBoundsIsDirty = ref.fBoundsIsDirty; if (!fBoundsIsDirty) { fBounds = ref.fBounds; diff --git a/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart b/lib/web_ui/lib/src/engine/html/path/path_to_svg.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart rename to lib/web_ui/lib/src/engine/html/path/path_to_svg.dart diff --git a/lib/web_ui/lib/src/engine/surface/path/path_utils.dart b/lib/web_ui/lib/src/engine/html/path/path_utils.dart similarity index 97% rename from lib/web_ui/lib/src/engine/surface/path/path_utils.dart rename to lib/web_ui/lib/src/engine/html/path/path_utils.dart index 18abeefa23968..9908cf52370c8 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_utils.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_utils.dart @@ -15,12 +15,12 @@ class SPathSegmentMask { /// Types of path operations. class SPathVerb { - static const int kMove = 1; // 1 point - static const int kLine = 2; // 2 points - static const int kQuad = 3; // 3 points - static const int kConic = 4; // 3 points + 1 weight - static const int kCubic = 5; // 4 points - static const int kClose = 6; // 0 points + static const int kMove = 0; // 1 point + static const int kLine = 1; // 2 points + static const int kQuad = 2; // 3 points + static const int kConic = 3; // 3 points + 1 weight + static const int kCubic = 4; // 4 points + static const int kClose = 5; // 0 points } class SPath { diff --git a/lib/web_ui/lib/src/engine/surface/path/path_windings.dart b/lib/web_ui/lib/src/engine/html/path/path_windings.dart similarity index 96% rename from lib/web_ui/lib/src/engine/surface/path/path_windings.dart rename to lib/web_ui/lib/src/engine/html/path/path_windings.dart index 23790890da780..554a5413c8e36 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_windings.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_windings.dart @@ -145,7 +145,8 @@ class PathWinding { } _QuadRoots quadRoots = _QuadRoots(); - final int n = quadRoots.findRoots(startY - 2 * y1 + endY, 2 * (y1 - startY), endY - y); + final int n = quadRoots.findRoots( + startY - 2 * y1 + endY, 2 * (y1 - startY), endY - y); assert(n <= 1); double xt; if (0 == n) { @@ -377,6 +378,9 @@ class PathIterator { int _verbIndex = 0; int _pointIndex = 0; + /// Maximum buffer size required for points in [next] calls. + static const int kMaxBufferSize = 8; + /// Returns true if first contour on path is closed. bool isClosedContour() { if (_verbCount == 0 || _verbIndex == _verbCount) { @@ -438,7 +442,17 @@ class PathIterator { pathRef.points[_pointIndex - 2], pathRef.points[_pointIndex - 1]); } - int peek() => pathRef._fVerbs[_verbIndex]; + int peek() { + if (_verbIndex < pathRef.countVerbs()) { + return pathRef._fVerbs[_verbIndex]; + } + if (_needClose && _segmentState == SPathSegmentState.kAfterPrimitive) { + return (_lastPointX != _moveToX || _lastPointY != _moveToY) + ? SPath.kLineVerb + : SPath.kCloseVerb; + } + return SPath.kDoneVerb; + } // Returns next verb and reads associated points into [outPts]. int next(Float32List outPts) { diff --git a/lib/web_ui/lib/src/engine/surface/path/tangent.dart b/lib/web_ui/lib/src/engine/html/path/tangent.dart similarity index 100% rename from lib/web_ui/lib/src/engine/surface/path/tangent.dart rename to lib/web_ui/lib/src/engine/html/path/tangent.dart diff --git a/lib/web_ui/lib/src/engine/surface/picture.dart b/lib/web_ui/lib/src/engine/html/picture.dart similarity index 87% rename from lib/web_ui/lib/src/engine/surface/picture.dart rename to lib/web_ui/lib/src/engine/html/picture.dart index 0249071a6fc93..520a4378f1408 100644 --- a/lib/web_ui/lib/src/engine/surface/picture.dart +++ b/lib/web_ui/lib/src/engine/html/picture.dart @@ -71,292 +71,9 @@ void _recycleCanvas(EngineCanvas? canvas) { } } - -/// Signature of a function that instantiates a [PersistedPicture]. -typedef PersistedPictureFactory = PersistedPicture Function( - double dx, - double dy, - ui.Picture picture, - int hints, -); - -/// Function used by the [SceneBuilder] to instantiate a picture layer. -PersistedPictureFactory persistedPictureFactory = standardPictureFactory; - -/// Instantiates an implementation of a picture layer that uses DOM, CSS, and -/// 2D canvas for painting. -PersistedStandardPicture standardPictureFactory( - double dx, double dy, ui.Picture picture, int hints) { - return PersistedStandardPicture(dx, dy, picture, hints); -} - -/// Instantiates an implementation of a picture layer that uses CSS Paint API -/// (part of Houdini) for painting. -PersistedHoudiniPicture houdiniPictureFactory( - double dx, double dy, ui.Picture picture, int hints) { - return PersistedHoudiniPicture(dx, dy, picture, hints); -} - -class PersistedHoudiniPicture extends PersistedPicture { - PersistedHoudiniPicture(double dx, double dy, ui.Picture picture, int hints) - : super(dx, dy, picture as EnginePicture, hints) { - if (!_cssPainterRegistered) { - _registerCssPainter(); - } - } - - static bool _cssPainterRegistered = false; - - @override - double matchForUpdate(PersistedPicture existingSurface) { - // Houdini is display list-based so all pictures are cheap to repaint. - // However, if the picture hasn't changed at all then it's completely - // free. - return existingSurface.picture == picture ? 0.0 : 1.0; - } - - static void _registerCssPainter() { - _cssPainterRegistered = true; - final dynamic css = js_util.getProperty(html.window, 'CSS'); - final dynamic paintWorklet = js_util.getProperty(css, 'paintWorklet'); - if (paintWorklet == null) { - html.window.console.warn( - 'WARNING: CSS.paintWorklet not available. Paint worklets are only ' - 'supported on sites served from https:// or http://localhost.'); - return; - } - js_util.callMethod( - paintWorklet, - 'addModule', - [ - '/packages/flutter_web_ui/assets/houdini_painter.js', - ], - ); - } - - /// Houdini does not paint to bitmap. - @override - int get bitmapPixelCount => 0; - - @override - void applyPaint(EngineCanvas? oldCanvas) { - _recycleCanvas(oldCanvas); - final HoudiniCanvas canvas = HoudiniCanvas(_optimalLocalCullRect); - _canvas = canvas; - domRenderer.clearDom(rootElement!); - rootElement!.append(_canvas!.rootElement); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - canvas.commit(); - } -} - -class PersistedStandardPicture extends PersistedPicture { - PersistedStandardPicture(double dx, double dy, ui.Picture picture, int hints) - : super(dx, dy, picture as EnginePicture, hints); - - @override - double matchForUpdate(PersistedStandardPicture existingSurface) { - if (existingSurface.picture == picture) { - // Picture is the same, return perfect score. - return 0.0; - } - - if (!existingSurface.picture.recordingCanvas!.didDraw) { - // The previous surface didn't draw anything and therefore has no - // resources to reuse. - return 1.0; - } - - final bool didRequireBitmap = - existingSurface.picture.recordingCanvas!.hasArbitraryPaint; - final bool requiresBitmap = picture.recordingCanvas!.hasArbitraryPaint; - if (didRequireBitmap != requiresBitmap) { - // Switching canvas types is always expensive. - return 1.0; - } else if (!requiresBitmap) { - // Currently DomCanvas is always expensive to repaint, as we always throw - // out all the DOM we rendered before. This may change in the future, at - // which point we may return other values here. - return 1.0; - } else { - final BitmapCanvas? oldCanvas = existingSurface._canvas as BitmapCanvas?; - if (oldCanvas == null) { - // We did not allocate a canvas last time. This can happen when the - // picture is completely clipped out of the view. - return 1.0; - } else if (!oldCanvas.doesFitBounds(_exactLocalCullRect!)) { - // The canvas needs to be resized before painting. - return 1.0; - } else { - final int newPixelCount = BitmapCanvas._widthToPhysical(_exactLocalCullRect!.width) - * BitmapCanvas._heightToPhysical(_exactLocalCullRect!.height); - final int oldPixelCount = - oldCanvas._widthInBitmapPixels * oldCanvas._heightInBitmapPixels; - - if (oldPixelCount == 0) { - return 1.0; - } - - final double pixelCountRatio = newPixelCount / oldPixelCount; - assert(0 <= pixelCountRatio && pixelCountRatio <= 1.0, - 'Invalid pixel count ratio $pixelCountRatio'); - return 1.0 - pixelCountRatio; - } - } - } - - @override - Matrix4? get localTransformInverse => null; - - @override - int get bitmapPixelCount { - if (_canvas is! BitmapCanvas) { - return 0; - } - - final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; - return bitmapCanvas.bitmapPixelCount; - } - - @override - void applyPaint(EngineCanvas? oldCanvas) { - if (picture.recordingCanvas!.hasArbitraryPaint) { - _applyBitmapPaint(oldCanvas); - } else { - _applyDomPaint(oldCanvas); - } - } - - void _applyDomPaint(EngineCanvas? oldCanvas) { - _recycleCanvas(oldCanvas); - _canvas = DomCanvas(); - domRenderer.clearDom(rootElement!); - rootElement!.append(_canvas!.rootElement); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - } - - void _applyBitmapPaint(EngineCanvas? oldCanvas) { - if (oldCanvas is BitmapCanvas && - oldCanvas.doesFitBounds(_optimalLocalCullRect!) && - oldCanvas.isReusable()) { - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.keptCount++; - } - oldCanvas.bounds = _optimalLocalCullRect!; - _canvas = oldCanvas; - oldCanvas.setElementCache(_elementCache); - _canvas!.clear(); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - } else { - // We can't use the old canvas because the size has changed, so we put - // it in a cache for later reuse. - _recycleCanvas(oldCanvas); - // We cannot paint immediately because not all canvases that we may be - // able to reuse have been released yet. So instead we enqueue this - // picture to be painted after the update cycle is done syncing the layer - // tree then reuse canvases that were freed up. - _paintQueue.add(_PaintRequest( - canvasSize: _optimalLocalCullRect!.size, - paintCallback: () { - _canvas = _findOrCreateCanvas(_optimalLocalCullRect!); - assert(_canvas is BitmapCanvas - && (_canvas as BitmapCanvas?)!._elementCache == _elementCache); - if (_debugExplainSurfaceStats) { - final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; - _surfaceStatsFor(this).paintPixelCount += - bitmapCanvas.bitmapPixelCount; - } - domRenderer.clearDom(rootElement!); - rootElement!.append(_canvas!.rootElement); - _canvas!.clear(); - picture.recordingCanvas!.apply(_canvas, _optimalLocalCullRect); - }, - )); - } - } - - /// Attempts to reuse a canvas from the [_recycledCanvases]. Allocates a new - /// one if unable to reuse. - /// - /// The best recycled canvas is one that: - /// - /// - Fits the requested [canvasSize]. This is a hard requirement. Otherwise - /// we risk clipping the picture. - /// - Is the smallest among all possible reusable canvases. This makes canvas - /// reuse more efficient. - /// - Contains no more than twice the number of requested pixels. This makes - /// sure we do not use too much memory for small canvases. - BitmapCanvas _findOrCreateCanvas(ui.Rect bounds) { - final ui.Size canvasSize = bounds.size; - BitmapCanvas? bestRecycledCanvas; - double lastPixelCount = double.infinity; - for (int i = 0; i < _recycledCanvases.length; i++) { - final BitmapCanvas candidate = _recycledCanvases[i]; - if (!candidate.isReusable()) { - continue; - } - - final ui.Size candidateSize = candidate.size; - final double candidatePixelCount = - candidateSize.width * candidateSize.height; - - final bool fits = candidate.doesFitBounds(bounds); - final bool isSmaller = candidatePixelCount < lastPixelCount; - if (fits && isSmaller) { - // [isTooSmall] is used to make sure that a small picture doesn't - // reuse and hold onto memory of a large canvas. - final double requestedPixelCount = bounds.width * bounds.height; - final bool isTooSmall = isSmaller && - requestedPixelCount > 1 && - (candidatePixelCount / requestedPixelCount) > 4; - if (!isTooSmall) { - bestRecycledCanvas = candidate; - lastPixelCount = candidatePixelCount; - final bool fitsExactly = candidateSize.width == canvasSize.width && - candidateSize.height == canvasSize.height; - if (fitsExactly) { - // No need to keep looking any more. - break; - } - } - } - } - - if (bestRecycledCanvas != null) { - if (_debugExplainSurfaceStats) { - _surfaceStatsFor(this).reuseCanvasCount++; - } - _recycledCanvases.remove(bestRecycledCanvas); - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.inRecycleCount = - _recycledCanvases.length; - } - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.reusedCount++; - } - bestRecycledCanvas.bounds = bounds; - bestRecycledCanvas.setElementCache(_elementCache); - return bestRecycledCanvas; - } - - if (_debugShowCanvasReuseStats) { - DebugCanvasReuseOverlay.instance.createdCount++; - } - final BitmapCanvas canvas = BitmapCanvas(bounds); - canvas.setElementCache(_elementCache); - if (_debugExplainSurfaceStats) { - _surfaceStatsFor(this) - ..allocateBitmapCanvasCount += 1 - ..allocatedBitmapSizeInPixels = - canvas._widthInBitmapPixels * canvas._heightInBitmapPixels; - } - return canvas; - } -} - /// A surface that uses a combination of ``, `