diff --git a/.ci.yaml b/.ci.yaml index 482c08a0a63ec..e544b7621cace 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -2358,7 +2358,6 @@ targets: task_name: basic_material_app_android__compile - name: Linux_pixel_7pro android_verified_input_test - bringup: true recipe: devicelab/devicelab_drone presubmit: false timeout: 60 @@ -2848,7 +2847,6 @@ targets: - name: Linux_mokey hybrid_android_views_integration_test recipe: devicelab/devicelab_drone presubmit: false - bringup: true # https://github.com/flutter/flutter/issues/148085 timeout: 60 properties: tags: > @@ -3254,7 +3252,6 @@ targets: task_name: platform_views_scroll_perf_impeller__timeline_summary - name: Linux_pixel_7pro platform_views_scroll_perf_impeller__timeline_summary - bringup: true # Flaky https://github.com/flutter/flutter/issues/172210 recipe: devicelab/devicelab_drone presubmit: false timeout: 60 @@ -5358,6 +5355,20 @@ targets: ["devicelab", "ios", "mac"] task_name: wide_gamut_ios + - name: Mac wide_gamut_macos + recipe: devicelab/devicelab_drone + bringup: true + presubmit: false + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"} + ] + tags: > + ["devicelab", "hostonly", "mac"] + task_name: wide_gamut_macos + - name: Mac_x64_ios hot_mode_dev_cycle_ios__benchmark recipe: devicelab/devicelab_drone presubmit: false diff --git a/DEPS b/DEPS index 689b2fd193b61..edfa4a7ea33bd 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'f37a22506eb4ea75892c53617a9e3db5a18c5cf7', + 'skia_revision': '8543ce512d5c8a873814357d05e3cb1b7cc3a86e', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. @@ -59,7 +59,7 @@ vars = { # updated revision list of existing dependencies. You will need to # gclient sync before and after update deps to ensure all deps are updated. # updated revision list of existing dependencies. - 'dart_revision': '56294a92d5cc358a02c3c57747f2eb571a59fe6c', + 'dart_revision': '8f778ffd318b4e0eeb699298062dd3f1b80b2cc0', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -317,7 +317,7 @@ deps = { Var('dart_git') + '/dart_style.git@f624489a5013ec58de469d4fd8793c283f62b5d8', 'engine/src/flutter/third_party/dart/third_party/pkg/dartdoc': - Var('dart_git') + '/dartdoc.git@03f48ee08eccdcf2e4cd06678501f07c182bebf5', + Var('dart_git') + '/dartdoc.git@af0085039035557c792b2d08965e24c2dd342d63', 'engine/src/flutter/third_party/dart/third_party/pkg/ecosystem': Var('dart_git') + '/ecosystem.git' + '@' + Var('dart_ecosystem_rev'), @@ -810,7 +810,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'UmQaaNuhkiuE8DzugvqDy3SW58aFti6A_VjUe1ttEWwC' + 'version': 'J2QdLcY2gyt4NP_xVkpr794Xj6oL7Vvn9zyr2Os4I_0C' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', diff --git a/README.md b/README.md index 34061d974d280..3973f048f753f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ [![Twitter handle][]][Twitter badge] [![BlueSky badge][]][BlueSky handle] [![codecov](https://codecov.io/gh/flutter/flutter/branch/master/graph/badge.svg?token=11yDrJU2M2)](https://codecov.io/gh/flutter/flutter) +[![LFX Health Score](https://insights.linuxfoundation.org/api/badge/health-score?project=flutter)](https://insights.linuxfoundation.org/project/flutter) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5631/badge)](https://bestpractices.coreinfrastructure.org/projects/5631) [![SLSA 1](https://slsa.dev/images/gh-badge-level1.svg)](https://slsa.dev) diff --git a/TESTOWNERS b/TESTOWNERS index 2b098b8538b55..daf37a681ff10 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -115,6 +115,8 @@ /dev/devicelab/bin/tasks/very_long_picture_scrolling_perf__e2e_summary.dart @flar @flutter/engine /dev/devicelab/bin/tasks/web_size__compile_test.dart @yjbanov @flutter/web /dev/devicelab/bin/tasks/wide_gamut_ios.dart @gaaclarke @flutter/engine +/dev/devicelab/bin/tasks/wide_gamut_macos.dart @gaaclarke @flutter/engine + ## Windows Android DeviceLab tests /dev/devicelab/bin/tasks/basic_material_app_win__compile.dart @bkonyi @flutter/tool diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index a0830e66ba809..d70a90a800225 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -837dbbdf62dae7a22b403cdbbd8fcd9b1cb32be3 +3bddf2c57eedbd88ace0e83890f8291c0652d44d diff --git a/dev/bots/check_tests_cross_imports.dart b/dev/bots/check_tests_cross_imports.dart index a1424b0aa886c..6b57feda3ec1f 100644 --- a/dev/bots/check_tests_cross_imports.dart +++ b/dev/bots/check_tests_cross_imports.dart @@ -107,7 +107,6 @@ class TestsCrossImportChecker { // TODO(justinmc): Fix all of these tests so there are no cross imports. // See https://github.com/flutter/flutter/issues/177028. static final Set knownWidgetsCrossImports = { - 'packages/flutter/test/widgets/basic_test.dart', 'packages/flutter/test/widgets/text_test.dart', 'packages/flutter/test/widgets/reorderable_list_test.dart', 'packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart', @@ -127,7 +126,6 @@ class TestsCrossImportChecker { 'packages/flutter/test/widgets/autofill_group_test.dart', 'packages/flutter/test/widgets/range_maintaining_scroll_physics_test.dart', 'packages/flutter/test/widgets/scroll_position_test.dart', - 'packages/flutter/test/widgets/sliver_tree_test.dart', 'packages/flutter/test/widgets/interactive_viewer_test.dart', 'packages/flutter/test/widgets/selectable_region_test.dart', 'packages/flutter/test/widgets/editable_text_scribe_test.dart', @@ -146,7 +144,6 @@ class TestsCrossImportChecker { 'packages/flutter/test/widgets/navigator_replacement_test.dart', 'packages/flutter/test/widgets/implicit_animations_test.dart', 'packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart', - 'packages/flutter/test/widgets/page_storage_test.dart', 'packages/flutter/test/widgets/sliver_main_axis_group_test.dart', 'packages/flutter/test/widgets/color_filter_test.dart', 'packages/flutter/test/widgets/semantics_merge_test.dart', @@ -155,7 +152,6 @@ class TestsCrossImportChecker { 'packages/flutter/test/widgets/slivers_padding_test.dart', 'packages/flutter/test/widgets/sliver_constraints_test.dart', 'packages/flutter/test/widgets/autocomplete_test.dart', - 'packages/flutter/test/widgets/expansible_test.dart', 'packages/flutter/test/widgets/decorated_sliver_test.dart', 'packages/flutter/test/widgets/shape_decoration_test.dart', 'packages/flutter/test/widgets/run_app_test.dart', @@ -168,7 +164,6 @@ class TestsCrossImportChecker { 'packages/flutter/test/widgets/router_test.dart', 'packages/flutter/test/widgets/scroll_notification_test.dart', 'packages/flutter/test/widgets/magnifier_test.dart', - 'packages/flutter/test/widgets/backdrop_filter_test.dart', 'packages/flutter/test/widgets/editable_text_test.dart', 'packages/flutter/test/widgets/dual_transition_builder_test.dart', 'packages/flutter/test/widgets/icon_test.dart', @@ -197,7 +192,6 @@ class TestsCrossImportChecker { 'packages/flutter/test/widgets/system_context_menu_test.dart', 'packages/flutter/test/widgets/scrollable_fling_test.dart', 'packages/flutter/test/widgets/debug_test.dart', - 'packages/flutter/test/widgets/banner_test.dart', 'packages/flutter/test/widgets/transformed_scrollable_test.dart', 'packages/flutter/test/widgets/run_app_async_test.dart', 'packages/flutter/test/widgets/scrollable_in_overlay_test.dart', @@ -216,7 +210,6 @@ class TestsCrossImportChecker { 'packages/flutter/test/widgets/scroll_activity_test.dart', 'packages/flutter/test/widgets/tap_region_test.dart', 'packages/flutter/test/widgets/lookup_boundary_test.dart', - 'packages/flutter/test/widgets/reassemble_test.dart', 'packages/flutter/test/widgets/html_element_view_test.dart', 'packages/flutter/test/widgets/navigator_test.dart', 'packages/flutter/test/widgets/text_semantics_test.dart', @@ -269,7 +262,6 @@ class TestsCrossImportChecker { 'packages/flutter/test/cupertino/form_row_test.dart', 'packages/flutter/test/cupertino/colors_test.dart', 'packages/flutter/test/cupertino/text_form_field_row_restoration_test.dart', - 'packages/flutter/test/cupertino/slider_test.dart', }; static final Set _knownCrossImports = knownWidgetsCrossImports.union( diff --git a/dev/customer_testing/tests.version b/dev/customer_testing/tests.version index 46e5009828f10..c6b3f20d27b68 100644 --- a/dev/customer_testing/tests.version +++ b/dev/customer_testing/tests.version @@ -1 +1 @@ -fa246b3c28af7b697c19d3b1a428c5b3ff98e0ca +b3e5a271c2702c1202832e86bb54f44587c3dd46 diff --git a/dev/devicelab/bin/tasks/wide_gamut_macos.dart b/dev/devicelab/bin/tasks/wide_gamut_macos.dart new file mode 100644 index 0000000000000..62da96c9165d2 --- /dev/null +++ b/dev/devicelab/bin/tasks/wide_gamut_macos.dart @@ -0,0 +1,12 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/integration_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.macos; + await task(createWideGamutTest()); +} diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index a5cae89b5d552..1ab96fa21d041 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -243,7 +243,7 @@ TaskFunction createWideGamutTest() { return IntegrationTest( '${flutterDirectory.path}/dev/integration_tests/wide_gamut_test', 'integration_test/app_test.dart', - createPlatforms: ['ios'], + createPlatforms: ['ios', 'macos'], ).call; } diff --git a/dev/integration_tests/hook_user_defines/pubspec.yaml b/dev/integration_tests/hook_user_defines/pubspec.yaml index 3516f38bcbb96..4e8ad6305d6ed 100644 --- a/dev/integration_tests/hook_user_defines/pubspec.yaml +++ b/dev/integration_tests/hook_user_defines/pubspec.yaml @@ -15,11 +15,11 @@ hooks: magic_value: 1000 dependencies: - hooks: 1.0.0 + hooks: 1.0.1 logging: 1.3.0 native_toolchain_c: 0.17.4 dev_dependencies: test: 1.29.0 -# PUBSPEC CHECKSUM: 76vsd6 +# PUBSPEC CHECKSUM: 3k33b2 diff --git a/dev/integration_tests/wide_gamut_test/.metadata b/dev/integration_tests/wide_gamut_test/.metadata index 68e3f85181587..7adaa085465fa 100644 --- a/dev/integration_tests/wide_gamut_test/.metadata +++ b/dev/integration_tests/wide_gamut_test/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: c379133df6d1f8893ad8c9d0393496c7d6eaf828 - channel: wide-gamut-integration-test + revision: "d7bb061cf8bc3176897c7a38bbefa4836d2f2549" + channel: "[user-branch]" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: c379133df6d1f8893ad8c9d0393496c7d6eaf828 - base_revision: c379133df6d1f8893ad8c9d0393496c7d6eaf828 + create_revision: d7bb061cf8bc3176897c7a38bbefa4836d2f2549 + base_revision: d7bb061cf8bc3176897c7a38bbefa4836d2f2549 - platform: ios - create_revision: c379133df6d1f8893ad8c9d0393496c7d6eaf828 - base_revision: c379133df6d1f8893ad8c9d0393496c7d6eaf828 + create_revision: d7bb061cf8bc3176897c7a38bbefa4836d2f2549 + base_revision: d7bb061cf8bc3176897c7a38bbefa4836d2f2549 # User provided section diff --git a/dev/integration_tests/wide_gamut_test/integration_test/app_test.dart b/dev/integration_tests/wide_gamut_test/integration_test/app_test.dart index be79826ffbb93..0d3c7bbc65d28 100644 --- a/dev/integration_tests/wide_gamut_test/integration_test/app_test.dart +++ b/dev/integration_tests/wide_gamut_test/integration_test/app_test.dart @@ -2,15 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math' as math; +import 'dart:convert' show base64Decode; import 'dart:typed_data'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:wide_gamut_test/main.dart' as app; +/// BGRA10_XR has ~0.002 step size, so 0.01 is needed for 10-bit formats. +const double _bgra10Epsilon = 0.01; + // See: https://developer.apple.com/documentation/metal/mtlpixelformat/mtlpixelformatbgr10_xr. double _decodeBGR10(int x) { const max = 1.25098; @@ -20,23 +24,6 @@ double _decodeBGR10(int x) { return (x * slope) + intercept; } -double _decodeHalf(int x) { - if (x == 0x7c00) { - return double.infinity; - } - if (x == 0xfc00) { - return -double.infinity; - } - final sign = x & 0x8000 == 0 ? 1.0 : -1.0; - final int exponent = (x >> 10) & 0x1f; - final int fraction = x & 0x3ff; - if (exponent == 0) { - return sign * math.pow(2.0, -14) * (fraction / 1024.0); - } else { - return sign * math.pow(2.0, exponent - 15) * (1.0 + fraction / 1024.0); - } -} - bool _isAlmost(double x, double y, double epsilon) { return (x - y).abs() < epsilon; } @@ -49,38 +36,6 @@ double _distanceSquared(double r, double g, double b, List color) { List _deepRed = [1.0931, -0.2268, -0.1501]; -(bool, List) _findRGBAF16Color( - Uint8List bytes, - int width, - int height, - List color, { - required double epsilon, -}) { - final byteData = ByteData.sublistView(bytes); - expect(bytes.lengthInBytes, width * height * 8); - expect(bytes.lengthInBytes, byteData.lengthInBytes); - var foundColor = false; - double minDistance = double.infinity; - var closestColor = [0, 0, 0]; - for (var i = 0; i < bytes.lengthInBytes; i += 8) { - final int pixel = byteData.getUint64(i, Endian.host); - final double blue = _decodeHalf((pixel >> 32) & 0xffff); - final double green = _decodeHalf((pixel >> 16) & 0xffff); - final double red = _decodeHalf((pixel >> 0) & 0xffff); - if (_isAlmost(red, color[0], epsilon) && - _isAlmost(green, color[1], epsilon) && - _isAlmost(blue, color[2], epsilon)) { - foundColor = true; - } - final double currentDistance = _distanceSquared(red, green, blue, color); - if (currentDistance < minDistance) { - minDistance = currentDistance; - closestColor = [red, green, blue]; - } - } - return (foundColor, closestColor); -} - (bool, List) _findBGRA10Color( Uint8List bytes, int width, @@ -113,55 +68,21 @@ List _deepRed = [1.0931, -0.2268, -0.1501]; return (foundColor, closestColor); } -(bool, List) _findBGR10Color( - Uint8List bytes, - int width, - int height, - List color, { - required double epsilon, -}) { - final byteData = ByteData.sublistView(bytes); - expect(bytes.lengthInBytes, width * height * 4); - expect(bytes.lengthInBytes, byteData.lengthInBytes); - var foundColor = false; - double minDistance = double.infinity; - var closestColor = [0, 0, 0]; - for (var i = 0; i < bytes.lengthInBytes; i += 4) { - final int pixel = byteData.getUint32(i, Endian.host); - final double blue = _decodeBGR10(pixel & 0x3ff); - final double green = _decodeBGR10((pixel >> 10) & 0x3ff); - final double red = _decodeBGR10((pixel >> 20) & 0x3ff); - if (_isAlmost(red, color[0], epsilon) && - _isAlmost(green, color[1], epsilon) && - _isAlmost(blue, color[2], epsilon)) { - foundColor = true; - } - final double currentDistance = _distanceSquared(red, green, blue, color); - if (currentDistance < minDistance) { - minDistance = currentDistance; - closestColor = [red, green, blue]; - } - } - return (foundColor, closestColor); -} - -(bool, List) _findColor(List result, List color, {double epsilon = 0.01}) { +(bool, List) _findColor(List result, List color, {double? epsilon}) { expect(result, isNotNull); expect(result.length, 4); final [int width, int height, String format, Uint8List bytes] = result; - return switch (format) { - 'MTLPixelFormatBGR10_XR' => _findBGR10Color(bytes, width, height, color, epsilon: epsilon), - 'MTLPixelFormatBGRA10_XR' => _findBGRA10Color(bytes, width, height, color, epsilon: epsilon), - 'MTLPixelFormatRGBA16Float' => _findRGBAF16Color(bytes, width, height, color, epsilon: epsilon), - _ => fail('Unsupported pixel format: $format'), - }; + if (format != 'MTLPixelFormatBGRA10_XR') { + throw UnsupportedError('Unsupported pixel format: $format'); + } + return _findBGRA10Color(bytes, width, height, color, epsilon: epsilon ?? _bgra10Epsilon); } class _HasColor extends Matcher { - const _HasColor(this.color, {this.epsilon = 0.01}); + const _HasColor(this.color, {this.epsilon}); final List color; - final double epsilon; + final double? epsilon; @override bool matches(dynamic item, Map matchState) { @@ -186,100 +107,123 @@ class _HasColor extends Matcher { Map matchState, bool verbose, ) { - final closest = matchState['closest'] as List; + final closest = matchState['closest'] as List?; + if (closest == null) { + return mismatchDescription.add('could not find any colors (unsupported pixel format?)'); + } return mismatchDescription.add('closest color to $color was $closest'); } } +/// Precache the Display P3 test image so it is fully decoded before we take a +/// screenshot. Must be called after [pumpAndSettle] so a [BuildContext] exists. +Future _precacheP3Image(WidgetTester tester) async { + final BuildContext context = tester.element(find.byType(app.MyApp)); + await tester.runAsync(() => precacheImage(MemoryImage(base64Decode(app.displayP3Logo)), context)); +} + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('end-to-end test', () { + const screenshotChannel = MethodChannel('flutter/screenshot'); testWidgets('look for display p3 deepest red', (WidgetTester tester) async { app.run(app.Setup.image); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + await _precacheP3Image(tester); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, _HasColor(_deepRed)); }); - testWidgets('look for display p3 deepest red', (WidgetTester tester) async { + testWidgets('look for display p3 deepest red (saveLayer)', (WidgetTester tester) async { app.run(app.Setup.canvasSaveLayer); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + await _precacheP3Image(tester); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; + expect(result, _HasColor(_deepRed)); + }); + testWidgets('p3 deepest red via codec API (ImageDescriptor)', (WidgetTester tester) async { + app.run(app.Setup.codecImage); + await tester.pumpAndSettle(); + // Wait for async _loadCodecImage() to complete and rebuild + await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 500))); + await tester.pumpAndSettle(); + + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, _HasColor(_deepRed)); }); testWidgets('no p3 deepest red without image', (WidgetTester tester) async { app.run(app.Setup.none); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, isNot(_HasColor(_deepRed))); expect(result, isNot(const _HasColor([0.0, 1.0, 0.0]))); }); testWidgets('p3 deepest red with blur', (WidgetTester tester) async { app.run(app.Setup.blur); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + await _precacheP3Image(tester); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, _HasColor(_deepRed)); expect(result, const _HasColor([0.0, 1.0, 0.0])); }); testWidgets('draw image with wide gamut works', (WidgetTester tester) async { app.run(app.Setup.drawnImage); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + // Wait for async _drawImage() to complete and rebuild + await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 500))); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, const _HasColor([0.0, 1.0, 0.0])); }); testWidgets('draw container with wide gamut works', (WidgetTester tester) async { app.run(app.Setup.container); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, _HasColor(_deepRed)); }); testWidgets('draw wide gamut linear gradient works', (WidgetTester tester) async { app.run(app.Setup.linearGradient); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; - expect(result, _HasColor(_deepRed)); + final result = await screenshotChannel.invokeMethod('test') as List; + + // Gradients need larger epsilon due to pixel sampling between gradient stops. + expect(result, _HasColor(_deepRed, epsilon: 0.02)); }); testWidgets('draw wide gamut radial gradient works', (WidgetTester tester) async { app.run(app.Setup.radialGradient); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, _HasColor(_deepRed, epsilon: 0.05)); }); testWidgets('draw wide gamut conical gradient works', (WidgetTester tester) async { app.run(app.Setup.conicalGradient); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; + final result = await screenshotChannel.invokeMethod('test') as List; expect(result, _HasColor(_deepRed, epsilon: 0.05)); }); testWidgets('draw wide gamut sweep gradient works', (WidgetTester tester) async { app.run(app.Setup.sweepGradient); - await tester.pumpAndSettle(const Duration(seconds: 2)); + await tester.pumpAndSettle(); - const channel = MethodChannel('flutter/screenshot'); - final result = await channel.invokeMethod('test') as List; - expect(result, _HasColor(_deepRed)); + final result = await screenshotChannel.invokeMethod('test') as List; + // Sweep gradient endpoint may not be sampled exactly at a pixel center. + expect(result, _HasColor(_deepRed, epsilon: 0.02)); }); }); } diff --git a/dev/integration_tests/wide_gamut_test/lib/main.dart b/dev/integration_tests/wide_gamut_test/lib/main.dart index 70796e1edf77b..cce203f79a232 100644 --- a/dev/integration_tests/wide_gamut_test/lib/main.dart +++ b/dev/integration_tests/wide_gamut_test/lib/main.dart @@ -9,7 +9,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; /// A 100x100 png in Display P3 colorspace. -const String _displayP3Logo = +const String displayP3Logo = 'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAABdWlDQ1BrQ0dDb2xv' 'clNwYWNlRGlzcGxheVAzAAAokXWQvUvDUBTFT6tS0DqIDh0cMolD1NIKdnFoKxRF' 'MFQFq1OafgltfCQpUnETVyn4H1jBWXCwiFRwcXAQRAcR3Zw6KbhoeN6XVNoi3sfl' @@ -142,6 +142,7 @@ void main() => run(Setup.sweepGradient); enum Setup { none, image, + codecImage, canvasSaveLayer, blur, drawnImage, @@ -153,7 +154,9 @@ enum Setup { } void run(Setup setup) { - runApp(MyApp(setup)); + // Use UniqueKey to force fresh State on each run. Needed for integration + // tests where runApp is called multiple times with different setups. + runApp(MyApp(setup, key: UniqueKey())); } class MyApp extends StatelessWidget { @@ -166,7 +169,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Wide Gamut Test', theme: ThemeData(primarySwatch: Colors.blue), - home: MyHomePage(_setup, title: 'Wide Gamut Test'), + home: MyHomePage(_setup, key: UniqueKey(), title: 'Wide Gamut Test'), ); } } @@ -203,6 +206,20 @@ class _SaveLayerDrawer extends CustomPainter { bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } +/// Load the Display P3 PNG via the low-level codec API +/// (ImageDescriptor.encoded -> instantiateCodec -> getNextFrame). +/// This path does NOT go through ImageDecoderImpeller. +Future _loadCodecImage() async { + final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List( + base64Decode(displayP3Logo), + ); + final ui.ImageDescriptor descriptor = await ui.ImageDescriptor.encoded(buffer); + final ui.Codec codec = await descriptor.instantiateCodec(); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + codec.dispose(); + return frameInfo.image; +} + Future _drawImage() async { final recorder = ui.PictureRecorder(); const markerSize = Size(120, 120); @@ -232,17 +249,6 @@ Future _drawImage() async { return completer.future; } -Future _loadImage() async { - final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List( - base64Decode(_displayP3Logo), - ); - final ui.ImageDescriptor descriptor = await ui.ImageDescriptor.encoded(buffer); - final ui.Codec codec = await descriptor.instantiateCodec(); - final ui.FrameInfo frameInfo = await codec.getNextFrame(); - codec.dispose(); - return frameInfo.image; -} - class MyHomePage extends StatefulWidget { const MyHomePage(this.setup, {super.key, required this.title}); @@ -259,8 +265,16 @@ class _MyHomePageState extends State { @override void initState() { switch (widget.setup) { + case Setup.image || Setup.blur: + break; case Setup.canvasSaveLayer: - _loadImage().then( + _loadCodecImage().then( + (ui.Image? value) => setState(() { + _image = value; + }), + ); + case Setup.codecImage: + _loadCodecImage().then( (ui.Image? value) => setState(() { _image = value; }), @@ -271,9 +285,7 @@ class _MyHomePageState extends State { _image = value; }), ); - case Setup.image || - Setup.blur || - Setup.none || + case Setup.none || Setup.container || Setup.linearGradient || Setup.radialGradient || @@ -291,7 +303,11 @@ class _MyHomePageState extends State { case Setup.none: imageWidget = Container(); case Setup.image: - imageWidget = Image.memory(base64Decode(_displayP3Logo)); + imageWidget = Image.memory(base64Decode(displayP3Logo)); + case Setup.codecImage: + imageWidget = _image != null + ? RawImage(image: _image, width: 100, height: 100) + : const SizedBox(width: 100, height: 100); case Setup.drawnImage: imageWidget = CustomPaint(painter: _SaveLayerDrawer(_image)); case Setup.canvasSaveLayer: @@ -302,7 +318,7 @@ class _MyHomePageState extends State { const ColoredBox(color: Color(0xff00ff00), child: SizedBox(width: 100, height: 100)), ImageFiltered( imageFilter: ui.ImageFilter.blur(sigmaX: 6, sigmaY: 6), - child: Image.memory(base64Decode(_displayP3Logo)), + child: Image.memory(base64Decode(displayP3Logo)), ), ], ); diff --git a/dev/integration_tests/wide_gamut_test/macos/Runner/Info.plist b/dev/integration_tests/wide_gamut_test/macos/Runner/Info.plist new file mode 100644 index 0000000000000..0812d926489a2 --- /dev/null +++ b/dev/integration_tests/wide_gamut_test/macos/Runner/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + FLTEnableImpeller + + FLTEnableWideGamut + + + diff --git a/docs/ecosystem/release/README.md b/docs/ecosystem/release/README.md index 882c167841d85..5d4126fe4efb9 100644 --- a/docs/ecosystem/release/README.md +++ b/docs/ecosystem/release/README.md @@ -26,6 +26,81 @@ If it is a flake (for example, network issue), a Flutter team member can simply The most common source of failure of the `release` task is that another test failed; if that is due to flake, you will need to first re-run the failing test task, then once it's green re-run `release`. +## Batch release + +High-traffic packages can enable "Batch release" to group multiple commits into a single periodic release, rather than publishing after every commit (the default "Automatic release" behavior). + +### Configuration + +To enable batch release for a package: + +1. **Request a branch:** Ask a repo admin to create a protected `release-` branch. +2. **Submit a PR:** Create a PR with the following changes: + * Add `ci_config.yaml` to the package root: + ```yaml + release: + batch: true + ``` + * Create a `pending_changes` directory in the package root containing a `template.yaml` file: + ```yaml + # Use this file as a template to draft an unreleased changelog file. + # Make a copy of this file in the same directory, give it an approrpriate name, and fill in the details. + changelog: | + - Can include a list of changes. + - with markdown supported. + version: + ``` + * Add a workflow file `_batch.yml` to the [workflows directory](https://github.com/flutter/packages/blob/main/.github/workflows/): + ```yaml + name: "Creates Batch Release for " + + on: + workflow_dispatch: + schedule: + # Run every Monday at 8:00 AM. Update cron as needed. + - cron: "0 8 * * 1" + + jobs: + dispatch_release_pr: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f + with: + event-type: batch-release-pr + client-payload: '{"package": ""}' + ``` + * Add a trigger to [release_from_branches.yml](https://github.com/flutter/packages/blob/main/.github/workflows/release_from_branches.yml): + ```yaml + on: + push: + branches: + - 'release-' + ``` + * Add a trigger to [sync_release_pr.yml](https://github.com/flutter/packages/blob/main/.github/workflows/sync_release_pr.yml): + ```yaml + on: + push: + branches: + - 'release-' + ``` +3. **Merge:** Merge the PR to finalize setup. + +### Workflow + +Once enabled, contributors **should not** modify `CHANGELOG.md` or `pubspec.yaml` directly. Instead, they must add a new file to the `pending_changes` directory for each PR. + +The release process runs automatically: + +1. The cron job in `_batch.yml` triggers a new release PR targeting the `release-` branch. +2. The PR aggregates all files in `pending_changes` to update `CHANGELOG.md` and `pubspec.yaml`. +3. A package owner reviews and merges the PR. +4. Merging triggers [release_from_branches.yml](https://github.com/flutter/packages/blob/main/.github/workflows/release_from_branches.yml), which publishes the package to pub.dev. +5. Simultaneously, [sync_release_pr.yml](https://github.com/flutter/packages/blob/main/.github/workflows/sync_release_pr.yml) creates a "sync PR" targeting the `main` branch. +6. A package owner reviews and merges the sync PR. + ## Manual release (only when necessary) If something has gone wrong that prevents auto-publishing—most commonly, an out-of-band breakage that caused post-submit tests to fail for reasons unrelated to the PR that should have been published—a Flutter team member can publish manually. (An alternative to publishing manually is to revert and re-land the relevant PR; this is especially worth considering for PRs that affect many plugins.) diff --git a/engine/src/flutter/ci/builders/linux_unopt.json b/engine/src/flutter/ci/builders/linux_unopt.json index 6866efbcb1739..693fe7dcb9093 100644 --- a/engine/src/flutter/ci/builders/linux_unopt.json +++ b/engine/src/flutter/ci/builders/linux_unopt.json @@ -16,6 +16,7 @@ "debug", "--unoptimized", "--prebuilt-dart-sdk", + "--asan", "--dart-debug", "--rbe", "--no-goma" diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc index 6767d4d7b57f0..c8c5c0cd78dd1 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc @@ -752,6 +752,101 @@ TEST_P(AiksTest, StrokedCirclesRenderCorrectly) { ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); } +namespace { +DlPath ManuallyConstructCirclePath(Scalar radius) { + DlPathBuilder path_builder; + // Circle as 4 cubic bezier segments (standard circle approximation) + // Using kappa = 0.5522847498 for circular arc approximation + const Scalar k = 0.5522847498f; + + path_builder.MoveTo(DlPoint(0.0f, -radius)); // Top + // Top to Right + path_builder.CubicCurveTo(DlPoint(radius * k, -radius), // + DlPoint(radius, -radius * k), // + DlPoint(radius, 0.0f)); + // Right to Bottom + path_builder.CubicCurveTo(DlPoint(radius, radius * k), // + DlPoint(radius * k, radius), // + DlPoint(0.0f, radius)); + // Bottom to Left + path_builder.CubicCurveTo(DlPoint(-radius * k, radius), // + DlPoint(-radius, radius * k), // + DlPoint(-radius, 0.0f)); + // Left to Top + path_builder.CubicCurveTo(DlPoint(-radius, -radius * k), // + DlPoint(-radius * k, -radius), // + DlPoint(0.0f, -radius)); + path_builder.Close(); + return path_builder.TakePath(); +} + +void DrawStrokedAndFilledCirclesWithZoom(AiksTest* test, + Scalar zoom, + Scalar radius, + Scalar stroke_width) { + DisplayListBuilder builder; + builder.Scale(test->GetContentScale().x, test->GetContentScale().y); + builder.DrawColor(DlColor::kWhite(), DlBlendMode::kSrc); + + DlPaint fill_paint; + fill_paint.setColor(DlColor::kBlue()); + + DlPaint stroke_paint; + stroke_paint.setColor(DlColor::kGreen()); + stroke_paint.setDrawStyle(DlDrawStyle::kStroke); + stroke_paint.setStrokeWidth(stroke_width); + + DlPath path = ManuallyConstructCirclePath(radius); + + constexpr Scalar kLeftX = 300.0f; + constexpr Scalar kRightX = 680.0f; + constexpr Scalar kTopY = 200.0f; + constexpr Scalar kBottomY = 580.0f; + + // Upper left quadrant is fill + stroke + builder.Save(); + builder.Translate(kLeftX, kTopY); + builder.Scale(zoom, zoom); + builder.DrawPath(path, fill_paint); + builder.DrawPath(path, stroke_paint); + builder.Restore(); + + // Upper right quadrant is fill only + builder.Save(); + builder.Translate(kRightX, kTopY); + builder.Scale(zoom, zoom); + builder.DrawPath(path, fill_paint); + builder.Restore(); + + // Lower left quadrant is stroke only + builder.Save(); + builder.Translate(kLeftX, kBottomY); + builder.Scale(zoom, zoom); + builder.DrawPath(path, stroke_paint); + builder.Restore(); + + // Lower right quadrant is a filled circle the size of the radius and + // the stroke combined for comparison to the stroked outlines. + builder.Save(); + builder.Translate(kRightX, kBottomY); + builder.Scale(zoom, zoom); + builder.DrawCircle({}, radius + stroke_width * 0.5f, fill_paint); + builder.Restore(); + + ASSERT_TRUE(test->OpenPlaygroundHere(builder.Build())); +} +} // namespace + +TEST_P(AiksTest, ZoomedStrokedPathRendersCorrectly) { + DrawStrokedAndFilledCirclesWithZoom(this, /*zoom=*/80.0f, /*radius=*/2.0f, + /*stroke_width=*/0.05f); +} + +TEST_P(AiksTest, StrokedPathWithLargeStrokeWidthRendersCorrectly) { + DrawStrokedAndFilledCirclesWithZoom(this, /*zoom=*/1.0f, /*radius=*/1.0f, + /*stroke_width=*/5.0f); +} + TEST_P(AiksTest, FilledEllipsesRenderCorrectly) { DisplayListBuilder builder; builder.Scale(GetContentScale().x, GetContentScale().y); @@ -1727,7 +1822,7 @@ TEST_P(AiksTest, ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); } -// This makes sure the WideGamut named tests use 16bit float pixel format. +// This makes sure the WideGamut named tests use 10-bit wide gamut pixel format. TEST_P(AiksTest, FormatWideGamut) { EXPECT_EQ(GetContext()->GetCapabilities()->GetDefaultColorFormat(), PixelFormat::kB10G10R10A10XR); diff --git a/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc b/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc index 99f650458e5f8..7d0f56282cb5d 100644 --- a/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc +++ b/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc @@ -220,8 +220,19 @@ class StrokePathSegmentReceiver : public PathAndArcSegmentReceiver { // curve as well. HandlePreviousJoin(start_perpendicular); - Scalar count = - std::ceilf(curve.SubdivisionCount(scale_ * half_stroke_width_)); + // We use the scale suggested by the transform basis which is the + // same scale that would be used for filling the path. But we also + // need to adjust the scale for the magnification of the curve + // features that occurs when we draw with a wide pen and the outer + // curves of that stroked path are larger than the base curve itself. + // So, we scale by both the transform basis and (half) the stroke + // width, but we also make sure that we aren't reducing the scale + // in the uncommon case that someone is drawing at a large scale + // with a very tiny stroke width. To accomplish this, we multiply + // the scale basis by half the stroke width, but make sure the width + // is at least 1.0 so that we don't reduce the natural transform scale. + Scalar stroke_scale = scale_ * std::max(1.0f, half_stroke_width_); + Scalar count = std::ceilf(curve.SubdivisionCount(stroke_scale)); Point prev = curve.p1; SeparatedVector2 prev_perpendicular = start_perpendicular; @@ -247,7 +258,9 @@ class StrokePathSegmentReceiver : public PathAndArcSegmentReceiver { const SeparatedVector2& cur_perpendicular) { if (prev_perpendicular.GetAlignment(cur_perpendicular) < trigs_[1].cos) { // We only connect 2 curved segments if their change in direction - // is faster than a single sample of a round join. + // is faster than a single sample of a round join. We always use a + // round join here because this is about smoothness of curves rather + // than a decoration for specific segments of the path. AppendVertices(cur, prev_perpendicular); AddJoin(Join::kRound, cur, prev_perpendicular, cur_perpendicular); } diff --git a/engine/src/flutter/impeller/playground/backend/metal/playground_impl_mtl.mm b/engine/src/flutter/impeller/playground/backend/metal/playground_impl_mtl.mm index a276c11aeaa69..218dbe51c03bf 100644 --- a/engine/src/flutter/impeller/playground/backend/metal/playground_impl_mtl.mm +++ b/engine/src/flutter/impeller/playground/backend/metal/playground_impl_mtl.mm @@ -76,12 +76,13 @@ return; } + std::optional wide_gamut_format = std::nullopt; + if (switches.enable_wide_gamut) { + wide_gamut_format = PixelFormat::kB10G10R10A10XR; + } auto context = ContextMTL::Create( switches.flags, ShaderLibraryMappingsForPlayground(), - is_gpu_disabled_sync_switch_, "Playground Library", - switches.enable_wide_gamut - ? std::optional(PixelFormat::kB10G10R10A10XR) - : std::nullopt); + is_gpu_disabled_sync_switch_, "Playground Library", wide_gamut_format); if (!context) { return; } diff --git a/engine/src/flutter/impeller/renderer/backend/metal/context_mtl.mm b/engine/src/flutter/impeller/renderer/backend/metal/context_mtl.mm index b64547b93f248..c842a5caa65ba 100644 --- a/engine/src/flutter/impeller/renderer/backend/metal/context_mtl.mm +++ b/engine/src/flutter/impeller/renderer/backend/metal/context_mtl.mm @@ -40,8 +40,11 @@ static bool DeviceSupportsComputeSubgroups(id device) { [device supportsFamily:MTLGPUFamilyMac2]; } -// See "Extended Range and wide color pixel formats" in the metal feature set -// tables. +// See "Extended Range and wide color pixel formats" in the Metal Feature Set +// Tables: https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf +// Wide gamut requires Apple3+ GPU family (supports 10-bit and F16 formats). +// This includes all iOS devices with A9+ chip and Apple Silicon Macs (M1+). +// Intel Macs (Mac2 family) do not support wide gamut. static bool DeviceSupportsExtendedRangeFormats(id device) { return [device supportsFamily:MTLGPUFamilyApple3]; } diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn index a410e9cf75eb9..47dfbd7e46849 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn +++ b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn @@ -13,6 +13,7 @@ if (enable_unittests) { "//flutter/lib/ui/fixtures/shaders/general_shaders", "//flutter/lib/ui/fixtures/shaders/supported_glsl_op_shaders", "//flutter/lib/ui/fixtures/shaders/supported_op_shaders", + "//flutter/lib/ui/fixtures/shaders/uniforms", ] } diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn b/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn index 37e3d04c6faac..5c17d13c62cbf 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn +++ b/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn @@ -30,7 +30,6 @@ if (enable_unittests) { "uniforms_reordered.frag", "uniforms_sorted.frag", "uniforms.frag", - "vec3_uniform.frag", ] group("general_shaders") { diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/BUILD.gn b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/BUILD.gn new file mode 100644 index 0000000000000..d75abef171ed9 --- /dev/null +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/BUILD.gn @@ -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. + +import("//build/compiled_action.gni") +import("//flutter/impeller/tools/impeller.gni") +import("//flutter/testing/testing.gni") + +if (enable_unittests) { + test_shaders = [ + "all_uniforms.frag", + "float_array_uniform.frag", + "float_uniform.frag", + "vec2_uniform.frag", + "vec2_array_uniform.frag", + "vec3_uniform.frag", + "vec3_array_uniform.frag", + "vec4_uniform.frag", + "vec4_array_uniform.frag", + ] + + group("uniforms") { + testonly = true + deps = [ ":fixtures" ] + } + + impellerc("compile_uniforms") { + mnemonic = "IMPELLERC_SKSL" + shaders = test_shaders + shader_target_flags = [ + "--sksl", + "--runtime-stage-metal", + "--runtime-stage-gles", + "--runtime-stage-vulkan", + ] + intermediates_subdir = "iplr" + sl_file_extension = "iplr" + iplr = true + } + + test_fixtures("fixtures") { + deps = [ ":compile_uniforms" ] + fixtures = get_target_outputs(":compile_uniforms") + dest = "$root_gen_dir/flutter/lib/ui" + } +} diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/all_uniforms.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/all_uniforms.frag new file mode 100644 index 0000000000000..2df139cbbdc85 --- /dev/null +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/all_uniforms.frag @@ -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. + +#include + +out vec4 fragColor; + +// One for each base type, 40 for the arrays +float N_COLOR_VALUES = 44; + +uniform float uFloat; + +uniform vec2 uVec2; +uniform vec3 uVec3; +uniform vec4 uVec4; + +const int ARRAY_SIZE = 10; + +uniform float[ARRAY_SIZE] uFloatArray; +uniform vec2[ARRAY_SIZE] uVec2Array; +uniform vec3[ARRAY_SIZE] uVec3Array; +uniform vec4[ARRAY_SIZE] uVec4Array; + +void main() { + float u = FlutterFragCoord().x / N_COLOR_VALUES; + + float increment = 1.0 / N_COLOR_VALUES; + + float offset = increment; + + if (u < offset) { + fragColor = vec4(uFloat, 0, 0, 1); + return; + } + offset += increment; + if (u < offset) { + fragColor = vec4(uVec2, 0, 1); + return; + } + offset += increment; + if (u < offset) { + fragColor = vec4(uVec3, 1); + return; + } + offset += increment; + if (u < offset) { + fragColor = uVec4; + return; + } + offset += increment; + + for (int i = 0; i < ARRAY_SIZE; ++i) { + if (u < offset) { + fragColor = vec4(uFloatArray[i], 0, 0, 1); + return; + } + offset += increment; + } + + for (int i = 0; i < ARRAY_SIZE; ++i) { + if (u < offset) { + fragColor = vec4(uVec2Array[i], 0, 1); + return; + } + offset += increment; + } + + for (int i = 0; i < ARRAY_SIZE; ++i) { + if (u < offset) { + fragColor = vec4(uVec3Array[i], 1); + return; + } + offset += increment; + } + + for (int i = 0; i < ARRAY_SIZE; ++i) { + if (u < offset) { + fragColor = uVec4Array[i]; + return; + } + offset += increment; + } +} \ No newline at end of file diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/float_array_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/float_array_uniform.frag similarity index 100% rename from engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/float_array_uniform.frag rename to engine/src/flutter/lib/ui/fixtures/shaders/uniforms/float_array_uniform.frag diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/float_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/float_uniform.frag new file mode 100644 index 0000000000000..b782c4b527003 --- /dev/null +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/float_uniform.frag @@ -0,0 +1,15 @@ +#version 320 es + +// 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. + +precision highp float; + +uniform float color_r; + +out vec4 fragColor; + +void main() { + fragColor = vec4(color_r, 0, 0, 1); +} diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec2_array_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec2_array_uniform.frag similarity index 100% rename from engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec2_array_uniform.frag rename to engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec2_array_uniform.frag diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec2_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec2_uniform.frag new file mode 100644 index 0000000000000..aa54b494e05a5 --- /dev/null +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec2_uniform.frag @@ -0,0 +1,15 @@ +#version 320 es + +// 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. + +precision highp float; + +uniform vec2 color_rg; + +out vec4 fragColor; + +void main() { + fragColor = vec4(color_rg, 0, 1); +} diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec3_array_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec3_array_uniform.frag similarity index 63% rename from engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec3_array_uniform.frag rename to engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec3_array_uniform.frag index 23acabaf5f6ff..3f63e13643a48 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec3_array_uniform.frag +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec3_array_uniform.frag @@ -7,6 +7,10 @@ // If updating this file, also update // engine/src/flutter/lib/web_ui/test/ui/fragment_shader_test.dart +#include + +uniform vec2 u_size; + precision highp float; uniform vec3[2] color_array; @@ -14,5 +18,11 @@ uniform vec3[2] color_array; out vec4 fragColor; void main() { - fragColor = vec4(mix(color_array[0], color_array[1], 0.5), 1); + vec2 uv = FlutterFragCoord().xy / u_size; + + if (uv.x < 0.5) { + fragColor = vec4(color_array[0], 1); + } else { + fragColor = vec4(color_array[1], 1); + } } diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec3_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec3_uniform.frag similarity index 77% rename from engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec3_uniform.frag rename to engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec3_uniform.frag index 2b1cadd8fb6f3..db1539de68713 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec3_uniform.frag +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec3_uniform.frag @@ -6,10 +6,10 @@ precision highp float; -uniform vec3[4] color_array; +uniform vec3 color_rgb; out vec4 fragColor; void main() { - fragColor = vec4(color_array[3].xyz, 1); + fragColor = vec4(color_rgb, 1); } diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec4_array_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec4_array_uniform.frag similarity index 64% rename from engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec4_array_uniform.frag rename to engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec4_array_uniform.frag index f8e98d4575767..d86687ef04356 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/vec4_array_uniform.frag +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec4_array_uniform.frag @@ -7,6 +7,10 @@ // If updating this file, also update // engine/src/flutter/lib/web_ui/test/ui/fragment_shader_test.dart +#include + +uniform vec2 u_size; + precision highp float; uniform vec4[2] color_array; @@ -14,5 +18,11 @@ uniform vec4[2] color_array; out vec4 fragColor; void main() { - fragColor = mix(color_array[0], color_array[1], 0.5); + vec2 uv = FlutterFragCoord().xy / u_size; + + if (uv.x < 0.5) { + fragColor = color_array[0]; + } else { + fragColor = color_array[1]; + } } diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec4_uniform.frag b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec4_uniform.frag new file mode 100644 index 0000000000000..4525a8b361b53 --- /dev/null +++ b/engine/src/flutter/lib/ui/fixtures/shaders/uniforms/vec4_uniform.frag @@ -0,0 +1,15 @@ +#version 320 es + +// 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. + +precision highp float; + +uniform vec4 color_rgba; + +out vec4 fragColor; + +void main() { + fragColor = color_rgba; +} diff --git a/engine/src/flutter/lib/ui/painting.dart b/engine/src/flutter/lib/ui/painting.dart index d3ca916fe41c7..a3cac1c7f032e 100644 --- a/engine/src/flutter/lib/ui/painting.dart +++ b/engine/src/flutter/lib/ui/painting.dart @@ -5809,7 +5809,7 @@ base class FragmentShader extends Shader { final slots = List.generate( size, - (i) => UniformFloatSlot._(this, name, info.index, i), + (i) => UniformFloatSlot._(this, name, i, info.index + i), ); _slots.removeWhere((WeakReference ref) => ref.target == null); _slots.addAll(slots.map((slot) => WeakReference(slot))); diff --git a/engine/src/flutter/lib/ui/painting/image_decoder_impeller.cc b/engine/src/flutter/lib/ui/painting/image_decoder_impeller.cc index 584dee2b8024b..18aad2cc600d6 100644 --- a/engine/src/flutter/lib/ui/painting/image_decoder_impeller.cc +++ b/engine/src/flutter/lib/ui/painting/image_decoder_impeller.cc @@ -136,6 +136,8 @@ absl::StatusOr CreateImageInfo( .makeAlphaType(alpha_type) .makeColorSpace(SkColorSpace::MakeSRGB()); } else if (is_wide_gamut) { + // Use 10-bit for opaque images (less memory: 4 bytes vs 8 bytes per pixel). + // Use F16 for images with alpha channel. SkColorType color_type = alpha_type == SkAlphaType::kOpaque_SkAlphaType ? kBGR_101010x_XR_SkColorType : kRGBA_F16_SkColorType; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 89b2ad6563e53..5a05862238c85 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -1045,12 +1045,12 @@ extension type SkImageFilterNamespace(JSObject _) implements JSObject { ); @JS('MakeMatrixTransform') - external SkImageFilter _MakeMatrixTransform( + external SkImageFilter? _MakeMatrixTransform( JSFloat32Array matrix, // 3x3 matrix CkFilterOptions filterOptions, void input, // we don't use this yet ); - SkImageFilter MakeMatrixTransform( + SkImageFilter? MakeMatrixTransform( Float32List matrix, // 3x3 matrix CkFilterOptions filterOptions, void input, // we don't use this yet diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart index e83b56546b875..2a475a9c93fcc 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart @@ -68,7 +68,7 @@ abstract class CkImageFilter implements CkManagedSkImageFilterConvertible, Layer toSkMatrixFromFloat32(Matrix4.identity().storage), toSkFilterOptions(ui.FilterQuality.none), null, - ); + )!; } // The blur ImageFilter will override this and return the necessary @@ -83,7 +83,10 @@ abstract class CkImageFilter implements CkManagedSkImageFilterConvertible, Layer @override ui.Rect filterBounds(ui.Rect input) { - late ui.Rect result; + // Default the result to zero. If the filter is null (which can happen if + // the filter is a non-invertible matrix filter), then the filter will + // remove all of its child content. + ui.Rect result = ui.Rect.zero; withSkImageFilter((SkImageFilter filter) { result = rectFromSkIRect(filter.getOutputBounds(toSkRect(input))); }, defaultBlurTileMode: ui.TileMode.decal); @@ -193,11 +196,16 @@ class _CkMatrixImageFilter extends CkImageFilter { SkImageFilterBorrow borrow, { ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, }) { - final SkImageFilter skImageFilter = canvasKit.ImageFilter.MakeMatrixTransform( + final SkImageFilter? skImageFilter = canvasKit.ImageFilter.MakeMatrixTransform( toSkMatrixFromFloat64(matrix), toSkFilterOptions(filterQuality), null, ); + if (skImageFilter == null) { + // The filter can be null if the matrix is non-invertible. In this case, we + // don't run the borrow callback. + return; + } borrow(skImageFilter); skImageFilter.delete(); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart index d27c70ca8884b..f0c7e172f098d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart @@ -124,7 +124,7 @@ class PlatformViewManager { /// ```html /// /// ``` /// /// The `arbitrary-html-elements` are the result of the call to the user-supplied diff --git a/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart b/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart index 984592d44ea60..40be594c6f233 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart @@ -143,6 +143,10 @@ Future testMain() async { // https://github.com/flutter/flutter/issues/152709 continue; } + if (testFile == 'b464333052.jpg') { + // This is an undecodable image used to test a Skia failure code path. + continue; + } testCodecs.add( UrlTestCodec( testFile, diff --git a/engine/src/flutter/lib/web_ui/test/ui/image_filter_non_invertible_matrix_test.dart b/engine/src/flutter/lib/web_ui/test/ui/image_filter_non_invertible_matrix_test.dart new file mode 100644 index 0000000000000..1e7f04d02c2cf --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/ui/image_filter_non_invertible_matrix_test.dart @@ -0,0 +1,102 @@ +// 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:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import '../common/rendering.dart'; +import '../common/test_initialization.dart'; +import 'utils.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false); + + test('scene.pushImageFilter with a non-invertible matrix does not crash', () async { + final sceneBuilder = ui.SceneBuilder(); + // A matrix of all zeros is non-invertible. + final Float64List nonInvertibleMatrix = Matrix4.zero().toFloat64(); + final filter = ui.ImageFilter.matrix(nonInvertibleMatrix); + + sceneBuilder.pushImageFilter(filter); + sceneBuilder.addPicture( + ui.Offset.zero, + drawPicture((ui.Canvas canvas) { + canvas.drawRect( + const ui.Rect.fromLTWH(0, 0, 100, 100), + ui.Paint()..color = const ui.Color(0xFF00FF00), + ); + }), + ); + sceneBuilder.pop(); + + final ui.Scene scene = sceneBuilder.build(); + await renderScene(scene); + // If we reached here without crashing, the test passed. + }); + + test('Paint with a non-invertible matrix ImageFilter does not crash', () async { + final Float64List nonInvertibleMatrix = Matrix4.zero().toFloat64(); + final filter = ui.ImageFilter.matrix(nonInvertibleMatrix); + + final paint = ui.Paint()..imageFilter = filter; + final ui.Picture picture = drawPicture((ui.Canvas canvas) { + canvas.drawRect(const ui.Rect.fromLTWH(0, 0, 100, 100), paint); + }); + + final sceneBuilder = ui.SceneBuilder(); + sceneBuilder.addPicture(ui.Offset.zero, picture); + final ui.Scene scene = sceneBuilder.build(); + await renderScene(scene); + + picture.dispose(); + // If we reached here without crashing, the test passed. + }); + + test('scene.pushImageFilter with a zero-scale matrix does not crash', () async { + final sceneBuilder = ui.SceneBuilder(); + // A matrix with zero scale is non-invertible. + final matrix = Matrix4.identity()..scale(0.0, 0.0, 1.0); + final filter = ui.ImageFilter.matrix(matrix.toFloat64()); + + sceneBuilder.pushImageFilter(filter); + sceneBuilder.addPicture( + ui.Offset.zero, + drawPicture((ui.Canvas canvas) { + canvas.drawRect( + const ui.Rect.fromLTWH(0, 0, 100, 100), + ui.Paint()..color = const ui.Color(0xFF00FF00), + ); + }), + ); + sceneBuilder.pop(); + + final ui.Scene scene = sceneBuilder.build(); + await renderScene(scene); + }); + + test('Paint with a zero-scale matrix ImageFilter does not crash', () async { + final matrix = Matrix4.identity()..scale(0.0, 0.0, 1.0); + final filter = ui.ImageFilter.matrix(matrix.toFloat64()); + + final paint = ui.Paint()..imageFilter = filter; + final ui.Picture picture = drawPicture((ui.Canvas canvas) { + canvas.drawRect(const ui.Rect.fromLTWH(0, 0, 100, 100), paint); + }); + + final sceneBuilder = ui.SceneBuilder(); + sceneBuilder.addPicture(ui.Offset.zero, picture); + final ui.Scene scene = sceneBuilder.build(); + await renderScene(scene); + + picture.dispose(); + }); +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/ContentSizingFlag.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/ContentSizingFlag.java index 34fda7f228128..e3bd83e7df19f 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/ContentSizingFlag.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/ContentSizingFlag.java @@ -32,7 +32,7 @@ static boolean isEnabled(Context context) { .getApplicationInfo(appContext.getPackageName(), PackageManager.GET_META_DATA); metaData = applicationInfo.metaData; } catch (NameNotFoundException ex) { - Log.e(TAG, "Could not get metadata"); + Log.e(TAG, "Could not get metadata", ex); } return metaData != null ? metaData.getBoolean(ENABLE_CONTENT_SIZING, DEFAULT) : DEFAULT; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm index 5e67acd73735d..064b2f40623a0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm @@ -7,12 +7,17 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterSceneDelegate.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Test.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" FLUTTER_ASSERT_ARC +@interface FlutterView (Testing) +- (BOOL)isWideGamutSupported; +@end + @interface FakeDelegate : NSObject @property(nonatomic) BOOL callbackCalled; @end @@ -181,10 +186,14 @@ - (void)testViewRemovedFromWindowAndAddedToNewScene { } - (NSDictionary*)createWindowMocks { + return [self createWindowMocksWithWideGamut:NO]; +} + +- (NSDictionary*)createWindowMocksWithWideGamut:(BOOL)enableWideGamut { id mockEngine = OCMClassMock([FlutterEngine class]); FlutterView* view = [[FlutterView alloc] initWithDelegate:mockEngine opaque:NO - enableWideGamut:NO]; + enableWideGamut:enableWideGamut]; id mockWindow = OCMClassMock([UIWindow class]); id mockWindowScene = OCMClassMock([UIWindowScene class]); @@ -208,4 +217,96 @@ - (NSDictionary*)createWindowMocks { }; } +#pragma mark - Wide Gamut Tests + +// Helper: add FlutterView to a real UIWindow so that layoutSubviews can access screen. +- (FlutterView*)createViewInWindowWithWideGamut:(BOOL)enableWideGamut { + FakeDelegate* delegate = [[FakeDelegate alloc] init]; + FlutterView* view = [[FlutterView alloc] initWithDelegate:delegate + opaque:NO + enableWideGamut:enableWideGamut]; + // Add to a real window so layoutSubviews has access to screen. + UIWindow* window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + [window addSubview:view]; + view.frame = window.bounds; + [view layoutSubviews]; + return view; +} + +- (void)testWideGamutViewSetsBGRA10XRPixelFormat { + FlutterView* view = [self createViewInWindowWithWideGamut:YES]; + // On a wide gamut capable device, the pixel format should be BGRA10_XR. + // On non-wide-gamut devices, it falls back to BGRA8Unorm. + if ([view isWideGamutSupported]) { + XCTAssertEqual(view.pixelFormat, MTLPixelFormatBGRA10_XR); + } else { + XCTAssertEqual(view.pixelFormat, MTLPixelFormatBGRA8Unorm); + } +} + +- (void)testStandardGamutViewKeepsBGRA8Unorm { + FlutterView* view = [self createViewInWindowWithWideGamut:NO]; + XCTAssertEqual(view.pixelFormat, MTLPixelFormatBGRA8Unorm); +} + +- (void)testWideGamutViewSetsExtendedSRGBColorSpace { + FlutterView* view = [self createViewInWindowWithWideGamut:YES]; + if ([view isWideGamutSupported]) { + CAMetalLayer* layer = (CAMetalLayer*)view.layer; + CGColorSpaceRef colorSpace = layer.colorspace; + XCTAssertNotNil((__bridge id)colorSpace); + CGColorSpaceRef extendedSRGB = CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB); + XCTAssertTrue(CFEqual(colorSpace, extendedSRGB)); + CGColorSpaceRelease(extendedSRGB); + } +} + +- (void)testStandardGamutViewDoesNotSetExtendedColorSpace { + FlutterView* view = [self createViewInWindowWithWideGamut:NO]; + CAMetalLayer* layer = (CAMetalLayer*)view.layer; + // Default CAMetalLayer colorspace is nil (device default sRGB). + XCTAssertNil((__bridge id)layer.colorspace); +} + +#pragma mark - FlutterOverlayView Wide Gamut Tests + +- (void)testOverlayViewWideGamutSetsBGRA10XR { + FlutterOverlayView* overlay = + [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA10_XR]; + CAMetalLayer* layer = (CAMetalLayer*)overlay.layer; + XCTAssertEqual(layer.pixelFormat, MTLPixelFormatBGRA10_XR); +} + +- (void)testOverlayViewWideGamutSetsExtendedSRGBColorSpace { + FlutterOverlayView* overlay = + [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA10_XR]; + CAMetalLayer* layer = (CAMetalLayer*)overlay.layer; + CGColorSpaceRef colorSpace = layer.colorspace; + XCTAssertNotNil((__bridge id)colorSpace); + CGColorSpaceRef extendedSRGB = CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB); + XCTAssertTrue(CFEqual(colorSpace, extendedSRGB)); + CGColorSpaceRelease(extendedSRGB); +} + +- (void)testOverlayViewStandardGamutKeepsBGRA8Unorm { + FlutterOverlayView* overlay = + [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA8Unorm]; + CAMetalLayer* layer = (CAMetalLayer*)overlay.layer; + XCTAssertEqual(layer.pixelFormat, MTLPixelFormatBGRA8Unorm); +} + +- (void)testOverlayViewStandardGamutDoesNotSetExtendedColorSpace { + FlutterOverlayView* overlay = + [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA8Unorm]; + CAMetalLayer* layer = (CAMetalLayer*)overlay.layer; + XCTAssertNil((__bridge id)layer.colorspace); +} + +- (void)testOverlayViewContentsScaleIsSet { + FlutterOverlayView* overlay = + [[FlutterOverlayView alloc] initWithContentsScale:3.0 pixelFormat:MTLPixelFormatBGRA10_XR]; + XCTAssertEqual(overlay.layer.contentsScale, 3.0); + XCTAssertEqual(overlay.layer.rasterizationScale, 3.0); +} + @end diff --git a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn index 9bc99a9e29fa7..ea069e2674648 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn @@ -133,6 +133,7 @@ source_set("flutter_framework_source") { "framework/Source/FlutterViewEngineProvider.h", "framework/Source/FlutterViewEngineProvider.mm", "framework/Source/FlutterViewProvider.h", + "framework/Source/FlutterView_Internal.h", "framework/Source/FlutterWindowController.h", "framework/Source/FlutterWindowController.mm", "framework/Source/KeyCodeMap.g.mm", diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm index 0ff1a887b6e6a..a6d2501e5ebdf 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm @@ -7,9 +7,24 @@ #include +#import + #include "flutter/shell/platform/common/engine_switches.h" static NSString* const kICUBundlePath = @"icudtl.dat"; + +static BOOL DoesHardwareSupportWideGamut() { + static BOOL result = NO; + static dispatch_once_t once_token = 0; + dispatch_once(&once_token, ^{ + id device = MTLCreateSystemDefaultDevice(); + // Wide gamut on macOS requires Apple3+ GPU family (Apple Silicon M1+). + // This uses 10-bit BGRA format (same as iOS) for consistency. + // Intel Macs (Mac1/Mac2 family) do not support wide gamut. + result = [device supportsFamily:MTLGPUFamilyApple3]; + }); + return result; +} static NSString* const kAppBundleIdentifier = @"io.flutter.flutter.app"; #pragma mark - Private interface declaration. @@ -68,6 +83,15 @@ - (BOOL)enableImpeller { return NO; } +- (BOOL)enableWideGamut { + NSNumber* enableWideGamut = + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTEnableWideGamut"]; + if (enableWideGamut != nil) { + return enableWideGamut.boolValue && DoesHardwareSupportWideGamut(); + } + return NO; +} + - (BOOL)enableFlutterGPU { NSNumber* enableFlutterGPU = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTEnableFlutterGPU"]; diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h index d99742350cb59..c082e033537ea 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h @@ -41,6 +41,11 @@ */ @property(nonatomic, readonly) BOOL enableFlutterGPU; +/** + * Whether wide gamut color support is enabled + */ +@property(nonatomic, readonly) BOOL enableWideGamut; + /** * Instead of looking up the assets and ICU data path in the application bundle, this initializer * allows callers to create a Dart project with custom locations specified for the both. diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 322c4f496e261..817152cfcd30d 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -29,9 +29,13 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTimeConverter.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.h" +#import +#import + @class FlutterEngineRegistrar; NSString* const kFlutterPlatformChannel = @"flutter/platform"; @@ -460,6 +464,9 @@ @implementation FlutterEngine { // A method channel for miscellaneous platform functionality. FlutterMethodChannel* _platformChannel; + // A method channel for taking screenshots via the rasterizer. + FlutterMethodChannel* _screenshotChannel; + // Whether the application is currently the active application. BOOL _active; @@ -494,6 +501,7 @@ @implementation FlutterEngine { } @synthesize windowController = _windowController; +@synthesize project = _project; - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project { return [self initWithName:labelPrefix project:project allowHeadlessExecution:YES]; @@ -715,6 +723,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { }; flutterArguments.engine_id = reinterpret_cast((__bridge void*)self); + flutterArguments.enable_wide_gamut = _project.enableWideGamut; BOOL mergedPlatformUIThread = YES; NSNumber* enableMergedPlatformUIThread = @@ -1395,6 +1404,79 @@ - (void)addInternalPlugins { [_platformChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [weakSelf handleMethodCall:call result:result]; }]; + + _screenshotChannel = + [FlutterMethodChannel methodChannelWithName:@"flutter/screenshot" + binaryMessenger:self.binaryMessenger + codec:[FlutterStandardMethodCodec sharedInstance]]; + [_screenshotChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + FlutterEngine* strongSelf = weakSelf; + if (!strongSelf) { + return result([FlutterError errorWithCode:@"invalid_state" + message:@"Engine deallocated." + details:nil]); + } + + FlutterViewController* viewController = + [strongSelf viewControllerForIdentifier:flutter::kFlutterImplicitViewId]; + if (!viewController) { + return result([FlutterError errorWithCode:@"failure" + message:@"No view controller." + details:nil]); + } + + NSArray* frontSurfaces = + viewController.flutterView.surfaceManager.frontSurfaces; + if (frontSurfaces.count == 0) { + return result([FlutterError errorWithCode:@"failure" + message:@"No front surfaces." + details:nil]); + } + + // Use the first front surface (the main backing store). + FlutterSurface* surface = frontSurfaces.firstObject; + IOSurfaceRef ioSurface = surface.ioSurface; + + size_t width = IOSurfaceGetWidth(ioSurface); + size_t height = IOSurfaceGetHeight(ioSurface); + size_t bytesPerRow = IOSurfaceGetBytesPerRow(ioSurface); + size_t bytesPerElement = IOSurfaceGetBytesPerElement(ioSurface); + uint32_t pixelFormat = (uint32_t)IOSurfaceGetPixelFormat(ioSurface); + + NSString* formatString; + switch (pixelFormat) { + case kCVPixelFormatType_40ARGBLEWideGamut: + formatString = @"MTLPixelFormatBGRA10_XR"; + break; + case kCVPixelFormatType_32BGRA: + formatString = @"MTLPixelFormatBGRA8Unorm"; + break; + default: + formatString = [NSString stringWithFormat:@"Unknown(%u)", pixelFormat]; + break; + } + + IOSurfaceLock(ioSurface, kIOSurfaceLockReadOnly, nil); + void* baseAddress = IOSurfaceGetBaseAddress(ioSurface); + + // Copy pixel data row by row into a tightly-packed buffer. + size_t packedBytesPerRow = width * bytesPerElement; + NSMutableData* packedData = [NSMutableData dataWithLength:packedBytesPerRow * height]; + uint8_t* dest = (uint8_t*)packedData.mutableBytes; + for (size_t row = 0; row < height; row++) { + memcpy(dest + row * packedBytesPerRow, (uint8_t*)baseAddress + row * bytesPerRow, + packedBytesPerRow); + } + + IOSurfaceUnlock(ioSurface, kIOSurfaceLockReadOnly, nil); + + return result(@[ + @(width), + @(height), + formatString, + [FlutterStandardTypedData typedDataWithBytes:packedData], + ]); + }]; } - (void)didUpdateMouseCursor:(NSCursor*)cursor { @@ -1415,6 +1497,7 @@ - (void)windowDidChangeScreen:(NSNotification*)notification { FlutterViewController* nextViewController; while ((nextViewController = [viewControllerEnumerator nextObject])) { [self updateWindowMetricsForViewController:nextViewController]; + [nextViewController updateWideGamutForScreen]; } } diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index cca50663008e8..51eab4ecf0e9a 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -87,6 +87,11 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { */ @property(nonatomic, readonly) BOOL running; +/** + * The project associated with this engine. + */ +@property(nonatomic, readonly, nonnull) FlutterDartProject* project; + /** * Provides the renderer config needed to initialize the engine and also handles external * texture management. diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h index 50f6a2ab32446..8e4169b468c1d 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h @@ -27,13 +27,17 @@ */ @interface FlutterSurface (Private) -- (nonnull instancetype)initWithSize:(CGSize)size device:(nonnull id)device; +- (nonnull instancetype)initWithSize:(CGSize)size + device:(nonnull id)device + enableWideGamut:(BOOL)enableWideGamut; @property(readonly, nonatomic, nonnull) IOSurfaceRef ioSurface; @property(readonly, nonatomic) CGSize size; @property(readonly, nonatomic) int64_t textureId; // Whether the surface is currently in use by the compositor. @property(readonly, nonatomic) BOOL isInUse; +// Whether the surface was created with wide gamut enabled. +@property(readonly, nonatomic) BOOL isWideGamut; @end diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm index 83c4ea2f8ebb6..963ac63831e35 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm @@ -15,6 +15,8 @@ @interface FlutterSurface () { id _texture; // Used for testing. BOOL _isInUseOverride; + // Whether this surface was created with wide gamut enabled. + BOOL _isWideGamut; } @end @@ -36,6 +38,10 @@ - (BOOL)isInUse { return _isInUseOverride || IOSurfaceIsInUse(_ioSurface); } +- (BOOL)isWideGamut { + return _isWideGamut; +} + - (BOOL)isInUseOverride { return _isInUseOverride; } @@ -44,11 +50,20 @@ - (void)setIsInUseOverride:(BOOL)isInUseOverride { _isInUseOverride = isInUseOverride; } -- (instancetype)initWithSize:(CGSize)size device:(id)device { +- (instancetype)initWithSize:(CGSize)size + device:(id)device + enableWideGamut:(BOOL)enableWideGamut { if (self = [super init]) { self->_size = size; - self->_ioSurface.Reset([FlutterSurface createIOSurfaceWithSize:size]); - self->_texture = [FlutterSurface createTextureForIOSurface:_ioSurface size:size device:device]; + self->_isWideGamut = enableWideGamut; + self->_ioSurface.Reset([FlutterSurface createIOSurfaceWithSize:size + enableWideGamut:enableWideGamut]); + MTLPixelFormat pixelFormat = + enableWideGamut ? MTLPixelFormatBGRA10_XR : MTLPixelFormatBGRA8Unorm; + self->_texture = [FlutterSurface createTextureForIOSurface:_ioSurface + size:size + device:device + pixelFormat:pixelFormat]; } return self; } @@ -74,9 +89,17 @@ + (FlutterSurface*)fromFlutterMetalTexture:(const FlutterMetalTexture*)texture { return (__bridge FlutterSurface*)texture->user_data; } -+ (IOSurfaceRef)createIOSurfaceWithSize:(CGSize)size { - unsigned pixelFormat = kCVPixelFormatType_32BGRA; - unsigned bytesPerElement = 4; ++ (IOSurfaceRef)createIOSurfaceWithSize:(CGSize)size enableWideGamut:(BOOL)enableWideGamut { + unsigned pixelFormat; + unsigned bytesPerElement; + if (enableWideGamut) { + // 10-bit wide gamut format (same as iOS) + pixelFormat = kCVPixelFormatType_40ARGBLEWideGamut; + bytesPerElement = 8; + } else { + pixelFormat = kCVPixelFormatType_32BGRA; + bytesPerElement = 4; + } size_t bytesPerRow = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size.width * bytesPerElement); size_t totalBytes = IOSurfaceAlignProperty(kIOSurfaceAllocSize, size.height * bytesPerRow); @@ -90,15 +113,20 @@ + (IOSurfaceRef)createIOSurfaceWithSize:(CGSize)size { }; IOSurfaceRef res = IOSurfaceCreate((CFDictionaryRef)options); - IOSurfaceSetValue(res, kIOSurfaceColorSpace, kCGColorSpaceSRGB); + if (enableWideGamut) { + IOSurfaceSetValue(res, kIOSurfaceColorSpace, kCGColorSpaceExtendedSRGB); + } else { + IOSurfaceSetValue(res, kIOSurfaceColorSpace, kCGColorSpaceSRGB); + } return res; } + (id)createTextureForIOSurface:(IOSurfaceRef)surface size:(CGSize)size - device:(id)device { + device:(id)device + pixelFormat:(MTLPixelFormat)pixelFormat { MTLTextureDescriptor* textureDescriptor = - [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:size.width height:size.height mipmapped:NO]; diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h index 888b41e181a1b..c2a2cf2e8d381 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h @@ -52,7 +52,16 @@ - (nullable instancetype)initWithDevice:(nonnull id)device commandQueue:(nonnull id)commandQueue layer:(nonnull CALayer*)containingLayer - delegate:(nonnull id)delegate; + delegate:(nonnull id)delegate + wideGamut:(BOOL)wideGamut; + +/** + * Updates the wide gamut setting. Flushes cached surfaces so new surfaces + * will be created with the updated pixel format. + * + * Must be called on the platform thread. + */ +- (void)setEnableWideGamut:(BOOL)enableWideGamut; /** * Returns a back buffer surface of the given size to which Flutter can render content. @@ -92,6 +101,11 @@ */ - (void)returnSurfaces:(nonnull NSArray*)surfaces; +/** + * Removes all cached surfaces. + */ +- (void)flush; + /** * Returns number of surfaces currently in cache. Used for tests. */ diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm index a021a8b625b5b..2852c825164d1 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm @@ -19,6 +19,7 @@ @interface FlutterSurfaceManager () { id _commandQueue; CALayer* _containingLayer; __weak id _delegate; + BOOL _wideGamut; // Available (cached) back buffer surfaces. These will be cleared during // present and replaced by current frong surfaces. @@ -104,13 +105,14 @@ @implementation FlutterSurfaceManager - (instancetype)initWithDevice:(id)device commandQueue:(id)commandQueue layer:(CALayer*)containingLayer - delegate:(__weak id)delegate { + delegate:(__weak id)delegate + wideGamut:(BOOL)wideGamut { if (self = [super init]) { _device = device; _commandQueue = commandQueue; _containingLayer = containingLayer; _delegate = delegate; - + _wideGamut = wideGamut; _backBufferCache = [[FlutterBackBufferCache alloc] init]; _frontSurfaces = [NSMutableArray array]; _layers = [NSMutableArray array]; @@ -118,6 +120,20 @@ - (instancetype)initWithDevice:(id)device return self; } +- (void)setEnableWideGamut:(BOOL)enableWideGamut { + FML_DCHECK([NSThread isMainThread]); + if (_wideGamut == enableWideGamut) { + return; + } + _wideGamut = enableWideGamut; + + // Flush cached surfaces since they have the wrong pixel format. + [_backBufferCache flush]; + + // Clear front surfaces — they will be replaced on the next present. + [_frontSurfaces removeAllObjects]; +} + - (FlutterBackBufferCache*)backBufferCache { return _backBufferCache; } @@ -133,7 +149,7 @@ - (NSArray*)layers { - (FlutterSurface*)surfaceForSize:(CGSize)size { FlutterSurface* surface = [_backBufferCache removeSurfaceForSize:size]; if (surface == nil) { - surface = [[FlutterSurface alloc] initWithSize:size device:_device]; + surface = [[FlutterSurface alloc] initWithSize:size device:_device enableWideGamut:_wideGamut]; } return surface; } @@ -152,6 +168,14 @@ - (BOOL)enableSurfaceDebugInfo { - (void)commit:(NSArray*)surfaces { FML_DCHECK([NSThread isMainThread]); + // Check if incoming surfaces match current wide gamut mode. + // If not, discard them by returning early - they will be released. + for (FlutterSurfacePresentInfo* info in surfaces) { + if (info.surface.isWideGamut != _wideGamut) { + return; + } + } + // Release all unused back buffer surfaces and replace them with front surfaces. [_backBufferCache returnSurfaces:_frontSurfaces]; @@ -343,6 +367,12 @@ - (void)returnSurfaces:(nonnull NSArray*)returnedSurfaces { [self performSelectorOnMainThread:@selector(reschedule) withObject:nil waitUntilDone:NO]; } +- (void)flush { + @synchronized(self) { + [_surfaces removeAllObjects]; + } +} + - (NSUInteger)count { @synchronized(self) { return _surfaces.count; @@ -350,9 +380,7 @@ - (NSUInteger)count { } - (void)onIdle { - @synchronized(self) { - [_surfaces removeAllObjects]; - } + [self flush]; } - (void)reschedule { diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm index 30a2248b935d8..e72311f96eb00 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm @@ -40,14 +40,15 @@ - (void)onPresent:(CGSize)frameSize namespace flutter::testing { -static FlutterSurfaceManager* CreateSurfaceManager(TestView* testView) { +static FlutterSurfaceManager* CreateSurfaceManager(TestView* testView, BOOL enableWideGamut = NO) { id device = MTLCreateSystemDefaultDevice(); id commandQueue = [device newCommandQueue]; CALayer* layer = reinterpret_cast(testView.layer); return [[FlutterSurfaceManager alloc] initWithDevice:device commandQueue:commandQueue layer:layer - delegate:testView]; + delegate:testView + wideGamut:enableWideGamut]; } static FlutterSurfacePresentInfo* CreatePresentInfo( @@ -298,4 +299,200 @@ - (void)onPresent:(CGSize)frameSize EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(0, 0))); } +TEST(FlutterSurfaceManager, WideGamutSurfaceHasCorrectPixelFormat) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + auto texture = surface.asFlutterMetalTexture; + id metalTexture = (__bridge id)texture.texture; + EXPECT_EQ(metalTexture.pixelFormat, MTLPixelFormatBGRA10_XR); + texture.destruction_callback(texture.user_data); +} + +TEST(FlutterSurfaceManager, StandardGamutSurfaceHasCorrectPixelFormat) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/NO); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + auto texture = surface.asFlutterMetalTexture; + id metalTexture = (__bridge id)texture.texture; + EXPECT_EQ(metalTexture.pixelFormat, MTLPixelFormatBGRA8Unorm); + texture.destruction_callback(texture.user_data); +} + +TEST(FlutterSurfaceManager, WideGamutLayerWorksWithoutExplicitContentsFormat) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(50, 30)]; + [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface, CGPointMake(0, 0)) ] + atTime:0 + notify:nil]; + + EXPECT_EQ(testView.layer.sublayers.count, 1ul); + EXPECT_NE(testView.layer.sublayers[0].contents, nil); +} + +TEST(FlutterSurfaceManager, WideGamutIOSurfaceHasCorrectColorSpace) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + IOSurfaceRef ioSurface = surface.ioSurface; + CFTypeRef colorSpace = IOSurfaceCopyValue(ioSurface, kIOSurfaceColorSpace); + if (colorSpace == nullptr) { + GTEST_FAIL() << "IOSurfaceCopyValue returned nullptr for kIOSurfaceColorSpace"; + return; + } + EXPECT_TRUE(CFEqual(colorSpace, kCGColorSpaceExtendedSRGB)); + CFRelease(colorSpace); +} + +TEST(FlutterSurfaceManager, StandardGamutIOSurfaceHasCorrectColorSpace) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/NO); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + IOSurfaceRef ioSurface = surface.ioSurface; + CFTypeRef colorSpace = IOSurfaceCopyValue(ioSurface, kIOSurfaceColorSpace); + if (colorSpace == nullptr) { + GTEST_FAIL() << "IOSurfaceCopyValue returned nullptr for kIOSurfaceColorSpace"; + return; + } + EXPECT_TRUE(CFEqual(colorSpace, kCGColorSpaceSRGB)); + CFRelease(colorSpace); +} + +TEST(FlutterSurfaceManager, WideGamutIOSurfaceHasCorrectPixelFormat) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + IOSurfaceRef ioSurface = surface.ioSurface; + uint32_t pixelFormat = (uint32_t)IOSurfaceGetPixelFormat(ioSurface); + EXPECT_EQ(pixelFormat, (uint32_t)kCVPixelFormatType_40ARGBLEWideGamut); +} + +TEST(FlutterSurfaceManager, StandardGamutIOSurfaceHasCorrectPixelFormat) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/NO); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + IOSurfaceRef ioSurface = surface.ioSurface; + uint32_t pixelFormat = (uint32_t)IOSurfaceGetPixelFormat(ioSurface); + EXPECT_EQ(pixelFormat, (uint32_t)kCVPixelFormatType_32BGRA); +} + +TEST(FlutterSurfaceManager, WideGamutIOSurfaceHasCorrectBytesPerElement) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + IOSurfaceRef ioSurface = surface.ioSurface; + size_t bytesPerElement = IOSurfaceGetBytesPerElement(ioSurface); + EXPECT_EQ(bytesPerElement, 8ul); +} + +TEST(FlutterSurfaceManager, StandardGamutIOSurfaceHasCorrectBytesPerElement) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/NO); + + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + IOSurfaceRef ioSurface = surface.ioSurface; + size_t bytesPerElement = IOSurfaceGetBytesPerElement(ioSurface); + EXPECT_EQ(bytesPerElement, 4ul); +} + +TEST(FlutterSurfaceManager, SurfaceCacheDoesNotMixGamutModes) { + TestView* testView = [[TestView alloc] init]; + // Create a wide gamut surface manager. + FlutterSurfaceManager* wideManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + auto wideSurface = [wideManager surfaceForSize:CGSizeMake(100, 100)]; + auto wideTexture = wideSurface.asFlutterMetalTexture; + id wideMetalTexture = (__bridge id)wideTexture.texture; + EXPECT_EQ(wideMetalTexture.pixelFormat, MTLPixelFormatBGRA10_XR); + + // Present and get it cached. + [wideManager presentSurfaces:@[ CreatePresentInfo(wideSurface) ] atTime:0 notify:nil]; + wideTexture.destruction_callback(wideTexture.user_data); + + // Request same size again - should get a recycled wide gamut surface. + auto recycledSurface = [wideManager surfaceForSize:CGSizeMake(100, 100)]; + auto recycledTexture = recycledSurface.asFlutterMetalTexture; + id recycledMetalTexture = (__bridge id)recycledTexture.texture; + EXPECT_EQ(recycledMetalTexture.pixelFormat, MTLPixelFormatBGRA10_XR); + recycledTexture.destruction_callback(recycledTexture.user_data); +} + +TEST(FlutterSurfaceManager, DynamicSwitchFromStandardToWideGamut) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/NO); + + // Start with standard gamut surface. + auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + auto texture1 = surface1.asFlutterMetalTexture; + id metalTexture1 = (__bridge id)texture1.texture; + EXPECT_EQ(metalTexture1.pixelFormat, MTLPixelFormatBGRA8Unorm); + [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil]; + texture1.destruction_callback(texture1.user_data); + + // Switch to wide gamut. + [surfaceManager setEnableWideGamut:YES]; + + // Verify cache was flushed. + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); + + // New surface should be wide gamut. + auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + auto texture2 = surface2.asFlutterMetalTexture; + id metalTexture2 = (__bridge id)texture2.texture; + EXPECT_EQ(metalTexture2.pixelFormat, MTLPixelFormatBGRA10_XR); + texture2.destruction_callback(texture2.user_data); +} + +TEST(FlutterSurfaceManager, DynamicSwitchFromWideToStandardGamut) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + // Start with wide gamut surface. + auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + auto texture1 = surface1.asFlutterMetalTexture; + id metalTexture1 = (__bridge id)texture1.texture; + EXPECT_EQ(metalTexture1.pixelFormat, MTLPixelFormatBGRA10_XR); + [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil]; + texture1.destruction_callback(texture1.user_data); + + // Switch to standard gamut. + [surfaceManager setEnableWideGamut:NO]; + + // Verify cache was flushed. + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); + + // New surface should be standard gamut. + auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + auto texture2 = surface2.asFlutterMetalTexture; + id metalTexture2 = (__bridge id)texture2.texture; + EXPECT_EQ(metalTexture2.pixelFormat, MTLPixelFormatBGRA8Unorm); + texture2.destruction_callback(texture2.user_data); +} + +TEST(FlutterSurfaceManager, DynamicSwitchNoOpWhenSameValue) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView, /*enableWideGamut=*/YES); + + // Cache a surface. + auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)]; + [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil]; + + auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)]; + [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil]; + EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul); + + // Set same value — cache should not be flushed. + [surfaceManager setEnableWideGamut:YES]; + EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul); +} + } // namespace flutter::testing diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h index 35b3430d9d090..73debbdb38a0b 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -41,7 +41,7 @@ commandQueue:(nonnull id)commandQueue delegate:(nonnull id)delegate viewIdentifier:(FlutterViewIdentifier)viewIdentifier - NS_DESIGNATED_INITIALIZER; + enableWideGamut:(BOOL)enableWideGamut NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(nullable NSOpenGLPixelFormat*)format NS_UNAVAILABLE; diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm index 078a353f214aa..1bdbd5844123e 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -25,7 +25,8 @@ @implementation FlutterView - (instancetype)initWithMTLDevice:(id)device commandQueue:(id)commandQueue delegate:(id)delegate - viewIdentifier:(FlutterViewIdentifier)viewIdentifier { + viewIdentifier:(FlutterViewIdentifier)viewIdentifier + enableWideGamut:(BOOL)enableWideGamut { self = [super initWithFrame:NSZeroRect]; if (self) { [self setWantsLayer:YES]; @@ -36,7 +37,8 @@ - (instancetype)initWithMTLDevice:(id)device _surfaceManager = [[FlutterSurfaceManager alloc] initWithDevice:device commandQueue:commandQueue layer:self.layer - delegate:self]; + delegate:self + wideGamut:enableWideGamut]; _resizeSynchronizer = [[FlutterResizeSynchronizer alloc] init]; } return self; @@ -50,6 +52,10 @@ - (FlutterSurfaceManager*)surfaceManager { return _surfaceManager; } +- (void)setEnableWideGamut:(BOOL)enableWideGamut { + [_surfaceManager setEnableWideGamut:enableWideGamut]; +} + - (void)shutDown { [_resizeSynchronizer shutDown]; } diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 4111bc52937ab..aa4cfed8afdaf 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -13,12 +13,14 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView_Internal.h" #include "flutter/shell/platform/embedder/embedder.h" #pragma mark - Static types and data. @@ -795,10 +797,25 @@ - (void)onAccessibilityStatusChanged:(BOOL)enabled { - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id)device commandQueue:(id)commandQueue { + FlutterDartProject* project = _project ?: self.engine.project; return [[FlutterView alloc] initWithMTLDevice:device commandQueue:commandQueue delegate:self - viewIdentifier:_viewIdentifier]; + viewIdentifier:_viewIdentifier + enableWideGamut:project.enableWideGamut]; +} + +- (void)updateWideGamutForScreen { + FlutterDartProject* project = _project ?: self.engine.project; + if (!project.enableWideGamut) { + return; + } + NSScreen* screen = self.view.window.screen; + if (screen == nil) { + return; + } + BOOL screenSupportsP3 = [screen canRepresentDisplayGamut:NSDisplayGamutP3]; + [self.flutterView setEnableWideGamut:screenSupportsP3]; } - (NSString*)lookupKeyForAsset:(NSString*)asset { diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index 47d0d8bd6aa3c..500f65c88e3ff 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -55,6 +55,12 @@ */ - (void)updateSemantics:(nonnull const FlutterSemanticsUpdate2*)update; +/** + * Updates the wide gamut surface format based on the current screen's + * display gamut. Should be called when the window moves to a different screen. + */ +- (void)updateWideGamutForScreen; + /** * Removes this controller from the engine. The controller is removed from the engine * on dealloc, however in multi-window scenario the controller needs to be unregistered diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm index d64c6d1603622..6778cd0170b0d 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm @@ -32,7 +32,8 @@ - (BOOL)viewShouldAcceptFirstResponder:(NSView*)view { FlutterView* view = [[FlutterView alloc] initWithMTLDevice:device commandQueue:queue delegate:delegate - viewIdentifier:kImplicitViewId]; + viewIdentifier:kImplicitViewId + enableWideGamut:NO]; EXPECT_EQ([view layer:view.layer shouldInheritContentsScale:3.0 fromWindow:view.window], YES); } @@ -75,7 +76,8 @@ - (void)set { TestFlutterView* view = [[TestFlutterView alloc] initWithMTLDevice:device commandQueue:queue delegate:delegate - viewIdentifier:kImplicitViewId]; + viewIdentifier:kImplicitViewId + enableWideGamut:NO]; NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered @@ -117,7 +119,8 @@ - (void)set { TestFlutterView* view = [[TestFlutterView alloc] initWithMTLDevice:device commandQueue:queue delegate:delegate - viewIdentifier:kImplicitViewId]; + viewIdentifier:kImplicitViewId + enableWideGamut:NO]; NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView_Internal.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView_Internal.h new file mode 100644 index 0000000000000..ec9e2cb9ded77 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterView_Internal.h @@ -0,0 +1,22 @@ +// 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_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVIEW_INTERNAL_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVIEW_INTERNAL_H_ + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" + +@interface FlutterView () + +/** + * Updates the wide gamut setting on the surface manager. Called when + * the window moves to a screen with different gamut support. + * + * Must be called on the platform thread. + */ +- (void)setEnableWideGamut:(BOOL)enableWideGamut; + +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVIEW_INTERNAL_H_ diff --git a/engine/src/flutter/shell/platform/embedder/embedder.cc b/engine/src/flutter/shell/platform/embedder/embedder.cc index 8900dc6599292..d2bca8e943278 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.cc +++ b/engine/src/flutter/shell/platform/embedder/embedder.cc @@ -1259,6 +1259,10 @@ MakeRenderTargetFromBackingStoreImpeller( FML_LOG(ERROR) << "Could not wrap embedder supplied Metal render texture."; return nullptr; } + + aiks_context->GetContext()->UpdateOffscreenLayerPixelFormat( + resolve_tex->GetTextureDescriptor().format); + resolve_tex->SetLabel("ImpellerBackingStoreResolve"); impeller::TextureDescriptor msaa_tex_desc; @@ -2098,6 +2102,7 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, settings.assets_path = args->assets_path; settings.leak_vm = !SAFE_ACCESS(args, shutdown_dart_vm_when_done, false); settings.old_gen_heap_size = SAFE_ACCESS(args, dart_old_gen_heap_size, -1); + settings.enable_wide_gamut = SAFE_ACCESS(args, enable_wide_gamut, false); if (!flutter::DartVM::IsRunningPrecompiledCode()) { // Verify the assets path contains Dart 2 kernel assets. diff --git a/engine/src/flutter/shell/platform/embedder/embedder.h b/engine/src/flutter/shell/platform/embedder/embedder.h index ac09b50adc35b..8b53a1361f818 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.h +++ b/engine/src/flutter/shell/platform/embedder/embedder.h @@ -2793,6 +2793,10 @@ typedef struct { /// `PlatformDispatcher.instance.engineId`. Can be used in native code to /// retrieve the engine instance that is running the Dart code. int64_t engine_id; + + /// If true, the engine will decode images in wide gamut color spaces + /// (Display P3) when supported. If false, images are decoded to sRGB. + bool enable_wide_gamut; } FlutterProjectArgs; typedef struct { diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc index 7ff7485ca04a7..069266f98d2b6 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc @@ -204,8 +204,10 @@ std::atomic_size_t EmbedderTestTaskRunner::sEmbedderTaskRunnerIdentifiers = {}; TEST_F(EmbedderTest, CanSpecifyCustomUITaskRunner) { auto& context = GetEmbedderContext(); - auto ui_task_runner = CreateNewThread("test_ui_thread"); - auto platform_task_runner = CreateNewThread("test_platform_thread"); + auto ui_thread = std::make_unique("test_ui_thread"); + auto ui_task_runner = ui_thread->GetTaskRunner(); + auto platform_thread = std::make_unique("test_platform_thread"); + auto platform_task_runner = platform_thread->GetTaskRunner(); static std::mutex engine_mutex; UniqueEngine engine; @@ -267,6 +269,12 @@ TEST_F(EmbedderTest, CanSpecifyCustomUITaskRunner) { platform_task_runner->PostTask([&kill_latch] { kill_latch.Signal(); }); }); kill_latch.Wait(); + + // Shut down the threads before exiting the test. There may still be + // pending tasks queued to the task runners, and they must not run + // after the engine goes out of scope. + ui_thread.reset(); + platform_thread.reset(); } TEST_F(EmbedderTest, IgnoresStaleTasks) { diff --git a/engine/src/flutter/sky/packages/sky_engine/LICENSE b/engine/src/flutter/sky/packages/sky_engine/LICENSE index 3b9a389e11b57..a489af5a7e49d 100644 --- a/engine/src/flutter/sky/packages/sky_engine/LICENSE +++ b/engine/src/flutter/sky/packages/sky_engine/LICENSE @@ -17175,6 +17175,13 @@ limitations under the License. -------------------------------------------------------------------------------- skia +Copyright 2026 Google Inc. + +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +skia + Copyright 2026 Google LLC Use of this source code is governed by a BSD-style license that can be diff --git a/engine/src/flutter/testing/dart/fragment_shader_test.dart b/engine/src/flutter/testing/dart/fragment_shader_test.dart index 5c70b6f46a492..ccb4a9182a747 100644 --- a/engine/src/flutter/testing/dart/fragment_shader_test.dart +++ b/engine/src/flutter/testing/dart/fragment_shader_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert' as convert; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'dart:ui'; @@ -52,7 +53,7 @@ void main() async { expect(identical(programA, programB), true); }); - group('FragmentProgram getUniform*', () { + group('getUniformFloat slots', () { late FragmentShader shader; setUpAll(() async { @@ -74,199 +75,459 @@ void main() async { expect(slots[i].shaderIndex, equals(i)); } }); + }); - test('FragmentProgram getUniformFloat unknown', () async { - expect( - () { - shader.getUniformFloat('unknown'); - }, - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('No uniform named "unknown"'), - ), - ), - ); - }); + group('FragmentShader uniforms', () { + late Map shaderMap; - test('FragmentProgram getUniformFloat offset overflow', () async { - expect( - () => shader.getUniformFloat('iVec2Uniform', 2), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Index `2` out of bounds for `iVec2Uniform`.'), - ), - ), - ); + setUpAll(() async { + shaderMap = { + UniformFloatSlot: (await FragmentProgram.fromAsset( + 'float_uniform.frag.iplr', + )).fragmentShader(), + UniformVec2Slot: (await FragmentProgram.fromAsset( + 'vec2_uniform.frag.iplr', + )).fragmentShader(), + UniformVec3Slot: (await FragmentProgram.fromAsset( + 'vec3_uniform.frag.iplr', + )).fragmentShader(), + UniformVec4Slot: (await FragmentProgram.fromAsset( + 'vec4_uniform.frag.iplr', + )).fragmentShader(), + UniformArray: (await FragmentProgram.fromAsset( + 'float_array_uniform.frag.iplr', + )).fragmentShader(), + UniformArray: (await FragmentProgram.fromAsset( + 'vec2_array_uniform.frag.iplr', + )).fragmentShader(), + UniformArray: (await FragmentProgram.fromAsset( + 'vec3_array_uniform.frag.iplr', + )).fragmentShader(), + UniformArray: (await FragmentProgram.fromAsset( + 'vec4_array_uniform.frag.iplr', + )).fragmentShader(), + }; }); - test('FragmentProgram getUniformFloat offset underflow', () async { - expect( - () => shader.getUniformFloat('iVec2Uniform', -1), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Index `-1` out of bounds for `iVec2Uniform`.'), + group('float', () { + test('set using setUniformFloat', () async { + final FragmentShader shader = shaderMap[UniformFloatSlot]!; + const color = Color.fromARGB(255, 255, 0, 0); + shader.setFloat(0, color.r); + _expectShaderRendersColor(shader, color); + }); + + test('set using getUniformFloat', () async { + final FragmentShader shader = shaderMap[UniformFloatSlot]!; + const color = Color.fromARGB(255, 50, 0, 0); + shader.getUniformFloat('color_r').set(color.r); + _expectShaderRendersColor(shader, color); + }); + + test('getUniformFloat offset overflow', () async { + final FragmentShader shader = shaderMap[UniformFloatSlot]!; + expect( + () => shader.getUniformFloat('color_r', 2), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Index `2` out of bounds for `color_r`.'), + ), ), - ), - ); - }); - - test('FragmentProgram getUniformVec2', () async { - final UniformVec2Slot slot = shader.getUniformVec2('iVec2Uniform'); - slot.set(6.0, 7.0); - }); - - test('FragmentProgram getUniformVec2 wrong size', () async { - expect( - () => shader.getUniformVec2('iVec3Uniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('`iVec3Uniform` has size 3, not size 2.'), + ); + }); + + test('getUniformFloat offset underflow', () async { + final FragmentShader shader = shaderMap[UniformFloatSlot]!; + expect( + () => shader.getUniformFloat('color_r', -1), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Index `-1` out of bounds for `color_r`.'), + ), ), - ), - ); - expect( - () => shader.getUniformVec2('iFloatUniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('`iFloatUniform` has size 1, not size 2.'), + ); + }); + }); + group('vec2', () { + test('set using setFloat', () async { + final FragmentShader shader = shaderMap[UniformVec2Slot]!; + const color = Color.fromARGB(255, 255, 255, 0); + shader.setFloat(0, color.r); + shader.setFloat(1, color.g); + _expectShaderRendersColor(shader, color); + }); + + test('set using getUniformVec2', () async { + final FragmentShader shader = shaderMap[UniformVec2Slot]!; + const color = Color.fromARGB(255, 50, 50, 0); + shader.getUniformVec2('color_rg').set(color.r, color.g); + _expectShaderRendersColor(shader, color); + }); + + test('wrong datatype', () async { + final FragmentShader shader = shaderMap[UniformVec3Slot]!; + expect( + () => shader.getUniformVec2('color_rgb'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('`color_rgb` has size 3, not size 2.'), + ), ), - ), - ); + ); + }); }); - - test('FragmentProgram getUniformVec3', () async { - final UniformVec3Slot slot = shader.getUniformVec3('iVec3Uniform'); - slot.set(0.8, 0.1, 0.3); + group('vec3', () { + test('set using setFloat', () async { + final FragmentShader shader = shaderMap[UniformVec3Slot]!; + const color = Color.fromARGB(255, 67, 42, 12); + shader.setFloat(0, color.r); + shader.setFloat(1, color.g); + shader.setFloat(2, color.b); + // Note: The original test also called getUniformVec3 after setFloat. + // Assuming this was intentional to test idempotency or a specific interaction. + shader.getUniformVec3('color_rgb').set(color.r, color.g, color.b); + _expectShaderRendersColor(shader, color); + }); + + test('set using getUniformVec3', () async { + final FragmentShader shader = shaderMap[UniformVec3Slot]!; + const color = Color.fromARGB(255, 42, 67, 12); + shader.getUniformVec3('color_rgb').set(color.r, color.g, color.b); + _expectShaderRendersColor(shader, color); + }); + + test('wrong datatype', () async { + final FragmentShader shader = shaderMap[UniformVec2Slot]!; + expect( + () => shader.getUniformVec3('color_rg'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('`color_rg` has size 2, not size 3.'), + ), + ), + ); + }); }); - test('FragmentProgram getUniformVec3 wrong size', () async { - expect( - () => shader.getUniformVec3('iVec2Uniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('`iVec2Uniform` has size 2, not size 3.'), - ), - ), - ); - expect( - () => shader.getUniformVec3('iVec4Uniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('`iVec4Uniform` has size 4, not size 3.'), + group('vec4', () { + test('set using setFloat', () async { + const color = Color.fromARGB(255, 67, 42, 12); + final FragmentShader shader = shaderMap[UniformVec4Slot]!; + shader.setFloat(0, color.r); + shader.setFloat(1, color.g); + shader.setFloat(2, color.b); + shader.setFloat(3, color.a); + _expectShaderRendersColor(shader, color); + }); + + test('set using getUniformFloat', () async { + const color = Color.fromARGB(255, 12, 37, 27); + final FragmentShader shader = shaderMap[UniformVec4Slot]!; + shader.getUniformVec4('color_rgba').set(color.r, color.g, color.b, color.a); + _expectShaderRendersColor(shader, color); + }); + + test('wrong datatype', () async { + final FragmentShader shader = shaderMap[UniformVec3Slot]!; + expect( + () => shader.getUniformVec4('color_rgb'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('`color_rgb` has size 3, not size 4.'), + ), ), - ), - ); + ); + }); }); - test('FragmentProgram getUniformVec4', () async { - final UniformVec4Slot slot = shader.getUniformVec4('iVec4Uniform'); - slot.set(11.0, 22.0, 19.0, 96.0); + group('float array', () { + test('set using setFloat', () { + const color = Color.fromARGB(255, 11, 22, 96); + final FragmentShader shader = shaderMap[UniformArray]!; + shader.setFloat(0, color.r); + shader.setFloat(1, color.g); + shader.setFloat(2, color.b); + shader.setFloat(3, color.a); + _expectShaderRendersColor(shader, color); + }); + + test('set using getUniformFloatArray', () async { + const color = Color.fromARGB(255, 96, 11, 22); + final FragmentShader shader = shaderMap[UniformArray]!; + final UniformArray colorRgba = shader.getUniformFloatArray('color_array'); + colorRgba[0].set(color.r); + colorRgba[1].set(color.g); + colorRgba[2].set(color.b); + colorRgba[3].set(color.a); + _expectShaderRendersColor(shader, color); + }); }); - test('FragmentProgram getUniformVec4 wrong size', () async { - expect( - () => shader.getUniformVec4('iVec3Uniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('`iVec3Uniform` has size 3, not size 4.'), + group('vec2 array', () { + test('set using setFloat', () async { + const color = Color.fromARGB(255, 67, 42, 12); + final FragmentShader shader = shaderMap[UniformArray]!; + shader.setFloat(0, color.r); + shader.setFloat(1, color.g); + shader.setFloat(2, color.b); + shader.setFloat(3, color.a); + _expectShaderRendersColor(shader, color); + }); + + test('set using getUniformVec2Array', () async { + const color = Color.fromARGB(255, 1, 73, 26); + final FragmentShader shader = shaderMap[UniformArray]!; + final UniformArray colorRgba = shader.getUniformVec2Array('color_array'); + colorRgba[0].set(color.r, color.g); + colorRgba[1].set(color.b, color.a); + _expectShaderRendersColor(shader, color); + }); + + test('wrong datatype', () async { + final FragmentShader shader = shaderMap[UniformVec3Slot]!; + expect( + () => shader.getUniformVec2Array('color_rgb'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Uniform size (3) for "color_rgb" is not a multiple of 2.'), + ), ), - ), - ); - }); - - test('FragmentProgram getUniformArray float', () async { - final UniformArray slots = shader.getUniformFloatArray( - 'iFloatArrayUniform', - ); - expect(slots.length, 10); - slots[0].set(1.0); - slots[1].set(1.0); + ); + }); }); - test('FragmentProgram getUniformArray not found', () async { - expect( - () => shader.getUniformFloatArray('unknown'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('No uniform named "unknown".'), + group('vec3 array', () { + test('set using setFloat', () async { + const cpuColors = [Color.fromARGB(255, 67, 42, 12), Color.fromARGB(255, 11, 22, 96)]; + final FragmentShader shader = shaderMap[UniformArray]!; + shader.setFloat(0, 2); + shader.setFloat(1, 2); + shader.setFloat(2, cpuColors[0].r); + shader.setFloat(3, cpuColors[0].g); + shader.setFloat(4, cpuColors[0].b); + shader.setFloat(5, cpuColors[1].r); + shader.setFloat(6, cpuColors[1].g); + shader.setFloat(7, cpuColors[1].b); + _expectShaderRendersBarcode(shader, cpuColors); + }); + + test('set using getUniformVec3Array', () async { + const cpuColors = [Color.fromARGB(255, 11, 22, 96), Color.fromARGB(255, 67, 42, 12)]; + final FragmentShader shader = shaderMap[UniformArray]!; + shader.getUniformVec2('u_size').set(2, 2); + final UniformArray gpuColors = shader.getUniformVec3Array('color_array'); + gpuColors[0].set(cpuColors[0].r, cpuColors[0].g, cpuColors[0].b); + gpuColors[1].set(cpuColors[1].r, cpuColors[1].g, cpuColors[1].b); + _expectShaderRendersBarcode(shader, cpuColors); + }); + + test('wrong datatype', () async { + final FragmentShader shader = shaderMap[UniformVec4Slot]!; + expect( + () => shader.getUniformVec3Array('color_rgba'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Uniform size (4) for "color_rgba" is not a multiple of 3.'), + ), ), - ), - ); + ); + }); }); - test('FragmentProgram getUniformArrayVec2', () async { - final UniformArray slots = shader.getUniformVec2Array('iVec2ArrayUniform'); - expect(slots.length, 3); - slots[0].set(1.0, 1.0); + group('vec4 array', () { + test('set using setFloat', () async { + const cpuColors = [Color.fromARGB(77, 67, 42, 12), Color.fromARGB(51, 11, 22, 96)]; + final FragmentShader shader = shaderMap[UniformArray]!; + // 'u_size' + shader.setFloat(0, 2); + shader.setFloat(1, 2); + shader.setFloat(2, cpuColors[0].r); + shader.setFloat(3, cpuColors[0].g); + shader.setFloat(4, cpuColors[0].b); + shader.setFloat(5, cpuColors[0].a); + shader.setFloat(6, cpuColors[1].r); + shader.setFloat(7, cpuColors[1].g); + shader.setFloat(8, cpuColors[1].b); + shader.setFloat(9, cpuColors[1].a); + _expectShaderRendersBarcode(shader, cpuColors); + }); + + test('set using getUniformVec4Array', () async { + const cpuColors = [Color.fromARGB(51, 11, 22, 96), Color.fromARGB(77, 67, 42, 12)]; + final FragmentShader shader = shaderMap[UniformArray]!; + shader.getUniformVec2('u_size').set(2, 2); + final UniformArray colors = shader.getUniformVec4Array('color_array'); + colors[0].set(cpuColors[0].r, cpuColors[0].g, cpuColors[0].b, cpuColors[0].a); + colors[1].set(cpuColors[1].r, cpuColors[1].g, cpuColors[1].b, cpuColors[1].a); + _expectShaderRendersBarcode(shader, cpuColors); + }); + + test('wrong datatype', () async { + final FragmentShader shader = shaderMap[UniformVec3Slot]!; + expect( + () => shader.getUniformVec4Array('color_rgb'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Uniform size (3) for "color_rgb" is not a multiple of 4.'), + ), + ), + ); + }); }); - test('FragmentProgram getUniformArrayVec2 wrong type', () async { - expect( - () => shader.getUniformVec2Array('iVec3ArrayUniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Uniform size (9) for "iVec3ArrayUniform" is not a multiple of 2.'), + group('all uniforms', () { + late FragmentProgram program; + late List cpuColors; + final random = Random(1337); + setUpAll(() async { + program = await FragmentProgram.fromAsset('all_uniforms.frag.iplr'); + }); + + setUp(() async { + cpuColors = List.empty(growable: true); + // uFloat + cpuColors.add(Color.fromARGB(255, random.nextInt(255), 0, 0)); + // uVec2 + cpuColors.add(Color.fromARGB(255, random.nextInt(255), random.nextInt(255), 0)); + // uVec3 + cpuColors.add( + Color.fromARGB(255, random.nextInt(255), random.nextInt(255), random.nextInt(255)), + ); + // uVec4 + cpuColors.add( + Color.fromARGB( + random.nextInt(255), + random.nextInt(255), + random.nextInt(255), + random.nextInt(255), ), - ), - ); - }); + ); + for (var i = 0; i < 10; ++i) { + cpuColors.add(Color.fromARGB(255, random.nextInt(255), 0, 0)); + } - test('FragmentProgram getUniformArrayVec3', () async { - final UniformArray slots = shader.getUniformVec3Array('iVec3ArrayUniform'); - expect(slots.length, 3); - slots[0].set(1.0, 1.0, 1.0); - }); + for (var i = 0; i < 10; ++i) { + cpuColors.add(Color.fromARGB(255, random.nextInt(255), random.nextInt(255), 0)); + } - test('FragmentProgram getUniformArrayVec3 wrong type', () async { - expect( - () => shader.getUniformVec3Array('iFloatArrayUniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Uniform size (10) for "iFloatArrayUniform" is not a multiple of 3.'), - ), - ), - ); - }); + for (var i = 0; i < 10; ++i) { + cpuColors.add( + Color.fromARGB(255, random.nextInt(255), random.nextInt(255), random.nextInt(255)), + ); + } + for (var i = 0; i < 10; ++i) { + cpuColors.add( + Color.fromARGB( + random.nextInt(255), + random.nextInt(255), + random.nextInt(255), + random.nextInt(255), + ), + ); + } + }); - test('FragmentProgram getUniformArrayVec4', () async { - final UniformArray slots = shader.getUniformVec4Array('iVec4ArrayUniform'); - expect(slots.length, 3); - slots[0].set(1.0, 1.0, 1.0, 1.0); - }); + test('set using setFloat', () async { + final FragmentShader shader = program.fragmentShader(); + // uFloat + shader.setFloat(0, cpuColors[0].r); + //uVec2 + shader.setFloat(1, cpuColors[1].r); + shader.setFloat(2, cpuColors[1].g); + //uVec3 + shader.setFloat(3, cpuColors[2].r); + shader.setFloat(4, cpuColors[2].g); + shader.setFloat(5, cpuColors[2].b); + //uVec4 + shader.setFloat(6, cpuColors[3].r); + shader.setFloat(7, cpuColors[3].g); + shader.setFloat(8, cpuColors[3].b); + shader.setFloat(9, cpuColors[3].a); + + var shaderOffset = 10; + var colorOffset = 4; + + for (var i = 0; i < 10; ++i) { + shader.setFloat(shaderOffset++, cpuColors[colorOffset++].r); + } + for (var i = 0; i < 10; ++i) { + shader.setFloat(shaderOffset++, cpuColors[colorOffset].r); + shader.setFloat(shaderOffset++, cpuColors[colorOffset++].g); + } + for (var i = 0; i < 10; ++i) { + shader.setFloat(shaderOffset++, cpuColors[colorOffset].r); + shader.setFloat(shaderOffset++, cpuColors[colorOffset].g); + shader.setFloat(shaderOffset++, cpuColors[colorOffset++].b); + } + for (var i = 0; i < 10; ++i) { + shader.setFloat(shaderOffset++, cpuColors[colorOffset].r); + shader.setFloat(shaderOffset++, cpuColors[colorOffset].g); + shader.setFloat(shaderOffset++, cpuColors[colorOffset].b); + shader.setFloat(shaderOffset++, cpuColors[colorOffset++].a); + } + _expectShaderRendersBarcode(shader, cpuColors); + }); - test('FragmentProgram getUniformArrayVec4 wrong type', () async { - expect( - () => shader.getUniformVec4Array('iFloatArrayUniform'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Uniform size (10) for "iFloatArrayUniform" is not a multiple of 4.'), - ), - ), - ); + test('set using getUniform*', () async { + final FragmentShader shader = program.fragmentShader(); + shader.getUniformFloat('uFloat').set(cpuColors[0].r); + shader.getUniformVec2('uVec2').set(cpuColors[1].r, cpuColors[1].g); + shader.getUniformVec3('uVec3').set(cpuColors[2].r, cpuColors[2].g, cpuColors[2].b); + shader + .getUniformVec4('uVec4') + .set(cpuColors[3].r, cpuColors[3].g, cpuColors[3].b, cpuColors[3].a); + + final UniformArray floatArray = shader.getUniformFloatArray( + 'uFloatArray', + ); + final UniformArray vec2Array = shader.getUniformVec2Array('uVec2Array'); + final UniformArray vec3Array = shader.getUniformVec3Array('uVec3Array'); + final UniformArray vec4Array = shader.getUniformVec4Array('uVec4Array'); + + var colorOffset = 4; + + for (var i = 0; i < 10; ++i) { + floatArray[i].set(cpuColors[colorOffset++].r); + } + for (var i = 0; i < 10; ++i) { + vec2Array[i].set(cpuColors[colorOffset].r, cpuColors[colorOffset].g); + ++colorOffset; + } + for (var i = 0; i < 10; ++i) { + vec3Array[i].set( + cpuColors[colorOffset].r, + cpuColors[colorOffset].g, + cpuColors[colorOffset].b, + ); + ++colorOffset; + } + for (var i = 0; i < 10; ++i) { + vec4Array[i].set( + cpuColors[colorOffset].r, + cpuColors[colorOffset].g, + cpuColors[colorOffset].b, + cpuColors[colorOffset].a, + ); + ++colorOffset; + } + _expectShaderRendersBarcode(shader, cpuColors); + }); }); }); @@ -526,109 +787,6 @@ void main() async { shader.dispose(); }); - group('FragmentProgram getUniform*', () { - late FragmentShader shader; - - setUpAll(() async { - final FragmentProgram program = await FragmentProgram.fromAsset('uniforms.frag.iplr'); - shader = program.fragmentShader(); - }); - - _runSkiaTest('FragmentProgram uniform info', () async { - final List slots = [ - shader.getUniformFloat('iFloatUniform'), - shader.getUniformFloat('iVec2Uniform', 0), - shader.getUniformFloat('iVec2Uniform', 1), - shader.getUniformFloat('iMat2Uniform', 0), - shader.getUniformFloat('iMat2Uniform', 1), - shader.getUniformFloat('iMat2Uniform', 2), - shader.getUniformFloat('iMat2Uniform', 3), - ]; - for (var i = 0; i < slots.length; ++i) { - expect(slots[i].shaderIndex, equals(i)); - } - }); - - test('FragmentProgram getUniformFloat unknown', () async { - try { - shader.getUniformFloat('unknown'); - fail('Unreachable'); - } catch (e) { - expect(e.toString(), contains('No uniform named "unknown".')); - } - }); - - _runSkiaTest('FragmentProgram getUniformFloat offset overflow', () async { - try { - shader.getUniformFloat('iVec2Uniform', 2); - fail('Unreachable'); - } catch (e) { - expect(e.toString(), contains('Index `2` out of bounds for `iVec2Uniform`.')); - } - }); - - _runSkiaTest('FragmentProgram getUniformFloat offset underflow', () async { - try { - shader.getUniformFloat('iVec2Uniform', -1); - fail('Unreachable'); - } catch (e) { - expect(e.toString(), contains('Index `-1` out of bounds for `iVec2Uniform`.')); - } - }); - - _runSkiaTest('FragmentProgram getUniformVec2', () async { - final UniformVec2Slot slot = shader.getUniformVec2('iVec2Uniform'); - slot.set(6.0, 7.0); - }); - - _runSkiaTest('FragmentProgram getUniformVec2 wrong size', () async { - try { - shader.getUniformVec2('iVec3Uniform'); - fail('Unreachable'); - } catch (e) { - expect(e.toString(), contains('`iVec3Uniform` has size 3, not size 2.')); - } - try { - shader.getUniformVec2('iFloatUniform'); - } catch (e) { - expect(e.toString(), contains('`iFloatUniform` has size 1, not size 2.')); - } - }); - - _runSkiaTest('FragmentProgram getUniformVec3', () async { - final UniformVec3Slot slot = shader.getUniformVec3('iVec3Uniform'); - slot.set(0.8, 0.1, 0.3); - }); - - _runSkiaTest('FragmentProgram getUniformVec3 wrong size', () async { - try { - shader.getUniformVec3('iVec2Uniform'); - fail('Unreachable'); - } catch (e) { - expect(e.toString(), contains('`iVec2Uniform` has size 2, not size 3.')); - } - try { - shader.getUniformVec3('iVec4Uniform'); - } catch (e) { - expect(e.toString(), contains('`iVec4Uniform` has size 4, not size 3.')); - } - }); - - _runSkiaTest('FragmentProgram getUniformVec4', () async { - final UniformVec4Slot slot = shader.getUniformVec4('iVec4Uniform'); - slot.set(11.0, 22.0, 19.0, 96.0); - }); - - _runSkiaTest('FragmentProgram getUniformVec4 wrong size', () async { - try { - shader.getUniformVec4('iVec3Uniform'); - fail('Unreachable'); - } catch (e) { - expect(e.toString(), contains('`iVec3Uniform` has size 3, not size 4.')); - } - }); - }); - _runSkiaTest('FragmentProgram getImageSampler wrong type', () async { final FragmentProgram program = await FragmentProgram.fromAsset('uniform_ordering.frag.iplr'); final FragmentShader shader = program.fragmentShader(); @@ -695,18 +853,6 @@ void main() async { shader.dispose(); }); - test('FragmentShader shader with array uniforms renders correctly', () async { - final FragmentProgram program = await FragmentProgram.fromAsset('uniform_arrays.frag.iplr'); - - final FragmentShader shader = program.fragmentShader(); - for (var i = 0; i < 20; i++) { - shader.setFloat(i, i.toDouble()); - } - - await _expectShaderRendersGreen(shader); - shader.dispose(); - }); - test('FragmentShader shader with mat2 uniform renders correctly', () async { final FragmentProgram program = await FragmentProgram.fromAsset('uniform_mat2.frag.iplr'); @@ -752,23 +898,6 @@ void main() async { }, ); - _runImpellerTest( - 'Shader Compiler appropriately pads vec3 uniform arrays', - () async { - final FragmentProgram program = await FragmentProgram.fromAsset('vec3_uniform.frag.iplr'); - final FragmentShader shader = program.fragmentShader(); - - // Set the last vec3 in the uniform array to green. The shader will read this - // value, and if the uniforms were padded correctly will render green. - shader.setFloat(9, 0); // color_array[3].x - shader.setFloat(10, 1.0); // color_array[3].y - shader.setFloat(11, 0); // color_array[3].z - - await _expectShaderRendersGreen(shader); - }, - skip: Platform.executableArguments.contains('--impeller-backend=metal'), - ); - _runImpellerTest('ImageFilter.shader can be applied to canvas operations', () async { final FragmentProgram program = await FragmentProgram.fromAsset('filter_shader.frag.iplr'); final FragmentShader shader = program.fragmentShader(); @@ -887,6 +1016,34 @@ void _expectFragmentShadersRenderGreen(Map programs) { } } +Future _expectShaderRendersBarcode(Shader shader, List barcodeColors) async { + final ByteData renderedBytes = (await _imageByteDataFromShader( + shader: shader, + imageDimension: barcodeColors.length, + ))!; + + expect(renderedBytes.lengthInBytes % 4, 0); + final List renderedColors = List.generate(barcodeColors.length, (int xCoord) { + return Color.fromARGB( + renderedBytes.getUint8(xCoord * 4 + 3), + renderedBytes.getUint8(xCoord * 4), + renderedBytes.getUint8(xCoord * 4 + 1), + renderedBytes.getUint8(xCoord * 4 + 2), + ); + }); + + for (var i = 0; i < barcodeColors.length; ++i) { + final Color renderedColor = renderedColors[i]; + final Color expectedColor = barcodeColors[i]; + final reasonString = + 'Comparison failed on color $i. \nExpected: $expectedColor.\nActual: $renderedColor.'; + expect(renderedColor.r.clamp(-1, 1), closeTo(expectedColor.r, 0.06), reason: reasonString); + expect(renderedColor.g.clamp(-1, 1), closeTo(expectedColor.g, 0.06), reason: reasonString); + expect(renderedColor.b.clamp(-1, 1), closeTo(expectedColor.b, 0.06), reason: reasonString); + expect(renderedColor.a.clamp(-1, 1), closeTo(expectedColor.a, 0.06), reason: reasonString); + } +} + Future _expectShaderRendersColor(Shader shader, Color color) async { final ByteData renderedBytes = (await _imageByteDataFromShader( shader: shader, diff --git a/engine/src/flutter/testing/lsan_suppressions.txt b/engine/src/flutter/testing/lsan_suppressions.txt index 52ad4c64ee8e8..9089bfd34e675 100644 --- a/engine/src/flutter/testing/lsan_suppressions.txt +++ b/engine/src/flutter/testing/lsan_suppressions.txt @@ -65,3 +65,6 @@ leak:egl::* # False positives in libfontconfig. http://crbug.com/39050 leak:libfontconfig + +# False positives in D-Bus. +leak:libdbus-1.so diff --git a/examples/multiple_windows/lib/app/dialog_window_content.dart b/examples/multiple_windows/lib/app/dialog_window_content.dart index 8886ad083abd8..ee2deed927dc8 100644 --- a/examples/multiple_windows/lib/app/dialog_window_content.dart +++ b/examples/multiple_windows/lib/app/dialog_window_content.dart @@ -82,17 +82,16 @@ class DialogWindowContent extends StatelessWidget { listenable: windowManager, builder: (BuildContext context, Widget? child) { final List childViews = []; - for (final KeyedWindow window in windowManager.windows) { - if (window.parent == window.controller) { - childViews.add( - WindowContent( - controller: window.controller, - windowKey: window.key, - onDestroyed: () => windowManager.remove(window.key), - onError: () => windowManager.remove(window.key), - ), - ); - } + final childWindowList = windowManager.getWindows(parent: window); + for (final KeyedWindow childWindow in childWindowList) { + childViews.add( + WindowContent( + controller: childWindow.controller, + windowKey: childWindow.key, + onDestroyed: () => windowManager.remove(childWindow.key), + onError: () => windowManager.remove(childWindow.key), + ), + ); } return ViewCollection(views: childViews); diff --git a/examples/multiple_windows/lib/app/main_window.dart b/examples/multiple_windows/lib/app/main_window.dart index 874a893e7896e..564bafe768665 100644 --- a/examples/multiple_windows/lib/app/main_window.dart +++ b/examples/multiple_windows/lib/app/main_window.dart @@ -15,39 +15,66 @@ import 'regular_window_edit_dialog.dart'; import 'dialog_window_edit_dialog.dart'; import 'tooltip_window_edit_dialog.dart'; import 'tooltip_button.dart'; +import 'window_content.dart'; class MainWindow extends StatelessWidget { - const MainWindow({super.key}); + const MainWindow({super.key, required this.controller}); + + final RegularWindowController controller; @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Multi Window Reference App')), - body: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 60, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: _WindowsTable(), + final WindowManager windowManager = WindowManagerAccessor.of(context); + + return ViewAnchor( + view: ListenableBuilder( + listenable: windowManager, + builder: (BuildContext context, Widget? child) { + final List childViews = []; + for (final KeyedWindow window in windowManager.getWindows( + parent: controller, + )) { + childViews.add( + WindowContent( + controller: window.controller, + windowKey: window.key, + onDestroyed: () => windowManager.remove(window.key), + onError: () => windowManager.remove(window.key), + ), + ); + } + + return ViewCollection(views: childViews); + }, + ), + child: Scaffold( + appBar: AppBar(title: const Text('Multi Window Reference App')), + body: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 60, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: _WindowsTable(), + ), ), - ), - ], + ], + ), ), - ), - Expanded( - flex: 40, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [Expanded(child: _WindowCreatorCard())], + Expanded( + flex: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [Expanded(child: _WindowCreatorCard())], + ), ), - ), - ], + ], + ), ), ); } @@ -167,60 +194,84 @@ class _WindowCreatorCard extends StatelessWidget { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - OutlinedButton( - onPressed: () { - final UniqueKey key = UniqueKey(); - windowManager.add( - KeyedWindow( - key: key, - controller: RegularWindowController( - delegate: CallbackRegularWindowControllerDelegate( - onDestroyed: () => windowManager.remove(key), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OutlinedButton( + onPressed: () { + final UniqueKey key = UniqueKey(); + windowManager.add( + KeyedWindow( + key: key, + controller: RegularWindowController( + delegate: CallbackRegularWindowControllerDelegate( + onDestroyed: () => windowManager.remove(key), + ), + title: 'Regular', + preferredSize: windowSettings.regularSize, + ), ), - title: 'Regular', - preferredSize: windowSettings.regularSize, - ), - ), - ); - }, - child: const Text('Regular'), - ), - const SizedBox(height: 8), - TooltipButton(parentController: windowController), - const SizedBox(height: 8), - OutlinedButton( - onPressed: () { - final UniqueKey key = UniqueKey(); - windowManager.add( - KeyedWindow( - key: key, - controller: DialogWindowController( - delegate: CallbackDialogWindowControllerDelegate( - onDestroyed: () => windowManager.remove(key), + ); + }, + child: const Text('Regular'), + ), + const SizedBox(height: 8), + TooltipButton(parentController: windowController), + const SizedBox(height: 8), + OutlinedButton( + onPressed: () { + final UniqueKey key = UniqueKey(); + windowManager.add( + KeyedWindow( + key: key, + controller: DialogWindowController( + delegate: CallbackDialogWindowControllerDelegate( + onDestroyed: () => windowManager.remove(key), + ), + title: 'Modeless Dialog', + preferredSize: windowSettings.dialogSize, + ), ), - title: 'Modeless Dialog', - preferredSize: windowSettings.dialogSize, - ), + ); + }, + child: const Text('Modeless Dialog'), + ), + const SizedBox(height: 8), + OutlinedButton( + onPressed: () { + final UniqueKey key = UniqueKey(); + windowManager.add( + KeyedWindow( + key: key, + controller: DialogWindowController( + delegate: CallbackDialogWindowControllerDelegate( + onDestroyed: () => windowManager.remove(key), + ), + title: 'Modal Dialog', + preferredSize: windowSettings.dialogSize, + parent: windowController, + ), + ), + ); + }, + child: const Text('Modal Dialog'), + ), + const SizedBox(height: 8), + Container( + alignment: Alignment.bottomRight, + child: TextButton( + child: const Text('SETTINGS'), + onPressed: () { + showWindowSettingsDialog(context, windowSettings); + }, ), - ); - }, - child: const Text('Modeless Dialog'), + ), + const SizedBox(width: 8), + ], ), - const SizedBox(height: 8), - Container( - alignment: Alignment.bottomRight, - child: TextButton( - child: const Text('SETTINGS'), - onPressed: () { - showWindowSettingsDialog(context, windowSettings); - }, - ), - ), - const SizedBox(width: 8), - ], + ), ), ], ), diff --git a/examples/multiple_windows/lib/app/models.dart b/examples/multiple_windows/lib/app/models.dart index 8a3ce39058a63..126e7a34cdf72 100644 --- a/examples/multiple_windows/lib/app/models.dart +++ b/examples/multiple_windows/lib/app/models.dart @@ -11,13 +11,24 @@ import 'package:flutter/src/widgets/_window_positioner.dart'; class KeyedWindow { KeyedWindow({ - this.parent, this.isMainWindow = false, required this.key, required this.controller, }); - final BaseWindowController? parent; + BaseWindowController? get parent { + switch (controller) { + case RegularWindowController(): + return null; + case DialogWindowController dialogController: + return dialogController.parent; + case TooltipWindowController tooltipController: + return tooltipController.parent; + default: + throw Exception('Unknown controller type'); + } + } + final bool isMainWindow; final UniqueKey key; final BaseWindowController controller; @@ -44,6 +55,10 @@ class WindowManager extends ChangeNotifier { _windows.removeWhere((KeyedWindow window) => window.key == key); notifyListeners(); } + + Iterable getWindows({required BaseWindowController? parent}) { + return _windows.where((KeyedWindow window) => window.parent == parent); + } } /// Provides access to the [WindowManager] from the widget tree. diff --git a/examples/multiple_windows/lib/app/regular_window_content.dart b/examples/multiple_windows/lib/app/regular_window_content.dart index ced02a97bdb19..11da41d51a9c4 100644 --- a/examples/multiple_windows/lib/app/regular_window_content.dart +++ b/examples/multiple_windows/lib/app/regular_window_content.dart @@ -110,17 +110,17 @@ class RegularWindowContent extends StatelessWidget { listenable: windowManager, builder: (BuildContext context, Widget? child) { final List childViews = []; - for (final KeyedWindow window in windowManager.windows) { - if (window.parent == window.controller) { - childViews.add( - WindowContent( - controller: window.controller, - windowKey: window.key, - onDestroyed: () => windowManager.remove(window.key), - onError: () => windowManager.remove(window.key), - ), - ); - } + for (final KeyedWindow childWindow in windowManager.getWindows( + parent: window, + )) { + childViews.add( + WindowContent( + controller: childWindow.controller, + windowKey: childWindow.key, + onDestroyed: () => windowManager.remove(childWindow.key), + onError: () => windowManager.remove(childWindow.key), + ), + ); } return ViewCollection(views: childViews); diff --git a/examples/multiple_windows/lib/main.dart b/examples/multiple_windows/lib/main.dart index b8a9d1ec6871c..6ff652421f8f5 100644 --- a/examples/multiple_windows/lib/main.dart +++ b/examples/multiple_windows/lib/main.dart @@ -66,7 +66,7 @@ class _MultiWindowAppState extends State { Widget build(BuildContext context) { final Widget mainWindowWidget = RegularWindow( controller: controller, - child: MaterialApp(home: MainWindow()), + child: MaterialApp(home: MainWindow(controller: controller)), ); return WindowManagerAccessor( windowManager: windowManager, @@ -76,12 +76,10 @@ class _MultiWindowAppState extends State { listenable: windowManager, builder: (BuildContext context, Widget? child) { final List childViews = [mainWindowWidget]; - for (final KeyedWindow window in windowManager.windows) { - // This check renders windows that are not nested below another window as - // a child window (e.g. a popup as a child of another window) in addition - // to the main window, which is special as it is the one that is currently - // being rendered. - if (window.parent == null && !window.isMainWindow) { + for (final KeyedWindow window in windowManager.getWindows( + parent: null, + )) { + if (!window.isMainWindow) { childViews.add( WindowContent( controller: window.controller, diff --git a/examples/multiple_windows/test/multiple_windows_test.dart b/examples/multiple_windows/test/multiple_windows_test.dart index 20975677d652b..15d3a8efa1bb0 100644 --- a/examples/multiple_windows/test/multiple_windows_test.dart +++ b/examples/multiple_windows/test/multiple_windows_test.dart @@ -64,6 +64,20 @@ See: https://github.com/flutter/flutter/issues/30701. expect(find.widgetWithText(AppBar, 'Dialog'), findsOneWidget); }); + testWidgets('Can create a modal dialog of the main window', ( + WidgetTester tester, + ) async { + multiple_windows.main(); + await tester.pump(); // triggers a frame + + final toTap = find.widgetWithText(OutlinedButton, 'Modal Dialog'); + expect(toTap, findsOneWidget); + await tester.tap(toTap); + await tester.pump(); + + expect(find.widgetWithText(AppBar, 'Dialog'), findsOneWidget); + }); + testWidgets('Can create a modal dialog of a regular window', ( WidgetTester tester, ) async { @@ -112,4 +126,16 @@ See: https://github.com/flutter/flutter/issues/30701. expect(find.widgetWithText(AppBar, 'Dialog'), findsNothing); }); + + testWidgets('Can create a tooltip window', (WidgetTester tester) async { + multiple_windows.main(); + await tester.pump(); // triggers a frame + + final toTap = find.widgetWithText(OutlinedButton, 'Show Tooltip'); + expect(toTap, findsOneWidget); + await tester.tap(toTap); + await tester.pump(); + + expect(find.text('Tooltip Window'), findsOneWidget); + }); } diff --git a/packages/flutter/lib/src/material/dropdown_menu_form_field.dart b/packages/flutter/lib/src/material/dropdown_menu_form_field.dart index cf47b8e029093..de18fc6a4ca56 100644 --- a/packages/flutter/lib/src/material/dropdown_menu_form_field.dart +++ b/packages/flutter/lib/src/material/dropdown_menu_form_field.dart @@ -40,6 +40,8 @@ class DropdownMenuFormField extends FormField { double? menuHeight, Widget? leadingIcon, Widget? trailingIcon, + bool showTrailingIcon = true, + FocusNode? trailingIconFocusNode, Widget? label, String? hintText, String? helperText, @@ -58,6 +60,7 @@ class DropdownMenuFormField extends FormField { this.onSelected, FocusNode? focusNode, bool? requestFocusOnTap, + bool selectOnly = false, EdgeInsetsGeometry? expandedInsets, Offset? alignmentOffset, FilterCallback? filterCallback, @@ -67,6 +70,8 @@ class DropdownMenuFormField extends FormField { DropdownMenuCloseBehavior closeBehavior = DropdownMenuCloseBehavior.all, int maxLines = 1, TextInputAction? textInputAction, + double? cursorHeight, + MenuController? menuController, super.restorationId, super.onSaved, AutovalidateMode autovalidateMode = AutovalidateMode.disabled, @@ -110,6 +115,8 @@ class DropdownMenuFormField extends FormField { menuHeight: menuHeight, leadingIcon: leadingIcon, trailingIcon: trailingIcon, + showTrailingIcon: showTrailingIcon, + trailingIconFocusNode: trailingIconFocusNode, selectedTrailingIcon: selectedTrailingIcon, enableFilter: enableFilter, enableSearch: enableSearch, @@ -124,6 +131,7 @@ class DropdownMenuFormField extends FormField { onSelected: field.didChange, focusNode: focusNode, requestFocusOnTap: requestFocusOnTap, + selectOnly: selectOnly, expandedInsets: expandedInsets, alignmentOffset: alignmentOffset, filterCallback: filterCallback, @@ -133,6 +141,8 @@ class DropdownMenuFormField extends FormField { dropdownMenuEntries: dropdownMenuEntries, maxLines: maxLines, textInputAction: textInputAction, + cursorHeight: cursorHeight, + menuController: menuController, ), ); }, diff --git a/packages/flutter/lib/src/rendering/decorated_sliver.dart b/packages/flutter/lib/src/rendering/decorated_sliver.dart index 78d32ae9e8faf..474c35e395cad 100644 --- a/packages/flutter/lib/src/rendering/decorated_sliver.dart +++ b/packages/flutter/lib/src/rendering/decorated_sliver.dart @@ -5,7 +5,6 @@ import 'object.dart'; import 'proxy_box.dart'; import 'proxy_sliver.dart'; -import 'sliver.dart'; /// Paints a [Decoration] either before or after its child paints. /// @@ -96,26 +95,12 @@ class RenderDecoratedSliver extends RenderProxySliver { if (child == null || !child!.geometry!.visible) { return; } - // In the case where the child sliver has infinite scroll extent, the decoration - // should only extend down to the bottom cache extent. - final double cappedMainAxisExtent = child!.geometry!.scrollExtent.isInfinite - ? constraints.scrollOffset + child!.geometry!.cacheExtent + constraints.cacheOrigin - : child!.geometry!.scrollExtent; - final (Size childSize, Offset scrollOffset) = switch (constraints.axis) { - Axis.horizontal => ( - Size(cappedMainAxisExtent, constraints.crossAxisExtent), - Offset(-constraints.scrollOffset, 0.0), - ), - Axis.vertical => ( - Size(constraints.crossAxisExtent, cappedMainAxisExtent), - Offset(0.0, -constraints.scrollOffset), - ), - }; - offset += (child!.parentData! as SliverPhysicalParentData).paintOffset; + + final Rect paintRect = getMaxPaintRect(); void paintDecoration() => _painter!.paint( context.canvas, - offset + scrollOffset, - configuration.copyWith(size: childSize), + offset + paintRect.topLeft, + configuration.copyWith(size: paintRect.size), ); switch (position) { case DecorationPosition.background: @@ -126,4 +111,11 @@ class RenderDecoratedSliver extends RenderProxySliver { paintDecoration(); } } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(_decoration.toDiagnosticsNode(name: 'decoration')); + properties.add(DiagnosticsProperty('configuration', configuration)); + } } diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index b2104aca183c7..e60f585522ab6 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -26,6 +26,7 @@ import 'basic.dart'; import 'framework.dart'; import 'layout_builder.dart'; import 'lookup_boundary.dart'; +import 'media_query.dart'; import 'ticker_provider.dart'; /// The signature of the widget builder callback used in @@ -2029,11 +2030,22 @@ class _OverlayPortalState extends State { child: Semantics(traversalParentIdentifier: this, child: widget.child), ); } + + final _OverlayEntryLocation overlayLocation = _getLocation(zOrderIndex, widget.overlayLocation); + final MediaQueryData overlayData = MediaQuery.of(overlayLocation._childModel.context); + final MediaQueryData data = MediaQuery.of(context).copyWith( + padding: overlayData.padding, + viewInsets: overlayData.viewInsets, + viewPadding: overlayData.viewPadding, + ); return _OverlayPortal( - overlayLocation: _getLocation(zOrderIndex, widget.overlayLocation), + overlayLocation: overlayLocation, overlayChild: _DeferredLayout( childIdentifier: this, - child: Builder(builder: widget.overlayChildBuilder), + child: MediaQuery( + data: data, + child: Builder(builder: widget.overlayChildBuilder), + ), ), child: Semantics(traversalParentIdentifier: this, child: widget.child), ); diff --git a/packages/flutter/test/cupertino/slider_test.dart b/packages/flutter/test/cupertino/slider_test.dart index 48c3e55c72080..3e26cb8487fac 100644 --- a/packages/flutter/test/cupertino/slider_test.dart +++ b/packages/flutter/test/cupertino/slider_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -42,17 +41,15 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, ), ); }, @@ -80,17 +77,15 @@ void main() { textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, ), ); }, @@ -121,20 +116,18 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - onChangeStart: (double value) { - numberOfTimesOnChangeStartIsCalled++; - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + onChangeStart: (double value) { + numberOfTimesOnChangeStartIsCalled++; + }, ), ); }, @@ -166,20 +159,18 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - onChangeEnd: (double value) { - numberOfTimesOnChangeEndIsCalled++; - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + onChangeEnd: (double value) { + numberOfTimesOnChangeEndIsCalled++; + }, ), ); }, @@ -210,23 +201,21 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - onChangeStart: (double value) { - startValue = value; - }, - onChangeEnd: (double value) { - endValue = value; - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + onChangeStart: (double value) { + startValue = value; + }, + onChangeEnd: (double value) { + endValue = value; + }, ), ); }, @@ -273,17 +262,15 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, ), ); }, @@ -359,17 +346,15 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, ), ); }, @@ -402,27 +387,25 @@ void main() { textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - onChangeStart: (double value) { - setState(() { - startValue = value; - }); - }, - onChangeEnd: (double value) { - setState(() { - endValue = value; - }); - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + onChangeStart: (double value) { + setState(() { + startValue = value; + }); + }, + onChangeEnd: (double value) { + setState(() { + endValue = value; + }); + }, ), ); }, @@ -792,17 +775,15 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Material( - child: Center( - child: CupertinoSlider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - ), + return Center( + child: CupertinoSlider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, ), ); }, diff --git a/packages/flutter/test/material/backdrop_filter_test.dart b/packages/flutter/test/material/backdrop_filter_test.dart new file mode 100644 index 0000000000000..ec53fca0fd1cb --- /dev/null +++ b/packages/flutter/test/material/backdrop_filter_test.dart @@ -0,0 +1,191 @@ +// Copyright 2014 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. + +@Tags(['reduced-test-set']) +library; + +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets("Material2 - BackdropFilter's cull rect does not shrink", ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Text('0 0 ' * 10000), + Center( + // ClipRect needed for filtering the 200x200 area instead of the + // whole screen. + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + alignment: Alignment.center, + width: 200.0, + height: 200.0, + child: const Text('Hello World'), + ), + ), + ), + ), + ], + ), + ), + ), + ); + await expectLater( + find.byType(RepaintBoundary).first, + matchesGoldenFile('m2_backdrop_filter_test.cull_rect.png'), + ); + }); + + testWidgets("Material3 - BackdropFilter's cull rect does not shrink", ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Text('0 0 ' * 10000), + Center( + // ClipRect needed for filtering the 200x200 area instead of the + // whole screen. + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + alignment: Alignment.center, + width: 200.0, + height: 200.0, + child: const Text('Hello World'), + ), + ), + ), + ), + ], + ), + ), + ), + ); + await expectLater( + find.byType(RepaintBoundary).first, + matchesGoldenFile('m3_backdrop_filter_test.cull_rect.png'), + ); + }); + + testWidgets('Material2 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + body: Opacity( + opacity: 0.9, + child: Stack( + fit: StackFit.expand, + children: [ + Text('0 0 ' * 10000), + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // ClipRect needed for filtering the 200x200 area instead of the + // whole screen. + children: [ + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + alignment: Alignment.center, + width: 200.0, + height: 200.0, + color: Colors.yellow.withAlpha(0x7), + ), + ), + ), + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + blendMode: BlendMode.src, + child: Container( + alignment: Alignment.center, + width: 200.0, + height: 200.0, + color: Colors.yellow.withAlpha(0x7), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + await expectLater( + find.byType(RepaintBoundary).first, + matchesGoldenFile('m2_backdrop_filter_test.saveLayer.blendMode.png'), + ); + }); + + testWidgets('Material3 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Opacity( + opacity: 0.9, + child: Stack( + fit: StackFit.expand, + children: [ + Text('0 0 ' * 10000), + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // ClipRect needed for filtering the 200x200 area instead of the + // whole screen. + children: [ + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + alignment: Alignment.center, + width: 200.0, + height: 200.0, + color: Colors.yellow.withAlpha(0x7), + ), + ), + ), + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + blendMode: BlendMode.src, + child: Container( + alignment: Alignment.center, + width: 200.0, + height: 200.0, + color: Colors.yellow.withAlpha(0x7), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + await expectLater( + find.byType(RepaintBoundary).first, + matchesGoldenFile('m3_backdrop_filter_test.saveLayer.blendMode.png'), + ); + }); +} diff --git a/packages/flutter/test/material/dropdown_menu_form_field_test.dart b/packages/flutter/test/material/dropdown_menu_form_field_test.dart index 224c084fbcc12..f0edb286d64a1 100644 --- a/packages/flutter/test/material/dropdown_menu_form_field_test.dart +++ b/packages/flutter/test/material/dropdown_menu_form_field_test.dart @@ -202,6 +202,64 @@ void main() { expect(dropdownMenu.trailingIcon, trailingIcon); }); + testWidgets('Passes showTrailingIcon to underlying DropdownMenu', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: DropdownMenuFormField(dropdownMenuEntries: menuEntries)), + ), + ); + + // Check default value. + DropdownMenu dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.showTrailingIcon, true); + + const showTrailingIcon = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenuFormField( + showTrailingIcon: showTrailingIcon, + dropdownMenuEntries: menuEntries, + ), + ), + ), + ); + + dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.showTrailingIcon, showTrailingIcon); + }); + + testWidgets('Passes trailingIconFocusNode to underlying DropdownMenu', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: DropdownMenuFormField(dropdownMenuEntries: menuEntries)), + ), + ); + + // Check default value. + DropdownMenu dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.trailingIconFocusNode, null); + + final trailingIconFocusNode = FocusNode(); + addTearDown(trailingIconFocusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenuFormField( + trailingIconFocusNode: trailingIconFocusNode, + dropdownMenuEntries: menuEntries, + ), + ), + ), + ); + + dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.trailingIconFocusNode, trailingIconFocusNode); + }); + testWidgets('Passes label to underlying InputDecoration', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -620,6 +678,33 @@ void main() { expect(dropdownMenu.requestFocusOnTap, requestFocusOnTap); }); + testWidgets('Passes selectOnly to underlying DropdownMenu', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: DropdownMenuFormField(dropdownMenuEntries: menuEntries)), + ), + ); + + // Check default value. + DropdownMenu dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.selectOnly, false); + + const selectOnly = true; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenuFormField( + selectOnly: selectOnly, + dropdownMenuEntries: menuEntries, + ), + ), + ), + ); + + dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.selectOnly, selectOnly); + }); + testWidgets('Passes expandedInsets to underlying DropdownMenu', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -851,6 +936,60 @@ void main() { expect(dropdownMenu.textInputAction, textInputAction); }); + testWidgets('Passes cursorHeight to underlying DropdownMenu', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: DropdownMenuFormField(dropdownMenuEntries: menuEntries)), + ), + ); + + // Check default value. + DropdownMenu dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.cursorHeight, null); + + const cursorHeight = 4.0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenuFormField( + cursorHeight: cursorHeight, + dropdownMenuEntries: menuEntries, + ), + ), + ), + ); + + dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.cursorHeight, cursorHeight); + }); + + testWidgets('Passes menuController to underlying DropdownMenu', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: DropdownMenuFormField(dropdownMenuEntries: menuEntries)), + ), + ); + + // Check default value. + DropdownMenu dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.menuController, null); + + final menuController = MenuController(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenuFormField( + menuController: menuController, + dropdownMenuEntries: menuEntries, + ), + ), + ), + ); + + dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.menuController, menuController); + }); + testWidgets('Passes restorationId to underlying DropdownMenu', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/backdrop_filter_test.dart b/packages/flutter/test/widgets/backdrop_filter_test.dart index c393af6d1fd7d..8a9b8e9e3e9cf 100644 --- a/packages/flutter/test/widgets/backdrop_filter_test.dart +++ b/packages/flutter/test/widgets/backdrop_filter_test.dart @@ -9,8 +9,8 @@ library; import 'dart:ui'; -import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -18,34 +18,33 @@ void main() { final backdropKey = BackdropKey(); Widget build({required bool enableKeys}) { - return MaterialApp( - home: Scaffold( - body: ListView( - children: [ - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), - backdropGroupKey: enableKeys ? backdropKey : null, - child: Container( - color: Colors.black.withAlpha(40), - height: 200, - child: const Text('Item 1'), - ), + return Directionality( + textDirection: TextDirection.ltr, + child: ListView( + children: [ + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), + backdropGroupKey: enableKeys ? backdropKey : null, + child: Container( + color: const Color(0x28000000), + height: 200, + child: const Text('Item 1'), ), ), - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), - backdropGroupKey: enableKeys ? backdropKey : null, - child: Container( - color: Colors.black.withAlpha(40), - height: 200, - child: const Text('Item 1'), - ), + ), + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), + backdropGroupKey: enableKeys ? backdropKey : null, + child: Container( + color: const Color(0x28000000), + height: 200, + child: const Text('Item 1'), ), ), - ], - ), + ), + ], ), ); } @@ -71,33 +70,32 @@ void main() { WidgetTester tester, ) async { Widget build() { - return MaterialApp( - home: Scaffold( - body: BackdropGroup( - child: ListView( - children: [ - ClipRect( - child: BackdropFilter.grouped( - filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), - child: Container( - color: Colors.black.withAlpha(40), - height: 200, - child: const Text('Item 1'), - ), + return Directionality( + textDirection: TextDirection.ltr, + child: BackdropGroup( + child: ListView( + children: [ + ClipRect( + child: BackdropFilter.grouped( + filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), + child: Container( + color: const Color(0x28000000), + height: 200, + child: const Text('Item 1'), ), ), - ClipRect( - child: BackdropFilter.grouped( - filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), - child: Container( - color: Colors.black.withAlpha(40), - height: 200, - child: const Text('Item 1'), - ), + ), + ClipRect( + child: BackdropFilter.grouped( + filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), + child: Container( + color: const Color(0x28000000), + height: 200, + child: const Text('Item 1'), ), ), - ], - ), + ), + ], ), ), ); @@ -112,182 +110,4 @@ void main() { expect(layers.length, 2); expect(layers[0].backdropKey, layers[1].backdropKey); }); - - testWidgets("Material2 - BackdropFilter's cull rect does not shrink", ( - WidgetTester tester, - ) async { - await tester.pumpWidget( - MaterialApp( - theme: ThemeData(useMaterial3: false), - home: Scaffold( - body: Stack( - fit: StackFit.expand, - children: [ - Text('0 0 ' * 10000), - Center( - // ClipRect needed for filtering the 200x200 area instead of the - // whole screen. - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - alignment: Alignment.center, - width: 200.0, - height: 200.0, - child: const Text('Hello World'), - ), - ), - ), - ), - ], - ), - ), - ), - ); - await expectLater( - find.byType(RepaintBoundary).first, - matchesGoldenFile('m2_backdrop_filter_test.cull_rect.png'), - ); - }); - - testWidgets("Material3 - BackdropFilter's cull rect does not shrink", ( - WidgetTester tester, - ) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Stack( - fit: StackFit.expand, - children: [ - Text('0 0 ' * 10000), - Center( - // ClipRect needed for filtering the 200x200 area instead of the - // whole screen. - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - alignment: Alignment.center, - width: 200.0, - height: 200.0, - child: const Text('Hello World'), - ), - ), - ), - ), - ], - ), - ), - ), - ); - await expectLater( - find.byType(RepaintBoundary).first, - matchesGoldenFile('m3_backdrop_filter_test.cull_rect.png'), - ); - }); - - testWidgets('Material2 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - theme: ThemeData(useMaterial3: false), - home: Scaffold( - body: Opacity( - opacity: 0.9, - child: Stack( - fit: StackFit.expand, - children: [ - Text('0 0 ' * 10000), - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - // ClipRect needed for filtering the 200x200 area instead of the - // whole screen. - children: [ - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - alignment: Alignment.center, - width: 200.0, - height: 200.0, - color: Colors.yellow.withAlpha(0x7), - ), - ), - ), - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - blendMode: BlendMode.src, - child: Container( - alignment: Alignment.center, - width: 200.0, - height: 200.0, - color: Colors.yellow.withAlpha(0x7), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); - await expectLater( - find.byType(RepaintBoundary).first, - matchesGoldenFile('m2_backdrop_filter_test.saveLayer.blendMode.png'), - ); - }); - - testWidgets('Material3 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Opacity( - opacity: 0.9, - child: Stack( - fit: StackFit.expand, - children: [ - Text('0 0 ' * 10000), - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - // ClipRect needed for filtering the 200x200 area instead of the - // whole screen. - children: [ - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - alignment: Alignment.center, - width: 200.0, - height: 200.0, - color: Colors.yellow.withAlpha(0x7), - ), - ), - ), - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - blendMode: BlendMode.src, - child: Container( - alignment: Alignment.center, - width: 200.0, - height: 200.0, - color: Colors.yellow.withAlpha(0x7), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); - await expectLater( - find.byType(RepaintBoundary).first, - matchesGoldenFile('m3_backdrop_filter_test.saveLayer.blendMode.png'), - ); - }); } diff --git a/packages/flutter/test/widgets/banner_test.dart b/packages/flutter/test/widgets/banner_test.dart index 8659591809d12..75ba8802a6703 100644 --- a/packages/flutter/test/widgets/banner_test.dart +++ b/packages/flutter/test/widgets/banner_test.dart @@ -4,10 +4,12 @@ import 'dart:math' as math; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; +import 'widgets_app_tester.dart'; + class TestCanvas implements Canvas { final List invocations = []; @@ -276,9 +278,9 @@ void main() { debugDisableShadows = true; }); - testWidgets('Banner widget in MaterialApp', (WidgetTester tester) async { + testWidgets('Banner widget in WidgetsApp', (WidgetTester tester) async { debugDisableShadows = false; - await tester.pumpWidget(const MaterialApp(home: Placeholder())); + await tester.pumpWidget(const TestWidgetsApp(home: Placeholder())); expect( find.byType(CheckedModeBanner), paints diff --git a/packages/flutter/test/widgets/basic_test.dart b/packages/flutter/test/widgets/basic_test.dart index ddb5be3048dc9..0c53426881f3a 100644 --- a/packages/flutter/test/widgets/basic_test.dart +++ b/packages/flutter/test/widgets/basic_test.dart @@ -13,11 +13,13 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'semantics_tester.dart'; +import 'utils.dart'; +import 'widgets_app_tester.dart'; void main() { group('RawImage', () { @@ -59,7 +61,7 @@ void main() { const double width = 1; const double height = 1; const scale = 2.0; - const Color color = Colors.black; + const color = Color(0xFF000000); const Animation opacity = AlwaysStoppedAnimation(0.0); const BlendMode colorBlendMode = BlendMode.difference; const BoxFit fit = BoxFit.contain; @@ -333,33 +335,32 @@ void main() { testWidgets('Semantics can set attributed Text', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key, - attributedLabel: AttributedString( - 'label', - attributes: [ - SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), - ], - ), - attributedValue: AttributedString( - 'value', - attributes: [ - LocaleStringAttribute( - range: const TextRange(start: 0, end: 5), - locale: const Locale('en', 'MX'), - ), - ], - ), - attributedHint: AttributedString( - 'hint', - attributes: [ - SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), - ], - ), - child: const Placeholder(), + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( + key: key, + attributedLabel: AttributedString( + 'label', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + attributedValue: AttributedString( + 'value', + attributes: [ + LocaleStringAttribute( + range: const TextRange(start: 0, end: 5), + locale: const Locale('en', 'MX'), + ), + ], ), + attributedHint: AttributedString( + 'hint', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), + ], + ), + child: const Placeholder(), ), ), ); @@ -388,18 +389,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics( - key: key2, - role: SemanticsRole.alertDialog, - child: const Placeholder(), - ), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, role: SemanticsRole.alertDialog, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -413,14 +406,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics(key: key2, textField: true, child: const Placeholder()), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, textField: true, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -434,14 +423,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics(key: key2, link: true, child: const Placeholder()), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, link: true, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -455,18 +440,14 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics( - key: key2, - scopesRoute: true, - explicitChildNodes: true, - child: const Placeholder(), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics( + key: key2, + scopesRoute: true, + explicitChildNodes: true, + child: const Placeholder(), ), ), ); @@ -481,14 +462,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics(key: key2, header: true, child: const Placeholder()), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, header: true, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -506,14 +483,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics(key: key2, image: true, child: const Placeholder()), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, image: true, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -527,14 +500,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics(key: key2, slider: true, child: const Placeholder()), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, slider: true, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -548,14 +517,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics(key: key2, keyboardKey: true, child: const Placeholder()), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, keyboardKey: true, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -569,14 +534,10 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.dialog, - child: Semantics(key: key2, slider: true, child: const Placeholder()), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.dialog, + child: Semantics(key: key2, slider: true, child: const Placeholder()), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -589,15 +550,7 @@ void main() { testWidgets('Semantics can set controls visibility of nodes', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key, - controlsNodes: const {'abc'}, - child: const Placeholder(), - ), - ), - ), + Semantics(key: key, controlsNodes: const {'abc'}, child: const Placeholder()), ); final SemanticsNode node = tester.getSemantics(find.byKey(key)); final SemanticsData data = node.getSemanticsData(); @@ -608,17 +561,10 @@ void main() { testWidgets('Semantics can set controls visibility of nodes', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key, - controlsNodes: const {'abc', 'ghi'}, - child: Semantics( - controlsNodes: const {'abc', 'def'}, - child: const Placeholder(), - ), - ), - ), + Semantics( + key: key, + controlsNodes: const {'abc', 'ghi'}, + child: Semantics(controlsNodes: const {'abc', 'def'}, child: const Placeholder()), ), ); final SemanticsNode node = tester.getSemantics(find.byKey(key)); @@ -630,14 +576,10 @@ void main() { testWidgets('Semantics can set semantics input type', (WidgetTester tester) async { final key1 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - inputType: SemanticsInputType.phone, - child: const SizedBox(width: 10, height: 10), - ), - ), + Semantics( + key: key1, + inputType: SemanticsInputType.phone, + child: const SizedBox(width: 10, height: 10), ), ); final SemanticsNode node1 = tester.getSemantics(find.byKey(key1)); @@ -647,11 +589,7 @@ void main() { testWidgets('Semantics can set alert rule', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics(key: key, role: SemanticsRole.alert, child: const Placeholder()), - ), - ), + Semantics(key: key, role: SemanticsRole.alert, child: const Placeholder()), ); final SemanticsNode node = tester.getSemantics(find.byKey(key)); final SemanticsData data = node.getSemanticsData(); @@ -661,11 +599,7 @@ void main() { testWidgets('Semantics can set status rule', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics(key: key, role: SemanticsRole.status, child: const Placeholder()), - ), - ), + Semantics(key: key, role: SemanticsRole.status, child: const Placeholder()), ); final SemanticsNode node = tester.getSemantics(find.byKey(key)); final SemanticsData data = node.getSemanticsData(); @@ -675,10 +609,23 @@ void main() { testWidgets('Semantics can merge attributed strings', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key, + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( + key: key, + attributedLabel: AttributedString( + 'label', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + attributedHint: AttributedString( + 'hint', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), + ], + ), + child: Semantics( attributedLabel: AttributedString( 'label', attributes: [ @@ -691,21 +638,7 @@ void main() { SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), ], ), - child: Semantics( - attributedLabel: AttributedString( - 'label', - attributes: [ - SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), - ], - ), - attributedHint: AttributedString( - 'hint', - attributes: [ - SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), - ], - ), - child: const Placeholder(), - ), + child: const Placeholder(), ), ), ), @@ -731,19 +664,15 @@ void main() { final key1 = UniqueKey(); final key2 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key1, - role: SemanticsRole.list, - container: true, - child: Semantics( - key: key2, - role: SemanticsRole.listItem, - container: true, - child: const Placeholder(), - ), - ), + Semantics( + key: key1, + role: SemanticsRole.list, + container: true, + child: Semantics( + key: key2, + role: SemanticsRole.listItem, + container: true, + child: const Placeholder(), ), ), ); @@ -756,13 +685,7 @@ void main() { testWidgets('Semantics can use form', (WidgetTester tester) async { final key1 = UniqueKey(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics(key: key1, role: SemanticsRole.form, container: true), - ), - ), - ); + await tester.pumpWidget(Semantics(key: key1, role: SemanticsRole.form, container: true)); final SemanticsNode formNode = tester.getSemantics(find.byKey(key1)); expect(formNode.role, SemanticsRole.form); @@ -773,27 +696,26 @@ void main() { ) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - key: key, - attributedLabel: AttributedString( - 'label1', - attributes: [ - SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), - ], - ), + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( + key: key, + attributedLabel: AttributedString( + 'label1', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + child: Semantics( + label: 'label2', child: Semantics( - label: 'label2', - child: Semantics( - attributedLabel: AttributedString( - 'label3', - attributes: [ - SpellOutStringAttribute(range: const TextRange(start: 1, end: 3)), - ], - ), - child: const Placeholder(), + attributedLabel: AttributedString( + 'label3', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 1, end: 3)), + ], ), + child: const Placeholder(), ), ), ), @@ -812,8 +734,9 @@ void main() { 'Semantics with attributedValue should be recognized as containing text and not fail', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Semantics( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( attributedValue: AttributedString('test value'), child: const Placeholder(), ), @@ -827,8 +750,9 @@ void main() { 'Semantics with attributedDecreasedValue should be recognized as containing text and not fail', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Semantics( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( attributedDecreasedValue: AttributedString('test value'), child: const Placeholder(), ), @@ -842,8 +766,9 @@ void main() { 'Semantics with attributedIncreasedValue should be recognized as containing text and not fail', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Semantics( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( attributedIncreasedValue: AttributedString('test value'), child: const Placeholder(), ), @@ -857,8 +782,9 @@ void main() { 'Semantics with decreasedValue should be recognized as containing text and not fail', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Semantics(decreasedValue: 'test value', child: const Placeholder()), + Directionality( + textDirection: TextDirection.ltr, + child: Semantics(decreasedValue: 'test value', child: const Placeholder()), ), ); expect(tester.takeException(), isNull); @@ -869,8 +795,9 @@ void main() { 'Semantics with increasedValue should be recognized as containing text and not fail', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Semantics(increasedValue: 'test value', child: const Placeholder()), + Directionality( + textDirection: TextDirection.ltr, + child: Semantics(increasedValue: 'test value', child: const Placeholder()), ), ); expect(tester.takeException(), isNull); @@ -881,8 +808,9 @@ void main() { 'Semantics with attributedHint should be recognized as containing text and not fail', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Semantics( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( attributedHint: AttributedString('test value'), child: const Placeholder(), ), @@ -903,10 +831,10 @@ void main() { const double fontSize2 = 12; await tester.pumpWidget( - MaterialApp( - theme: ThemeData(useMaterial3: false), - home: Scaffold( - body: Row( + TestWidgetsApp( + home: Align( + alignment: Alignment.topLeft, + child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ @@ -970,10 +898,10 @@ void main() { const double fontSize2 = 12; await tester.pumpWidget( - MaterialApp( - theme: ThemeData(useMaterial3: false), - home: Scaffold( - body: Row( + TestWidgetsApp( + home: Align( + alignment: Alignment.topLeft, + child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ @@ -1260,7 +1188,7 @@ void main() { Transform.scale( scale: 1.04, child: const ColoredBox( - color: Colors.orange, + color: Color(0xFFFF9800), child: Padding( padding: EdgeInsets.all(2), child: Row( @@ -1268,42 +1196,42 @@ void main() { crossAxisAlignment: CrossAxisAlignment.end, children: [ ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), child: Padding( padding: EdgeInsets.all(4.0), child: Text( 'Short', - style: TextStyle(fontSize: 16, color: Colors.black), + style: TextStyle(fontSize: 16, color: Color(0xFF000000)), ), ), ), ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), child: Padding( padding: EdgeInsets.all(4.0), child: Text( 'Just text ', - style: TextStyle(fontSize: 14, color: Colors.black), + style: TextStyle(fontSize: 14, color: Color(0xFF000000)), ), ), ), ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), child: Padding( padding: EdgeInsets.all(4.0), child: Text( ' Tall text ', - style: TextStyle(fontSize: 18, color: Colors.black), + style: TextStyle(fontSize: 18, color: Color(0xFF000000)), ), ), ), ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), child: Padding( padding: EdgeInsets.all(4.0), child: Text( 'Medium', - style: TextStyle(fontSize: 32, color: Colors.black), + style: TextStyle(fontSize: 32, color: Color(0xFF000000)), ), ), ), @@ -1315,7 +1243,7 @@ void main() { Transform.scale( scale: 1.04, child: const ColoredBox( - color: Colors.orange, + color: Color(0xFFFF9800), isAntiAlias: false, child: Padding( padding: EdgeInsets.all(2), @@ -1324,46 +1252,46 @@ void main() { crossAxisAlignment: CrossAxisAlignment.end, children: [ ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), isAntiAlias: false, child: Padding( padding: EdgeInsets.all(4.0), child: Text( 'Short', - style: TextStyle(fontSize: 16, color: Colors.black), + style: TextStyle(fontSize: 16, color: Color(0xFF000000)), ), ), ), ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), isAntiAlias: false, child: Padding( padding: EdgeInsets.all(4.0), child: Text( 'Just text ', - style: TextStyle(fontSize: 14, color: Colors.black), + style: TextStyle(fontSize: 14, color: Color(0xFF000000)), ), ), ), ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), isAntiAlias: false, child: Padding( padding: EdgeInsets.all(4.0), child: Text( ' Tall text ', - style: TextStyle(fontSize: 18, color: Colors.black), + style: TextStyle(fontSize: 18, color: Color(0xFF000000)), ), ), ), ColoredBox( - color: Colors.white, + color: Color(0xFFFFFFFF), isAntiAlias: false, child: Padding( padding: EdgeInsets.all(4.0), child: Text( 'Medium', - style: TextStyle(fontSize: 32, color: Colors.black), + style: TextStyle(fontSize: 32, color: Color(0xFF000000)), ), ), ), @@ -1382,7 +1310,7 @@ void main() { dimension: 50, child: Transform.rotate( angle: math.pi / 5, - child: const ColoredBox(color: Colors.blue), + child: const ColoredBox(color: Color(0xFF2196F3)), ), ), ), @@ -1394,7 +1322,10 @@ void main() { dimension: 50, child: Transform.rotate( angle: math.pi / 5, - child: const ColoredBox(color: Colors.amber, isAntiAlias: false), + child: const ColoredBox( + color: Color(0xFFFFC107), + isAntiAlias: false, + ), ), ), ), @@ -1408,7 +1339,10 @@ void main() { angle: math.pi / 5, child: Transform.scale( scale: 1.2, - child: const ColoredBox(color: Colors.teal, isAntiAlias: false), + child: const ColoredBox( + color: Color(0xFF009688), + isAntiAlias: false, + ), ), ), ), @@ -1543,10 +1477,11 @@ void main() { testWidgets('does not change semantics when not ignoring', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: IgnorePointer( + Directionality( + textDirection: TextDirection.ltr, + child: IgnorePointer( ignoring: false, - child: ElevatedButton(key: key, onPressed: () {}, child: const Text('button')), + child: TestButton(key: key, onPressed: () {}, child: const Text('button')), ), ), ); @@ -1569,8 +1504,9 @@ void main() { final key2 = UniqueKey(); final key3 = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: TestIgnorePointer( + Directionality( + textDirection: TextDirection.ltr, + child: TestIgnorePointer( child: Semantics( key: key1, label: '1', @@ -1641,10 +1577,11 @@ void main() { final semantics = SemanticsTester(tester); final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: IgnorePointer( + Directionality( + textDirection: TextDirection.ltr, + child: IgnorePointer( ignoringSemantics: true, - child: ElevatedButton(key: key, onPressed: () {}, child: const Text('button')), + child: TestButton(key: key, onPressed: () {}, child: const Text('button')), ), ), ); @@ -1655,9 +1592,10 @@ void main() { testWidgets('ignores user interactions', (WidgetTester tester) async { final key = UniqueKey(); await tester.pumpWidget( - MaterialApp( - home: IgnorePointer( - child: ElevatedButton(key: key, onPressed: () {}, child: const Text('button')), + Directionality( + textDirection: TextDirection.ltr, + child: IgnorePointer( + child: TestButton(key: key, onPressed: () {}, child: const Text('button')), ), ), ); diff --git a/packages/flutter/test/widgets/decorated_sliver_test.dart b/packages/flutter/test/widgets/decorated_sliver_test.dart index bde4cf5656542..27791ee2edf68 100644 --- a/packages/flutter/test/widgets/decorated_sliver_test.dart +++ b/packages/flutter/test/widgets/decorated_sliver_test.dart @@ -267,7 +267,7 @@ void main() { expect( find.byKey(key), paints..rect( - rect: const Offset(0.5, -199.5) & const Size(99, 499), + rect: const Offset(0.5, 0.5) & const Size(99, 499), color: black, style: PaintingStyle.stroke, strokeWidth: 1.0, @@ -354,7 +354,7 @@ void main() { expect( find.byKey(key), paints..rect( - rect: const Offset(-199.5, 0.5) & const Size(499, 99), + rect: const Offset(0.5, 0.5) & const Size(499, 99), color: black, style: PaintingStyle.stroke, strokeWidth: 1.0, @@ -504,6 +504,111 @@ void main() { ); }, ); + + /// Regression test for https://github.com/flutter/flutter/issues/179801 + testWidgets('DecoratedSliver works with PinnedHeaderSliver basic scroll', ( + WidgetTester tester, + ) async { + const key = Key('DecoratedSliver with border'); + const black = Color(0xFF000000); + final controller = ScrollController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + height: 100, + width: 300, + child: CustomScrollView( + controller: controller, + slivers: [ + DecoratedSliver( + key: key, + decoration: BoxDecoration(border: Border.all()), + sliver: const PinnedHeaderSliver(child: SizedBox(height: 50, width: 300)), + ), + const SliverToBoxAdapter(child: SizedBox(height: 1000)), + ], + ), + ), + ), + ), + ); + + expect( + find.byKey(key), + paints..rect( + rect: const Offset(0.5, 0.5) & const Size(299, 49), + color: black, + style: PaintingStyle.stroke, + strokeWidth: 1.0, + ), + ); + + controller.jumpTo(200); + await tester.pumpAndSettle(); + expect( + find.byKey(key), + paints..rect( + rect: const Offset(0.5, 0.5) & const Size(299, 49), + color: black, + style: PaintingStyle.stroke, + strokeWidth: 1.0, + ), + ); + }); + + /// Regression test for https://github.com/flutter/flutter/issues/179801 + testWidgets('DecoratedSliver works with PinnedHeaderSliver overscroll', ( + WidgetTester tester, + ) async { + const key = Key('DecoratedSliver with border'); + + await tester.pumpWidget( + MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: SizedBox( + height: 500, + width: 300, + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + DecoratedSliver( + key: key, + decoration: BoxDecoration(border: Border.all()), + sliver: const SliverAppBar( + pinned: true, + stretch: true, + title: SizedBox(height: 50, width: 300), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 1000)), + ], + ), + ), + ), + ), + ); + + await tester.drag(find.byType(CustomScrollView), const Offset(0, 45)); + await tester.pump(); + + expect( + find.byKey(key), + paints..something((methodName, arguments) { + if (methodName != #drawRRect) { + return false; + } + final Rect rect = (arguments[0] as RRect).outerRect; + expect(rect, rectMoreOrLessEquals(const Rect.fromLTRB(0, 0, 300, 81.08), epsilon: 0.01)); + return true; + }), + ); + }); } class TestDecoration extends Decoration { diff --git a/packages/flutter/test/widgets/expansible_test.dart b/packages/flutter/test/widgets/expansible_test.dart index 4fa2c6394c323..8e3cbf60795f5 100644 --- a/packages/flutter/test/widgets/expansible_test.dart +++ b/packages/flutter/test/widgets/expansible_test.dart @@ -2,14 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'widgets_app_tester.dart'; void main() { testWidgets('Controller expands and collapses the widget', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), @@ -41,7 +42,7 @@ void main() { }); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), @@ -78,7 +79,7 @@ void main() { final controller = ExpansibleController(); controller.expand(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: SingleChildScrollView( child: Column( children: [ @@ -111,7 +112,7 @@ void main() { testWidgets('Can compose header and body with expansibleBuilder', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), @@ -156,7 +157,7 @@ void main() { final controller1 = ExpansibleController(); final controller2 = ExpansibleController(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: SingleChildScrollView( child: Column( children: [ @@ -200,7 +201,7 @@ void main() { testWidgets('Respects animation duration and curves', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, duration: const Duration(milliseconds: 120), @@ -223,15 +224,15 @@ void main() { // Check that the curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 90.08984375); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 56.08984375); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 64.0); // Since the animation has completed, the vertical position doesn't change. await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 64.0); await tester.pumpAndSettle(); await tester.tap(find.text('Header')); @@ -239,7 +240,7 @@ void main() { // Check that the reverse curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 80.91015625); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 46.91015625); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); @@ -257,13 +258,11 @@ void main() { }); await tester.pumpWidget( - MaterialApp( - home: Material( - child: Expansible( - controller: controller1, - headerBuilder: (_, _) => const Text('Header'), - bodyBuilder: (_, _) => const Text('Body'), - ), + TestWidgetsApp( + home: Expansible( + controller: controller1, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), ), ), ); @@ -282,13 +281,11 @@ void main() { expect(find.text('Body'), findsNothing); await tester.pumpWidget( - MaterialApp( - home: Material( - child: Expansible( - controller: controller2, - headerBuilder: (_, _) => const Text('Header'), - bodyBuilder: (_, _) => const Text('Body'), - ), + TestWidgetsApp( + home: Expansible( + controller: controller2, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), ), ), ); @@ -318,13 +315,11 @@ void main() { }); await tester.pumpWidget( - MaterialApp( - home: Material( - child: Expansible( - controller: controller1, - headerBuilder: (_, _) => const Text('Header'), - bodyBuilder: (_, _) => const Text('Body'), - ), + TestWidgetsApp( + home: Expansible( + controller: controller1, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), ), ), ); @@ -338,13 +333,11 @@ void main() { expect(find.text('Body'), findsOne); await tester.pumpWidget( - MaterialApp( - home: Material( - child: Expansible( - controller: controller2, - headerBuilder: (_, _) => const Text('Header'), - bodyBuilder: (_, _) => const Text('Body'), - ), + TestWidgetsApp( + home: Expansible( + controller: controller2, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), ), ), ); @@ -367,7 +360,7 @@ void main() { testWidgets('Respects animationStyle duration and curves', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, animationStyle: const AnimationStyle( @@ -392,15 +385,15 @@ void main() { // Check that the curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 90.08984375); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 56.08984375); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 64.0); // Since the animation has completed, the vertical position doesn't change. await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 64.0); await tester.pumpAndSettle(); await tester.tap(find.text('Header')); @@ -408,7 +401,7 @@ void main() { // Check that the reverse curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 80.91015625); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 46.91015625); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); @@ -422,7 +415,7 @@ void main() { ) async { final controller = ExpansibleController(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, animationStyle: const AnimationStyle( @@ -448,13 +441,13 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); // With linear curve at 50ms out of 100ms, should be at approximately 50% height final double midAnimationY = tester.getBottomLeft(find.byType(Placeholder)).dy; - // Should be more than base (48.0) and less than fully expanded (98.0) - expect(midAnimationY, greaterThan(48.0)); - expect(midAnimationY, lessThan(98.0)); + // Should be more than base (14.0) and less than fully expanded (64.0) + expect(midAnimationY, greaterThan(14.0)); + expect(midAnimationY, lessThan(64.0)); // Animation should complete at 100ms await tester.pump(const Duration(milliseconds: 50) + const Duration(microseconds: 1)); - expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); + expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 64.0); controller.dispose(); }); @@ -462,7 +455,7 @@ void main() { testWidgets('AnimationStyle.noAnimation disables animation', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, animationStyle: AnimationStyle.noAnimation, @@ -517,7 +510,7 @@ void main() { addTearDown(controller.dispose); await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), diff --git a/packages/flutter/test/widgets/implicit_animations_test.dart b/packages/flutter/test/widgets/implicit_animations_test.dart index 30e2071f27c08..7c0983698b0dd 100644 --- a/packages/flutter/test/widgets/implicit_animations_test.dart +++ b/packages/flutter/test/widgets/implicit_animations_test.dart @@ -680,6 +680,21 @@ void main() { // TODO(nate-thegrate): add every class! }); + testWidgets('AnimatedScale does not crash at zero area', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox.shrink( + child: AnimatedScale(scale: 2, duration: Duration(milliseconds: 300), child: Text('X')), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(tester.getSize(find.byType(AnimatedScale)), Size.zero); + }); + testWidgets('AnimatedRotation does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( diff --git a/packages/flutter/test/widgets/overlay_portal_test.dart b/packages/flutter/test/widgets/overlay_portal_test.dart index 0ad0b7f026875..437262cc5ed35 100644 --- a/packages/flutter/test/widgets/overlay_portal_test.dart +++ b/packages/flutter/test/widgets/overlay_portal_test.dart @@ -154,6 +154,129 @@ void main() { expect(directionSeenByOverlayChild, textDirection); }); + testWidgets( + 'OverlayPortal overlayChild located in root Overlay receives MediaQuery properties from root Overlay context', + (WidgetTester tester) async { + final controller = OverlayPortalController(); + const rootPadding = EdgeInsets.all(10); + const innerPadding = EdgeInsets.all(20); + + MediaQueryData? overlayChildData; + OverlayEntry? outerEntry; + OverlayEntry? innerEntry; + addTearDown(() { + outerEntry?.remove(); + outerEntry?.dispose(); + innerEntry?.remove(); + innerEntry?.dispose(); + }); + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(padding: rootPadding), + child: Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + outerEntry = OverlayEntry( + builder: (BuildContext context) { + return MediaQuery( + data: const MediaQueryData(padding: innerPadding), + child: Overlay( + initialEntries: [ + innerEntry = OverlayEntry( + builder: (BuildContext context) { + return OverlayPortal( + controller: controller, + overlayLocation: OverlayChildLocation.rootOverlay, + overlayChildBuilder: (BuildContext context) { + overlayChildData = MediaQuery.of(context); + return const SizedBox(); + }, + child: const SizedBox(), + ); + }, + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ); + + controller.show(); + await tester.pump(); + + expect(overlayChildData?.padding, rootPadding); + }, + ); + + testWidgets('OverlayPortal overlayChild receives MediaQuery properties from Overlay context', ( + WidgetTester tester, + ) async { + final controller = OverlayPortalController(); + const expectedPadding = EdgeInsets.all(10); + const expectedViewInsets = EdgeInsets.only(bottom: 300); + const expectedViewPadding = EdgeInsets.only(top: 50, bottom: 20); + const expectedSize = Size(800, 600); + + MediaQueryData? overlayChildData; + OverlayEntry? entry; + addTearDown(() { + entry?.remove(); + entry?.dispose(); + }); + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData( + padding: expectedPadding, + viewInsets: expectedViewInsets, + viewPadding: expectedViewPadding, + size: expectedSize, + ), + child: Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + entry = OverlayEntry( + builder: (BuildContext context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + padding: EdgeInsets.zero, + viewInsets: EdgeInsets.zero, + viewPadding: EdgeInsets.zero, + ), + child: OverlayPortal( + controller: controller, + overlayChildBuilder: (BuildContext context) { + overlayChildData = MediaQuery.of(context); + return const SizedBox(); + }, + child: const SizedBox(), + ), + ); + }, + ), + ], + ), + ), + ), + ); + + controller.show(); + await tester.pump(); + + expect(overlayChildData?.padding, expectedPadding); + expect(overlayChildData?.viewInsets, expectedViewInsets); + expect(overlayChildData?.viewPadding, expectedViewPadding); + expect(overlayChildData?.size, expectedSize); + }); + testWidgets('The overlay portal update semantics does not dirty overlay', ( WidgetTester tester, ) async { diff --git a/packages/flutter/test/widgets/page_storage_test.dart b/packages/flutter/test/widgets/page_storage_test.dart index 30b01f0b2c681..21ffea322794d 100644 --- a/packages/flutter/test/widgets/page_storage_test.dart +++ b/packages/flutter/test/widgets/page_storage_test.dart @@ -2,9 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'widgets_app_tester.dart'; + void main() { testWidgets('PageStorage read and write', (WidgetTester tester) async { const Key builderKey = PageStorageKey('builderKey'); @@ -12,7 +14,7 @@ void main() { var storedValue = 0; await tester.pumpWidget( - MaterialApp( + TestWidgetsApp( home: StatefulBuilder( key: builderKey, builder: (BuildContext context, StateSetter setter) { @@ -40,7 +42,7 @@ void main() { var storedValue = 0; Widget buildWidthKey(Key key) { - return MaterialApp( + return TestWidgetsApp( home: StatefulBuilder( key: key, builder: (BuildContext context, StateSetter setter) { diff --git a/packages/flutter/test/widgets/reassemble_test.dart b/packages/flutter/test/widgets/reassemble_test.dart index 813ec52a50d27..2fb1b9291fd4c 100644 --- a/packages/flutter/test/widgets/reassemble_test.dart +++ b/packages/flutter/test/widgets/reassemble_test.dart @@ -2,12 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'widgets_app_tester.dart'; + void main() { testWidgets('reassemble does not crash', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: Text('Hello World'))); + await tester.pumpWidget(const TestWidgetsApp(home: Text('Hello World'))); await tester.pump(); tester.binding.reassembleApplication(); await tester.pump(); diff --git a/packages/flutter/test/widgets/sliver_tree_test.dart b/packages/flutter/test/widgets/sliver_tree_test.dart index 6c046ea966bed..2b29bac6730ad 100644 --- a/packages/flutter/test/widgets/sliver_tree_test.dart +++ b/packages/flutter/test/widgets/sliver_tree_test.dart @@ -7,10 +7,12 @@ @Tags(['reduced-test-set']) library; -import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'utils.dart'; + List> simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( @@ -79,8 +81,9 @@ void main() { final children = >[TreeSliverNode('child')]; final node = TreeSliverNode('parent', children: children, expanded: true); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver(tree: >[node]), ], @@ -132,8 +135,9 @@ void main() { final controller = TreeSliverController(); TreeSliverController? returnedController; await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, @@ -158,8 +162,9 @@ void main() { testWidgets('Can get default controller on TreeSliver', (WidgetTester tester) async { TreeSliverController? returnedController; await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, @@ -183,8 +188,9 @@ void main() { testWidgets('Can get node for TreeSliverNode.content', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), @@ -196,8 +202,9 @@ void main() { testWidgets('Can get isExpanded for a node', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), @@ -209,8 +216,9 @@ void main() { testWidgets('Can get isActive for a node', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), @@ -225,8 +233,9 @@ void main() { testWidgets('Can toggleNode, to collapse or expand', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), @@ -257,8 +266,9 @@ void main() { testWidgets('Can expandNode, then collapseAll', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), @@ -290,8 +300,9 @@ void main() { testWidgets('Can collapseNode, then expandAll', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), @@ -361,8 +372,9 @@ void main() { var toggled = false; TreeSliverNode? toggledNode; await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, @@ -388,8 +400,9 @@ void main() { toggledNode = null; // Use toggleNodeWith to make the whole row trigger the node state. await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, @@ -474,8 +487,9 @@ void main() { AnimationStyle? style; await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, @@ -497,8 +511,9 @@ void main() { expect(style, TreeSliver.defaultToggleAnimationStyle); await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, @@ -523,8 +538,9 @@ void main() { style = null; await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, @@ -575,20 +591,28 @@ void main() { ]; final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: StatefulBuilder( + Directionality( + textDirection: TextDirection.ltr, + child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Scaffold( - body: CustomScrollView( - slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - setState(() { - simpleNodeSet.add(TreeSliverNode('Added root')); - }); - }, - ), + return Column( + children: [ + TestButton( + onPressed: () { + setState(() { + simpleNodeSet.add(TreeSliverNode('Added root')); + }); + }, + child: const Text('Add root'), + ), + Expanded( + child: CustomScrollView( + slivers: [ + TreeSliver(tree: simpleNodeSet, controller: controller), + ], + ), + ), + ], ); }, ), @@ -606,7 +630,7 @@ void main() { expect(find.text('Root 3'), findsOneWidget); expect(find.text('Added root'), findsNothing); - await tester.tap(find.byType(FloatingActionButton)); + await tester.tap(find.text('Add root')); await tester.pump(); expect(find.text('Root 0'), findsOneWidget); @@ -645,20 +669,28 @@ void main() { ]; final controller = TreeSliverController(); await tester.pumpWidget( - MaterialApp( - home: StatefulBuilder( + Directionality( + textDirection: TextDirection.ltr, + child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Scaffold( - body: CustomScrollView( - slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - setState(() { - simpleNodeSet[1].children.add(TreeSliverNode('Added child')); - }); - }, - ), + return Column( + children: [ + TestButton( + onPressed: () { + setState(() { + simpleNodeSet[1].children.add(TreeSliverNode('Added child')); + }); + }, + child: const Text('Add child'), + ), + Expanded( + child: CustomScrollView( + slivers: [ + TreeSliver(tree: simpleNodeSet, controller: controller), + ], + ), + ), + ], ); }, ), @@ -674,7 +706,7 @@ void main() { expect(find.text('Child 2:0'), findsNothing); expect(find.text('Child 2:1'), findsNothing); expect(find.text('Root 3'), findsOneWidget); - await tester.tap(find.byType(FloatingActionButton)); + await tester.tap(find.text('Add child')); await tester.pump(); expect(find.text('Root 0'), findsOneWidget); expect(find.text('Root 1'), findsOneWidget); @@ -723,8 +755,9 @@ void main() { ]; await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( slivers: [ TreeSliver( tree: tree, @@ -810,35 +843,34 @@ void main() { ]; Widget buildTreeSliver(TreeSliverController controller) { - return MaterialApp( - home: Scaffold( - body: CustomScrollView( - shrinkWrap: true, - slivers: [ - TreeSliver( - tree: tree, - controller: controller, - toggleAnimationStyle: const AnimationStyle( - curve: Curves.easeInOut, - duration: Duration(milliseconds: 200), - ), - treeNodeBuilder: - ( - BuildContext context, - TreeSliverNode node, - AnimationStyle animationStyle, - ) { - final Widget child = GestureDetector( - key: ValueKey(node.content! as String), - behavior: HitTestBehavior.translucent, - onTap: () => controller.toggleNode(node), - child: TreeSliver.defaultTreeNodeBuilder(context, node, animationStyle), - ); - return child; - }, + return Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + shrinkWrap: true, + slivers: [ + TreeSliver( + tree: tree, + controller: controller, + toggleAnimationStyle: const AnimationStyle( + curve: Curves.easeInOut, + duration: Duration(milliseconds: 200), ), - ], - ), + treeNodeBuilder: + ( + BuildContext context, + TreeSliverNode node, + AnimationStyle animationStyle, + ) { + final Widget child = GestureDetector( + key: ValueKey(node.content! as String), + behavior: HitTestBehavior.translucent, + onTap: () => controller.toggleNode(node), + child: TreeSliver.defaultTreeNodeBuilder(context, node, animationStyle), + ); + return child; + }, + ), + ], ), ); } @@ -888,7 +920,7 @@ void main() { TreeSliverNode node, AnimationStyle animationStyle, ) { - return const ColoredBox(color: Colors.red); + return const ColoredBox(color: Color(0xFFF44336)); }, ), ], @@ -960,7 +992,7 @@ void main() { TreeSliverNode node, AnimationStyle animationStyle, ) { - return const ColoredBox(color: Colors.red); + return const ColoredBox(color: Color(0xFFF44336)); }, ), const SliverToBoxAdapter(child: SizedBox(height: 20)), diff --git a/packages/flutter/test/widgets/utils.dart b/packages/flutter/test/widgets/utils.dart index 2454e14317448..e449ef16d9015 100644 --- a/packages/flutter/test/widgets/utils.dart +++ b/packages/flutter/test/widgets/utils.dart @@ -40,12 +40,17 @@ class TestButton extends StatelessWidget { final VoidCallback? onPressed; final Widget child; + void _onFocus() => focusNode?.requestFocus(); + @override Widget build(BuildContext context) { return Semantics( + label: 'button', button: true, enabled: onPressed != null, onTap: onPressed, + onFocus: _onFocus, + focusable: true, child: FocusableActionDetector( enabled: onPressed != null, focusNode: focusNode, diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 1dca5d33f0210..d2544ef060cf0 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -163,16 +163,22 @@ class CapturedAccessibilityAnnouncement { } class _TestFlutterView implements FlutterView { - _TestFlutterView({required this.controller, required TestPlatformDispatcher platformDispatcher}) - : _platformDispatcher = platformDispatcher, - _viewId = _nextViewId++ { + _TestFlutterView({ + required this.controller, + required TestPlatformDispatcher platformDispatcher, + this.constraints, + this.onRender, + }) : _platformDispatcher = platformDispatcher, + _viewId = _nextViewId++ { platformDispatcher.addTestView(this); } static int _nextViewId = 1; final BaseWindowController controller; final TestPlatformDispatcher _platformDispatcher; + final BoxConstraints? constraints; final int _viewId; + final void Function(Size? size)? onRender; @override double get devicePixelRatio => display.devicePixelRatio; @@ -190,7 +196,14 @@ class _TestFlutterView implements FlutterView { ui.ViewPadding get padding => ui.ViewPadding.zero; @override - ui.ViewConstraints get physicalConstraints => ui.ViewConstraints.tight(physicalSize); + ui.ViewConstraints get physicalConstraints => constraints != null + ? ui.ViewConstraints( + minWidth: constraints!.minWidth, + maxWidth: constraints!.maxWidth, + minHeight: constraints!.minHeight, + maxHeight: constraints!.maxHeight, + ) + : ui.ViewConstraints.tight(physicalSize); @override ui.Size get physicalSize => controller.contentSize * devicePixelRatio; @@ -214,7 +227,9 @@ class _TestFlutterView implements FlutterView { ui.DisplayCornerRadii? get displayCornerRadii => null; @override - void render(ui.Scene scene, {ui.Size? size}) {} + void render(ui.Scene scene, {ui.Size? size}) { + onRender?.call(size); + } @override void updateSemantics(ui.SemanticsUpdate update) {} @@ -297,7 +312,11 @@ class _TestRegularWindowController extends RegularWindowController with _ChildWi _title = title ?? 'Test Window', super.empty() { _constrainToBounds(); - rootView = _TestFlutterView(controller: this, platformDispatcher: platformDispatcher); + rootView = _TestFlutterView( + controller: this, + platformDispatcher: platformDispatcher, + constraints: _constraints, + ); // Automatically activate the window when created. activate(); @@ -445,7 +464,11 @@ class _TestDialogWindowController extends DialogWindowController with _ChildWind _title = title ?? 'Test Window', super.empty() { _constrainToBounds(); - rootView = _TestFlutterView(controller: this, platformDispatcher: platformDispatcher); + rootView = _TestFlutterView( + controller: this, + platformDispatcher: platformDispatcher, + constraints: _constraints, + ); _addChildToParent(parent, this); // Automatically activate the window when created. @@ -529,6 +552,76 @@ class _TestDialogWindowController extends DialogWindowController with _ChildWind } } +class _TestTooltipWindowController extends TooltipWindowController with _ChildWindowHierarchyMixin { + _TestTooltipWindowController({ + required TooltipWindowControllerDelegate delegate, + required TestPlatformDispatcher platformDispatcher, + required this.windowingOwner, + required BoxConstraints preferredConstraints, + required bool isSizedToContent, + required ui.Rect anchorRect, + required WindowPositioner positioner, + required BaseWindowController parent, + }) : _delegate = delegate, + _constraints = preferredConstraints, + _isSizedToContent = isSizedToContent, + _anchorRect = anchorRect, + _positioner = positioner, + _parent = parent, + super.empty() { + rootView = _TestFlutterView( + controller: this, + platformDispatcher: platformDispatcher, + constraints: _constraints, + onRender: (size) { + if (_isSizedToContent && _lastRenderedSize != size) { + _lastRenderedSize = size; + scheduleMicrotask(() { + notifyListeners(); + }); + } + }, + ); + _addChildToParent(parent, this); + } + + final TooltipWindowControllerDelegate _delegate; + final _TestWindowingOwner windowingOwner; + BoxConstraints _constraints; + ui.Rect _anchorRect; + WindowPositioner _positioner; + final BaseWindowController _parent; + final bool _isSizedToContent; + Size? _lastRenderedSize; + + @override + Size get contentSize => + _isSizedToContent && _lastRenderedSize != null ? _lastRenderedSize! : _constraints.biggest; + + @override + BaseWindowController get parent => _parent; + + @override + void setConstraints(BoxConstraints constraints) { + _constraints = constraints; + notifyListeners(); + } + + @override + void updatePosition({Rect? anchorRect, WindowPositioner? positioner}) { + _anchorRect = anchorRect ?? _anchorRect; + _positioner = positioner ?? _positioner; + } + + @override + void destroy() { + _delegate.onWindowDestroyed(); + removeAllChildren(); + windowingOwner.deactivateWindowController(this); + _removeChildFromParent(parent, this); + } +} + class _TestPopupWindowController extends PopupWindowController with _ChildWindowHierarchyMixin { _TestPopupWindowController({ required PopupWindowControllerDelegate delegate, @@ -544,7 +637,11 @@ class _TestPopupWindowController extends PopupWindowController with _ChildWindow _positioner = positioner, _parent = parent, super.empty() { - rootView = _TestFlutterView(controller: this, platformDispatcher: platformDispatcher); + rootView = _TestFlutterView( + controller: this, + platformDispatcher: platformDispatcher, + constraints: _constraints, + ); _addChildToParent(parent, this); // Automatically activate the window when created. @@ -741,8 +838,16 @@ class _TestWindowingOwner extends WindowingOwner { required WindowPositioner positioner, required BaseWindowController parent, }) { - // TODO(mattkae): implement createTooltipWindowController - throw UnimplementedError(); + return _TestTooltipWindowController( + delegate: delegate, + platformDispatcher: _platformDispatcher, + windowingOwner: this, + preferredConstraints: preferredConstraints, + isSizedToContent: isSizedToContent, + anchorRect: anchorRect, + positioner: positioner, + parent: parent, + ); } @override diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index 02a1e88d84652..0167a811e70b6 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -296,48 +296,24 @@ class Context { _embedAppFramework(xcodeFrameworksDir, codesign ? expandedCodeSignIdentity : null); - var shouldEmbedFlutterFramework = true; - if (_usingFlutterFrameworkSwiftPackage()) { - final bool isFrameworkCorrect = _validateFlutterFramework( - buildMode: parseFlutterBuildMode(), - platform: platform, - builtProductsDir: environment['BUILT_PRODUCTS_DIR'], - targetBuildDir: xcodeFrameworksDir, - ); - if (isFrameworkCorrect) { - // If the engine is correct, skip embedding. - shouldEmbedFlutterFramework = false; - } else { - // If the engine is wrong in either BUILT_PRODUCTS_DIR or TARGET_BUILD_DIR, call unpack - // again so the correct framework is copied into BUILT_PRODUCTS_DIR to then be embedded - // into TARGET_BUILD_DIR below. - unpackFor(platform, 'embed'); - } - } + // Embed the actual Flutter.framework that the Flutter app expects to run against, + // which could be a local build or an arch/type-specific build. + switch (platform) { + case TargetPlatform.ios: + runRsync('${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', '$xcodeFrameworksDir/'); + case TargetPlatform.macos: + runRsync( + extraArgs: ['--filter', '- Headers', '--filter', '- Modules'], + '${environment['BUILT_PRODUCTS_DIR']}/FlutterMacOS.framework', + '$xcodeFrameworksDir/', + ); - if (shouldEmbedFlutterFramework) { - // Embed the actual Flutter.framework that the Flutter app expects to run against, - // which could be a local build or an arch/type-specific build. - switch (platform) { - case TargetPlatform.ios: - runRsync( - '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', - '$xcodeFrameworksDir/', - ); - case TargetPlatform.macos: - runRsync( - extraArgs: ['--filter', '- Headers', '--filter', '- Modules'], - '${environment['BUILT_PRODUCTS_DIR']}/FlutterMacOS.framework', - '$xcodeFrameworksDir/', + if (codesign) { + _codesignFramework( + expandedCodeSignIdentity, + '$xcodeFrameworksDir/FlutterMacOS.framework/FlutterMacOS', ); - - if (codesign) { - _codesignFramework( - expandedCodeSignIdentity, - '$xcodeFrameworksDir/FlutterMacOS.framework/FlutterMacOS', - ); - } - } + } } _embedNativeAssets( @@ -359,115 +335,6 @@ class Context { } } - /// Returns `true` if a directory exists at `FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH`. - bool _usingFlutterFrameworkSwiftPackage() { - final String? swiftPackagePath = environment['FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH']; - if (swiftPackagePath == null) { - return false; - } - final swiftPackage = Directory(swiftPackagePath); - return swiftPackage.existsSync(); - } - - /// Returns `true` if the Flutter/FlutterMacOS framework Info.plist in [builtProductsDir] and - /// [targetBuildDir] matches the framework Info.plist from the engine cache for the - /// corresponding [platform] and [buildMode]. - /// - /// The Info.plist contains the build mode and engine version. - /// - /// This validation will always fail when using a local engine since the engine version will not match. - bool _validateFlutterFramework({ - required String buildMode, - required TargetPlatform platform, - required String? builtProductsDir, - required String? targetBuildDir, - }) { - if (builtProductsDir == null) { - echo('Unable to locate $builtProductsDir; falling back to direct embedding.'); - return false; - } - if (targetBuildDir == null) { - echo('Unable to locate $targetBuildDir; falling back to direct embedding.'); - return false; - } - try { - final infoPlistFromBuild = File('$builtProductsDir/${platform.infoPlistPath}'); - if (!infoPlistFromBuild.existsSync()) { - echo('Unable to locate $infoPlistFromBuild; falling back to direct embedding.'); - return false; - } - final infoPlistFromEmbedded = File('$targetBuildDir/${platform.infoPlistPath}'); - if (!infoPlistFromEmbedded.existsSync()) { - echo('Unable to locate $infoPlistFromEmbedded; falling back to direct embedding.'); - return false; - } - - final File? infoPlistFromEngineCache = _infoPlistFromEngineCache(buildMode, platform); - if (infoPlistFromEngineCache == null || !infoPlistFromEngineCache.existsSync()) { - echo('Unable to locate $infoPlistFromEngineCache; falling back to direct embedding.'); - return false; - } - final String expectedInfoPlist = infoPlistFromEngineCache.readAsStringSync(); - if (infoPlistFromBuild.readAsStringSync() != expectedInfoPlist) { - echo( - 'Initially processed Flutter framework did not match expectations; falling back to direct embedding.', - ); - return false; - } - if (infoPlistFromEmbedded.readAsStringSync() != expectedInfoPlist) { - echo( - 'Initially embedded Flutter framework did not match expectations; falling back to direct embedding.', - ); - return false; - } - return infoPlistFromBuild.readAsStringSync() == expectedInfoPlist && - infoPlistFromEmbedded.readAsStringSync() == expectedInfoPlist; - } on Exception catch (e) { - // Use `echo` instead of `echoError` so it does not cause the build to fail. - echo('$e\n'); - echo( - 'An error occured while validating the Flutter framework; falling back to direct embedding.\n', - ); - } - return false; - } - - /// Find the Info.plist of the Flutter/FlutterMacOS framework for the corresponding [buildMode] - /// and [platform]. - File? _infoPlistFromEngineCache(String buildMode, TargetPlatform platform) { - final String artifactMode = buildMode == 'debug' - ? platform.artifactName - : '${platform.artifactName}-$buildMode'; - final xcframework = Directory( - '${environment['FLUTTER_ROOT'] ?? ''}/bin/cache/artifacts/engine/$artifactMode/${platform.frameworkName}.xcframework', - ); - switch (platform) { - case TargetPlatform.ios: - final String? sdkRoot = environment['SDKROOT']?.toLowerCase(); - if (sdkRoot == null || !sdkRoot.contains('iphone')) { - return null; - } - final bool simulatorSDK = sdkRoot.contains('simulator'); - for (final FileSystemEntity entity in xcframework.listSync()) { - final String platformBaseName = Uri.parse(entity.path).pathSegments.last; - if (entity is Directory && platformBaseName.startsWith('ios-')) { - final bool isSimulatorDirectory = platformBaseName.endsWith('-simulator'); - if (simulatorSDK == isSimulatorDirectory) { - return File('${entity.path}/${platform.infoPlistPath}'); - } - } - } - case TargetPlatform.macos: - for (final FileSystemEntity entity in xcframework.listSync()) { - final String platformBaseName = Uri.parse(entity.path).pathSegments.last; - if (entity is Directory && platformBaseName.startsWith('macos-')) { - return File('${entity.path}/${platform.infoPlistPath}'); - } - } - } - return null; - } - void _embedNativeAssets( TargetPlatform platform, { required String xcodeFrameworksDir, @@ -865,21 +732,4 @@ class Context { } } -enum TargetPlatform { - ios(artifactName: 'ios', frameworkName: 'Flutter', infoPlistPath: 'Flutter.framework/Info.plist'), - macos( - artifactName: 'darwin-x64', - frameworkName: 'FlutterMacOS', - infoPlistPath: 'FlutterMacOS.framework/Resources/Info.plist', - ); - - const TargetPlatform({ - required this.artifactName, - required this.frameworkName, - required this.infoPlistPath, - }); - - final String artifactName; - final String frameworkName; - final String infoPlistPath; -} +enum TargetPlatform { ios, macos } diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index f3f3a8bfa4386..536f934664d69 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -475,7 +475,6 @@ class AndroidGradleBuilder implements AndroidBuilder { // All automatically created files should exist. if (configOnly) { - _logger.printStatus('Config complete.'); return; } diff --git a/packages/flutter_tools/lib/src/build_system/targets/darwin.dart b/packages/flutter_tools/lib/src/build_system/targets/darwin.dart index 09c29a8773e55..f68285a389617 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/darwin.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/darwin.dart @@ -9,7 +9,6 @@ import '../../base/io.dart'; import '../../build_info.dart'; import '../../darwin/darwin.dart'; import '../../globals.dart' as globals show stdio; -import '../../project.dart'; import '../build_system.dart'; abstract class UnpackDarwin extends Target { @@ -18,22 +17,6 @@ abstract class UnpackDarwin extends Target { @visibleForOverriding FlutterDarwinPlatform get darwinPlatform; - @override - Future canSkip(Environment environment) async { - final String? buildScript = environment.defines[kXcodeBuildScript]; - final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir); - final XcodeBasedProject xcodeProject = darwinPlatform.xcodeProject(flutterProject); - if (buildScript == kXcodeBuildScriptValueBuild && - xcodeProject.usesSwiftPackageManager && - xcodeProject.flutterFrameworkSwiftPackageDirectory.existsSync()) { - // Skip copying the Flutter framework during the build Run Script if Swift Package Manager - // is being used and the FlutterFramework swift package exists. Swift Package Manager now - // handles the Flutter framework. - return true; - } - return false; - } - /// Copies the [framework] artifact using `rsync` to the [Environment.outputDir]. /// Throws an error if copy fails. @protected diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 3d8fc9235e433..aee2994b17179 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -299,21 +299,7 @@ abstract class UnpackIOS extends UnpackDarwin { } await thinFramework(environment, frameworkBinaryPath, archs); - var codesignFramework = true; - if (environment.defines[kXcodeBuildScript] == kXcodeBuildScriptValuePrepare) { - // Skip codesigning during "prepare" when using SwiftPM. When SwiftPM places the Flutter - // framework in the BUILT_PRODUCTS_DIR, it does not codesign it (it is later codesigned - // in TARGET_BUILD_DIR). Skipping codesigning will improve the caching for the "prepare" script. - final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir); - final XcodeBasedProject xcodeProject = darwinPlatform.xcodeProject(flutterProject); - if (xcodeProject.usesSwiftPackageManager && - xcodeProject.flutterFrameworkSwiftPackageDirectory.existsSync()) { - codesignFramework = false; - } - } - if (codesignFramework) { - await _signFramework(environment, frameworkBinary, buildMode); - } + await _signFramework(environment, frameworkBinary, buildMode); } Future _copyFrameworkDysm( diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 4387c80a80a6c..407216cf21943 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -44,16 +44,6 @@ abstract class UnpackMacOS extends UnpackDarwin { @override List get outputs { - // Swift Package Manager will also produce the FlutterMacOS framework. If both SwiftPM and - // "Flutter Assemble" output the framework, the build will fail with an error about multiple - // commands producing the same output. Only output the framework if the project isn't using - // SwiftPM. - final FlutterProject flutterProject = FlutterProject.current(); - final MacOSProject xcodeProject = flutterProject.macos; - if (xcodeProject.usesSwiftPackageManager && - xcodeProject.flutterFrameworkSwiftPackageDirectory.existsSync()) { - return []; - } return [kFlutterMacOSFrameworkBinarySource]; } diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 12625e1c8143a..c10d1ff158a1e 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -699,6 +699,26 @@ class RunCommand extends RunCommandBase { throwToolExit('Skwasm renderer requires --wasm'); } + if (argResults?.wasParsed(FlutterOptions.kWebExperimentalHotReload) ?? false) { + final bool webEnableHotReload = boolArg(FlutterOptions.kWebExperimentalHotReload); + if (webEnableHotReload) { + globals.printWarning( + 'Hot reload on the web is now enabled by default. ' + 'The "--${FlutterOptions.kWebExperimentalHotReload}" flag is deprecated ' + 'and will be removed in an upcoming release.', + ); + } else { + globals.printWarning( + 'Hot reload on the web is now enabled by default. ' + 'The "--no-${FlutterOptions.kWebExperimentalHotReload}" flag is deprecated ' + 'and will be removed in an upcoming release. ' + 'If your web development workflow depends on disabling hot reload, ' + 'please open an issue explaining why at ' + 'https://github.com/dart-lang/sdk/issues/new?template=5_web_hot_reload.yml.', + ); + } + } + final String? flavor = stringArg('flavor'); final bool flavorsSupportedOnEveryDevice = devices!.every( (Device device) => device.supportsFlavors, diff --git a/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart index 3925a813ef1d0..67d88fcee6638 100644 --- a/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart +++ b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart @@ -65,13 +65,6 @@ class DarwinDependencyManagement { Future setUp({required FlutterDarwinPlatform platform}) async { final XcodeBasedProject xcodeProject = platform.xcodeProject(_project); if (xcodeProject.usesSwiftPackageManager) { - // When using SwiftPM, Xcode outputs the FlutterMacOS framework binary, so - // reset the output list file to avoid conflicts. - if (platform == FlutterDarwinPlatform.macos && - _project.macos.outputFileList.existsSync() && - _project.macos.outputFileList.readAsStringSync().contains('FlutterMacOS')) { - _project.macos.outputFileList.writeAsStringSync(''); - } await _swiftPackageManager.generatePluginsSwiftPackage(_plugins, platform, xcodeProject); } else if (xcodeProject.flutterPluginSwiftPackageInProjectSettings) { // If Swift Package Manager is not enabled but the project is already diff --git a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart index 89d167f6b27f5..4164c4a5fcda5 100644 --- a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -204,11 +204,7 @@ class SwiftPackageManager { targets: [ SwiftPackageTarget.defaultTarget( name: kFlutterGeneratedFrameworkSwiftPackageTargetName, - dependencies: [SwiftPackageTargetDependency.target(name: frameworkName)], - ), - SwiftPackageTarget.binaryTarget( - name: frameworkName, - relativePath: '$frameworkName.xcframework', + dependencies: [], ), ], templateRenderer: _templateRenderer, diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 4dacd644873d4..8921c1ca92d1d 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -697,14 +697,6 @@ class AndroidProject extends FlutterProjectPlatform { .childFile('GeneratedPluginRegistrant.java'); } - File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk'); - - Directory get gradleAppOutV1Directory { - return globals.fs.directory( - globals.fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'), - ); - } - /// Whether the current flutter project has an Android sub-project. @override bool existsSync() { diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 663cb25d406a7..091be82f45785 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -372,7 +372,9 @@ abstract class FlutterCommand extends Command { ); argParser.addFlag( FlutterOptions.kWebExperimentalHotReload, - help: 'Enables new module format that supports hot reload.', + help: + '(deprecated; will be removed in a future release) ' + 'Enables new module format that supports hot reload.', defaultsTo: true, hide: !verboseHelp, ); diff --git a/packages/flutter_tools/lib/src/test/test_compiler.dart b/packages/flutter_tools/lib/src/test/test_compiler.dart index 637ac16b2c719..539b74952b729 100644 --- a/packages/flutter_tools/lib/src/test/test_compiler.dart +++ b/packages/flutter_tools/lib/src/test/test_compiler.dart @@ -102,7 +102,7 @@ class TestCompiler { /// /// If [testTimeRecorder] is passed, times will be recorded in it. TestCompiler( - this.buildInfo, + BuildInfo buildInfo, this.flutterProject, { String? precompiledDillPath, this.testTimeRecorder, @@ -121,6 +121,7 @@ class TestCompiler { ), ), shouldCopyDillFile = precompiledDillPath == null { + this.buildInfo = buildInfo.copyWith(initializeFromDill: testFilePath); // Compiler maintains and updates single incremental dill file. // Incremental compilation requests done for each test copy that file away // for independent execution. @@ -144,7 +145,7 @@ class TestCompiler { final compilerController = StreamController<_CompilationRequest>(); final compilationQueue = <_CompilationRequest>[]; final FlutterProject? flutterProject; - final BuildInfo buildInfo; + late final BuildInfo buildInfo; final String testFilePath; final bool shouldCopyDillFile; final TestTimeRecorder? testTimeRecorder; diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index a0182581e9ef4..5d0f88bd2a186 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -58,8 +58,8 @@ dependencies: pubspec_parse: 1.5.0 graphs: 2.3.2 - hooks_runner: 1.0.1 - hooks: 1.0.0 + hooks_runner: 1.0.2 + hooks: 1.0.1 code_assets: 1.0.0 data_assets: 0.19.6 @@ -128,4 +128,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: etects +# PUBSPEC CHECKSUM: nl0tev diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index c65ae4b685bb4..3fd81acf81c2e 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -1638,6 +1638,99 @@ server: Logger: () => BufferLogger.test(), }, ); + + group('run --web-experimental-hot-reload flag', () { + late BufferLogger logger; + late TestDeviceManager testDeviceManager; + late FileSystem fileSystem; + + setUp(() { + logger = BufferLogger.test(); + + final fakeDevice = FakeDevice(); + testDeviceManager = TestDeviceManager(logger: logger)..devices = [fakeDevice]; + testDeviceManager.specifiedDeviceId = fakeDevice.id; + + fileSystem = MemoryFileSystem.test(); + fileSystem.currentDirectory.childFile('pubspec.yaml').writeAsStringSync('name: my_app'); + writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); + final Directory libDir = fileSystem.currentDirectory.childDirectory('lib'); + libDir.createSync(); + final File mainFile = libDir.childFile('main.dart'); + mainFile.writeAsStringSync('void main() {}'); + }); + testUsingContext( + 'no warning triggered when web hot reload flag not present', + () async { + final CommandRunner runner = createTestCommandRunner( + TestRunCommandThatOnlyValidates(), + ); + await runner.run(['run']); + expect(testLogger.warningText, isEmpty); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + Logger: () => logger, + DeviceManager: () => testDeviceManager, + }, + initializeFlutterRoot: false, + ); + + testUsingContext( + 'warning triggered when web hot reload flag is passed (enabled)', + () async { + final CommandRunner runner = createTestCommandRunner( + TestRunCommandThatOnlyValidates(), + ); + await runner.run(['run', '--web-experimental-hot-reload']); + expect( + testLogger.warningText, + contains( + 'Hot reload on the web is now enabled by default. ' + 'The "--web-experimental-hot-reload" flag is deprecated ' + 'and will be removed in an upcoming release.', + ), + ); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + Logger: () => logger, + DeviceManager: () => testDeviceManager, + }, + initializeFlutterRoot: false, + ); + + testUsingContext( + 'warning triggered when web hot reload flag is passed (disabled)', + () async { + final CommandRunner runner = createTestCommandRunner( + TestRunCommandThatOnlyValidates(), + ); + await runner.run(['run', '--no-web-experimental-hot-reload']); + + expect( + testLogger.warningText, + contains( + 'Hot reload on the web is now enabled by default. ' + 'The "--no-web-experimental-hot-reload" flag is deprecated ' + 'and will be removed in an upcoming release. ' + 'If your web development workflow depends on disabling hot reload, ' + 'please open an issue explaining why at ' + 'https://github.com/dart-lang/sdk/issues/new?template=5_web_hot_reload.yml.', + ), + ); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + Logger: () => logger, + DeviceManager: () => testDeviceManager, + }, + initializeFlutterRoot: false, + ); + }); } class TestDeviceManager extends DeviceManager { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index dc598f32d162d..4bef0c334040f 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -15,7 +15,6 @@ import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/ios.dart'; -import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; @@ -1330,104 +1329,6 @@ void main() { expect(processManager, hasNoRemainingExpectations); }); - - testUsingContext( - 'skips codesigning in pre-action', - () async { - binary.createSync(recursive: true); - final Directory projectDirectory = fileSystem.systemTempDirectory.childDirectory( - 'my_project', - ); - projectDirectory.childFile('pubspec.yaml').createSync(recursive: true); - projectDirectory.childDirectory('ios').createSync(); - projectDirectory - .childDirectory('ios') - .childDirectory('Flutter') - .childDirectory('ephemeral') - .childDirectory('Packages') - .childDirectory('.packages') - .childDirectory('FlutterFramework') - .createSync(recursive: true); - - final environment = Environment.test( - fileSystem.currentDirectory, - processManager: processManager, - artifacts: artifacts, - logger: logger, - fileSystem: fileSystem, - outputDir: outputDir, - projectDir: projectDirectory, - defines: { - kIosArchs: 'arm64', - kSdkRoot: 'path/to/iPhoneOS.sdk', - kCodesignIdentity: 'ABC123', - kXcodeBuildScript: 'prepare', - }, - ); - - processManager.addCommands([ - copyPhysicalFrameworkCommand, - lipoCommandNonFatResult, - lipoVerifyArm64Command, - ]); - const Target target = DebugUnpackIOS(); - for (final Target dep in target.dependencies) { - await dep.build(environment); - } - await target.build(environment); - - expect(processManager, hasNoRemainingExpectations); - }, - overrides: { - ProcessManager: () => processManager, - FileSystem: () => fileSystem, - Artifacts: () => artifacts, - FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), - XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), - }, - ); - - testUsingContext( - 'can be skipped during build with Swift Package Manager', - () async { - binary.createSync(recursive: true); - final Directory projectDirectory = fileSystem.systemTempDirectory.childDirectory( - 'my_project', - ); - projectDirectory.childFile('pubspec.yaml').createSync(recursive: true); - projectDirectory.childDirectory('ios').createSync(); - projectDirectory - .childDirectory('ios') - .childDirectory('Flutter') - .childDirectory('ephemeral') - .childDirectory('Packages') - .childDirectory('.packages') - .childDirectory('FlutterFramework') - .createSync(recursive: true); - final environment = Environment.test( - fileSystem.currentDirectory, - processManager: processManager, - artifacts: artifacts, - logger: logger, - fileSystem: fileSystem, - outputDir: outputDir, - projectDir: projectDirectory, - defines: { - kIosArchs: 'arm64', - kSdkRoot: 'path/to/iPhoneOS.sdk', - kCodesignIdentity: 'ABC123', - kXcodeBuildScript: 'build', - }, - ); - - const Target target = DebugUnpackIOS(); - expect(await target.canSkip(environment), isTrue); - }, - overrides: { - FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), - XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), - }, - ); }); group('DebugIosLLDBInit', () { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index 102cebd75c9ee..b01b86b46d485 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -974,40 +974,6 @@ void main() { }, ); - testUsingContext( - 'can be skipped with Swift Package Manager', - () async { - final Directory projectDirectory = fileSystem.systemTempDirectory.childDirectory( - 'my_project', - ); - projectDirectory.childFile('pubspec.yaml').createSync(recursive: true); - projectDirectory.childDirectory('macos').createSync(); - projectDirectory - .childDirectory('macos') - .childDirectory('Flutter') - .childDirectory('ephemeral') - .childDirectory('Packages') - .childDirectory('.packages') - .childDirectory('FlutterFramework') - .createSync(recursive: true); - final environment = Environment.test( - fileSystem.currentDirectory, - processManager: processManager, - artifacts: artifacts, - logger: logger, - fileSystem: fileSystem, - projectDir: projectDirectory, - defines: {kXcodeBuildScript: 'build'}, - ); - const Target target = ReleaseUnpackMacOS(); - expect(await target.canSkip(environment), isTrue); - }, - overrides: { - FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), - XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), - }, - ); - group('FlutterMacOS output', () { late MemoryFileSystem testFileSystem; @@ -1019,20 +985,6 @@ void main() { .directory('macos/Flutter/ephemeral/Packages/.packages/FlutterFramework') .createSync(recursive: true); }); - - testUsingContext( - 'not included when using SwiftPM', - () async { - const Target target = ReleaseUnpackMacOS(); - expect(target.outputs.contains(kFlutterMacOSFrameworkBinarySource), isFalse); - }, - overrides: { - FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), - XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), - FileSystem: () => testFileSystem, - ProcessManager: () => FakeProcessManager.any(), - }, - ); testUsingContext( 'included when not using SwiftPM', () async { diff --git a/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart index 360ab5b220be6..0cf3fbddcf21b 100644 --- a/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart @@ -730,44 +730,6 @@ void main() { }); } - testWithoutContext( - 'resets output files containing FlutterMacOS when using SwiftPM with macOS project', - () async { - final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( - fs: testFileSystem, - fakeFlutterVersion: FakeFlutterVersion(), - ); - final swiftPackageManager = FakeSwiftPackageManager(expectedPlugins: []); - final cocoaPods = FakeCocoaPods(); - final FlutterProject project = FakeFlutterProject( - usesSwiftPackageManager: true, - fileSystem: testFileSystem, - ); - final MacOSProject xcodeProject = project.macos; - xcodeProject.xcodeProjectInfoFile.createSync(recursive: true); - xcodeProject.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); - xcodeProject.outputFileList - ..createSync(recursive: true) - ..writeAsStringSync('FlutterMacOS'); - - final dependencyManagement = DarwinDependencyManagement( - project: project, - plugins: [], - cocoapods: cocoaPods, - swiftPackageManager: swiftPackageManager, - fileSystem: testFileSystem, - featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, - analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - ); - await dependencyManagement.setUp(platform: FlutterDarwinPlatform.macos); - expect(xcodeProject.outputFileList.readAsStringSync(), isEmpty); - }, - ); - testWithoutContext( 'does not reset output files not containing FlutterMacOS when using SwiftPM with macOS project', () async { diff --git a/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart index bf2bbad6a23db..a319c954786b7 100644 --- a/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart @@ -92,6 +92,40 @@ let package = Package( ] ) '''); + + expect( + project.flutterFrameworkSwiftPackageDirectory.childFile('Package.swift').existsSync(), + isTrue, + ); + expect( + project.flutterFrameworkSwiftPackageDirectory + .childFile('Package.swift') + .readAsStringSync(), + ''' +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "FlutterFramework", + products: [ + .library(name: "FlutterFramework", targets: ["FlutterFramework"]) + ], + dependencies: [ +$_doubleIndent + ], + targets: [ + .target( + name: "FlutterFramework" + ) + ] +) +''', + ); }); testWithoutContext( diff --git a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart index aef14e12acdce..8bb21ea086426 100644 --- a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart +++ b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart @@ -271,6 +271,9 @@ class FakeTestCompiler extends TestCompiler { @override Future createCompiler() async { + // Verify that the compiler was correctly initialized with the generated + // dill for the test. + expect(buildInfo.initializeFromDill, testFilePath); return residentCompiler; } } diff --git a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart index 8383ff3026341..e724c9ee6d2cd 100644 --- a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:io' as io; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -12,7 +11,6 @@ import 'package:flutter_tools/src/base/io.dart'; import '../../bin/xcode_backend.dart'; import '../src/common.dart' hide Context; import '../src/fake_process_manager.dart'; -import '../src/io.dart'; void main() { late MemoryFileSystem fileSystem; @@ -1092,916 +1090,6 @@ void main() { expect(testContext.processManager.hasRemainingExpectations, isFalse); }); - group('when using SwiftPM', () { - test('skips embedding if already valid framework for iOS physical', () async { - final memoryFileSystem = MemoryFileSystem.test(); - final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem); - await io.IOOverrides.runWithIOOverrides(() async { - final Directory buildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-iphoneos', - )..createSync(recursive: true); - final Directory targetBuildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-iphoneos', - )..createSync(recursive: true); - final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter') - ..createSync(recursive: true); - const appPath = '/path/to/my_flutter_app'; - const platformDirPath = '$appPath/ios'; - const frameworksFolderPath = 'Runner.app/Frameworks'; - final Directory flutterSwiftPackageDir = memoryFileSystem.directory( - '$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework', - )..createSync(recursive: true); - - const matchingInfoPlist = 'asdf'; - buildDir.childFile('Flutter.framework/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - targetBuildDir.childFile('$frameworksFolderPath/Flutter.framework/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - flutterRootDir.childFile( - '${flutterRootDir.path}/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Info.plist', - ) - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - - final Directory flutterAssetsDir = targetBuildDir.childDirectory( - '$frameworksFolderPath/App.framework/flutter_assets', - )..createSync(recursive: true); - const ffiPackageName = 'package_a'; - flutterAssetsDir - .childFile('NativeAssetsManifest.json') - .writeAsStringSync( - jsonEncode({ - 'format-version': [1, 0, 0], - 'native-assets': { - 'ios_arm64': { - 'package:$ffiPackageName/native_asset.dart': [ - 'absolute', - '$ffiPackageName.framework/$ffiPackageName', - ], - }, - }, - }), - ); - const flutterBuildDir = 'build'; - final Directory nativeAssetsDir = memoryFileSystem.directory( - '$appPath/$flutterBuildDir/native_assets/ios/', - ); - nativeAssetsDir.createSync(recursive: true); - final Directory ffiPackageDir = nativeAssetsDir.childDirectory( - '$ffiPackageName.framework', - )..createSync(); - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').createSync(); - nativeAssetsDir.childFile('random.txt').createSync(); - // In addition to the ffiPackageName framework, create an additional unrelated framework in - // the same directory. It should not get copied since it is not referenced in the manifest. - final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework') - ..createSync(); - unrelatedFramework.childFile('random.txt').createSync(); - - const infoPlistPath = 'Runner.app/Info.plist'; - final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath'); - infoPlist.createSync(recursive: true); - const buildMode = 'Debug'; - final testContext = TestContext( - ['embed_and_thin', 'ios'], - { - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'CONFIGURATION': buildMode, - 'FLUTTER_ROOT': flutterRootDir.path, - 'INFOPLIST_PATH': infoPlistPath, - 'SOURCE_ROOT': platformDirPath, - 'FLUTTER_APPLICATION_PATH': appPath, - 'FLUTTER_BUILD_DIR': flutterBuildDir, - 'TARGET_BUILD_DIR': targetBuildDir.path, - 'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath, - 'EXPANDED_CODE_SIGN_IDENTITY': '12312313', - 'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path, - 'SDKROOT': 'iphoneos', - }, - commands: [ - FakeCommand( - command: [ - 'mkdir', - '-p', - '--', - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - buildDir.childDirectory('App.framework').path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - ffiPackageDir.path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').path, - '${buildDir.path}/', - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSBonjourServices', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-insert', - 'NSBonjourServices.0', - '-string', - '_dartVmService._tcp', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSLocalNetworkUsageDescription', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - ], - fileSystem: memoryFileSystem, - )..run(); - - expect(testContext.processManager.hasRemainingExpectations, isFalse); - }, flutterIOOverrides); - }); - - test('skips embedding if already valid framework for iOS simulator', () async { - final memoryFileSystem = MemoryFileSystem.test(); - final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem); - await io.IOOverrides.runWithIOOverrides(() async { - final Directory buildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-iphonesimulator', - )..createSync(recursive: true); - final Directory targetBuildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-iphonesimulator', - )..createSync(recursive: true); - final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter') - ..createSync(recursive: true); - const appPath = '/path/to/my_flutter_app'; - const platformDirPath = '$appPath/ios'; - const frameworksFolderPath = 'Runner.app/Frameworks'; - final Directory flutterSwiftPackageDir = memoryFileSystem.directory( - '$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework', - )..createSync(recursive: true); - - const matchingInfoPlist = 'asdf'; - buildDir.childFile('Flutter.framework/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - targetBuildDir.childFile('$frameworksFolderPath/Flutter.framework/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - flutterRootDir.childFile( - '${flutterRootDir.path}/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Info.plist', - ) - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - - final Directory flutterAssetsDir = targetBuildDir.childDirectory( - '$frameworksFolderPath/App.framework/flutter_assets', - )..createSync(recursive: true); - const ffiPackageName = 'package_a'; - flutterAssetsDir - .childFile('NativeAssetsManifest.json') - .writeAsStringSync( - jsonEncode({ - 'format-version': [1, 0, 0], - 'native-assets': { - 'ios_arm64': { - 'package:$ffiPackageName/native_asset.dart': [ - 'absolute', - '$ffiPackageName.framework/$ffiPackageName', - ], - }, - }, - }), - ); - const flutterBuildDir = 'build'; - final Directory nativeAssetsDir = memoryFileSystem.directory( - '$appPath/$flutterBuildDir/native_assets/ios/', - ); - nativeAssetsDir.createSync(recursive: true); - final Directory ffiPackageDir = nativeAssetsDir.childDirectory( - '$ffiPackageName.framework', - )..createSync(); - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').createSync(); - nativeAssetsDir.childFile('random.txt').createSync(); - // In addition to the ffiPackageName framework, create an additional unrelated framework in - // the same directory. It should not get copied since it is not referenced in the manifest. - final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework') - ..createSync(); - unrelatedFramework.childFile('random.txt').createSync(); - - const infoPlistPath = 'Runner.app/Info.plist'; - final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath'); - infoPlist.createSync(recursive: true); - const buildMode = 'Debug'; - final testContext = TestContext( - ['embed_and_thin', 'ios'], - { - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'CONFIGURATION': buildMode, - 'FLUTTER_ROOT': flutterRootDir.path, - 'INFOPLIST_PATH': infoPlistPath, - 'SOURCE_ROOT': platformDirPath, - 'FLUTTER_APPLICATION_PATH': appPath, - 'FLUTTER_BUILD_DIR': flutterBuildDir, - 'TARGET_BUILD_DIR': targetBuildDir.path, - 'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath, - 'EXPANDED_CODE_SIGN_IDENTITY': '12312313', - 'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path, - 'SDKROOT': 'iphonesimulator', - }, - commands: [ - FakeCommand( - command: [ - 'mkdir', - '-p', - '--', - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - buildDir.childDirectory('App.framework').path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - ffiPackageDir.path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').path, - '${buildDir.path}/', - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSBonjourServices', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-insert', - 'NSBonjourServices.0', - '-string', - '_dartVmService._tcp', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSLocalNetworkUsageDescription', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - ], - fileSystem: memoryFileSystem, - )..run(); - - expect(testContext.processManager.hasRemainingExpectations, isFalse); - }, flutterIOOverrides); - }); - - test('skips embedding if already valid framework for macos', () async { - final memoryFileSystem = MemoryFileSystem.test(); - final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem); - await io.IOOverrides.runWithIOOverrides(() async { - final Directory buildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-macosx', - )..createSync(recursive: true); - final Directory targetBuildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-macosx', - )..createSync(recursive: true); - final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter') - ..createSync(recursive: true); - const appPath = '/path/to/my_flutter_app'; - const platformDirPath = '$appPath/macos'; - const frameworksFolderPath = 'Runner.app/Frameworks'; - final Directory flutterSwiftPackageDir = memoryFileSystem.directory( - '$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework', - )..createSync(recursive: true); - - const matchingInfoPlist = 'asdf'; - buildDir.childFile('FlutterMacOS.framework/Resources/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - targetBuildDir.childFile( - '$frameworksFolderPath/FlutterMacOS.framework/Resources/Info.plist', - ) - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - flutterRootDir.childFile( - '${flutterRootDir.path}/bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Resources/Info.plist', - ) - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - - final Directory flutterAssetsDir = targetBuildDir.childDirectory( - '$frameworksFolderPath/App.framework/Resources/flutter_assets', - )..createSync(recursive: true); - const ffiPackageName = 'package_a'; - flutterAssetsDir - .childFile('NativeAssetsManifest.json') - .writeAsStringSync( - jsonEncode({ - 'format-version': [1, 0, 0], - 'native-assets': { - 'macos_arm64': { - 'package:$ffiPackageName/native_asset.dart': [ - 'absolute', - '$ffiPackageName.framework/$ffiPackageName', - ], - }, - }, - }), - ); - const flutterBuildDir = 'build'; - final Directory nativeAssetsDir = memoryFileSystem.directory( - '$appPath/$flutterBuildDir/native_assets/macos/', - ); - nativeAssetsDir.createSync(recursive: true); - final Directory ffiPackageDir = nativeAssetsDir.childDirectory( - '$ffiPackageName.framework', - )..createSync(); - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').createSync(); - nativeAssetsDir.childFile('random.txt').createSync(); - // In addition to the ffiPackageName framework, create an additional unrelated framework in - // the same directory. It should not get copied since it is not referenced in the manifest. - final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework') - ..createSync(); - unrelatedFramework.childFile('random.txt').createSync(); - - const infoPlistPath = 'Runner.app/Info.plist'; - final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath'); - infoPlist.createSync(recursive: true); - const buildMode = 'Debug'; - const codesignIdentity = '12312313'; - final testContext = TestContext( - ['embed_and_thin', 'macos'], - { - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'CONFIGURATION': buildMode, - 'FLUTTER_ROOT': flutterRootDir.path, - 'INFOPLIST_PATH': infoPlistPath, - 'SOURCE_ROOT': platformDirPath, - 'FLUTTER_APPLICATION_PATH': appPath, - 'FLUTTER_BUILD_DIR': flutterBuildDir, - 'TARGET_BUILD_DIR': targetBuildDir.path, - 'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath, - 'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path, - 'SDKROOT': 'macosx', - 'EXPANDED_CODE_SIGN_IDENTITY': codesignIdentity, - }, - commands: [ - FakeCommand( - command: [ - 'mkdir', - '-p', - '--', - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - buildDir.childDirectory('App.framework').path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'codesign', - '--force', - '--verbose', - '--sign', - codesignIdentity, - '--', - targetBuildDir - .childDirectory(frameworksFolderPath) - .childFile('App.framework/App') - .path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - ffiPackageDir.path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'codesign', - '--force', - '--verbose', - '--sign', - codesignIdentity, - '--', - targetBuildDir - .childDirectory(frameworksFolderPath) - .childFile('$ffiPackageName.framework/$ffiPackageName') - .path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').path, - '${buildDir.path}/', - ], - ), - ], - fileSystem: memoryFileSystem, - )..run(); - - expect(testContext.processManager.hasRemainingExpectations, isFalse); - }, flutterIOOverrides); - }); - - test('unpacks before embedding if not valid framework for iOS', () async { - final memoryFileSystem = MemoryFileSystem.test(); - final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem); - await io.IOOverrides.runWithIOOverrides(() async { - final Directory buildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-iphoneos', - )..createSync(recursive: true); - final Directory targetBuildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-iphoneos', - )..createSync(recursive: true); - final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter') - ..createSync(recursive: true); - const appPath = '/path/to/my_flutter_app'; - const platformDirPath = '$appPath/ios'; - const frameworksFolderPath = 'Runner.app/Frameworks'; - final Directory flutterSwiftPackageDir = memoryFileSystem.directory( - '$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework', - )..createSync(recursive: true); - - const matchingInfoPlist = 'asdf'; - buildDir.childFile('Flutter.framework/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync('not matching'); - targetBuildDir.childFile('$frameworksFolderPath/Flutter.framework/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - flutterRootDir.childFile( - '${flutterRootDir.path}/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Info.plist', - ) - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - - final Directory flutterAssetsDir = targetBuildDir.childDirectory( - '$frameworksFolderPath/App.framework/flutter_assets', - )..createSync(recursive: true); - const ffiPackageName = 'package_a'; - flutterAssetsDir - .childFile('NativeAssetsManifest.json') - .writeAsStringSync( - jsonEncode({ - 'format-version': [1, 0, 0], - 'native-assets': { - 'ios_arm64': { - 'package:$ffiPackageName/native_asset.dart': [ - 'absolute', - '$ffiPackageName.framework/$ffiPackageName', - ], - }, - }, - }), - ); - const flutterBuildDir = 'build'; - final Directory nativeAssetsDir = memoryFileSystem.directory( - '$appPath/$flutterBuildDir/native_assets/ios/', - ); - nativeAssetsDir.createSync(recursive: true); - final Directory ffiPackageDir = nativeAssetsDir.childDirectory( - '$ffiPackageName.framework', - )..createSync(); - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').createSync(); - nativeAssetsDir.childFile('random.txt').createSync(); - // In addition to the ffiPackageName framework, create an additional unrelated framework in - // the same directory. It should not get copied since it is not referenced in the manifest. - final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework') - ..createSync(); - unrelatedFramework.childFile('random.txt').createSync(); - const infoPlistPath = 'Runner.app/Info.plist'; - final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath'); - infoPlist.createSync(recursive: true); - const buildMode = 'Debug'; - final testContext = TestContext( - ['embed_and_thin', 'ios'], - { - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'CONFIGURATION': buildMode, - 'FLUTTER_ROOT': flutterRootDir.path, - 'INFOPLIST_PATH': infoPlistPath, - 'SOURCE_ROOT': platformDirPath, - 'FLUTTER_APPLICATION_PATH': appPath, - 'FLUTTER_BUILD_DIR': flutterBuildDir, - 'TARGET_BUILD_DIR': targetBuildDir.path, - 'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath, - 'EXPANDED_CODE_SIGN_IDENTITY': '12312313', - 'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path, - 'SDKROOT': 'iphoneos', - }, - commands: [ - FakeCommand( - command: [ - 'mkdir', - '-p', - '--', - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - buildDir.childDirectory('App.framework').path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - '${flutterRootDir.path}/bin/flutter', - 'assemble', - '--no-version-check', - '--output=${buildDir.path}/', - '-dTargetPlatform=ios', - '-dTargetFile=lib/main.dart', - '-dBuildMode=debug', - '-dConfiguration=Debug', - '-dIosArchs=', - '-dSdkRoot=iphoneos', - '-dSplitDebugInfo=', - '-dTreeShakeIcons=', - '-dTrackWidgetCreation=', - '-dDartObfuscation=', - '-dAction=', - '-dFrontendServerStarterPath=', - '--ExtraGenSnapshotOptions=', - '--DartDefines=', - '--ExtraFrontEndOptions=', - '-dSrcRoot=', - '-dXcodeBuildScript=embed', - '-dTargetDeviceOSVersion=', - '-dCodesignIdentity=12312313', - 'debug_unpack_ios', - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - buildDir.childDirectory('Flutter.framework').path, - '${targetBuildDir.childDirectory(frameworksFolderPath).path}/', - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - ffiPackageDir.path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').path, - '${buildDir.path}/', - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSBonjourServices', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-insert', - 'NSBonjourServices.0', - '-string', - '_dartVmService._tcp', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSLocalNetworkUsageDescription', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - ], - fileSystem: memoryFileSystem, - )..run(); - - expect(testContext.processManager.hasRemainingExpectations, isFalse); - }, flutterIOOverrides); - }); - - test('unpacks before embedding if not valid framework for macos', () async { - final memoryFileSystem = MemoryFileSystem.test(); - final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem); - await io.IOOverrides.runWithIOOverrides(() async { - final Directory buildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-macosx', - )..createSync(recursive: true); - final Directory targetBuildDir = memoryFileSystem.directory( - '/path/to/Build/Products/Debug-macosx', - )..createSync(recursive: true); - final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter') - ..createSync(recursive: true); - const appPath = '/path/to/my_flutter_app'; - const platformDirPath = '$appPath/macos'; - const frameworksFolderPath = 'Runner.app/Frameworks'; - final Directory flutterSwiftPackageDir = memoryFileSystem.directory( - '$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework', - )..createSync(recursive: true); - - const matchingInfoPlist = 'asdf'; - buildDir.childFile('FlutterMacOS.framework/Resources/Info.plist') - ..createSync(recursive: true) - ..writeAsStringSync('not matching'); - targetBuildDir.childFile( - '$frameworksFolderPath/FlutterMacOS.framework/Resources/Info.plist', - ) - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - flutterRootDir.childFile( - '${flutterRootDir.path}/bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Resources/Info.plist', - ) - ..createSync(recursive: true) - ..writeAsStringSync(matchingInfoPlist); - - final Directory flutterAssetsDir = targetBuildDir.childDirectory( - '$frameworksFolderPath/App.framework/Resources/flutter_assets', - )..createSync(recursive: true); - const ffiPackageName = 'package_a'; - flutterAssetsDir - .childFile('NativeAssetsManifest.json') - .writeAsStringSync( - jsonEncode({ - 'format-version': [1, 0, 0], - 'native-assets': { - 'macos_arm64': { - 'package:$ffiPackageName/native_asset.dart': [ - 'absolute', - '$ffiPackageName.framework/$ffiPackageName', - ], - }, - }, - }), - ); - const flutterBuildDir = 'build'; - final Directory nativeAssetsDir = memoryFileSystem.directory( - '$appPath/$flutterBuildDir/native_assets/macos/', - ); - nativeAssetsDir.createSync(recursive: true); - final Directory ffiPackageDir = nativeAssetsDir.childDirectory( - '$ffiPackageName.framework', - )..createSync(); - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').createSync(); - nativeAssetsDir.childFile('random.txt').createSync(); - // In addition to the ffiPackageName framework, create an additional unrelated framework in - // the same directory. It should not get copied since it is not referenced in the manifest. - final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework') - ..createSync(); - unrelatedFramework.childFile('random.txt').createSync(); - const infoPlistPath = 'Runner.app/Info.plist'; - final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath'); - infoPlist.createSync(recursive: true); - const buildMode = 'Debug'; - final testContext = TestContext( - ['embed_and_thin', 'macos'], - { - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'CONFIGURATION': buildMode, - 'FLUTTER_ROOT': flutterRootDir.path, - 'INFOPLIST_PATH': infoPlistPath, - 'SOURCE_ROOT': platformDirPath, - 'FLUTTER_APPLICATION_PATH': appPath, - 'FLUTTER_BUILD_DIR': flutterBuildDir, - 'TARGET_BUILD_DIR': targetBuildDir.path, - 'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath, - 'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path, - 'SDKROOT': 'macosx', - }, - commands: [ - FakeCommand( - command: [ - 'mkdir', - '-p', - '--', - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - buildDir.childDirectory('App.framework').path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - '${flutterRootDir.path}/bin/flutter', - 'assemble', - '--no-version-check', - '--output=${buildDir.path}/', - '-dTargetPlatform=darwin', - '-dTargetFile=lib/main.dart', - '-dBuildMode=debug', - '-dConfiguration=Debug', - '-dDarwinArchs=', - '-dSdkRoot=macosx', - '-dSplitDebugInfo=', - '-dTreeShakeIcons=', - '-dTrackWidgetCreation=', - '-dDartObfuscation=', - '-dAction=', - '-dFrontendServerStarterPath=', - '--ExtraGenSnapshotOptions=', - '--DartDefines=', - '--ExtraFrontEndOptions=', - '-dSrcRoot=', - '-dXcodeBuildScript=embed', - 'debug_unpack_macos', - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - '--filter', - '- Headers', - '--filter', - '- Modules', - buildDir.childDirectory('FlutterMacOS.framework').path, - '${targetBuildDir.childDirectory(frameworksFolderPath).path}/', - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - ffiPackageDir.path, - targetBuildDir.childDirectory(frameworksFolderPath).path, - ], - ), - FakeCommand( - command: [ - 'rsync', - '-8', - '-av', - '--delete', - '--filter', - '- .DS_Store', - nativeAssetsDir.childDirectory('$ffiPackageName.framework.dSYM').path, - '${buildDir.path}/', - ], - ), - ], - fileSystem: memoryFileSystem, - )..run(); - - expect(testContext.processManager.hasRemainingExpectations, isFalse); - }, flutterIOOverrides); - }); - }); - test('reports error message for invalid native assets manifest', () { final Directory buildDir = fileSystem.directory('/path/to/Build/Products/Debug') ..createSync(recursive: true); diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_new_output_dir_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_new_output_dir_test.dart index c59992e7f4cc6..91670ab2a64b0 100644 --- a/packages/flutter_tools/test/integration.shard/android_plugin_new_output_dir_test.dart +++ b/packages/flutter_tools/test/integration.shard/android_plugin_new_output_dir_test.dart @@ -49,6 +49,9 @@ dependencies: ...getLocalEngineArguments(), 'build', 'aar', + '--no-debug', + '--no-profile', + '--target-platform=android-arm', ], workingDirectory: projectPath); diff --git a/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart b/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart index 2508590e483fb..9e61555f9a5fc 100644 --- a/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart +++ b/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart @@ -46,12 +46,13 @@ void main() { ...getLocalEngineArguments(), 'build', 'apk', + '-v', '--target-platform=android-arm', '--config-only', ], workingDirectory: exampleAppDir.path); expect(gradleFile, exists); - expect(result.stdout, contains(RegExp(r'Config complete'))); - expect(result.stdout, isNot(contains(RegExp(r'Running Gradle task')))); + expect(result.stdout, isNot(contains('Running Gradle task'))); + expect(result.stdout, isNot(contains('assembleRelease'))); }); } diff --git a/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart b/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart index 08cbddd349f17..9100a9b6e4b06 100644 --- a/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart +++ b/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart @@ -382,9 +382,8 @@ void main() { '-dXcodeBuildScript=prepare', '$unpackTarget: Starting due to', '-dXcodeBuildScript=build', - 'Skipping target: $unpackTarget', ], - unexpectedLines: [], + unexpectedLines: ['Skipping target: $unpackTarget'], ); await SwiftPackageManagerUtils.buildApp( diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart index 283e0fa68245d..dbf8b238ad7f7 100644 --- a/packages/flutter_tools/test/src/common.dart +++ b/packages/flutter_tools/test/src/common.dart @@ -52,7 +52,7 @@ String getFlutterRoot() { } Error invalidScript() => StateError( - 'Could not determine flutter_tools/ path from script URL (${globals.platform.script}); consider setting FLUTTER_ROOT explicitly.', + 'Could not determine flutter_tools/ path from script URL (${platform.script}); consider setting FLUTTER_ROOT explicitly.', ); Uri scriptUri; diff --git a/pubspec.lock b/pubspec.lock index e26a299172262..34d818d8fcc66 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -350,10 +350,10 @@ packages: dependency: "direct main" description: name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" html: dependency: "direct main" description: @@ -955,10 +955,10 @@ packages: dependency: "direct main" description: name: video_player_avfoundation - sha256: "7cc0a9257103851eb299a2407e895b0fd6832d323dcfde622a23cdc25a1de269" + sha256: f46e9e20f1fe429760cf4dc118761336320d1bec0f50d255930c2355f2defb5b url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.9.1" video_player_platform_interface: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4bb50d6bab60f..bc75043477e4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -115,7 +115,7 @@ dependencies: google_mobile_ads: 5.1.0 googleapis: 14.0.0 googleapis_auth: 2.0.0 - hooks: 1.0.0 + hooks: 1.0.1 html: 0.15.6 http: 1.6.0 http_multi_server: 3.2.2 @@ -186,7 +186,7 @@ dependencies: vector_math: 2.2.0 video_player: 2.10.1 video_player_android: 2.9.1 - video_player_avfoundation: 2.9.0 + video_player_avfoundation: 2.9.1 video_player_platform_interface: 6.6.0 video_player_web: 2.4.0 vm_service: 15.0.2 @@ -216,4 +216,4 @@ dependencies: dev_dependencies: ffigen: 20.1.1 -# PUBSPEC CHECKSUM: ag2v9d +# PUBSPEC CHECKSUM: 7759h0