From c4d08b7364b6dec482badf4ad0f158974e71594f Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 31 Jul 2026 10:06:02 -0700 Subject: [PATCH 001/330] Use devicectl for screenshots on Xcode 27, remove idevicescreenshot artifact (#189091) On Xcode 27 use `devicectl device capture screenshot` instead of idevicescreenshot, which stopped working a few years ago in iOS 17 / Xcode 15. This will reach stable after Xcode 27 releases. 1. Add the `devicectl device capture` code for Xcode 27. 2. Change the < Xcode 27 fallback to `toolExit` with: "`flutter screenshot` requires Xcode 27 or higher." Since Xcode 15 is our minimum now as of https://github.com/flutter/flutter/pull/180531, `idevicescreenshot` doesn't work with any configuration with latest Flutter so there's no reason to keep around the fallbacks if < Xcode 27. 3. Remove `_iMobileDevice.takeScreenshot` and `idevicescreenshot` artifact. Note I left in the `run_verify_binaries_codesigned_tests` check because the binary will still be shipping in the libimobiledevice tar ball until we remove it from [the recipe](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/ios_usb_dependencies/ios-usb-dependencies.py#48). https://github.com/flutter/flutter/blob/cf9e8afe9a5e601158517782b5b824a328bb2c68/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart#L154 Related to https://github.com/flutter/flutter/issues/6118 Fixes https://github.com/flutter/flutter/issues/128598 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> --- packages/flutter_tools/lib/src/artifacts.dart | 3 +- .../lib/src/commands/screenshot.dart | 2 + .../flutter_tools/lib/src/flutter_cache.dart | 2 +- .../lib/src/ios/core_devices.dart | 54 +++++ .../flutter_tools/lib/src/ios/devices.dart | 40 +++- packages/flutter_tools/lib/src/ios/mac.dart | 27 +-- .../test/general.shard/cache_test.dart | 5 +- .../general.shard/ios/core_devices_test.dart | 127 ++++++++++++ .../test/general.shard/ios/devices_test.dart | 196 +++++++++++++++++- .../test/general.shard/ios/mac_test.dart | 79 ------- 10 files changed, 414 insertions(+), 121 deletions(-) diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index 2c4d4b731b535..96f0b12a99f73 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -161,9 +161,10 @@ enum HostArtifact { iosDeploy('ios-deploy'), idevicesyslog('idevicesyslog'), - idevicescreenshot('idevicescreenshot'), iproxy('iproxy'), + idevicescreenshot('idevicescreenshot'), + /// The root of the sky_engine package. skyEnginePath('sky_engine'), diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart index 7141adbd67999..44c20038a8379 100644 --- a/packages/flutter_tools/lib/src/commands/screenshot.dart +++ b/packages/flutter_tools/lib/src/commands/screenshot.dart @@ -125,6 +125,8 @@ class ScreenshotCommand extends FlutterCommand { try { await device!.takeScreenshot(outputFile); + } on ToolExit { + rethrow; } on Exception catch (error) { throwToolExit('Error taking screenshot: $error'); } diff --git a/packages/flutter_tools/lib/src/flutter_cache.dart b/packages/flutter_tools/lib/src/flutter_cache.dart index bf144dbc2a5e5..606fcdc4527f3 100644 --- a/packages/flutter_tools/lib/src/flutter_cache.dart +++ b/packages/flutter_tools/lib/src/flutter_cache.dart @@ -823,7 +823,7 @@ class IosUsbArtifacts extends CachedArtifact { // used for additional download checks below, so we can re-download if they are // missing. static const _kExecutables = >{ - 'libimobiledevice': ['idevicescreenshot', 'idevicesyslog'], + 'libimobiledevice': ['idevicesyslog'], 'libusbmuxd': ['iproxy'], }; diff --git a/packages/flutter_tools/lib/src/ios/core_devices.dart b/packages/flutter_tools/lib/src/ios/core_devices.dart index f0c96f0a3c5fa..90796f6a11425 100644 --- a/packages/flutter_tools/lib/src/ios/core_devices.dart +++ b/packages/flutter_tools/lib/src/ios/core_devices.dart @@ -991,6 +991,60 @@ class IOSCoreDeviceControl { IOSCoreDeviceRunningProcess.fromJson(processObject), ]; } + + /// Captures a screenshot from the device and saves it to the destination. + /// + /// Returns true if successfully able to take screenshot. + Future takeScreenshot({required String deviceId, required String destination}) async { + if (!_xcode.isDevicectlInstalled) { + _logger.printError('devicectl is not installed.'); + return false; + } + + final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.'); + final File output = tempDirectory.childFile('screenshot_results.json'); + output.createSync(); + + final command = [ + ..._xcode.xcrunCommand(), + 'devicectl', + 'device', + 'capture', + 'screenshot', + '--device', + deviceId, + '--destination', + destination, + '--json-output', + output.path, + ]; + + try { + await _processUtils.run(command, throwOnError: true); + final String stringOutput = output.readAsStringSync(); + + try { + final Object? decoded = json.decode(stringOutput); + if (decoded is Map) { + final Object? decodeResult = decoded['info']; + if (decodeResult is Map && decodeResult['outcome'] == 'success') { + return true; + } + } + _logger.printError('devicectl returned unexpected JSON response: $stringOutput'); + return false; + } on FormatException { + _logger.printError('devicectl returned non-JSON response: $stringOutput'); + return false; + } + } finally { + try { + tempDirectory.deleteSync(recursive: true); + } on FileSystemException { + // Ignore. + } + } + } } class IOSCoreDevice { diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 48f9fa1ebdb70..c74e1f38a5ce9 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -1073,8 +1073,7 @@ class IOSDevice extends Device { // However, it doesn't work reliably until Xcode 26. // Use LLDB if Xcode version is greater than 26 and the feature is enabled. final Version? xcodeVersion = globals.xcode?.currentVersion; - final bool lldbFeatureEnabled = featureFlags.isLLDBDebuggingEnabled; - if (xcodeVersion != null && xcodeVersion.major >= 26 && lldbFeatureEnabled) { + if (xcodeVersion != null && xcodeVersion.major >= 26 && featureFlags.isLLDBDebuggingEnabled) { final DeviceLogReader deviceLogReader = getLogReader( app: package, usingCISystem: debuggingOptions.usingCISystem, @@ -1323,17 +1322,42 @@ class IOSDevice extends Device { @override bool get supportsScreenshot { - if (isCoreDevice) { - // `idevicescreenshot` stopped working with iOS 17 / Xcode 15 - // (https://github.com/flutter/flutter/issues/128598). - return false; + final Version? xcodeVersion = globals.xcode?.currentVersion; + if (isCoreDevice && xcodeVersion != null && xcodeVersion.major >= 27) { + return globals.xcode!.isDevicectlInstalled; } - return _iMobileDevice.isInstalled; + return false; } @override Future takeScreenshot(File outputFile) async { - await _iMobileDevice.takeScreenshot(outputFile, id, connectionInterface); + final Version? xcodeVersion = globals.xcode?.currentVersion; + if (isCoreDevice && xcodeVersion != null && xcodeVersion.major >= 27) { + var success = false; + try { + success = await _coreDeviceControl.takeScreenshot( + deviceId: id, + destination: outputFile.path, + ); + } on Exception catch (error) { + final errorMessage = error.toString(); + if (errorMessage.contains('CoreDeviceError error 4000') || + errorMessage.contains('CoreDeviceError error 4016') || + errorMessage.contains('RemotePairingError error 2') || + errorMessage.contains('Connection was invalidated')) { + throwToolExit( + 'Failed to establish a connection to the device. ' + 'Please make sure the device is available and try again.', + ); + } + throwToolExit('Failed to take screenshot with devicectl: $error'); + } + if (success) { + return; + } + throwToolExit('Failed to take screenshot with devicectl.'); + } + throwToolExit('flutter screenshot requires Xcode 27 or higher.'); } @override diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 4a4fa844df7f6..4d30668e5d174 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -79,10 +79,8 @@ class IMobileDevice { required ProcessManager processManager, required Logger logger, }) : _idevicesyslogPath = artifacts.getHostArtifact(HostArtifact.idevicesyslog).path, - _idevicescreenshotPath = artifacts.getHostArtifact(HostArtifact.idevicescreenshot).path, _dyLdLibEntry = cache.dyLdLibEntry, - _processUtils = ProcessUtils(logger: logger, processManager: processManager), - _processManager = processManager; + _processUtils = ProcessUtils(logger: logger, processManager: processManager); /// Create an [IMobileDevice] for testing. factory IMobileDevice.test({required ProcessManager processManager}) { @@ -96,13 +94,9 @@ class IMobileDevice { } final String _idevicesyslogPath; - final String _idevicescreenshotPath; final MapEntry _dyLdLibEntry; - final ProcessManager _processManager; final ProcessUtils _processUtils; - late final bool isInstalled = _processManager.canRun(_idevicescreenshotPath); - /// Starts `idevicesyslog` and returns the running process. Future startLogger(String deviceID, bool isWirelesslyConnected) { return _processUtils.start([ @@ -112,25 +106,6 @@ class IMobileDevice { if (isWirelesslyConnected) '--network', ], environment: Map.fromEntries(>[_dyLdLibEntry])); } - - /// Captures a screenshot to the specified outputFile. - Future takeScreenshot( - File outputFile, - String deviceID, - DeviceConnectionInterface interfaceType, - ) { - return _processUtils.run( - [ - _idevicescreenshotPath, - outputFile.path, - '--udid', - deviceID, - if (interfaceType == DeviceConnectionInterface.wireless) '--network', - ], - throwOnError: true, - environment: Map.fromEntries(>[_dyLdLibEntry]), - ); - } } Future buildXcodeProject({ diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart index ce8133a794701..a2936dcc97e69 100644 --- a/packages/flutter_tools/test/general.shard/cache_test.dart +++ b/packages/flutter_tools/test/general.shard/cache_test.dart @@ -993,13 +993,12 @@ void main() { platform: FakePlatform(operatingSystem: 'macos'), ); iosUsbArtifacts.location.createSync(); - final File ideviceScreenshotFile = iosUsbArtifacts.location.childFile('idevicescreenshot') + final File ideviceSyslogFile = iosUsbArtifacts.location.childFile('idevicesyslog') ..createSync(); - iosUsbArtifacts.location.childFile('idevicesyslog').createSync(); expect(iosUsbArtifacts.isUpToDateInner(fileSystem), true); - ideviceScreenshotFile.deleteSync(); + ideviceSyslogFile.deleteSync(); expect(iosUsbArtifacts.isUpToDateInner(fileSystem), false); }, diff --git a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart index f73621ed93bd9..0fb813bb4d0f4 100644 --- a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart @@ -953,6 +953,16 @@ void main() { expect(logger.errorText, contains('devicectl is not installed.')); expect(status, isFalse); }); + + testWithoutContext('fails to take screenshot', () async { + final bool status = await deviceControl.takeScreenshot( + deviceId: 'device-id', + destination: '/path/to/screenshot.png', + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(logger.errorText, contains('devicectl is not installed.')); + expect(status, isFalse); + }); }); }); @@ -3887,6 +3897,116 @@ invalid JSON expect(logger.traceText, contains('Error reading output file')); }); }); + + group('take screenshot', () { + const deviceId = 'device-id'; + const destination = '/path/to/screenshot.png'; + + testWithoutContext('Successful screenshot', () async { + const deviceControlOutput = + ''' +{ + "info" : { + "arguments" : [ + "devicectl", + "device", + "capture", + "screenshot", + "--device", + "$deviceId", + "--destination", + "$destination" + ], + "outcome" : "success" + } +} +'''; + final File tempFile = fileSystem.systemTempDirectory + .childDirectory('core_devices.rand0') + .childFile('screenshot_results.json'); + fakeProcessManager.addCommand( + FakeCommand( + command: [ + 'xcrun', + 'devicectl', + 'device', + 'capture', + 'screenshot', + '--device', + deviceId, + '--destination', + destination, + '--json-output', + tempFile.path, + ], + onRun: (_) { + tempFile.writeAsStringSync(deviceControlOutput); + }, + ), + ); + + final bool success = await deviceControl.takeScreenshot( + deviceId: deviceId, + destination: destination, + ); + expect(success, isTrue); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(tempFile, isNot(exists)); + }); + + testWithoutContext('failed screenshot', () async { + const deviceControlOutput = + ''' +{ + "info" : { + "arguments" : [ + "devicectl", + "device", + "capture", + "screenshot", + "--device", + "$deviceId", + "--destination", + "$destination" + ], + "outcome" : "failure" + } +} +'''; + final File tempFile = fileSystem.systemTempDirectory + .childDirectory('core_devices.rand0') + .childFile('screenshot_results.json'); + fakeProcessManager.addCommand( + FakeCommand( + command: [ + 'xcrun', + 'devicectl', + 'device', + 'capture', + 'screenshot', + '--device', + deviceId, + '--destination', + destination, + '--json-output', + tempFile.path, + ], + onRun: (_) { + tempFile.writeAsStringSync(deviceControlOutput); + }, + ), + ); + + final bool success = await deviceControl.takeScreenshot( + deviceId: deviceId, + destination: destination, + ); + expect(success, isFalse); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(tempFile, isNot(exists)); + expect(logger.errorText, contains('devicectl returned unexpected JSON response')); + }); + }); }); } @@ -3898,6 +4018,7 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { this.launchResult, this.terminateSuccess = true, this.runningProcesses = const [], + this.takeScreenshotSuccess = true, }); bool installSuccess; @@ -3908,6 +4029,12 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { int? processTerminated; List runningProcesses; bool get terminateProcessCalled => processTerminated != null; + bool takeScreenshotSuccess; + + @override + Future takeScreenshot({required String deviceId, required String destination}) async { + return takeScreenshotSuccess; + } @override Future> getCoreDevices({ diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 2ae98c8f8a4ad..45dae90a19898 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device_port_forwarder.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/application_package.dart'; import 'package:flutter_tools/src/ios/core_devices.dart'; import 'package:flutter_tools/src/ios/devices.dart'; @@ -32,7 +33,7 @@ import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; -import '../../src/fake_process_manager.dart'; +import '../../src/context.dart'; void main() { final macPlatform = FakePlatform(operatingSystem: 'macos'); @@ -582,6 +583,176 @@ void main() { expect(process3.killed, true); }); }); + + group('screenshot', () { + late FakeIOSCoreDeviceControl fakeCoreDeviceControl; + late IOSDevice device; + late File outputFile; + + setUp(() { + fakeCoreDeviceControl = coreDeviceControl as FakeIOSCoreDeviceControl; + outputFile = fileSystem.file('screenshot.png'); + }); + + testUsingContext('supportsScreenshot is false on CoreDevice with Xcode < 27', () async { + device = IOSDevice( + 'device-123', + iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), + fileSystem: fileSystem, + logger: logger, + platform: macPlatform, + iosDeploy: iosDeploy, + analytics: FakeAnalytics(), + iMobileDevice: iMobileDevice, + coreDeviceControl: fakeCoreDeviceControl, + coreDeviceLauncher: coreDeviceLauncher, + xcodeDebug: xcodeDebug, + name: 'iPhone 1', + sdkVersion: '17.0', + cpuArch: CpuArch.arm64, + connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, + isPaired: true, + devModeEnabled: true, + isCoreDevice: true, + ); + + expect(device.supportsScreenshot, isFalse); + }, overrides: {Xcode: () => FakeXcode(currentVersion: Version(15, 0, 0))}); + + testUsingContext( + 'supportsScreenshot is true on CoreDevice with Xcode 27+ and devicectl installed', + () async { + device = IOSDevice( + 'device-123', + iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), + fileSystem: fileSystem, + logger: logger, + platform: macPlatform, + iosDeploy: iosDeploy, + analytics: FakeAnalytics(), + iMobileDevice: iMobileDevice, + coreDeviceControl: fakeCoreDeviceControl, + coreDeviceLauncher: coreDeviceLauncher, + xcodeDebug: xcodeDebug, + name: 'iPhone 1', + sdkVersion: '17.0', + cpuArch: CpuArch.arm64, + connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, + isPaired: true, + devModeEnabled: true, + isCoreDevice: true, + ); + + final fakeXcode = globals.xcode! as FakeXcode; + fakeXcode.isDevicectlInstalled = true; + expect(device.supportsScreenshot, isTrue); + + fakeXcode.isDevicectlInstalled = false; + expect(device.supportsScreenshot, isFalse); + }, + overrides: {Xcode: () => FakeXcode(currentVersion: Version(27, 0, 0))}, + ); + + testUsingContext('takeScreenshot uses devicectl on CoreDevice with Xcode 27+', () async { + device = IOSDevice( + 'device-123', + iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), + fileSystem: fileSystem, + logger: logger, + platform: macPlatform, + iosDeploy: iosDeploy, + analytics: FakeAnalytics(), + iMobileDevice: iMobileDevice, + coreDeviceControl: fakeCoreDeviceControl, + coreDeviceLauncher: coreDeviceLauncher, + xcodeDebug: xcodeDebug, + name: 'iPhone 1', + sdkVersion: '17.0', + cpuArch: CpuArch.arm64, + connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, + isPaired: true, + devModeEnabled: true, + isCoreDevice: true, + ); + + fakeCoreDeviceControl.takeScreenshotSuccess = true; + await device.takeScreenshot(outputFile); + + fakeCoreDeviceControl.takeScreenshotSuccess = false; + expect(() => device.takeScreenshot(outputFile), throwsToolExit()); + }, overrides: {Xcode: () => FakeXcode(currentVersion: Version(27, 0, 0))}); + + testUsingContext( + 'takeScreenshot throws a ToolExit with actionable message when CoreDevice is locked/unreachable', + () async { + device = IOSDevice( + 'device-123', + iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), + fileSystem: fileSystem, + logger: logger, + platform: macPlatform, + iosDeploy: iosDeploy, + analytics: FakeAnalytics(), + iMobileDevice: iMobileDevice, + coreDeviceControl: fakeCoreDeviceControl, + coreDeviceLauncher: coreDeviceLauncher, + xcodeDebug: xcodeDebug, + name: 'iPhone 1', + sdkVersion: '17.0', + cpuArch: CpuArch.arm64, + connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, + isPaired: true, + devModeEnabled: true, + isCoreDevice: true, + ); + + fakeCoreDeviceControl.takeScreenshotException = Exception( + 'ERROR: A connection to this device could not be established. (com.apple.dt.CoreDeviceError error 4000 (0xFA0))', + ); + expect( + () => device.takeScreenshot(outputFile), + throwsToolExit( + message: + 'Failed to establish a connection to the device. Please make sure the device is available and try again.', + ), + ); + }, + overrides: {Xcode: () => FakeXcode(currentVersion: Version(27, 0, 0))}, + ); + + testUsingContext('takeScreenshot throws ToolExit on CoreDevice with Xcode < 27', () async { + device = IOSDevice( + 'device-123', + iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), + fileSystem: fileSystem, + logger: logger, + platform: macPlatform, + iosDeploy: iosDeploy, + analytics: FakeAnalytics(), + iMobileDevice: iMobileDevice, + coreDeviceControl: fakeCoreDeviceControl, + coreDeviceLauncher: coreDeviceLauncher, + xcodeDebug: xcodeDebug, + name: 'iPhone 1', + sdkVersion: '17.0', + cpuArch: CpuArch.arm64, + connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, + isPaired: true, + devModeEnabled: true, + isCoreDevice: true, + ); + + expect( + () => device.takeScreenshot(outputFile), + throwsToolExit(message: 'flutter screenshot requires Xcode 27 or higher.'), + ); + }, overrides: {Xcode: () => FakeXcode(currentVersion: Version(26, 0, 0))}); + }); }); group('polling', () { @@ -1161,10 +1332,29 @@ class FakeXcodeDebug extends Fake implements XcodeDebug { bool get debugStarted => false; } -class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {} +class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { + bool takeScreenshotSuccess = true; + Exception? takeScreenshotException; + + @override + Future takeScreenshot({required String deviceId, required String destination}) async { + if (takeScreenshotException != null) { + throw takeScreenshotException!; + } + return takeScreenshotSuccess; + } +} class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} class FakeAnalytics extends Fake implements Analytics {} -class FakeXcode extends Fake implements Xcode {} +class FakeXcode extends Fake implements Xcode { + FakeXcode({this.currentVersion, this.isDevicectlInstalled = true}); + + @override + final Version? currentVersion; + + @override + bool isDevicectlInstalled; +} diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index 620e20c2fe094..c91ede19d924c 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -91,85 +91,6 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); }); }); - - group('screenshot', () { - late FakeProcessManager fakeProcessManager; - late File outputFile; - - setUp(() { - fakeProcessManager = FakeProcessManager.empty(); - outputFile = MemoryFileSystem.test().file('image.png'); - }); - - testWithoutContext('error if idevicescreenshot is not installed', () async { - // Let `idevicescreenshot` fail with exit code 1. - fakeProcessManager.addCommand( - FakeCommand( - command: ['HostArtifact.idevicescreenshot', outputFile.path, '--udid', '1234'], - environment: const {'DYLD_LIBRARY_PATH': '/path/to/libraries'}, - exitCode: 1, - ), - ); - - final iMobileDevice = IMobileDevice( - artifacts: artifacts, - cache: cache, - processManager: fakeProcessManager, - logger: logger, - ); - - expect( - () async => - iMobileDevice.takeScreenshot(outputFile, '1234', DeviceConnectionInterface.attached), - throwsA(anything), - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - }); - - testWithoutContext('idevicescreenshot captures and returns USB screenshot', () async { - fakeProcessManager.addCommand( - FakeCommand( - command: ['HostArtifact.idevicescreenshot', outputFile.path, '--udid', '1234'], - environment: const {'DYLD_LIBRARY_PATH': '/path/to/libraries'}, - ), - ); - - final iMobileDevice = IMobileDevice( - artifacts: artifacts, - cache: cache, - processManager: fakeProcessManager, - logger: logger, - ); - - await iMobileDevice.takeScreenshot(outputFile, '1234', DeviceConnectionInterface.attached); - expect(fakeProcessManager, hasNoRemainingExpectations); - }); - - testWithoutContext('idevicescreenshot captures and returns network screenshot', () async { - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'HostArtifact.idevicescreenshot', - outputFile.path, - '--udid', - '1234', - '--network', - ], - environment: const {'DYLD_LIBRARY_PATH': '/path/to/libraries'}, - ), - ); - - final iMobileDevice = IMobileDevice( - artifacts: artifacts, - cache: cache, - processManager: fakeProcessManager, - logger: logger, - ); - - await iMobileDevice.takeScreenshot(outputFile, '1234', DeviceConnectionInterface.wireless); - expect(fakeProcessManager, hasNoRemainingExpectations); - }); - }); }); group('Diagnose Xcode build failure', () { From c1e0e16833282d886505691094ae3b19a11c1a53 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 31 Jul 2026 14:03:11 -0400 Subject: [PATCH 002/330] Roll Dart SDK from c3acfc2479f6 to 65b163be2485 (1 revision) (#190358) https://dart.googlesource.com/sdk.git/+log/c3acfc2479f6..65b163be2485 2026-07-31 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-82.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC aaclarke@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 1666d37147f40..ea8fd8654160b 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,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': 'c3acfc2479f6eae42f7cbafe5e5518a2dd757b81', + 'dart_revision': '65b163be2485aa0aa3fd0ab7d5333f2fa317a986', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From 24debf8bbfc44134b71dca9739abda02e5e73b93 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 31 Jul 2026 11:41:42 -0700 Subject: [PATCH 003/330] flutter_tools: Use new FileSystemExtension from devtools (#190360) --- .../lib/src/widget_preview/persistent_preferences.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/widget_preview/persistent_preferences.dart b/packages/flutter_tools/lib/src/widget_preview/persistent_preferences.dart index f962f4b4cc5a3..ff0b584280209 100644 --- a/packages/flutter_tools/lib/src/widget_preview/persistent_preferences.dart +++ b/packages/flutter_tools/lib/src/widget_preview/persistent_preferences.dart @@ -25,7 +25,7 @@ class PersistentPreferences { @visibleForTesting late final File file = fs.file( - fs.path.join(devtools.LocalFileSystem.devToolsDir(), _kPreferencesFileName), + fs.path.join(devtools.FileSystemExtension.devToolsDir, _kPreferencesFileName), ); late Map _map; From c8a3c2bc381041394c703b73758d027c56bf1877 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:26:21 -0700 Subject: [PATCH 004/330] [windows]: Uses offscreen MSAA when implicit msaa isn't available. (#190256) fixes the portion of https://github.com/flutter/flutter/issues/190060 that deals with MSAA ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../platform/windows/compositor_opengl.cc | 113 +++++++++++++---- .../platform/windows/compositor_opengl.h | 3 + .../windows/compositor_opengl_unittests.cc | 117 ++++++++++++++++++ 3 files changed, 207 insertions(+), 26 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/compositor_opengl.cc b/engine/src/flutter/shell/platform/windows/compositor_opengl.cc index 33f87711668e2..d1b1f5bc71a9f 100644 --- a/engine/src/flutter/shell/platform/windows/compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/windows/compositor_opengl.cc @@ -5,6 +5,7 @@ #include "flutter/shell/platform/windows/compositor_opengl.h" #include "GLES3/gl3.h" +#include "flutter/fml/logging.h" #include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" @@ -18,6 +19,7 @@ constexpr uint32_t kWindowFrameBufferId = 0; struct FramebufferBackingStore { uint32_t framebuffer_id = 0; uint32_t texture_id = 0; + uint32_t color_renderbuffer_id = 0; uint32_t depth_stencil_id = 0; }; @@ -51,48 +53,94 @@ bool CompositorOpenGL::CreateBackingStore( auto store = std::make_unique(); - gl_->GenTextures(1, &store->texture_id); gl_->GenFramebuffers(1, &store->framebuffer_id); - gl_->BindFramebuffer(GL_FRAMEBUFFER, store->framebuffer_id); - gl_->BindTexture(GL_TEXTURE_2D, store->texture_id); - gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - gl_->TexImage2D(GL_TEXTURE_2D, 0, format_.general_format, config.size.width, - config.size.height, 0, format_.general_format, - GL_UNSIGNED_BYTE, nullptr); - gl_->BindTexture(GL_TEXTURE_2D, 0); - if (enable_impeller_) { if (supports_implicit_msaa_) { + gl_->GenTextures(1, &store->texture_id); + gl_->BindTexture(GL_TEXTURE_2D, store->texture_id); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl_->TexImage2D(GL_TEXTURE_2D, 0, format_.sized_format, config.size.width, + config.size.height, 0, format_.general_format, + GL_UNSIGNED_BYTE, nullptr); + gl_->BindTexture(GL_TEXTURE_2D, 0); + // MSAA color attachment gl_->FramebufferTexture2DMultisampleEXT( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store->texture_id, 0, 4); - } else { - gl_->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - GL_TEXTURE_2D, store->texture_id, 0); - } - // Impeller always requires depth/stencil attachment. - gl_->GenRenderbuffers(1, &store->depth_stencil_id); - gl_->BindRenderbuffer(GL_RENDERBUFFER, store->depth_stencil_id); - if (supports_implicit_msaa_) { + // Impeller always requires depth/stencil attachment. + gl_->GenRenderbuffers(1, &store->depth_stencil_id); + gl_->BindRenderbuffer(GL_RENDERBUFFER, store->depth_stencil_id); gl_->RenderbufferStorageMultisampleEXT( GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, config.size.width, config.size.height); + gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, store->depth_stencil_id); + gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, store->depth_stencil_id); + } else if (supports_offscreen_msaa_) { + // MSAA color renderbuffer attachment + gl_->GenRenderbuffers(1, &store->color_renderbuffer_id); + gl_->BindRenderbuffer(GL_RENDERBUFFER, store->color_renderbuffer_id); + gl_->RenderbufferStorageMultisample( + GL_RENDERBUFFER, 4, format_.sized_format, config.size.width, + config.size.height); + gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, + store->color_renderbuffer_id); + + // MSAA depth/stencil attachment + gl_->GenRenderbuffers(1, &store->depth_stencil_id); + gl_->BindRenderbuffer(GL_RENDERBUFFER, store->depth_stencil_id); + gl_->RenderbufferStorageMultisample( + GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, config.size.width, + config.size.height); + gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, store->depth_stencil_id); + gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, store->depth_stencil_id); } else { + gl_->GenTextures(1, &store->texture_id); + gl_->BindTexture(GL_TEXTURE_2D, store->texture_id); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl_->TexImage2D(GL_TEXTURE_2D, 0, format_.sized_format, config.size.width, + config.size.height, 0, format_.general_format, + GL_UNSIGNED_BYTE, nullptr); + gl_->BindTexture(GL_TEXTURE_2D, 0); + + gl_->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, store->texture_id, 0); + + gl_->GenRenderbuffers(1, &store->depth_stencil_id); + gl_->BindRenderbuffer(GL_RENDERBUFFER, store->depth_stencil_id); gl_->RenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, config.size.width, config.size.height); + gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, store->depth_stencil_id); + gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, store->depth_stencil_id); } - gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, - GL_RENDERBUFFER, store->depth_stencil_id); - gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, - GL_RENDERBUFFER, store->depth_stencil_id); } else { + gl_->GenTextures(1, &store->texture_id); + gl_->BindTexture(GL_TEXTURE_2D, store->texture_id); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl_->TexImage2D(GL_TEXTURE_2D, 0, format_.sized_format, config.size.width, + config.size.height, 0, format_.general_format, + GL_UNSIGNED_BYTE, nullptr); + gl_->BindTexture(GL_TEXTURE_2D, 0); + gl_->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store->texture_id, 0); } @@ -118,8 +166,12 @@ bool CompositorOpenGL::CollectBackingStore(const FlutterBackingStore* store) { store->open_gl.framebuffer.user_data); gl_->DeleteFramebuffers(1, &user_data->framebuffer_id); - gl_->DeleteTextures(1, &user_data->texture_id); - + if (user_data->texture_id != 0) { + gl_->DeleteTextures(1, &user_data->texture_id); + } + if (user_data->color_renderbuffer_id != 0) { + gl_->DeleteRenderbuffers(1, &user_data->color_renderbuffer_id); + } if (user_data->depth_stencil_id != 0) { gl_->DeleteRenderbuffers(1, &user_data->depth_stencil_id); } @@ -237,6 +289,15 @@ bool CompositorOpenGL::Initialize() { supports_implicit_msaa_ = gl_->GetCapabilities()->SupportsImplicitResolvingMSAA(); + supports_offscreen_msaa_ = gl_->GetCapabilities()->SupportsOffscreenMSAA(); + + if (enable_impeller_ && !supports_implicit_msaa_ && + !supports_offscreen_msaa_) { + // TODO(190362): I suspect this branch is never taken since ANGLE will + // support offscreen MSAA. We should investigate removing this branch and + // the implicit MSAA branch in the rendering functions. + FML_LOG(WARNING) << "Rendering without MSAA."; + } is_initialized_ = true; return true; diff --git a/engine/src/flutter/shell/platform/windows/compositor_opengl.h b/engine/src/flutter/shell/platform/windows/compositor_opengl.h index eaf702c2065b4..0304d168d4287 100644 --- a/engine/src/flutter/shell/platform/windows/compositor_opengl.h +++ b/engine/src/flutter/shell/platform/windows/compositor_opengl.h @@ -66,6 +66,9 @@ class CompositorOpenGL : public Compositor { // Whether the OpenGL context supports implicit MSAA. bool supports_implicit_msaa_ = false; + // Whether the OpenGL context supports offscreen MSAA. + bool supports_offscreen_msaa_ = false; + // Initialize the compositor. This must run on the raster thread. bool Initialize(); diff --git a/engine/src/flutter/shell/platform/windows/compositor_opengl_unittests.cc b/engine/src/flutter/shell/platform/windows/compositor_opengl_unittests.cc index 97fb7bd934b48..14c2d9adcbcf7 100644 --- a/engine/src/flutter/shell/platform/windows/compositor_opengl_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/compositor_opengl_unittests.cc @@ -104,6 +104,39 @@ const impeller::ProcTableGLES::Resolver kMockResolverWithMSAA = return kMockResolver(name); }; +void MockGetIntegervWithOffscreenMSAA(GLenum name, int* value) { + if (name == GL_NUM_EXTENSIONS) { + *value = 1; + } else if (name == GL_MAX_SAMPLES) { + *value = 4; + } else { + *value = 0; + } +} + +const unsigned char* MockGetStringWithOffscreenMSAA(GLenum name) { + switch (name) { + case GL_VERSION: + return reinterpret_cast("OpenGL ES 3.0"); + case GL_SHADING_LANGUAGE_VERSION: + return reinterpret_cast("OpenGL ES GLSL ES 3.0"); + default: + return reinterpret_cast(""); + } +} + +const impeller::ProcTableGLES::Resolver kMockResolverWithOffscreenMSAA = + [](const char* name) -> void* { + std::string_view function_name{name}; + + if (function_name == "glGetString") { + return reinterpret_cast(&MockGetStringWithOffscreenMSAA); + } else if (function_name == "glGetIntegerv") { + return reinterpret_cast(&MockGetIntegervWithOffscreenMSAA); + } + return kMockResolver(name); +}; + class CompositorOpenGLTest : public WindowsTest { public: CompositorOpenGLTest() = default; @@ -254,6 +287,90 @@ TEST_F(CompositorOpenGLTest, CreateBackingStoreImpellerMSAA) { ASSERT_TRUE(compositor.CollectBackingStore(&backing_store)); } +// Verifies that when implicit MSAA is unsupported but offscreen MSAA is +// supported (OpenGL ES 3.0+ with GL_MAX_SAMPLES >= 4), CompositorOpenGL +// creates 4x MSAA color and depth/stencil renderbuffers. +TEST_F(CompositorOpenGLTest, CreateBackingStoreImpellerOffscreenMSAA) { + UseHeadlessEngine(); + + static int framebuffer_texture2d_calls = 0; + static int framebuffer_texture2d_multisample_calls = 0; + static int renderbuffer_storage_multisample_calls = 0; + static int renderbuffer_storage_multisample_samples = 0; + static int framebuffer_renderbuffer_calls = 0; + static int delete_renderbuffers_calls = 0; + static int delete_textures_calls = 0; + + framebuffer_texture2d_calls = 0; + framebuffer_texture2d_multisample_calls = 0; + renderbuffer_storage_multisample_calls = 0; + renderbuffer_storage_multisample_samples = 0; + framebuffer_renderbuffer_calls = 0; + delete_renderbuffers_calls = 0; + delete_textures_calls = 0; + + const impeller::ProcTableGLES::Resolver resolver = + [](const char* name) -> void* { + std::string_view function_name{name}; + if (function_name == "glFramebufferTexture2D") { + return reinterpret_cast( + +[](GLenum, GLenum, GLenum, GLuint, GLint) { + framebuffer_texture2d_calls++; + }); + } else if (function_name == "glFramebufferTexture2DMultisampleEXT") { + return reinterpret_cast( + +[](GLenum, GLenum, GLenum, GLuint, GLint, GLsizei) { + framebuffer_texture2d_multisample_calls++; + }); + } else if (function_name == "glGenRenderbuffers") { + return reinterpret_cast(+[](GLsizei n, GLuint* renderbuffers) { + static GLuint next_id = 1; + for (GLsizei i = 0; i < n; i++) { + renderbuffers[i] = next_id++; + } + }); + } else if (function_name == "glRenderbufferStorageMultisample") { + return reinterpret_cast(+[](GLenum target, GLsizei samples, + GLenum internalformat, GLsizei width, + GLsizei height) { + renderbuffer_storage_multisample_calls++; + renderbuffer_storage_multisample_samples = samples; + }); + } else if (function_name == "glFramebufferRenderbuffer") { + return reinterpret_cast(+[](GLenum, GLenum, GLenum, GLuint) { + framebuffer_renderbuffer_calls++; + }); + } else if (function_name == "glDeleteRenderbuffers") { + return reinterpret_cast( + +[](GLsizei n, const GLuint* renderbuffers) { + delete_renderbuffers_calls += n; + }); + } else if (function_name == "glDeleteTextures") { + return reinterpret_cast(+[](GLsizei n, const GLuint* textures) { + delete_textures_calls += n; + }); + } + return kMockResolverWithOffscreenMSAA(name); + }; + + auto compositor = + CompositorOpenGL{engine(), resolver, /*enable_impeller=*/true}; + FlutterBackingStoreConfig config = {}; + FlutterBackingStore backing_store = {}; + + EXPECT_CALL(*render_context(), MakeCurrent).WillOnce(Return(true)); + ASSERT_TRUE(compositor.CreateBackingStore(config, &backing_store)); + EXPECT_EQ(framebuffer_texture2d_calls, 0); + EXPECT_EQ(framebuffer_texture2d_multisample_calls, 0); + EXPECT_EQ(renderbuffer_storage_multisample_calls, 2); + EXPECT_EQ(renderbuffer_storage_multisample_samples, 4); + EXPECT_EQ(framebuffer_renderbuffer_calls, 3); + + ASSERT_TRUE(compositor.CollectBackingStore(&backing_store)); + EXPECT_EQ(delete_renderbuffers_calls, 2); + EXPECT_EQ(delete_textures_calls, 0); +} + TEST_F(CompositorOpenGLTest, InitializationFailure) { UseHeadlessEngine(); From 2e5da0324514f0e4bf38e305745841278d6daef6 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:00:32 -0700 Subject: [PATCH 005/330] [web] Remove in-repo agent documentation (#190326) Removes some AI-generated design docs that I added to the repo. As we try to learn best practices for agentic engineering, I think we shouldn't pollute the repo with a bunch of docs (which are already out of date, by the way) until we have more standardized practices for agentic context management/knowledge management. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/web_ui/docs/FALLBACK_FONT_SERVICE.md | 131 ------------------ .../flutter/lib/web_ui/docs/IMAGE_DECODING.md | 105 -------------- .../web_ui/docs/IMAGE_DECODING_THROTTLING.md | 110 --------------- 3 files changed, 346 deletions(-) delete mode 100644 engine/src/flutter/lib/web_ui/docs/FALLBACK_FONT_SERVICE.md delete mode 100644 engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING.md delete mode 100644 engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING_THROTTLING.md diff --git a/engine/src/flutter/lib/web_ui/docs/FALLBACK_FONT_SERVICE.md b/engine/src/flutter/lib/web_ui/docs/FALLBACK_FONT_SERVICE.md deleted file mode 100644 index 681abbdc55d6c..0000000000000 --- a/engine/src/flutter/lib/web_ui/docs/FALLBACK_FONT_SERVICE.md +++ /dev/null @@ -1,131 +0,0 @@ -# FallbackFontService Design Document - -## Section 0: The Problem Statement - -In Flutter Web applications, text rendering depends on the fonts provided by the developer in the application's asset bundle. When a piece of text contains characters (such as CJK scripts, emojis, or rare symbols) that are not covered by any of the fonts included in the asset bundle, the engine invokes an automatic "font fallback" system. This system identifies the missing characters and attempts to download the appropriate "Noto" fonts from a CDN (typically Google Fonts) to ensure the text is legible. - -Currently, this fallback system has a critical reliability flaw: **It does not gracefully handle network or server failures.** - -If a required fallback font fails to download—whether due to a 404 error, a broken CDN link, or a transient network interruption—the engine enters an **infinite loop**. Because it does not track these failures effectively, it continuously attempts to download the same failing font. Each attempt notifies the application that "fonts have changed," triggering a UI relayout. This relayout rediscovers that the characters are still missing and restarts the download attempt immediately. - -For the end-user and the business, this results in: -1. **System Instability:** The infinite loop consumes excessive CPU and battery power, causing the web application to become laggy, hot, or unresponsive. -2. **Network Abuse:** The application spams font servers with thousands of redundant requests, which can lead to rate-limiting, increased infrastructure costs, and a poor reputation for the app's network behavior. -3. **Broken Aesthetics:** Users are left with "tofu" (empty boxes), and the engine fails to provide a "best-effort" recovery—such as attempting a secondary fallback font that might actually be reachable. -4. **Operational Noise:** Browser consoles are flooded with identical error logs, making it nearly impossible for developers to debug other issues or maintain a production-grade application. - -We need a solution that makes the font fallback system **autonomous and resilient**, ensuring it can recover from transient errors, find alternative fonts when primary ones fail, and—above all—terminate its attempts once it has exhausted all viable options. - -## Section 1: The Technical Implementation Plan - -To solve this problem, we are rebuilding the font fallback system as a dedicated, smart background service called the **FallbackFontService**. This service will act like a "concierge" for missing characters: instead of the rendering engine frantically trying to manage downloads on its own, it will simply hand a list of missing characters to this service and trust it to handle the rest. - -The plan consists of four major components working together: - -### 1. The "Request Queue" (Skia-Driven Discovery) -Currently, the engine manually checks every string of text to see if it *might* need a fallback font based on its own records. We will replace this manual check with a more direct approach: we will ask the underlying graphics engine (**Skia**) for the truth. During the "layout" phase (when the app calculates exactly where text should go), we will call a specific function to get the exact list of characters that the current fonts—including any fallback fonts already loaded—could not handle. These "unresolved" characters are added to a global "Unprocessed" list in the service. By letting Skia be the source of truth, we ensure the service only acts when a character is genuinely missing from the screen. - -### 2. The Autonomous Manager (FallbackFontService) -The service manages its own state independently of the main application's UI cycle, using an event-driven "convergence" model. When new characters arrive in the "Unprocessed" list, the service: -* **Filters out duplicates:** It ignores characters it is already trying to download or that it has already failed to find. -* **Picks the best fonts:** It uses "greedy" logic to find the smallest number of fonts that cover the most missing characters. -* **Consults the Fallback Data:** The service uses a mapping of characters to fonts stored in `lib/src/engine/font_fallback_data.dart`. This is a large, generated file that encodes exactly which Noto fonts provide glyphs for which Unicode ranges. -* **Manages downloads:** It handles the actual HTTP requests, ensuring we don't overwhelm the user's connection by limiting how many fonts download at once. - -### 3. The Smart Retry & Recovery System -This is the "brain" of the fix. If a font fails to download, the service doesn't just give up or loop forever. -* **Transient Errors:** If the network blips, it waits 1 second and tries again (up to 3 times). -* **Permanent Failures:** If a font is simply not there (a 404 error) or fails all retries, the service marks that font as "Permanently Unavailable." -* **Self-Healing:** The service then immediately re-evaluates the missing characters. Because it knows which fonts are broken, it will automatically look for the "next best" font to cover those characters. If no other fonts exist, it marks those characters as "Unsupported" and stops trying. This is what finally breaks the infinite loop. -* **Global Kill Switch:** To protect against systemic misconfigurations (e.g., a broken `fontFallbackBaseUrl`), the service tracks total permanent failures. If 10 fonts fail permanently and zero have succeeded, the service declares itself "broken" and stops all future attempts for the session. -* **Per-Component Cap:** To prevent a single character from triggering hundreds of requests (e.g., a common character covered by many fonts), the service limits the number of candidate fonts it will attempt for any single Unicode component to 5. If all 5 fail, the component is marked as unsupported. - -### 4. The Feedback Loop -The service only talks back to the Flutter framework when it actually succeeds. When a font is successfully downloaded and registered, the service tells the framework: *"I have new fonts; please redraw the screen."* If a font fails and no replacement can be found, the service stays silent. The user will see a placeholder (like an empty box), but the app will remain stable and the network will go quiet. - -### Ecosystem Fit -This feature sits deep within the "Web Engine" layer of Flutter. It bridges the gap between the high-level Flutter framework and the low-level browser environment. By moving this logic into a centralized service, we make the engine more efficient for all Flutter Web developers, providing a "fire and forget" system that handles the complexities of global typography and unreliable networks automatically. - -## Section 2: Alternatives Considered - -During the design process, we explored several other approaches but ultimately ruled them out in favor of the more robust `FallbackFontService` model. - -### 1. Immediate "Best-Effort" Replacement -We considered a strategy where, if the primary font (Font A) failed even once, the service would immediately start downloading the secondary font (Font B). -* **Why we ruled it out:** This was deemed too aggressive and wasteful of the user's bandwidth. In many cases, a single network blip is temporary. By jumping to Font B immediately, we risked downloading multiple large fonts for the same set of characters, potentially causing "layout jitter" where text changes its appearance multiple times as different fonts arrive. We decided it was better to give the primary font a fair chance to succeed through retries before looking for a substitute. - -### 2. Dependency on the Framework for Retries -One early thought was to keep the current model where the framework's "re-layout" signal drives the retry logic. In this model, we would simply stop the loop by marking a font as failed. -* **Why we ruled it out:** This kept the engine in a "passive" state. If we failed to download any fonts in a batch and didn't notify the framework, the engine would essentially "fall asleep" and never try to find a replacement for those missing characters until some other unrelated UI change happened. We realized the engine needs to be **proactive**—it should be able to say, "Font A failed; let me immediately see if Font B can help," without needing the framework to ask it to try again. - -### 3. Filtering characters before enqueuing them -We discussed filtering out characters that were already covered by "pending" downloads before they even entered the service's queue. -* **Why we ruled it out:** This created a dangerous "memory loss" problem. If we filtered out a character because Font A was *supposed* to cover it, and then Font A failed to download, the service would have no record that the character still needed covering. By keeping all missing characters in the "Unprocessed" list until they are truly resolved or exhausted, we ensure that no requirement is ever forgotten, regardless of how many individual fonts fail. - -### 4. Maintaining a manual "Shadow Cache" of fonts -We considered having the service maintain its own comprehensive list of every character covered by every font it has ever seen to avoid talking to Skia so often. -* **Why we ruled it out:** This added unnecessary complexity and the risk of the service getting "out of sync" with the actual graphics engine. Since Skia is the ultimate authority on what it can and cannot render, it is much simpler and more accurate to ask it for the "unresolved" list directly during layout. This eliminates the need for the service to try and mirror Skia's complex internal font-matching logic. - -## Section 3: Detailed Implementation Plan - -This section outlines the surgical changes required to implement the `FallbackFontService` architecture. The goal is to centralize fallback logic, utilize Skia’s internal layout state, and implement a resilient retry/recovery loop. - -### 1. New Core Infrastructure - -* **File: `lib/src/engine/font_fallback_service.dart` (New File)** - * **Rationale:** To house the `FallbackFontService` class. This centralizes the `_unprocessedCodePoints`, `_pendingFonts`, and `_permanentlyUnavailableFonts` state. It will contain the "Event-Driven Convergence" logic, the stateless greedy algorithm, and the smart fetcher. -* **File: `lib/src/engine/noto_font.dart` (Refactor)** - * **Rationale:** To make the `NotoFont` data class stateless, removing any internal tracking that would interfere with the `FallbackFontService` greedy selection algorithm. -* **File: `lib/src/engine/font_fallbacks.dart` (Major Refactor)** - * **Rationale:** We will refactor the existing `FontFallbackManager` and `_FallbackFontDownloadQueue` logic into the new service. The `NotoFont` and `FallbackFontComponent` classes must be made stateless (removing `coverCount` and `coverComponents`) to allow the greedy algorithm to run safely and predictably during autonomous re-evaluations. - -### 2. Renderer Interface Updates - -* **File: `lib/src/engine/canvaskit/canvaskit_api.dart`** - * **Rationale:** Add the missing JS-Interop binding for `getUnresolvedCodepoints()` to the `SkParagraph` extension type. - * **Technical Detail:** The underlying JS/WASM method on the `SkParagraph` object takes **no arguments** and returns a `JSArray` representing the Unicode code points. - * **Usage:** This is the "source of truth" required to move away from string-based discovery. -* **File: `lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph.dart`** - * **Rationale:** Ensure the FFI binding `paragraphGetUnresolvedCodePoints` is correctly exposed and documented for use in the unified fallback path. -* **File: `lib/src/engine/font_fallbacks.dart` (Interface change)** - * **Rationale:** Update the `FallbackFontRegistry` abstract class. Change `loadFallbackFont(String name, String url)` to `Future loadFallbackFont(String name, Uint8List bytes)`. The return value indicates whether the renderer successfully registered the font. This shifts HTTP responsibility to the `FallbackFontService` and allows it to track registration failures. - -### 3. Renderer Implementation Updates - -* **File: `lib/src/engine/canvaskit/fonts.dart` (`SkiaFontCollection`)** -* **File: `lib/src/engine/skwasm/skwasm_impl/font_collection.dart` (`SkwasmFontCollection`)** - * **Rationale:** Both font collections now implement the unified `FlutterFontCollection` interface, which mandates the presence of a `FontFallbackManager` and a `FallbackFontRegistry`. - * **Architecture:** Each collection now owns its respective registry implementation (`SkiaFallbackRegistry` and `SkwasmFallbackRegistry`) and initializes a `FontFallbackManager` to bridge the gap between the `FallbackFontService` and the renderer-specific font stack. - * **State Management:** - * `SkiaFontCollection` was updated to maintain a `registeredFallbackFonts` list, and its `_registerWithFontProvider()` method now rebuilds the Skia font provider by combining both asset fonts and dynamically loaded fallback fonts. - * `SkwasmFontCollection` now utilizes `setDefaultFontFamilies()` to synchronize the renderer's default font stack with the global fallback list managed by the service. - * **Lifecycle:** Added `debugResetFallbackFonts()` to both implementations to ensure clean state during unit and golden testing, allowing the `FallbackFontService` to be reset independently of the main font stack. - -* **File: `lib/src/engine/canvaskit/fonts.dart` (`SkiaFallbackRegistry`)** -* **File: `lib/src/engine/skwasm/skwasm_impl/font_collection.dart` (`SkwasmFallbackRegistry`)** - * **Rationale:** These new registry classes implement the `FallbackFontRegistry` interface, providing the concrete logic for injecting font bytes into the respective WASM heaps and triggering the necessary font-provider updates. - * **Technical Detail:** - * `loadFallbackFont(name, bytes)` handles the creation of a typeface from raw bytes. - * `updateFallbackFontFamilies(families)` triggers the renderer-specific logic to update the font-matching order (e.g., rebuilding the `TypefaceFontProvider` in Skia or updating the default text style in Skwasm). -* **File: `lib/src/engine/canvaskit/text.dart` (`CkParagraph.layout`)** -* **File: `lib/src/engine/skwasm/skwasm_impl/paragraph.dart` (`SkwasmParagraph.layout`)** - * **Rationale:** Update the `layout()` method in both renderers to call `getUnresolvedCodepoints()` from Skia. If unresolved characters are found, they will call `FallbackFontService.instance.addMissingCodePoints(list)`. This unified the discovery mechanism for both backends. - * **Optimization:** Added a `_hasCheckedForMissingCodePoints` flag to both paragraph implementations to ensure that we only query Skia once per paragraph life-cycle, avoiding redundant work during repeated layouts. -* **File: `lib/src/engine/canvaskit/text.dart` (`CkParagraphBuilder.addText`)** - * **Rationale:** Remove the call to `ensureFontsSupportText()`. This eliminates the expensive string-based check during paragraph building, significantly improving performance for text-heavy applications. - -### 4. Cleanup and Performance - -* **File: `lib/src/engine/font_change_util.dart`** - * **Rationale:** Verify the debouncing logic in `sendFontChangeMessage()`. We will rely on this to ensure that if multiple fonts in a batch succeed, we only trigger a single framework relayout per animation frame. -* **File: `lib/src/engine/dom.dart`** - * **Rationale:** Ensure `httpFetch` is robustly exposed for the `FallbackFontService` to use for its "sophisticated" fetching (checking `response.ok` for 404s). - -### 5. Testing and Validation - -* **File: `test/engine/font_fallback_service_test.dart` (New File)** - * **Rationale:** Create a suite of unit tests for the new service. These will mock network failures, 404s, and successful downloads to verify that the "True Missing" logic correctly falls back to alternative fonts and eventually terminates the retry loop. -* **File: `test/ui/fallback_fonts_golden_test.dart`** - * **Rationale:** Update existing golden tests to use the new `waitForIdle()` definition and ensure that "Permanent Failures" correctly render as tofu without causing infinite test timeouts. -* **File: `lib/src/engine/configuration.dart`** - * **Rationale:** Add a new configuration flag (e.g., `debugSkipFontRetryDelay`) to allow tests to run without waiting for the 1-second backoff timer. diff --git a/engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING.md b/engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING.md deleted file mode 100644 index e938141218fd2..0000000000000 --- a/engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING.md +++ /dev/null @@ -1,105 +0,0 @@ -# Image Decoding in Flutter Web - -## Overview - -The image decoding system in the Flutter Web engine is designed to provide high-performance, memory-efficient image loading and rendering across a wide variety of browsers. Its primary purpose is to bridge the gap between Flutter's `dart:ui` API and the various image decoding capabilities provided by modern web browsers. - -### Implementation Sketch - -The system is built on a pluggable architecture that adapts to the active rendering backend (**CanvasKit** or **Skwasm**) and the capabilities of the host browser. - -1. **Backend Abstraction**: The `Renderer` class serves as the entry point. It delegates image codec instantiation to backend-specific implementations, ensuring that the resulting `ui.Image` objects (e.g., `CkImage` or `SkwasmImage`) are compatible with the current rendering pipeline. -2. **Multi-Path Decoding**: - * **WebCodecs (`ImageDecoder` API)**: The primary, high-performance path. It uses hardware-accelerated decoding to produce `VideoFrame` objects, supporting both static and animated images. - * **HTML `` Element**: A robust fallback for static images. It utilizes the browser's native `HTMLImageElement.decode()` API to decode images asynchronously. - * **WASM-based Decoders**: Fallback decoders implemented in WebAssembly (e.g., Skia's built-in codecs) are used for animated images when the `ImageDecoder` API is not available. -3. **Source Preservation**: `ui.Image` implementations often maintain a reference to their original browser-native source (like an `ImageBitmap` or `HTMLImageElement`). This allows for efficient pixel read-back and avoids slow GPU-to-CPU memory transfers. -4. **Transformation and Optimization**: - * **Resizing Codecs**: Images can be resized immediately after decoding to minimize memory usage. - * **Iterative Downscaling**: To maintain high visual quality, the engine performs multi-step downscaling for large scale factors, bypassing limitations in browser-side mipmap generation. - -## Entrypoints - -The Flutter framework interacts with the web engine through a set of APIs defined in `dart:ui`. These APIs are the starting point for any image loading operation. - -### Codec Instantiation - -The most common way images are loaded is by creating a `ui.Codec`, which manages the decoding and frame-by-frame access of an image. - -* **`instantiateImageCodec(Uint8List list, ...)`**: The primary entrypoint for decoding encoded image bytes (JPEG, PNG, GIF, etc.). -* **`instantiateImageCodecFromBuffer(ImmutableBuffer buffer, ...)`**: Similar to the above, but uses an `ImmutableBuffer` for memory efficiency. -* **`instantiateImageCodecWithSize(ImmutableBuffer buffer, ...)`**: Allows the framework to request a specific target size during the decoding process. -* **`ImageDescriptor.instantiateCodec(...)`**: Decodes an image based on a descriptor that provides metadata like width, height, and pixel format. - -On the framework side, these are typically invoked by an `ImageProvider` (like `NetworkImage` or `AssetImage`) during the image resolution process. - -### Direct Decoding - -For simpler use cases or raw pixel data, the following APIs are used: - -* **`decodeImageFromList(Uint8List list, ...)`**: A convenience wrapper that decodes an image and returns a single `ui.Image` via a callback. -* **`decodeImageFromPixels(Uint8List pixels, ...)`**: Creates a `ui.Image` directly from a buffer of raw pixel data. - -### Rendering Entrypoints - -Once an image is decoded into a `ui.Image` object, it is displayed using the `Canvas` API: - -* **`Canvas.drawImage(ui.Image image, Offset p, Paint paint)`**: Draws the entire image at a specific point. -* **`Canvas.drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint)`**: Draws a sub-region of the image into a target rectangle on the canvas. This is where the engine's **Iterative Downscaling** logic is often triggered if the destination rectangle is significantly smaller than the source. - -## Memory Management - -Memory management for images in Flutter Web is a multi-layered process involving the Dart VM, the browser's JavaScript/DOM environment, and the WebAssembly (WASM) heap used by the renderers. - -### Reference Counting and Disposal - -Because the rendering backends (CanvasKit and Skwasm) store image data in a private WASM heap, the Dart garbage collector cannot automatically reclaim that memory. - -* **`CountedRef`**: The engine uses a reference-counting mechanism (`CkCountedRef` in CanvasKit) to track how many Dart-side proxies are pointing to a single WASM-side image object. -* **Explicit Disposal**: It is critical that the Flutter framework calls `image.dispose()` when an image is no longer needed. This decrements the reference count and, when it reaches zero, triggers the actual deletion of the object from the WASM heap. - -### Preservation of Original Image Source - -In addition to the WASM-side representation, the engine typically retains a reference to the **original browser-native source** (e.g., an `HTMLImageElement`, `ImageBitmap`, or `VideoFrame`). - -* **Workaround for CanvasKit Bug**: This is primarily done to work around a bug in CanvasKit where calling `readPixels` on a texture-backed `SkImage` can fail and return entirely black pixels. By keeping the DOM source, the engine can reliably extract pixel data for `toByteData()`. -* **Ref-Counting of the Source**: The `ImageSource` object itself is ref-counted separately from the WASM handle. When a `ui.Image` is cloned, the new instance increments the `refCount` on the same `ImageSource`. This ensures that the browser-native resource (like an `ImageBitmap`) is only closed/released when all clones that depend on it have been disposed. - -### Lazy Texture Uploads - -The CanvasKit backend makes extensive use of Skia's **Lazy Images** (e.g., `MakeLazyImageFromImageBitmap`). - -* **On-Demand Upload**: Instead of immediately copying the image pixels into a GPU texture, the engine creates a "lazy" wrapper. The actual texture upload to the WebGL/WebGPU context happens at the last possible moment—right before the image is drawn to a surface. -* **Multi-Surface Support**: This lazy behavior is what enables the **`MultiSurfaceRasterizer`** to work. Since the texture isn't tied to a specific context until draw time, it can be uploaded to different canvases or handled correctly if a WebGL context is lost and needs to be recovered. -* **Skwasm Note**: While Skwasm also uses texture sources, its current implementation is more tightly coupled to the active surface, and it does not yet support the `MultiSurfaceRasterizer`. - -### Resource Copies and Footprint - -At any given time, an active image might have several representations in memory: -1. **Encoded Bytes**: Present briefly during the initial fetch/load phase. -2. **Browser-Native Source**: The decoded `ImageBitmap` or `HTMLImageElement` managed by the browser. -3. **WASM Wrapper**: A small handle in the WASM heap representing the Skia/Skwasm image object. -4. **GPU Texture(s)**: One or more actual textures in GPU memory, potentially duplicated if the image is being drawn across multiple independent WebGL contexts (in `MultiSurfaceRasterizer` mode). - -## Relevant Files - -The following files constitute the core of the image decoding and rendering system in the Flutter Web engine. - -### Core Abstractions and Shared Logic - -* **`lib/painting.dart`**: Defines the `ui.Image`, `ui.Codec`, and `ui.ImmutableBuffer` interfaces as part of the `dart:ui` library. It also contains utility methods for image decoding like `decodeImageFromList`. -* **`lib/src/engine/renderer.dart`**: Contains the `Renderer` base class, which defines the interface for creating image codecs and images across different backends. -* **`lib/src/engine/image_decoder.dart`**: Implements `BrowserImageDecoder`, the base class for decoders using the browser's `ImageDecoder` (WebCodecs) API. It also contains `ResizingCodec` and the general `scaleImageIfNeeded` logic. -* **`lib/src/engine/html_image_element_codec.dart`**: Provides the base `HtmlImageElementCodec` which uses an off-screen HTML `` tag to decode static images asynchronously. - -### CanvasKit Backend (Skia-WASM) - -* **`lib/src/engine/canvaskit/renderer.dart`**: Implements `CanvasKitRenderer`, delegating image operations to `skiaInstantiateImageCodec` and backend-specific image creation methods. -* **`lib/src/engine/canvaskit/image.dart`**: The "brain" of CanvasKit image logic. It manages the selection between WebCodecs, `` tags, and Skia's own decoders. It also defines `CkImage`, which wraps a Skia `SkImage` while optionally retaining a reference to the original DOM source. -* **`lib/src/engine/canvaskit/canvas.dart`**: Implements the drawing commands for CanvasKit. It includes the `shouldIterativelyDownscale` check and calls into `getOrCreateDownscaledImage` to ensure high-quality rendering of large images. - -### Skwasm Backend (FFI-WASM) - -* **`lib/src/engine/skwasm/skwasm_impl/renderer.dart`**: Implements `SkwasmRenderer`, managing the lifecycle of `SkwasmImage` objects and choosing between `SkwasmBrowserImageDecoder` and other fallback strategies. -* **`lib/src/engine/skwasm/skwasm_impl/codecs.dart`**: Contains Skwasm-specific implementations of the decoding paths, including `SkwasmBrowserImageDecoder` and a WASM-based `SkwasmAnimatedImageDecoder`. -* **`lib/src/engine/skwasm/skwasm_impl/canvas.dart`**: Implements drawing logic for the Skwasm backend, mirroring the iterative downscaling optimizations found in CanvasKit. diff --git a/engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING_THROTTLING.md b/engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING_THROTTLING.md deleted file mode 100644 index f9d903a690499..0000000000000 --- a/engine/src/flutter/lib/web_ui/docs/IMAGE_DECODING_THROTTLING.md +++ /dev/null @@ -1,110 +0,0 @@ -# Image Decoding Throttling in Flutter Web - -## Section Zero: Business Problem Description - -The primary goal of this feature is to eliminate silent application crashes and rendering failures in Flutter Web applications that handle high volumes of image assets, specifically on browsers that rely on the `HTMLImageElement.decode()` API for image processing. - -Modern browsers that support the high-performance `ImageDecoder` (WebCodecs) API, such as Chrome, are generally robust when handling concurrent image decodes. However, for browsers where this API is unavailable—most notably **iOS Safari**—or in scenarios where the engine must fall back to using the standard HTML `` element for decoding, the system is highly susceptible to resource exhaustion. - -When a Flutter Web app attempts to decode many large images simultaneously using this fallback path, it can overwhelm the browser's internal image subsystem. This manifests in two critical ways: -1. **Silent Crashes (iOS Safari):** The most severe failure mode, where the entire web page crashes or reloads without any logged errors, providing a poor user experience. -2. **Encoding Errors:** On other browsers, forcing many simultaneous decodes through the `HTMLImageElement` path can trigger "EncodingErrors," causing assets to fail to render entirely. - -Currently, the Flutter Web engine issues these decoding requests as fast as the framework demands them. By introducing a "traffic controller" specifically for the `HTMLImageElement` decoding path, we aim to: - -* **Ensure Application Stability:** Prevent fatal browser crashes on mobile devices by smoothing out the resource demand and staying within the browser's concurrent processing limits. -* **Improve Rendering Reliability:** Ensure that every image intended for display is successfully processed, rather than failing due to browser-level synchronization or memory limits. -* **Optimize Memory Lifecycle:** Implement aggressive signaling to the browser to release heavy bitmap memory as soon as it is no longer needed, reducing the cumulative memory pressure that leads to these crashes. - -## Section One: Technical Implementation Plan - -The technical implementation introduces a centralized resource coordinator to manage the concurrency and memory impact of the `HTMLImageElement.decode()` execution path. By moving from an "eager" decoding model to a "throttled" model, we can prevent the browser's background decoding threads from exceeding system resource limits. - -### Core Components - -1. **The `ImageDecodingManager` (Resource Coordinator):** - A centralized singleton responsible for tracking active decoding operations. It manages a FIFO (First-In-First-Out) queue and enforces two primary safety constraints: - * **Concurrency Limit:** A maximum of 8 simultaneous `decode()` operations. - * **Memory Footprint Limit:** A maximum cumulative estimated footprint (128MB) for all in-flight decodes. - * **The "Greedy First" Rule:** To prevent deadlocks when an image exceeds the total budget (e.g., a single 200MB asset), the manager always allows the first item in the queue to proceed if no other decodes are active. - * **`cancel(Request request)`:** The manager provides an explicit `cancel` method. If an image is disposed of while waiting in the queue, this method is used to remove the request and reclaim the potential slot immediately. - * **Defensive Timeout:** To prevent a "hung" browser decode from permanently leaking a resource slot, a defensive timeout (e.g., 30 seconds) will be implemented. If `img.decode()` does not resolve within this window, the slot will be forcibly released to prevent a system-wide deadlock. - -2. **Refactored Codec Lifecycle:** - The `HtmlImageElementCodec` will be updated to split the image preparation into two distinct asynchronous phases: - * **Phase 1 (Sizing):** The image `src` is set, and we wait for the browser's `onload` or `onerror` event. If `onerror` fires, the process terminates with an error before requesting a slot from the manager. If `onload` fires, we obtain the `naturalWidth` and `naturalHeight` required to estimate the memory footprint (`width * height * 4`). - * **Phase 2 (Throttled Decode):** The codec requests a slot from the `ImageDecodingManager`. Once granted, it executes the high-latency `img.decode()` call. A `finally` block ensures that the manager is notified to release the resource slot regardless of the outcome. - * **Disposal during Queueing:** If `dispose()` is called while the codec is waiting in Phase 2, the codec must call `ImageDecodingManager.instance.cancel(request)` to remove itself from the queue and abort the process. This prevents wasting budget and avoid late-failure errors. - -3. **Aggressive Resource Reclamation:** - To mitigate "sticky" memory in iOS Safari, we will update the `ImageSource` disposal logic. Instead of relying solely on garbage collection, we will explicitly clear the `src` attribute and revoke object URLs immediately upon disposal. This signals the browser to purge the associated bitmap from its internal cache. - -### System Interaction Flow - -When the Flutter framework requests an image via `instantiateImageCodec`, the system follows this coordinated path: - -1. **Preparation:** The codec initializes the `HTMLImageElement` and waits for the metadata to load (`onload`). -2. **Accounting:** The codec calculates the estimated RGBA footprint and enters the `ImageDecodingManager` queue. -3. **Throttling:** The manager pauses the execution of the codec's `decode()` call until the active concurrency and memory usage fall within safe thresholds. -4. **Execution:** The browser performs the background CPU/GPU work to decompress the image data. -5. **Resolution:** The manager releases the reserved capacity, and the framework receives a `ui.Image` ready for rendering. -6. **Disposal:** When the framework disposes of the image, the engine explicitly unlinks the resource to reclaim memory. - -### Ecosystem Integration - -This change is internal to the Flutter Web engine's implementation of `dart:ui`. It specifically hardens the `HTMLImageElement` fallback path used by browsers like Safari without affecting the high-performance `ImageDecoder` (WebCodecs) path used by Chrome. - -## Section Two: Alternatives Considered - -### 1. Eliminating the `decode()` Call Entirely -We considered removing the call to `HTMLImageElement.decode()` and simply waiting for the `onload` event. -* **Why it was ruled out:** Removing `decode()` forces the browser to perform image decompression synchronously on the main thread during the next frame paint. This would introduce significant "jank" (dropped frames). Furthermore, it would remove our mechanism for controlling concurrency, potentially leading to the same crashes when multiple images are drawn for the first time in a single frame. - -### 2. Throttling Based on Encoded File Size -We initially discussed using the size of the encoded image bytes as the primary metric. -* **Why it was ruled out:** Encoded size is an unreliable proxy for actual memory pressure. A highly compressed 1MB JPEG could expand into a massive 40MB bitmap. Additionally, we cannot easily determine the file size of a URL-based image without an extra network request. Using a dimension-based estimate (`width * height * 4`) provides a more accurate and consistent measure. - -### 3. Automatic Dimension Sniffing via Header Parsing -We explored the idea of "sniffing" image file headers to determine dimensions before starting the loading process. -* **Why it was ruled out:** Image formats have complex bytecode standards. Writing a robust, cross-format header parser adds significant complexity. Waiting for the browser’s native `onload` event is a much more reliable way to obtain accurate dimensions. - -### 4. Implementing a "Retry-on-Error" Strategy -Since Chrome returns a catchable `EncodingError` when it is overwhelmed, we considered simply catching that error and retrying. -* **Why it was ruled out:** This approach does not work for **iOS Safari**, which simply crashes the entire process without throwing a catchable error. A proactive throttling strategy is required. - -## Section Three: Detailed Implementation Plan - -### 1. Core Logic & Coordination - -**File:** `lib/src/engine/image_decoding_manager.dart` (New File) -* **Rationale:** Central coordinator for image decoding resources. -* **Implementation:** Singleton `ImageDecodingManager` tracking `activeDecodesCount` and `activeDecodesBytes` with a FIFO queue and "Greedy First" logic. - -**File:** `lib/src/engine.dart` -* **Rationale:** Export the new manager. - -### 2. Refactoring the HTML Decoding Path - -**File:** `lib/src/engine/html_image_element_codec.dart` -* **Rationale:** Update base class for `` based decoding to support the throttled two-phase process. -* **Implementation:** Refactor `decode()` to wait for `onload`, then wait for a manager slot, then call `img.decode()`. Update `dispose()` to clear `src`. - -### 3. Backend-Specific Codec Updates - -**File:** `lib/src/engine/canvaskit/image.dart` -* **Rationale:** Update CanvasKit image sources for aggressive reclamation. -* **Implementation:** Update `_doClose()` in `ImageElementImageSource` and `ImageBitmapImageSource` to explicitly release browser resources. - -**File:** `lib/src/engine/skwasm/skwasm_impl/codecs.dart` -* **Rationale:** Ensure Skwasm-specific codecs benefit from the new logic. - -### 4. Verification & Testing - -**File:** `test/engine/image_decoding_manager_test.dart` (New File) -* **Rationale:** Unit test the manager's throttling and queueing logic. - -**File:** `test/ui/image/html_image_element_codec_test.dart` (Existing File) -* **Rationale:** Verify that the codec respects concurrency limits and correctly clears resources on disposal. - -**File:** `test/canvaskit/image_test.dart` (Existing File) -* **Rationale:** Regression testing for the CanvasKit pipeline. From 74701fbc0a3a19612b18edd138df85603ba4d596 Mon Sep 17 00:00:00 2001 From: "John \"codefu\" McDole" Date: Fri, 31 Jul 2026 15:18:06 -0700 Subject: [PATCH 006/330] chore: swiftshader mirrored + llvm16 (#181225) Updates SwiftShader to our own mirror and applied patches for llvm16: New head commit: 1be9f83618f8ba258431c0c13d7a083eb193df11 https://flutter.googlesource.com/third_party/swiftshader/+/refs/heads/flutter-a7c547b55 ``` llvm16 + riscv on flutter Reapply "Default to use llvm16" + --- LLVM: Fix missing MCExternalSymbolizer on ARM64 This adds MCExternalSymbolizer.cpp and MCSymbolizer.cpp back to the build files by removing them from the ignore list in generate_build_files.py. It also fixes two Windows-specific bugs in generate_build_files.py that caused it to output thousands of extra files and CRLF line endings on Windows. This reverts commit 5b0479bd2d15058aaa9eb490e364f920ff824a8c. --- LLVM: Fix MSVC 14.44 build error in CFG.h Add default constructor to SuccIterator to satisfy std::default_initializable required by MSVC's C++20/C++23 std::reverse_iterator. ``` Must land before #178712 --- DEPS | 5 +---- engine/src/flutter/sky/packages/sky_engine/LICENSE | 6 ------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/DEPS b/DEPS index ea8fd8654160b..d7377c9eefea3 100644 --- a/DEPS +++ b/DEPS @@ -10,7 +10,6 @@ vars = { 'android_git': 'https://android.googlesource.com', 'chromium_git': 'https://chromium.googlesource.com', - 'swiftshader_git': 'https://swiftshader.googlesource.com', 'dart_git': 'https://dart.googlesource.com', 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', @@ -186,7 +185,6 @@ vars = { "upstream_shelf": "https://github.com/dart-lang/shelf.git", "upstream_skia": "https://skia.googlesource.com/skia.git", "upstream_sqlite": "https://github.com/sqlite/sqlite.git", - "upstream_SwiftShader": "https://swiftshader.googlesource.com/SwiftShader.git", "upstream_tar": "https://github.com/simolus3/tar.git", "upstream_test": "https://github.com/dart-lang/test.git", "upstream_usage": "https://github.com/dart-lang/usage.git", @@ -224,7 +222,6 @@ allowed_hosts = [ 'flutter.googlesource.com', 'llvm.googlesource.com', 'skia.googlesource.com', - 'swiftshader.googlesource.com', ] deps = { @@ -544,7 +541,7 @@ deps = { Var('flutter_git') + '/third_party/pyyaml.git' + '@' + '03c67afd452cdff45b41bfe65e19a2fb5b80a0e8', 'engine/src/flutter/third_party/swiftshader': - Var('swiftshader_git') + '/SwiftShader.git' + '@' + '794b0cfce1d828d187637e6d932bae484fbe0976', + Var('flutter_git') + '/third_party/swiftshader.git' + '@' + '1be9f83618f8ba258431c0c13d7a083eb193df11', 'engine/src/flutter/third_party/angle': Var('flutter_git') + '/third_party/angle' + '@' + 'cc08479fbcc181697fa837069ce1103c58c15528', diff --git a/engine/src/flutter/sky/packages/sky_engine/LICENSE b/engine/src/flutter/sky/packages/sky_engine/LICENSE index 88f05bfe30c20..07be947142735 100644 --- a/engine/src/flutter/sky/packages/sky_engine/LICENSE +++ b/engine/src/flutter/sky/packages/sky_engine/LICENSE @@ -15937,12 +15937,6 @@ glfw Copyright 2014-2022 The Khronos Group Inc. -SPDX-License-Identifier: Apache-2.0 --------------------------------------------------------------------------------- -swiftshader - -Copyright 2014-2025 The Khronos Group Inc. - SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- vulkan From ef089e25c8f2b06bcb8f456fbb5474e56855a275 Mon Sep 17 00:00:00 2001 From: b-luk <97480502+b-luk@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:32:24 -0700 Subject: [PATCH 007/330] Eliminate some early returns in uber_sdf.frag to fix broken UberSDF AA on Windows (#190260) See https://github.com/flutter/flutter/issues/189827#issuecomment-5108403591 for some more context. That comment shows 2 issues: 1. UberSDF AA is broken on Windows 2. Tessellated AA is broken on OpenGLES Fixes https://github.com/flutter/flutter/issues/189827#issuecomment-5108403591 This PR fixes the first issue. The second issue is tracked at https://github.com/flutter/flutter/issues/190060. This PR flattens some branching in uber_sdf.frag to eliminate some early returns. Something about the dFdx and dFdy derivatives are incompatible with ANGLE and/or D3D, making uberSDF antialiasing fail on Windows. I don't know what the exact limits are of when dFdx and dFdy are allowed. From asking Gemini, it may be related to branching, non-uniform branching, loops, or early return statements. After some manual testing, eliminating early return statements in functions which use dFdx and dFdy (either directly, or indirectly through a child call) seems sufficient to fix this. ### Demo app
Demo: ```dart import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return InteractiveViewer( maxScale: 10, child: Container( color: Colors.black, child: CustomPaint(painter: TestPainter()), ), ); } } class TestPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { var filledPaint = Paint()..color = Colors.white; var strokedPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 5; const rect = Rect.fromLTWH(20, 20, 30.5, 15.5); // circle canvas.drawCircle(Offset(35, 25), 15.5, filledPaint); canvas.save(); canvas.translate(50, 0); canvas.drawCircle(Offset(35, 25), 15.5, strokedPaint); canvas.restore(); // oval canvas.translate(0, 50); canvas.drawOval(rect, filledPaint); canvas.save(); canvas.translate(50, 0); canvas.drawOval(rect, strokedPaint); canvas.restore(); // rounded rect canvas.translate(0, 50); canvas.drawRRect(RRect.fromRectAndRadius(rect, .circular(5)), filledPaint); canvas.save(); canvas.translate(50, 0); canvas.drawRRect( RRect.fromRectAndRadius(rect, .circular(5)), strokedPaint, ); canvas.restore(); // column 2 canvas.translate(120, -150); // rect canvas.translate(0, 50); canvas.drawRect(rect, filledPaint); canvas.save(); canvas.translate(50, 0); canvas.drawRect(rect, strokedPaint); canvas.restore(); // rotated rect canvas.translate(0, 50); canvas.save(); canvas.rotate(0.3); canvas.drawRect(rect, filledPaint); canvas.restore(); canvas.save(); canvas.translate(50, 0); canvas.rotate(0.3); canvas.drawRect(rect, strokedPaint); canvas.restore(); // RSuperellipse canvas.translate(0, 50); canvas.drawRSuperellipse( RSuperellipse.fromRectAndRadius(rect, .circular(5)), filledPaint, ); canvas.save(); canvas.translate(50, 0); canvas.drawRSuperellipse( RSuperellipse.fromRectAndRadius(rect, .circular(5)), strokedPaint, ); canvas.restore(); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } ```
### Before: image 500% zoom: image ### After: image 500% zoom: image Notice that stroked RSuperellipse does not have AA. This confused me for a long time. Eventually I figured out that stroked RSuperellipse is [always drawn with a path](https://github.com/flutter/flutter/blob/b1ae9ca144127fa87eb86d0cdf73f6c028964ec3/engine/src/flutter/display_list/dl_builder.cc#L1371-L1378). So it does not go through the UberSDF code. Paths render with tessellation, which is supposed to use MSAA but is currently not functioning properly on Windows. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> --- .../impeller/entity/shaders/uber_sdf.frag | 64 +++++++++++-------- engine/src/flutter/impeller/tools/malioc.json | 38 +++++------ 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/engine/src/flutter/impeller/entity/shaders/uber_sdf.frag b/engine/src/flutter/impeller/entity/shaders/uber_sdf.frag index d291dfc4a3980..97a5f5b715928 100644 --- a/engine/src/flutter/impeller/entity/shaders/uber_sdf.frag +++ b/engine/src/flutter/impeller/entity/shaders/uber_sdf.frag @@ -176,14 +176,16 @@ float roundRectPixelSize(vec2 p) { vec2 corner_center = frag_info.size - radius; vec2 q = abs(p) - corner_center; + float pixel_size; // If in the rounded corner arc, blend X and Y pixel sizes along the normal. if (q.x > 0.0 && q.y > 0.0) { - return length(normalize(q) * device_pixel_size); + pixel_size = length(normalize(q) * device_pixel_size); + } else { + // Otherwise, we are closer to a straight edge. Get pixel size in the + // direction perpendicular to the closer edge. + pixel_size = (q.x > q.y) ? device_pixel_size.x : device_pixel_size.y; } - - // Otherwise, we are closer to a straight edge. Get pixel size in the - // direction perpendicular to the closer edge. - return (q.x > q.y) ? device_pixel_size.x : device_pixel_size.y; + return pixel_size; } float pixelSize(float sdf) { @@ -195,26 +197,30 @@ float pixelSize(float sdf) { // Returns vec2(sdf, pixel_size). vec2 filledSDF(vec2 p) { float sdf; + float pixel_size; if (frag_info.type < 0.5) { // Circle sdf = distanceFromCircle(p, frag_info.size.x); + pixel_size = pixelSize(sdf); } else if (frag_info.type < 1.5) { // Rect sdf = distanceFromRect(p, frag_info.size); // Rect has its own separate logic for calculating pixel size. - return vec2(sdf, rectPixelSize(p)); + pixel_size = rectPixelSize(p); } else if (frag_info.type < 2.5) { // Oval sdf = distanceFromOval(p, frag_info.size); + pixel_size = pixelSize(sdf); } else if (frag_info.type < 3.5) { // Rounded Rect - // RoundRect has its own separate logic for calculating pixel size. sdf = distanceFromRoundedRect(p, frag_info.size, frag_info.radii); - return vec2(sdf, roundRectPixelSize(p)); + // RoundRect has its own separate logic for calculating pixel size. + pixel_size = roundRectPixelSize(p); } else { // Symmetric Rounded Superellipse sdf = distanceFromRoundedSuperellipse( p, frag_info.superellipse_degree, frag_info.superellipse_semi_axis, frag_info.radii.xy, frag_info.angle_span, frag_info.circle_center_top, frag_info.circle_center_right, frag_info.octant_offset_c, frag_info.superellipse_scale); + pixel_size = pixelSize(sdf); } - return vec2(sdf, pixelSize(sdf)); + return vec2(sdf, pixel_size); } // Evaluates the stroked SDF for the shape selected by frag_info.type. @@ -226,23 +232,31 @@ vec2 strokedSDF(vec2 p) { float half_stroke = max(frag_info.stroke_width, base_pixel_size) * 0.5; - if (frag_info.type >= 0.5 && frag_info.type < 1.5) { // Rect - - if (frag_info.stroke_join < 0.5) { // Miter - float outer = distanceFromRect(p, frag_info.size + half_stroke); - float inner = base_sdf + half_stroke; - float sdf = max(outer, -inner); - return vec2(sdf, pixelSize(sdf)); - } else if (frag_info.stroke_join < 1.5) { // Bevel - float outer = - distanceFromChamferRect(p, frag_info.size + half_stroke, half_stroke); - float inner = base_sdf + half_stroke; - float sdf = max(outer, -inner); - return vec2(sdf, pixelSize(sdf)); - } + float sdf; + float pixel_size; + if (frag_info.type >= 0.5 && frag_info.type < 1.5 && + frag_info.stroke_join < 0.5) { + // Rect with Miter join + float outer = distanceFromRect(p, frag_info.size + half_stroke); + float inner = base_sdf + half_stroke; + sdf = max(outer, -inner); + pixel_size = pixelSize(sdf); + } else if (frag_info.type >= 0.5 && frag_info.type < 1.5 && + frag_info.stroke_join >= 0.5 && frag_info.stroke_join < 1.5) { + // Rect with Bevel join + float outer = + distanceFromChamferRect(p, frag_info.size + half_stroke, half_stroke); + float inner = base_sdf + half_stroke; + sdf = max(outer, -inner); + pixel_size = pixelSize(sdf); + } else { + // All other shapes + vec2 sdf_and_pixel_size = + SDFStroke(base_sdf, base_pixel_size, frag_info.stroke_width); + sdf = sdf_and_pixel_size.x; + pixel_size = sdf_and_pixel_size.y; } - - return SDFStroke(base_sdf, base_pixel_size, frag_info.stroke_width); + return vec2(sdf, pixel_size); } // Converts linear coverage alpha to perceptual alpha. diff --git a/engine/src/flutter/impeller/tools/malioc.json b/engine/src/flutter/impeller/tools/malioc.json index da2d4782141bc..d93c1e6ad6f2d 100644 --- a/engine/src/flutter/impeller/tools/malioc.json +++ b/engine/src/flutter/impeller/tools/malioc.json @@ -8629,7 +8629,7 @@ "shortest_path_cycles": [ 0.53125, 0.53125, - 0.140625, + 0.15625, 0.5, 0.0, 0.25, @@ -8640,10 +8640,10 @@ "arith_fma" ], "total_cycles": [ - 13.375, - 13.375, - 4.65625, - 12.625, + 13.625, + 13.625, + 4.53125, + 13.4375, 0.0, 0.25, 0.0 @@ -8651,7 +8651,7 @@ }, "stack_spill_bytes": 0, "thread_occupancy": 100, - "uniform_registers_used": 44, + "uniform_registers_used": 46, "work_registers_used": 32 } } @@ -8659,7 +8659,7 @@ "Mali-T880": { "core": "Mali-T880", "filename": "flutter/impeller/entity/gles/uber_sdf.frag.gles", - "has_uniform_computation": false, + "has_uniform_computation": true, "type": "Fragment", "variants": { "Main": { @@ -8669,7 +8669,7 @@ "arithmetic" ], "longest_path_cycles": [ - 45.209999084472656, + 43.88999938964844, 3.0, 4.0 ], @@ -8690,13 +8690,13 @@ "arithmetic" ], "total_cycles": [ - 102.33333587646484, + 105.66666412353516, 3.0, - 16.0 + 24.0 ] }, "thread_occupancy": 100, - "uniform_registers_used": 5, + "uniform_registers_used": 7, "work_registers_used": 4 } } @@ -11823,7 +11823,7 @@ "uses_late_zs_update": false, "variants": { "Main": { - "fp16_arithmetic": 36, + "fp16_arithmetic": 35, "has_stack_spilling": false, "performance": { "longest_path_bound_pipelines": [ @@ -11833,7 +11833,7 @@ "longest_path_cycles": [ 4.0, 3.387500047683716, - 1.9249999523162842, + 1.9375, 4.0, 0.0, 0.25, @@ -11855,7 +11855,7 @@ "shortest_path_cycles": [ 0.5, 0.453125, - 0.15625, + 0.171875, 0.5, 0.0, 0.25, @@ -11866,10 +11866,10 @@ "arith_sfu" ], "total_cycles": [ - 12.625, - 12.125, - 4.800000190734863, - 12.625, + 13.4375, + 12.4375, + 4.875, + 13.4375, 0.0, 0.25, 0.0 @@ -11877,7 +11877,7 @@ }, "stack_spill_bytes": 0, "thread_occupancy": 100, - "uniform_registers_used": 50, + "uniform_registers_used": 48, "work_registers_used": 32 } } From 47f8e6efb027bd8ad5de69b1fd8271ac1a1034ac Mon Sep 17 00:00:00 2001 From: b-luk <97480502+b-luk@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:30:07 -0700 Subject: [PATCH 008/330] Primitive shape integration test (#190368) Adds an integration screenshot test for rendering primitive shapes, as shown in https://github.com/flutter/flutter/issues/189827#issuecomment-5108403591. Adds a CI task to run this on Windows. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .ci.yaml | 15 +++ TESTOWNERS | 1 + .../windows_primitive_shape_golden_test.dart | 12 ++ .../lib/tasks/integration_tests.dart | 7 ++ .../primitive_shape_test/README.md | 14 +++ .../integration_test/flutter_test_config.dart | 14 +++ .../primitive_shape_test.dart | 23 ++++ .../primitive_shape_test/lib/main.dart | 111 ++++++++++++++++++ .../primitive_shape_test/pubspec.yaml | 26 ++++ pubspec.yaml | 1 + 10 files changed, 224 insertions(+) create mode 100644 dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart create mode 100644 dev/integration_tests/primitive_shape_test/README.md create mode 100644 dev/integration_tests/primitive_shape_test/integration_test/flutter_test_config.dart create mode 100644 dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart create mode 100644 dev/integration_tests/primitive_shape_test/lib/main.dart create mode 100644 dev/integration_tests/primitive_shape_test/pubspec.yaml diff --git a/.ci.yaml b/.ci.yaml index d8f3a738f017c..2a7328234f192 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -7280,6 +7280,21 @@ targets: ] task_name: windows_desktop_impeller + - name: Windows windows_primitive_shape_golden_test + recipe: devicelab/devicelab_drone + presubmit: false + bringup: true + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"}, + {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"} + ] + task_name: windows_primitive_shape_golden_test + - name: Windows texture_impeller_windows recipe: devicelab/devicelab_drone presubmit: false diff --git a/TESTOWNERS b/TESTOWNERS index f84904777efe9..339d363e24288 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -332,6 +332,7 @@ /dev/devicelab/bin/tasks/windowing_test_windows.dart @mattkae @flutter/desktop /dev/devicelab/bin/tasks/windows_desktop_impeller.dart @jonahwilliams @flutter/engine /dev/devicelab/bin/tasks/windows_home_scroll_perf__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart @b-luk @flutter/engine /dev/devicelab/bin/tasks/windows_startup_test.dart @loic-sharma @flutter/desktop ## Host only framework tests diff --git a/dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart b/dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart new file mode 100644 index 0000000000000..62e2559b9b354 --- /dev/null +++ b/dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.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.windows; + await task(createPrimitiveShapeTest()); +} diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index e39d562fc3088..929e9d5aec4e2 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -230,6 +230,13 @@ TaskFunction createWindowsStartupDriverTest({String? deviceIdOverride}) { ).call; } +TaskFunction createPrimitiveShapeTest() { + return IntegrationTest( + '${flutterDirectory.path}/dev/integration_tests/primitive_shape_test', + 'integration_test/primitive_shape_test.dart', + ).call; +} + TaskFunction createWindowingDriverTest() { return () async { await flutter('config', options: const ['--enable-windowing']); diff --git a/dev/integration_tests/primitive_shape_test/README.md b/dev/integration_tests/primitive_shape_test/README.md new file mode 100644 index 0000000000000..09b656b4e4340 --- /dev/null +++ b/dev/integration_tests/primitive_shape_test/README.md @@ -0,0 +1,14 @@ +# Primitive Shape Integration Test + +This integration test suite validates rendering of primitive canvas shapes. + +## Running Locally +```sh +flutter test dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart -d +``` + +## Running via Devicelab on Windows +```sh +cd dev/devicelab +dart bin/run.dart -t windows_primitive_shape_golden_test +``` diff --git a/dev/integration_tests/primitive_shape_test/integration_test/flutter_test_config.dart b/dev/integration_tests/primitive_shape_test/integration_test/flutter_test_config.dart new file mode 100644 index 0000000000000..2057b6ef9f76c --- /dev/null +++ b/dev/integration_tests/primitive_shape_test/integration_test/flutter_test_config.dart @@ -0,0 +1,14 @@ +// 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 'dart:async'; +import 'package:flutter_goldens/flutter_goldens.dart' as flutter_goldens; + +/// Configures [goldenFileComparator] for the test suite using `package:flutter_goldens`. +/// +/// On CI/LUCI, if `GOLDCTL` environment variable is present, screenshots taken with +/// `matchesGoldenFile` will be uploaded to Skia Gold. +Future testExecutable(FutureOr Function() testMain) async { + return flutter_goldens.testExecutable(testMain, namePrefix: 'primitive_shape'); +} diff --git a/dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart b/dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart new file mode 100644 index 0000000000000..da35e8d662e40 --- /dev/null +++ b/dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart @@ -0,0 +1,23 @@ +// 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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:primitive_shape_test/main.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('renders primitive shapes', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); + await tester.pumpAndSettle(); + + // Take a screenshot of the test canvas widget. + await expectLater( + find.byKey(const Key('primitive_shape_canvas')), + matchesGoldenFile('primitive_shape_canvas_snapshot.png'), + ); + }); +} diff --git a/dev/integration_tests/primitive_shape_test/lib/main.dart b/dev/integration_tests/primitive_shape_test/lib/main.dart new file mode 100644 index 0000000000000..c69007b522a72 --- /dev/null +++ b/dev/integration_tests/primitive_shape_test/lib/main.dart @@ -0,0 +1,111 @@ +// 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/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Container( + key: const Key('primitive_shape_canvas'), + color: Colors.black, + width: double.infinity, + height: double.infinity, + child: CustomPaint(painter: TestPainter()), + ), + ), + ); + } +} + +class TestPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final filledPaint = Paint()..color = Colors.white; + final strokedPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 5; + const rect = Rect.fromLTWH(20, 20, 30.5, 15.5); + + // Column 1 + canvas.save(); + + // circle + canvas.drawCircle(const Offset(35, 25), 15.5, filledPaint); + canvas.save(); + canvas.translate(50, 0); + canvas.drawCircle(const Offset(35, 25), 15.5, strokedPaint); + canvas.restore(); + + // oval + canvas.translate(0, 50); + canvas.drawOval(rect, filledPaint); + canvas.save(); + canvas.translate(50, 0); + canvas.drawOval(rect, strokedPaint); + canvas.restore(); + + // rounded rect + canvas.translate(0, 50); + canvas.drawRRect(RRect.fromRectAndRadius(rect, const Radius.circular(5)), filledPaint); + canvas.save(); + canvas.translate(50, 0); + canvas.drawRRect(RRect.fromRectAndRadius(rect, const Radius.circular(5)), strokedPaint); + canvas.restore(); + + canvas.restore(); + + // Column 2 + canvas.save(); + canvas.translate(120, 0); + + // rect + canvas.drawRect(rect, filledPaint); + canvas.save(); + canvas.translate(50, 0); + canvas.drawRect(rect, strokedPaint); + canvas.restore(); + + // rotated rect + canvas.translate(0, 50); + canvas.save(); + canvas.rotate(0.3); + canvas.drawRect(rect, filledPaint); + canvas.restore(); + canvas.save(); + canvas.translate(50, 0); + canvas.rotate(0.3); + canvas.drawRect(rect, strokedPaint); + canvas.restore(); + + // RSuperellipse + canvas.translate(0, 50); + canvas.drawRSuperellipse( + RSuperellipse.fromRectAndRadius(rect, const Radius.circular(5)), + filledPaint, + ); + canvas.save(); + canvas.translate(50, 0); + canvas.drawRSuperellipse( + RSuperellipse.fromRectAndRadius(rect, const Radius.circular(5)), + strokedPaint, + ); + canvas.restore(); + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/dev/integration_tests/primitive_shape_test/pubspec.yaml b/dev/integration_tests/primitive_shape_test/pubspec.yaml new file mode 100644 index 0000000000000..1199f4e05d7f0 --- /dev/null +++ b/dev/integration_tests/primitive_shape_test/pubspec.yaml @@ -0,0 +1,26 @@ +name: primitive_shape_test +description: Integration test to capture primitive shape rendering snapshots and upload to Skia Gold. +publish_to: none + +environment: + sdk: ^3.11.0-0 + +resolution: workspace + +dependencies: + flutter: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + path: any + +dev_dependencies: + flutter_goldens: + sdk: flutter + flutter_test: + sdk: flutter + test: any + +# PUBSPEC CHECKSUM: fp4cre diff --git a/pubspec.yaml b/pubspec.yaml index 3af22515f270b..f7b7952208d4c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ workspace: - dev/integration_tests/link_hook - dev/integration_tests/new_gallery - dev/integration_tests/platform_interaction + - dev/integration_tests/primitive_shape_test - dev/integration_tests/record_use_test_app - dev/integration_tests/record_use_test_package - dev/integration_tests/release_smoke_test From cda25ea220c923a601dbc33ed4e9dd4feb662d64 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 31 Jul 2026 20:43:58 -0400 Subject: [PATCH 009/330] Roll Skia from f73c4510d12d to ebf50520d720 (6 revisions) (#190376) https://skia.googlesource.com/skia.git/+log/f73c4510d12d..ebf50520d720 2026-07-31 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 9b3fec1aec6c to 38725942468a (4 revisions) 2026-07-31 alexisdavidc@google.com [Bug] Delete friend class of ContextCtorAccessor 2026-07-31 kjlubick@google.com Fix SkPathBuilder::snapshot() with non-affine matrices 2026-07-31 michaelludwig@google.com [capture] Add virtual overrides that go to base canvas for slugs/pixel info 2026-07-31 alexisdavidc@google.com [SkContext] Begin Defining SkContext and SkContextOptions 2026-07-31 borenet@google.com Reland "[infra] Cleanup after removing Upload tasks" If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,bwils@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index d7377c9eefea3..db398104a88d5 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'f73c4510d12da77512253d3a00d72275b45427c7', + 'skia_revision': 'ebf50520d720a1ce9d842d942d04c6c39c3fbc7b', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 421defc7a78d7a3d7d18c95ab0ad514f318945af Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 1 Aug 2026 09:16:00 -0400 Subject: [PATCH 010/330] Roll Skia from ebf50520d720 to 32329e5643b5 (1 revision) (#190389) https://skia.googlesource.com/skia.git/+log/ebf50520d720..32329e5643b5 2026-08-01 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 38725942468a to ac9d03694598 (2 revisions) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,bwils@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index db398104a88d5..0859055df5a11 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'ebf50520d720a1ce9d842d942d04c6c39c3fbc7b', + 'skia_revision': '32329e5643b53a27b503d8cb03f27272dedcbab1', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From efaddfa4cbc6338c5c92f57b36eeb12bdea39097 Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:41:23 -0700 Subject: [PATCH 011/330] Revert "Improve non rect platform view rendering (#182662)" (#190003) This reverts commit 2d424bd2888e23ab2da21d40a0b1000960b49300. This caused a regression for an internal project. The project uses web view as rich text editor. The regression is that the buttons etc are gone. However, we were not able to repro. *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* NA. Internally tracked by b/515604266 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- engine/src/flutter/flow/view_slicer.cc | 17 +--- engine/src/flutter/flow/view_slicer.h | 6 +- .../src/flutter/flow/view_slicer_unittests.cc | 47 ++-------- .../external_view_embedder.cc | 3 +- .../external_view_embedder_2.cc | 3 +- .../Source/FlutterPlatformViewsController.mm | 82 +----------------- ...one SE (3rd generation)_26.2_simulator.png | Bin 21892 -> 21834 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 17434 -> 17409 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 21225 -> 21184 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 18421 -> 18371 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 22055 -> 22000 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 20669 -> 20628 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 24863 -> 24851 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 28185 -> 28109 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 24406 -> 24356 bytes 15 files changed, 14 insertions(+), 144 deletions(-) diff --git a/engine/src/flutter/flow/view_slicer.cc b/engine/src/flutter/flow/view_slicer.cc index 02e0665274dc9..22a2c10081c5b 100644 --- a/engine/src/flutter/flow/view_slicer.cc +++ b/engine/src/flutter/flow/view_slicer.cc @@ -15,8 +15,7 @@ std::unordered_map SliceViews( const std::vector& composition_order, const std::unordered_map>& slices, - const std::unordered_map& view_rects, - const std::unordered_set& views_with_underlay_preserved) { + const std::unordered_map& view_rects) { std::unordered_map overlay_layers; auto current_frame_view_count = composition_order.size(); @@ -111,17 +110,9 @@ std::unordered_map SliceViews( if (!full_joined_rect.IsEmpty()) { overlay_layers.insert({view_id, full_joined_rect}); - // If overlay has a non-rect clip, we need to preserve the underlay in - // that area to show correctly after overlay is clipped behind the - // platform view - const bool preserve_underlay = - views_with_underlay_preserved.find(view_id) != - views_with_underlay_preserved.end(); - if (!preserve_underlay) { - // Clip the background canvas, so it doesn't contain any of the pixels - // drawn on the overlay layer. - background_canvas->ClipRect(full_joined_rect, DlClipOp::kDifference); - } + // Clip the background canvas, so it doesn't contain any of the pixels + // drawn on the overlay layer. + background_canvas->ClipRect(full_joined_rect, DlClipOp::kDifference); } slice->render_into(background_canvas); } diff --git a/engine/src/flutter/flow/view_slicer.h b/engine/src/flutter/flow/view_slicer.h index db0b270dd56c1..dcdb2e945bc06 100644 --- a/engine/src/flutter/flow/view_slicer.h +++ b/engine/src/flutter/flow/view_slicer.h @@ -6,7 +6,6 @@ #define FLUTTER_FLOW_VIEW_SLICER_H_ #include -#include #include "display_list/dl_canvas.h" #include "flow/embedded_views.h" @@ -14,15 +13,12 @@ namespace flutter { /// @brief Compute the required overlay layers and clip the view slices /// according to the size and position of the platform views. -/// @param views_with_underlay_preserved The platform view IDs for which we -/// should not subtract the overlap from the background canvas. std::unordered_map SliceViews( DlCanvas* background_canvas, const std::vector& composition_order, const std::unordered_map>& slices, - const std::unordered_map& view_rects, - const std::unordered_set& views_with_underlay_preserved); + const std::unordered_map& view_rects); } // namespace flutter diff --git a/engine/src/flutter/flow/view_slicer_unittests.cc b/engine/src/flutter/flow/view_slicer_unittests.cc index 27c6a171b9afb..22313cae14a91 100644 --- a/engine/src/flutter/flow/view_slicer_unittests.cc +++ b/engine/src/flutter/flow/view_slicer_unittests.cc @@ -12,15 +12,6 @@ namespace flutter { namespace testing { namespace { -bool ContainsClipDifferenceRect(const sk_sp& display_list) { - for (DlIndex i = 0; i < display_list->op_count(); i++) { - if (display_list->GetOpType(i) == DisplayListOpType::kClipDifferenceRect) { - return true; - } - } - return false; -} - void AddSliceOfSize( std::unordered_map>& slices, int64_t id, @@ -43,7 +34,7 @@ TEST(ViewSlicerTest, CanSlicerNonOverlappingViews) { {1, DlRect::MakeLTRB(50, 50, 60, 60)}}; auto computed_overlays = - SliceViews(&builder, composition_order, slices, view_rects, {}); + SliceViews(&builder, composition_order, slices, view_rects); EXPECT_TRUE(computed_overlays.empty()); } @@ -59,7 +50,7 @@ TEST(ViewSlicerTest, IgnoresFractionalOverlaps) { {1, DlRect::MakeLTRB(50.5, 50.5, 100, 100)}}; auto computed_overlays = - SliceViews(&builder, composition_order, slices, view_rects, {}); + SliceViews(&builder, composition_order, slices, view_rects); EXPECT_TRUE(computed_overlays.empty()); } @@ -75,7 +66,7 @@ TEST(ViewSlicerTest, ComputesOverlapWith1PV) { {1, DlRect::MakeLTRB(0, 0, 100, 100)}}; auto computed_overlays = - SliceViews(&builder, composition_order, slices, view_rects, {}); + SliceViews(&builder, composition_order, slices, view_rects); EXPECT_EQ(computed_overlays.size(), 1u); auto overlay = computed_overlays.find(1); @@ -98,7 +89,7 @@ TEST(ViewSlicerTest, ComputesOverlapWith2PV) { }; auto computed_overlays = - SliceViews(&builder, composition_order, slices, view_rects, {}); + SliceViews(&builder, composition_order, slices, view_rects); EXPECT_EQ(computed_overlays.size(), 2u); @@ -132,7 +123,7 @@ TEST(ViewSlicerTest, OverlappingTwoPVs) { }; auto computed_overlays = - SliceViews(&builder, composition_order, slices, view_rects, {}); + SliceViews(&builder, composition_order, slices, view_rects); EXPECT_EQ(computed_overlays.size(), 1u); @@ -143,33 +134,5 @@ TEST(ViewSlicerTest, OverlappingTwoPVs) { EXPECT_EQ(overlay->second, DlRect::MakeLTRB(0, 0, 100, 100)); } -TEST(ViewSlicerTest, PreservesUnderlayForSelectedViews) { - std::vector composition_order = {1}; - std::unordered_map view_rects = { - {1, DlRect::MakeLTRB(0, 0, 100, 100)}}; - - std::unordered_map> - baseline_slices; - AddSliceOfSize(baseline_slices, 1, DlRect::MakeLTRB(0, 0, 50, 50)); - DisplayListBuilder baseline_builder(DlRect::MakeLTRB(0, 0, 100, 100)); - auto baseline_overlays = SliceViews(&baseline_builder, composition_order, - baseline_slices, view_rects, {}); - EXPECT_EQ(baseline_overlays.size(), 1u); - auto baseline_dl = baseline_builder.Build(); - EXPECT_TRUE(ContainsClipDifferenceRect(baseline_dl)); - - std::unordered_map> - preserve_slices; - AddSliceOfSize(preserve_slices, 1, DlRect::MakeLTRB(0, 0, 50, 50)); - std::unordered_set views_with_underlay_preserved = {1}; - DisplayListBuilder preserve_builder(DlRect::MakeLTRB(0, 0, 100, 100)); - auto preserve_overlays = - SliceViews(&preserve_builder, composition_order, preserve_slices, - view_rects, views_with_underlay_preserved); - EXPECT_EQ(preserve_overlays.size(), 1u); - auto preserve_dl = preserve_builder.Build(); - EXPECT_FALSE(ContainsClipDifferenceRect(preserve_dl)); -} - } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc index 8af15f632fca7..2d2efb737d2f0 100644 --- a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc +++ b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc @@ -86,8 +86,7 @@ void AndroidExternalViewEmbedder::SubmitFlutterView( SliceViews(frame->Canvas(), // composition_order_, // slices_, // - view_rects, // - {} // + view_rects // ); // Submit the background canvas frame before switching the GL context to diff --git a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc index cadb90f1459a6..e90b1d30d509d 100644 --- a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc +++ b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc @@ -99,8 +99,7 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView( SliceViews(frame->Canvas(), // composition_order_, // slices_, // - view_rects, // - {} // + view_rects // ); // If there is no overlay Surface, initialize one on the platform thread. This diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm index 8b9ac45822f1c..5b5d19327a57e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm @@ -7,7 +7,6 @@ #include "impeller/geometry/rounding_radii.h" #include "flutter/display_list/effects/image_filters/dl_blur_image_filter.h" -#include "flutter/display_list/geometry/dl_geometry_conversions.h" #include "flutter/display_list/utils/dl_matrix_clip_tracker.h" #include "flutter/flow/surface_frame.h" #include "flutter/flow/view_slicer.h" @@ -91,74 +90,6 @@ static CGRect GetCGRectFromDlRect(const DlRect& clipDlRect) { clipDlRect.GetHeight()); } -static bool HasNonRectClipForUnderlayCutout(const flutter::EmbeddedViewParams& params) { - auto iter = params.mutatorsStack().Begin(); - while (iter != params.mutatorsStack().End()) { - switch ((*iter)->GetType()) { - case flutter::MutatorType::kClipRRect: - case flutter::MutatorType::kClipRSE: - case flutter::MutatorType::kClipPath: - return true; - default: - break; - } - ++iter; - } - return false; -} - -// Overlay canvas needs to be clipped to the shape of platform view to ensure -// underlay shows up correctly, so that when there's backdrop filter, the region outside of platform -// view's shape is blurred. See: https://github.com/flutter/flutter/issues/150660 -static void ApplyNonRectClipToOverlayCanvas(flutter::DlCanvas* overlay_canvas, - const flutter::EmbeddedViewParams& params) { - flutter::DlMatrix transform; - auto iter = params.mutatorsStack().Begin(); - while (iter != params.mutatorsStack().End()) { - switch ((*iter)->GetType()) { - case flutter::MutatorType::kTransform: - transform = transform * (*iter)->GetMatrix(); - break; - case flutter::MutatorType::kClipRRect: { - if (transform.IsIdentity()) { - overlay_canvas->ClipRoundRect((*iter)->GetRRect(), flutter::DlClipOp::kIntersect, true); - } else { - auto path = flutter::DlPath::MakeRoundRect((*iter)->GetRRect()); - auto transformed_path = - flutter::DlPath(path.GetSkPath().makeTransform(flutter::ToSkMatrix(transform))); - overlay_canvas->ClipPath(transformed_path, flutter::DlClipOp::kIntersect, true); - } - break; - } - case flutter::MutatorType::kClipRSE: { - if (transform.IsIdentity()) { - overlay_canvas->ClipRoundSuperellipse((*iter)->GetRSE(), flutter::DlClipOp::kIntersect, - true); - } else { - auto path = flutter::DlPath::MakeRoundSuperellipse((*iter)->GetRSE()); - auto transformed_path = - flutter::DlPath(path.GetSkPath().makeTransform(flutter::ToSkMatrix(transform))); - overlay_canvas->ClipPath(transformed_path, flutter::DlClipOp::kIntersect, true); - } - break; - } - case flutter::MutatorType::kClipPath: { - if (transform.IsIdentity()) { - overlay_canvas->ClipPath((*iter)->GetPath(), flutter::DlClipOp::kIntersect, true); - } else { - auto transformed_path = flutter::DlPath( - (*iter)->GetPath().GetSkPath().makeTransform(flutter::ToSkMatrix(transform))); - overlay_canvas->ClipPath(transformed_path, flutter::DlClipOp::kIntersect, true); - } - break; - } - default: - break; - } - ++iter; - } -} - @interface FlutterPlatformViewsController () // The pool of reusable view layers. The pool allows to recycle layer in each frame. @@ -834,19 +765,13 @@ - (BOOL)submitFrame:(std::unique_ptr)background_frame std::vector> surfaceFrames; surfaceFrames.reserve(self.compositionOrder.size()); std::unordered_map viewRects; - std::unordered_set viewsWithUnderlayPreserved; for (int64_t viewId : self.compositionOrder) { - const flutter::EmbeddedViewParams& params = self.currentCompositionParams[viewId]; - viewRects[viewId] = params.finalBoundingRect(); - if (HasNonRectClipForUnderlayCutout(params)) { - viewsWithUnderlayPreserved.insert(viewId); - } + viewRects[viewId] = self.currentCompositionParams[viewId].finalBoundingRect(); } std::unordered_map overlayLayers = - SliceViews(background_frame->Canvas(), self.compositionOrder, self.slices, viewRects, - viewsWithUnderlayPreserved); + SliceViews(background_frame->Canvas(), self.compositionOrder, self.slices, viewRects); size_t requiredOverlayLayers = 0; for (int64_t viewId : self.compositionOrder) { @@ -882,9 +807,6 @@ - (BOOL)submitFrame:(std::unique_ptr)background_frame int restoreCount = overlayCanvas->GetSaveCount(); overlayCanvas->Save(); overlayCanvas->ClipRect(overlay->second); - if (viewsWithUnderlayPreserved.find(viewId) != viewsWithUnderlayPreserved.end()) { - ApplyNonRectClipToOverlayCanvas(overlayCanvas, self.currentCompositionParams[viewId]); - } overlayCanvas->Clear(flutter::DlColor::kTransparent()); self.slices[viewId]->render_into(overlayCanvas); overlayCanvas->RestoreToCount(restoreCount); diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 3447128ba4b76a26d359d9799f489ce8d5904329..f83029665cbe93a76de1bbe7163c1ebba7204250 100644 GIT binary patch delta 10956 zcmYjX2RPMl`_Ji+L&_GyLD_qgaZ1JyAv;?(AuB7sPKA=04vw9S%(C|gk)4&jGdg6? ztp9UB^-`mngxX zLu`})ypr7FUP*I340>WV8i<@btX5;Y9X@f``>tYa$C$HE;%;ntZ`N;(w|NgUF0RJd zk$Yh!xPGv?FpT}tsQvihr3C6>mW#HRq6od@gCViFZzYR7QHJC3OG>4D-NxL`xwpbI zXKp6psFd6TpVx2Bc*?h^Zus{d2d=1|{+{vNPZ2w-KOgnKVT44m4orXwK* zev|KR&t42EoZ*iC$a`e%TC4|TQpStdM2&B7GUw>PiNGR7-LvYf3(r^-*um!IV%Yn~stL1m`J5mHau zaE*b}EEh*7JeY!_vuzd#+Rlak*35UDj;BxWrfwE8PL#SYzR_OZYLFCQx$|?Iv}x-r z?!e-tLS>_VSJivJJ!B=_V#lOue+GA;1^spAHApEco~hpIbt}HuRk%Ro4w_EtE`|m@ zvTJ2VLZI`NUT9aciY#>>eSQ6SD=hsSXDE5LZ!s%ezN8Xl(+~BZ_6#k)Kc3z!KJKxf zF_)YBe2)7X6?KloWvlnH9Dj#ud`oN{x+5z`*uw<(_PkEJo1WymBJ6w9p^{o-v!fQe z%D{Qjs;fma(1l`PAiveCt3^QzRM%&NP6lwXiln)ky?<9#6vT!H6!vxNcUmq!E8dq^=#e9jO;++Eca|=iGg-vf(W`d_(*P>*P`aQ8C{IS8IfL> zua?fzRlJbf`JUvr`&U_RC+be|ZC=6pFGpDWcrvD+GeLfnm1&Gxh~pojWVxZ03cEdn zbQY$je-0 zP~+iV4@6&5RXM_PHjvQjR*I`hzc~GK$H)AP%y8PsjJT;&{;;ObJjl<=Tm7h^&UJcZ z_Ss0JyP%hkMg5=30_m@vab+#~@jR;4Qq;xq4@6I|2d*wT<$E1_46$5nH!L3RA2wFu z7-z?{i}vI;{TlrJy%&G9JdjLXU9T4_uMap}NQLesT;q3}4JxeIH;$J*Ep#%kuF zwd&h+JKa3XjBY(A`xeke)5Qr6jgEyj`qJIbPMVtbhteCX{7Y;O*3Z+A-q01SL>Uot zYl6TMHj!cHUhW4@>`Lp9x;(~IHgDlpJXS{hOV(F9%6oe zR;cmYO}03hXx}zr+uiEqSHoMx?WAIttY+5`!L}-5T4W|Jl ze>nB-L)ks&ivzc+rjdzO^@M|G&kjNYZ0038%K8QJ&bw~Ph-gs#-e?rcu3eBiN(;Ca zn#F1p$C@%T^YwyE!76GXV8)aG8~udQrI}Bgw~BAt+vz0AkBp2+iY4d!cSV*t zMKR0$9kY&()ctG*Cno=%9IR)jBUftq6_OWTcWpgC+iis;(`ug1hgh61F;OiG&$rcW z%2Nl$>AOk3g5E>hZzmg%zI|nDHA|blIJ@w%o{t-+3?e+oeGWP-OAnlkVk*e*fWl^f z%>Vqy+q>;v;^20f!bw}*%>Likf8G>^Zq05-NjB~mtu51p+!x)`G=tweZaPlBe)g56 zzRZ7(hnlf?UT*J4ZAq7#Yw`16>{<8p%m3<>- z#@FX}fyEgP!~1bIlc7oO08mZFLm0#^=p~uY2d$;!v+?y%&{cH)(Y&&M!>w;rp(ropj*&-31*(QJyNO2 zJaG96^mG>F+6-1T*R@ap>Nlx${ew6pX|?{P7Hgd+{&d^Z*3Ii~`NJzZPwsNQ=_k(PRlW|7iUl>V4T$v_`8*>LVZI$rkY z?5x+yT(z2#D#&?8YTR>p$fd9jl^E6FHhU^WD3z6HMda!m&p$;oQU1GEC?01%`ktgv zkEdx*y~*p>cW*;iC`a$bO9VOc2RT7r4}9HorSI%*)6v@;4xa^%;L^HP*gV0yRv=DS zDf-?Cf?QR**=-debGA{p8Q-B{Y2!l;1C%xT@wFm3ocIuS*`T%#aI#$QrinqGMIrG zzJWD?CkNo)cGQ<>6g6yqJezBQ98Lh^CQ-F*U)vXYXjeadK=JzC9LCUG+V|-T$sHw% zPe@|&*Sdh7?V94dm+CfZoqMLAk-t`&i&bHMbDP@zzXtS}TU5DDzaS)SpK`f^=7K{l z)L5oF&+b!%gZEYS@b7oj)roOLGEpGbCo0zVek0_&C>~bdV?`nF0oxK&{Z~?bqH6lD z!Agj$Z&9ghg}+4Af;u;F(pXU7yx1Q3akC7A;)Bhzk3}n-`|=IEet8LfpD;`0qtEG| zs#)fov#7>?1Mu?tOv6bJ7IF-{%SVd<=4o4kwwNYuc(Zo0as~!TY z)INtr3;1y*i7O1R4)nYiutR*z`^e-{SZg+$dl!lau%KB;(vGKJmo*@)mp4gg<$0>QHB z@bU8Uy&7;Ue18?*q8`+`s6G&K8wrE!P{(vdtnZt+HHd4RFx8C6psV2r>*JpDEg{v9 zDai<6Z%B?(cn)~{3Bojoo!J){g;EqiCDTTq2A9=}0gqo-L%*c*}2A`@2fkIf$x!~6pRtRplu;KW&6NK8%f zXceg6)3r1(VUkzUO~RqKIbd)~nwT!rwWF=G?|3O*teyzI5C?DT9Uh?JBjoBuJnWOv4!6*;> zi)OBUo8qME+ax#dJX>G*_IA6SQv?QoM+Lt4X9r+&?PpQvepB!emDJNYl>gngiI>oc zApV8{KD|lesi~>LJ{4oc^|zmH&E2JhLjbk^fGt)?R=G^l>NbUkG#kO@vqh}`l%@iW zt^_=7`bRuZgCgv+#3&Mp$s_3H20G*s2>aL_j;>#R5l$w7gi$x8b!K50<={)`c90`P zXNHAF!frG5?FX+W5H?8iG&7mNzg|Q%c`v}4SOjAIS)W^4EO3Abn%OioTt`U!@E0*SJe0b9Q-m)GOf9^*-R>*?vyHmt%x7chqq zJwcvEUR_*bXD?JmEERD#FiD5d;%Y;?9s;xD18;aMWZU0FhX-+cAwhVyj!h{=)USQIW#!`XT5 z@BSM5n%+G54fSSq+xB-Qsh7|TBw1-)g(g)3HlpADDDZO%`2qNN0L9kHvZ)Fb8}&Wp zzFvFE?7geHk%e!Yk9SwD5E0>Y-roc3#;~A=Azf@FqvUd~X!)z=2$H>ngFc625>8Yw zN)Z^MgAKhZi;+MfO)-wBqWoK^;O48Ow^F-$Qtym*Z`=6>bIYs1=mbo^KXeug0iN}y zRF%QF`{ZeUR2BXPJW2gAz5LxJG&c!&X9}dK?k^81G>mj_f1jp$KS>k#TNnbLv2GsI z0I*k25K#Izsgc@hGMMo%RxbhPFrm-S&%IvIa1+3O6K5rNy}JH5EZxiBI14Ud(RdXN zKDvznmjM&aI?xUr!DgZ*8>1V*zV(pPdhNLhBr-PQb#`c8!{XuR#Z(|%F(x2fu64?- z#KVt}&tF1$eYbg{z%tV0s*)H4%ex+Q69@Ts%?wikdqZpz-b)x%W}b$MN(X>?**$~n zXj2eUj{!1#XFtP_Q0HUl0uq`WJqwpT%a$&qpt-YGxD_@z*30 zY6PQTFzy@?tM=x6{Q}BWQ&!4q&5VYIvs!OnAjw|eLw?h<8!xpz_hb0BO4BwBoT^2qB6r{UchB{jZ} zWCsCM{*e#Bp$PsLI+oA&!vPI|Em@1e;gOpP$i|yU|0Y_NRMmE8>dpipE*F?%w#Yor zUqwa5MW>IA9F6kv@qs@iMnfRgEtuL!Ikm6`UQraxqQeOo5Yjb3gUVHa; zCd&`lO$)#{|3TJ6{8fD{Thv3B;*Y56ufxOdf2RMH!EuLQLO&cGx3}&mBS?QCar%3pW%eksthJ+vhiD6`n0ncK-2AN_TcPxU> zyk4sG^()0-)B+3vj23>P^_WwBDue|DH8;W}s%vNl`03qG|IdkW zD>DNUuzflq>%#LKwOgo+4hClqxE#K<7M^%fUt?OBjvD_82Gy#gi9NAksq2U{GnHT~D0|6MH zpQw@|dR_z7p=?B>77XI0gYD_mS4FbGoJWhrcO_ZOKG494HPitN`QgwO^ z!Y70H0qC1!fLT3LHJS-=wm^k7U zomE|jUR+8XNX%I9)dRZO#{Mxa@hBgt;<1|& zPD}tj?Ns0%R@A=F_tF7{s`=bUGl2d+-dU1-Qi(5%essWLjxH{}0h|sj=;y>wrfbDn$OcFN-)P(gAR^=oqkL`B z40&OzPYk^PE8~6tlKW=~Z*eW@lgY}W4O^ZVu#PA{Im9zGI4Cmox{H%MtQk~6I(}nn z6~0W+tuO&t$OQ81z^F(Q1w>0?*#Rc7prY9`WlF;tc;=XO6_52bgHimj?Oc&YKHcu> zr<(a-FM0vyQ*aVKB!(M$y5cBguI<6OwV) zNDdTIACI4$oFi)u@4fHdnQY{rfgv-E zH1y~sc#9Vnu==o9J^(H9$0d9KSx#JjQx4Z-6J2^ zoi(w$z@aUR-zE?&)Spet6Hy-tTMBGjNbMM=N%jF=EFXhn+@>6bjL8Wm6?HtQ`-Gqd zJ&p=39$;MLg#l%`2qHUOsD=bB!)#9z~0jG8`t}~8&u?l0r*t>yG*TD9_@`4a^zM# zFq#O|4{1C)e{XK~Lw+m{%>P@dfo!}%1Y{Curs=gh6)Ri9Ia z=xga;4xC zwLm0<@jXL|B0AZvQCb$0+8XLeNdW5KzvvR~LTFfIeEeuVlCM7`sDi*3tUAC18(Ac` z$a7KELR9vanQW+xIgmXsd@`1)zN?{l;xOePc!^1%YB-rkal{k?#E%!#Nug8lj!C)W zBd`*f;Rh#S{qp!*xR7-Y`GXOF(RY;p@%^FudR9ABZfUQIl>J%`DgZr?X$k(jkMPj` z0eXR-SAPL1i%(dd5HSATVUM}bv&gI)M3fRwZ43~zx-sMnD zIq~QW&PHKW_sj*9#9Lp9!N7qF5XeKXk4R#h+d!2vlwu2=bgzS@I6$?~c=C%F1`j2D zGWNyl2+FqwSu19M;u$i6DyX>9fX{scbH2ed@ih#gB}8`h>iy~H7wj>q&9&ZZr3D%f zfQu4Uk#F!>?#mpI8EdJ4?j$LERF+dy<5#M32j5JH@&Qj#UDZmHm2Q0(&KVPqN<6Lu zG0@AT;Us|7(8YB9YJ4jegF1&pplDj8J5Ydv3xtbiid3vC=i3y}ShUFH_|xUd;wgnI zMh%|;k(b*X;mV-Rj>vh;2#OqDPxZzqW=~ z>q+=btDge(@8^>G49L)U6wj=VyFN#eJO4x{u~-EZKOv*$lDc%@hxsYJ6#poD`KL~e z<5m++Ye|j!RPu*K&GQda(1$^v9IxM|c}-QaKbn9>Gc@Az2Yu-)nZ397}N|iYWu-G_{fXvyaWXMO{9I<^JWxyU2C&vlz5{5Wuh!h zSD{&5CGD*ha*&ZGDt_m+I?zh#8b{->cz9){^I^jtTt>#lz0K7YCGX3B=u~4%$M?j0_eAD*};lhs2(>nSiiMg8?lp^v43!w zbZjzp_u~?LfkJgPKSGZq&5HNkJj}|tCzC&WoHW#}+pear?oM*}@IAml?f~VBb7M-# zP1s8S0Re5ZFnYyDi;P3`0UsBSO;Ex%aoy5VyW3ua66&1pgl?=?)L_qBDqF*g{!F>#EJ;aa1a%j=gq}}S3 zl3O{0TM|`$W`k2xvO+>a&p+a`&W11MC!|lGr_tW3?!8aL<-;c+*uc-}*Q4#g{5ao@ zjyLiW=x;!8c8x{e^ku9r@vnQ9eJz9UOw=D7*S_;VteZ(Ey%`Dk#0p)m0~8r0Gmh}O zTu(IO5aSWw0UrYM6&h26T?9E-J-+$+J(MdKZFVG_Y);U88^V4D7YlAFAEoWI!XI1_hZGKfA$z=5&^6KzcNMA8F_g!r`0ic zTaEuM`tQJMocNbBi4~D>9ZwuVDG%~-<)$lcr$)xc?@sR6JSPu8kbgHv))a_b zxz5~U4n2wlpK@5^;_o?G^U4;f*ee9k{!{>Hs5pg$U0V?C;kOkq`oIrLb@f#;Z(uV3 z`WoM62w)KX))tM=Yb5yWDZXo1Zk?!=(b*W7_0rVv=!&L-QOBnQ&^BxMcGl7@jX3Xd z+2_EGC)B4I?ee#3<0r-JKL`qs=<7jNKo34b*648%H*B{v>OS*;p|28lO|PlwJdb09jFQH8lS&3cs=YQ8~aF-)l zpQbjS?k{=7o?EKf7O%;JiG_`Tc2<+{T8QUdGuodf`zFjAq6K^R5h(D)ikeQv-C7O+1vK{gCvIbZ=%-n#dDn+0qs-wt8n!Ra&d*ME2QN?D&EWY_Q?Dm5!?*$h zKY@m5K~MJ@On4%&O=R>gJWLjrTW{3~Pq$lUVb78{s9vif2lSRJM$O;mGZIpvQ72P6 zN2@vnP?iYcy4V8_attWB`5V+4XhISxbvxC%Jnn%eWfIvj(2tnH-kdWH<>A;f}CJg$osDVNa1@WBVR=7IYo z#DBGjV~NXsXe7z=G~7=#yAf@yF|ItGJ4NYI{tv0Y(K@*Dha8^LLI?JimKk4MM)8xG ze@9>qD=ivV6{TL>FeW{g!Sb9gbte>b11}+C8c>6IJu?jfWpsw@=vbJGr;020aIIdB z_H=d$oDE38`(t_cc;Zpg^($516&gh?e$5O$g9#IZ>EHTz8E2?Gwc=Moo+3{YCbVA8 zkLlcPI2$SKIh}m4N6E#FP{AacMp8JK1jG`CpZTorEm;H| zFa4&=gu{j?xqNv%&rgpXLpE(dB5P=Y(I~|6T(7jL3Gxr^@dnbJ(EJ<6BKoG<2)bA(>I!OEt!`cq&0bg3RpRnBM`gm^)4vZg~_kpq* zR2OXUYIq|K8z<2tIko*wN+)3RHd_Ku9l%G`Jfa>NM*ToO#Z^@Q1^xePP(gJ`EWRIP z)_)@Mo_|5lwbiz3=p*+?%8d*(Jt9CtS%x(pExe80FL+9FOS|TI395)`U>GQ$=&{H6 zzL_4(xrn}Ii_l-cGeSnkbx3#Az^5huH*&tj2(UKl0N8ZbGf(4yT`N)_tu{N3^O&cN zV!C5cE#WK#;Rj0K^*eh%J}BDvrMxM04$-}VRwvZ76)@-9nl5RHASb(xvNQ!|AhmqW zeuLj?bcB?~HAf)!==K?vSF~=X2vZZ{JCjoIodO-f`mGk5-kT~Jjj(w{4qAPmUnF@j zs?o0Txx}CoE6P$9ETqo_4!K&}8nLvi5brNflzl(#(a$jrpS&HwjT+!I0P}Z$)Iz~$ z!_;nrhfT;dV{lURc?DN@UD}cB!=!vr+3thhv1Xzsr~AHa*Dj0kam$-i$(?pu`MY0Q#0nG_@C4EM>2AM;Ny&YrB3kVtWN_(4V9#pTv83Fa)cG3^X$WA}5J;us0sKkV zMEGrd#QKt_ zrY6}TH1T6c{P;%{qcVM9>49_vY)DJDl=f zGa$C%(w2sFvn2ZhS7w=eekkjMw7&t4@I09!u}!1yMx91;Q}a5u$VD0F0@sW~-=irN zmlc_}z@B~l@y?3hUQzm*#3R-iEXsFfv*vM6qIBiOZOlp_D41(i$K&REojiKj$ET@iEZ@{pbCi<;^_-vb( zDS*c;JGlLZDyQ2NjBI}ytieU7q$)n^be<5Owi`CaV8g=kf~gU-AX!>&ZkuUx0m`lw znn+Qw(o$JX(EHz8q|w)|Qd|3b*rn#M!Qc$w%!-~6TIoyu_sk74fMMcJ)T~IJnnO`H zx!b3H)&F&*14jwK;}NjG;H)m9bc6$(*_dwdral)ho5(R9us9pw%=Hh)PpgDH0q7n+ zIoy~A95Vj!+1Py~FS-jWsaA7pdQfw@pIQF76fe;}XgdCFoVk>(CO)quD!Z3(tE0Cj z;ui&>_~JeLM=LhC9lQEYF6mIv?gK{o=&W$IH}rAy3kGEen~&3QYkc>}C9ZIF^L!oB z+EEPyu|zTg_pbI?Eq_kHMrm3*hr;FyG+z27dF&4P*!UcEKJXNK1tRql*f-Ge6|X(s z$;m_((UabUYP`1f+P$Onr@y|u0yTI$^4*+7kUP8wF4XWlOl;38VOK|Yp_W#6v(I-Q z{ggH^gHCcUoD0qVJwbm678G7ef4KiGQ|n2xHxkDK_mZ?W6E+s625LgpQf40EwVwL9g1r9wPMT zFfTWa!Ly19EXT4^%sC7h_~$yOHc_dk z$#~;lvYcAhn5G?RtAGfjX0d}c>`OU0Iw$oyd1==Bix<#Y*xwVz)69A05BUj~(D%Sx zT-051e{XMnvdV2?@#fu(5?I0skDQ>vwe?Raa;EuuxWRbC#tXNGh6ZnLHCB8x$_j>3 z9E9NPdg{+)l@?C>3WY-<>PSy_vqWcez(dzFujr##L&2NFi|FhB*fLC}J8_~c@8RFM z#UKmKm2@yC54&Gui=jdbp_j*syUta3IxGat^0!O7(DqH9d#eK}@GN>fH>o%4V@qs` zCz;c%YY%4v>S}9?C!Io2eS|HSCj)cU?XSJ>JA6+hkYF;Y{-7w%ZD;eeKYdRI+vzC{ zP63q>|xRDU&N+PNoPSmS+7|#n*e6xpT>SP0~@~j``H53d_NnGn2 zpz{Go);=kh*Fn@#n!xvQe_n&%Dopvz2FA%vxWjk2e}K;PLiu+Oz zlX^8(cVoMRBopvWiAF5vZM@jR3@jc5I{|EGG9k2iZ delta 11014 zcmYLv2UJr_7cRu038;iFN(qAW-kT5w1q1=*LQK}+UN~B6PARy8^f&wB< zklvNvdlh(-d;j;IE|+WNoSE6P=iA@@_MX#`PaIT094$l~5+kfY504EYxR^xy!;n9Enw|lEClG#zvd%f?H_M|$KBJO3FYWFb z_ml#Ua{S)eLRH5KTYQyP{97uHm{{(G$*&%i`Z=G~e?On&mN9KpDGMJ8wH(Q{PAyY> zG|%$t<~O4tuRt^+u#_AoLUf1d!etwuniGG{_=&cagUJowhrip_hbDqwFvH-bFNJ;&=f*Lo|4rFB-I@8aV>d((MZYWhcjy&i8B^9vI z^&?Ni^Y3a;LqXkXB6Re#c0-;&ddv zzNGHd`D~u~@#CU+ecReijq|d#{`3Z`-4C7(JJY^XC%Y}=&Zh;9H4bh+$WME}q(jH} zygdBQikaE@R#aoP@wCAC_6{TC%ejF9{p6{G2Il(bIo57Jx+>;k|Il@(EFC67BabI3 zBOlitKJYHD+r;&({_V7Vdc(wj^NZ5Oi;tV>g=>yy$f31v8I5?=NdCJ0(Z2_c=Mz~M zPUGaO4^lT|HzyCyh8LIX517usnf--YgSJoh*L*j>T;gyndk{#3bt4tcDK)~c4qPa1 zs4+Ls@IG!hJ)N4;>$b|}8u=xfXkl z|Lv({x;?6Enrc6@lRib;hyQubZDpo4J2VDk|Qm%Jhu; zDYu7;I|qQ%73ultH4~h+T{Y#sz*arKAa_um%u^CfL=0P^j_&C6Gd^3{bSd!*x9n>H zj~J&8TaQ&d9}Y{NrEl1F^Cm%`Dr*Lq&t^_%Bn*#+HT;*mZ~GRVhwwP_3It6(N&h>0 z`{Y1?&l%b1De<)KY^i8Xx&LYF3KE@tyA#LiblkwE4}kU_)GVU_&l}6*Q!kp_w!5EfNhS z{Kh>=oBd;BXQ9yHZRo(0)tZ*~TI~MQRh|ljpxU=BcbPAZBYV$U&?#^plkF}^;SKsKRi2-H#B`?JG17@T)VqX2T>F~ zG9mYmOnh?R0a-bFH^8t}*2D2<@!yUEXeW6kB$VTkthRFDYj|)LNykw~(6+_idTp`UO;Y+J{RZgsv$CdqUAjbpjbzP3 zGPA?xAU-`L3LR~O4R4(78y^bUUu%r0U^;U;Es2^qIO=gG*Tyt&1PVU-GhV&F5#VW8 zbtdFvX4juP=`?0~Is{D!`#ZOo1iDbYH=d_TuR6#{q@D5@!xa_r%p{J~xG(&uwJArj z`^j#7eP^3)A_w{CEUuZI?-dRC`dWT_)zICIb&28~pFiyOOh|;RBNHz7E7sJW*beB) zkxuz;@UYk+MxG$@ik)Yn7L6pU-HOULIg(j73Qnd3h339<=!cF!+uanzan15zrPXYg zmZ}-xjDiYVci!Um|{v6Sn zQ+S%D>$?04-^S$Y_|9YPCMFOw3eOf6xgK*%^=zm5ndgm221ACk+N1Uy)e~rj&O^V` zsu}C-)v~pj{L!zLQ{ zIy#+#RF4LAKZqN*_|C?>(H@u@H};u2N%PrBHpAsNE$%0mM(?fk2$)D)S1JT}44H7S zkF(Stt@pK%%loa3&Q^ZN-H57(BT)amZTd3C)HMm3H)O&3>DF+(2Ua&UosHP!yzx5Lr9>wCQlQHsfJ5nAJZ z0(O5*^0tfz_(EUvkjouzh?LlQI1`tXEYgGLw=rzuxzc)4{lFOIea& z0lP5*Vwlo2Gn>+HJRaCG;-x}k(5JAX4jrZ}iTb674ui(WYg1=hULF!^!jkU!uFXO$ z{zoy)N9!_^&#J1uY0p!x9K3HX?MgrXWioeihbq6&N6a|cdm6hibv&2_z+BPBWJLAU zV%&J3#+|NVUdawkUoyQ+ufgfJGpSM;6dD!DK<~?)FMGqLui3(ilUe=e zwp!}iSWjd+2NS96b~~$nk(#HzL;gD_4gjGGB(JVG%J4jt-|bb9@m%LU*#AaX+rkU| z*lbcw3a4aeb{yHZS0?N5EijPu^|(`Yf|?%>@{A7pdQ(J-53iV5^iFMtNVHpVUmHm!!wLj!y{-;ZK*14;9O!tw;OxPquX8Y^ z2UU9m#@Ts8TjO^8siY#Z`;&X~nGy)a&2*WKh@gX!^xPPr83d0Y-${@Mf z!9z3Ip7<*d^X7Q?XeFCa=rFcbHKnE-^*Hl|8dg~3N6yUrk2L zDo=k6r{d5^>wJW{3q}*xX1f3lnRq_@Jirb+qV$@_*>2NB|MR~Me{X_9r5fWL%fm4Z zaFFQYiUSoD0x{X(z``#~{rov2v>MJi=*odI-M+zO)?s1*yZN6GeR+B5JR%FKmqDgx?(8;hc_DV^Y#pgWsfy?oea!!gnfCtat-y zF#AmBn<5$_n0VzvM3pHXeKn4f}V@xZtKmX6Ka^J;dyz>BFr* z{{H^n5o&7x9cs13FVD46_m&zOm^U@THDKpjLel^CCcZgy5>q+hF*aOt3noP)>9(k; z&Pqi~3=<+%wr;)eKT+duVqyYWtt233b*49GTBP2n8T>aZ{SEiTC#}4^bNcAcqubAC zl>`1z(G$adBEi6r@suecGSwp*N&n;Bcpm-j8|m#}MRg3ftABfoB92kULs8-k7O644 z_%myZgwGNN-{wWfSIgC`3>9yzu6FKdzJAlR0xPcDCPl*F5Mow^FjUlYeH@qfjD`%0 zhc~NQ=Z;#-5X0b~0dA}1?6q*bX${B5!5~mcMMDY6891$dF^My7-c@JHp2uZ-_WP*b z2XPo2A*u&({JYU-Z~5M9S}_l{GIcaDc5}7wXlH@yA7v`Q{K{(Jjl1>!C;MN&iYY=F zSlEop>cxs8J23i}0(h3m9glCT&grHmO_6eC53cXK1)g-?BRt`V$Ni9+9lnfAx$P^i zP(Pqcc%q2m)?Qp+H!ZRJruOMAOSC)Ib)nn(HvAOOUK5-cuQoGi*t9RbR4{j%#g&x;hf{6> zUn_v%(!zzLcwKi2#8h$j2kMEb0Cg@+rkL8nYh2a)V{6&(1zzqR?DX`MYxzwPuruR6 z>`|WZI{&~Tuhv6xFn;73M?D+HJc00Y<@oa-yw%hk(H2hnb+sYN@|316nNc(daHW?*RPe|}jbkLJJt$B&UJ{`**1F)eYlY(P|OO)XKauD3V zuD*$+_)vm5ya;K(anNA|P}4%AoVlsw*l;k&1Rb4%DKSth65^u(1Guot+PKKb$h5Sy z?C5d@G!2+Q=kiN+6k5{(Jfcs0V()(wFK_|&@L|lEcTdys;!R?hD=KWaSRCKU&`lp| zqnnYFbF08)$Qi&zjSpqVT^ewDjDNVw60HFNe8{!w>cBJU!bIaoK3OR2Dv+b4vEDoL z_vlIkk*F{Xx>bk0^O`}ehZFoIk0MM0QC40)@#!g{#HdDv3B?;$KhJ^E=6|o!d|d@j zFWu34M}&<)d`;^3XjIA1nw6Cdx994eoHYHh{OcZ}z#=?K+DVRd)ewkh^OrXp{Mfa!y*;f3oX$7I873`@Xg+l5nN2#GCOdX4GYSIyMKJV z&VC@@5W)2TEdT=$-C5}QkM{$c+_8&VxXr^@x?ri(EEM!3*hOa7sX%P$-vz*}Af_oZ$!^4wODO zRZ)?ro0|^D94AZ)1tPZl4)t9K4T97QTLmm-+drJ-fgbiht_Nqf*GZ(eBwT+==hYxw&FnAvqYRC)_mRnk| zPka*Q&?R(9INZ(wWUD9e-w&+yJ{rVaF3>|{>zR*yw#Kg|Ly-Vt8Kl4fdVTAB$$Jd3 zB3J;llzVIpfd1*iIOCB~7QU1++x;eocQD+4ME|7&iX7n|i2^+%)5uT^4eSi9gPli# zwE@qn>?|(cz>bn73F-N2i&3I`H37gBj>Oc_&xl1@DLIDF;FxK^v_khX8uDUCA(5M( z+?M(V2L~hE^dvw?Y7(evL>US5rsSl3&EGj|hxrBsfFeF($WC$s89{pZB2)xRgp~!! zU|aN^C5s>{7W4{wKBU|w9N-HKFJ8^pw>Gik$xP6%Ha7U+N5eQpkx}4g%uQHHwy;IM z)y=f)EYZZo`qsjN%217RU{eI(MUh;sXrK%Ri%f2?pU>aA4pafE7EIO|drQ@l*?x=a zkR0pMIN;uK-n4lR;@cFRpwntLj+56V(bUGeuM7^9$;A_VX+cWR&N7$omVUTi5-V}Htb*P8yyj{NuL!<-_I|Y=o z8x_Mu`QbIlar_^-Z(+cB;njZ6UT2HGioJ3}Jn1Mo@_jEE35;dLmFs78(h3X^q+cE3%R5*g#1Ow#Z>p>QRrSy zux8e5xhk3u7+w|;6abDU9ypg<{1QcbOw(w&!|l1mtSyL|6zIt9^p4JSdHI@0G=kU> zL=Z=kaT4xI(q}j#TNQndkFaZ8lK!*H9#0_ttFP55&;av)^8jW#7Zo)*pJLa>D%-B0 zePD255P{z}rtG3Kz0;>?3zDP!0>bke8k3s+<>=k z(L`7kg7JsE`)n;wsj$iLnwlED!pH9YHa7@I??G40iCQAwF&hp#MO4up*^U^uP>ceAzddJTD-!kL zKEag&KXhD8CE4}Bn2)RiPhk)}NG?yjHr-M&T_nu~)-|=Y4yJ{q1R|0L@0>agsG;Lv zK*k!epA_~3dCCm{l8fg$7i#s{e^tv5TNfG#*3AJXK1824&gTfb%XsUgXaI8AmMY4; z8Epn8324)M0?(&J^jjzJ8fp!^2e<`M{IB1JA?FL~JP&P*n}Tr>v|0vg}p(Hm)kD}w;|07b`2MwP_~5T+MP%VddmB{*1$88bui;bqhI5VWjW!cDN< zm+*Uz7JlE+o-Nvwn9gQfuK71>G&AAnBYK_~l(J@%8xCF~2Hgv6N+Fn1#VwLY_4P{$ zX%#osO8e_%%s$~)<5~|s-}&Kj|P<#{-1Kzj=#Bvn4TQJC+c1@87>?h=@ZWL_vJ@qdeYFq6K+? z1pin?&5lZJie!}eO=?gE7*i9N@a0awHB1G`YgF+uFOLI1Sf&8bX8-SI>V3+uUu#35 zH*3K3>Tn>s_#tqdHZCrLb5lNfnUy(&8ER0K1(JfpV9S2m8lhxT(IN z$zm`ezH}yxgYMI zwfV4KfONMa%9X(p5kjCq7$?K!E*T7~ob+*kytrk88T$z(1|jxzTMgCC*I|r|j1EIZ zslK=E32e{>%oXF%Wx@K929b#zT~^Zpt~Lg)-Xi|x4_vKm9UD7%l^9kEqOP-B)eiO( zRCt+;K3FQ{YQpJ(Y9Pn&vRf2}9j;ew%4pO1B(p$TB@m+n+y+@+ymZN*K_=hz@h5jm zgodEgL`}JRd_e-hm@XMG2`LH+PTdPqfNDMibf{T?+`o$;DKTL%b5TN%X)7a_Jn;3I;K%F>FE``9RFpRD3LPtE0A%PWjVGy9y zE1}&e<=Q}BH9KaOQ&vmjIXujX`68Kfs5YDvuJd7?evf9q^XP$w_I|{4@+N%wm26 z)Hzn+dMO~em}0o0xMP;SJFq1xmnbfOt4t;MZ1AJ0;cu@2kr- z9j_l1|JAP1L!4hBPY5}F{qi&iOBP&^Ncz(M+sv18hao0Omki@x7$I@rLdV;^t@A*N z-pwelA8sh?E&13xsQXWv{`#GgI#CM?(Na5mu8In}QL=B%?^aGiiyPK1LAy;5_FU|J z6`_u16A)ZaH~TJ;X8szKlmK){@ef=uJIa5I3=BS(SIwOLK0YD4EkjD?)4Cuu;8x~6 zSwD4{R$6~ja4RSGj_5;g?fl%X4-o{Kh2MN&-JK$( zt15>e`r3tlzq!c$<%2mEZu0MrkJA3J`U_c0-@;klB-U?wZQu6H9sC4EvI-?9ZRmQ zZPqrF6gV{6mtuD}74RrC3=|d?P3mc~xF`#%d#f-d5HzzqS{A9eH&%*li$imTIXd=nF!Y(BoJ=?-8|O{OTS&mQrjK$uI8#@)#VMwURE zxM4s2FlVd&a&dy))2E;d!PNLum>9$#C2iOWVV5)gs;=cUoF2Q>`r|RNvNob+>+9>I zt^c-lI`z;+I`g2nS4yIlhItI9<>ljTtnYnW#Dm{B+c+2DlRE}Y(2uZZIY50i$V6ozGrC~QtPcmIX+)Ui@6k?LTlYTlhV4en4)lQMsm6eR&bFi@)MC`} zgBAo@FN#nPV*LIdZiQeR-eTfm-{xavbKUR6hYlTmO0X$Tz>AbD14RR*IG*i^JhqjT zw^r;*kBNr$C$D>l%W>dx86m2LHqk~w)=fE&NRY`Q&biS9#~I+&(2b zIuPTg4_TXf&&9(H#O`EV1UMvBtsKlqtI=ag#Pbw)AW=zJ5at(^U!jyuGYlAxCf&8I z8mOdpT&#sfDz#z(zYr?ppoNmqkdWnUJkTtZ*Nz&rPGTPq=r#Ie3-6WciV(v@sKfZ< zkphMXnRWDO46nr0GI9!g>#Hpadva$WOJ7e1hfx8>E_SpvsBP0QZg{n8nO1F)*3KEb zlI&Q^gWS72bsrVSLFFG`w9EleNoZa^Hz0|(#(Fd61Um>*H@zU?@?Urua@jqe2wP2P zFVy|r{^82v%8NZF<(<^wwOJz1)yZvGcsWY&T69jM5YIQ^{!^tWCMebJ(Hw$}SFkv1 zxEd4!3&Z1(&w=l}!OIpxe~1oyO&hm~>IW6(Lgu^y%$<^xe^QgvW)+{2kgzyeW|;R= zA7tnOBv=z@rLNAYWpz5G0e!UEUf1X?98q82ZxOY<+XBDT1DkeXLb<|sX4_*}`)h|) zGl*fS1*G4g)f|$p2F{T{*L!hrLbcw=;{lBP}%vk9ecud%% zHL%L(SACXp61Sz&GCPVeW6gID7qu=zKus`+g6BXJsTGB23)0iJPxIm)t>u2~((c|= z$y@=E&kou?{X;{Z^PRZw7)RFLYgo&LSFNIu?1G-Puq(R`F;Huipaq1qSr_VB`ufb} zA<3)4wcv#PcY1hXfTyoFL4V5iaC64sqG1Z2j?;REby%A#A$Qb*&?xpb6lCCCHf)o^L4>y+1|)&mrX5JB4}Ve0T|8!PhR zcO1f+-om;HgE!NrI?pbvS_{i~NUH;004+9)zs5=5+f-7HI3J4~N#lf1Ha`^zTVEyv zP{*K`b>@5DD_lVjv0;(09a{C_=)!@SnHfiS17`p#kgM2jFL0-E_Q zTVDZ&;~=^)>3AuxP4oFnnnWXXoaOxOZm5@9IJKzsZo);%FMQ{>*K``G(($KvsP9ye zyyDvtVQiYt03wPBlgbh8js2jF6ZGNld~!zJeTQfUnag|rcg$8E_rLkUl{FA5M{$xn z#MGsyZGwJ3d3O}hM4+|I8@<-3umJHlf^K`Ye&$8s{&p{J*Q4z@so)KB;PK9fptZ{G zr}Ny!#pP*8Bs;1YgKw;y{Xw!7+1QTEzDsyIE7ZV)dnNLS<1^RviRuW0L_I(Ob{LqH zv@SHa96_u08J6g)!9JET2pla4ZgI2^*F76$9(|L>vXMNX4}iALdo@F3(r3{QAS)jO zCO3}9PmScvr&`z-@d~EQgfCob+*#`&<|dSL zK0xx_cQXT@BKX`aNN#e2<-F(VUsFG5XlOWd^V9_Ob9*D8k|z;4!bkG`+6)%Y1Y0(& zy$>OgbD3{tHTMHC)Id+gSR`~PKKq;}*523jt<0nFd82x|oJn;wB|#nMf$@q5;0A(@ z-}pmbX$?}1&qb^S-x<$C-ykEt7d;GaP7;9BWzC{7)vs}$YT$j7+%T_VE?6)nczu1N zt6wrK7&In9~iu%^^j{Foi2 zWI_vjXyjp{UHehq&qr9x^J-qLzv&w1cC@vHJwR_0E&?F&jGo&ENS1H%?mq{#6+A*q6 zV+E952<*B62I%(XgT3ROj&KZyd_6# zw5Le}`dvx2(~ANI<$_RD+g(~&k@4D`{#rPQWMg+JFNVB?2BL&b{N|1TTc~NMJNAky z5?XBFW^a09q|`RHP@F>fLB=oNDpW2}-d~Kh4uqE~t;o5*6m^a&PF<+dBQ&+cgcmbO!P-FFc zr?n&Ewm;qAf1=gT$jE|$Kt%-R+uoT5vV`e-T1x9+5J~sCfU!5Lq<-*pzqm=$%rZLg8&ND`#&YG}iUW){c|b=At9$op-$x zvs;XiLsjt$%4#mk7^O6R=oOY1iZeTOcCLOt_rS8tE(}Ot5jq#$+!q6XqU25A2Tj0Z z=@31lj1YH8<${UqzMr!*A0ysCu3g1?>9KQ{N1VvP>}H>9UIcUAzJtD7RLNOVAKw^ zbs=4GtxPOT|KtHj8h237KjdbGN&?*`Rja;F_5vYYps30z1=lJ+&pH&>?-zO27Z~vB z@#n*__mP*4IgOUOTWYK;&tL0Qhb+2KpJ?blS2uP{g+CUE{^^Szj2idxD*qC zxbsm!zXoZD?WOYXrgDc2Eq1MNcCOj~7HC*l)55hz=7AFX1Lt(U+P%yeSQPkv=asP*SG9u)Pcwb}p({p( zDA1AcGWVVKn`!>pJSP%71HHF&FnW)7pEn8E^xxQ)#^GGKKx-lk{`mTU%lzo+U}Z!3 z(}|i+ccV~b1md#s<@5M%M$WPdy0JuB=>(e58q%(v+%MH`epBF6wOlT9Isiefn;-&~ zFsqaGC)i6&Np^N~h-_{1PmQ8pA6;gKl{cDc-U;Gt{;hSnXtE{w)`I5;@0QrjHr}0a z+{kgepP^s+MJS&l4h_d#zK~P~K4hiXQc=94Q~FH!7zKXaxHd#`CSO-txAUTW+zWi# PN~Cg6U9sq{`HTMpAuLt- diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 1180dff87bbbcd74cb816bd2048075f9bcb0eb59..4d818e2723b98be99127f1b24d11383eb03dc5da 100644 GIT binary patch delta 5516 zcmYjV2{=@3|2C(iX2{#vB3j12h3rM;lzm?+LgKY0ODL&?Gt(k1B1=rxSBx!@Wm+WD zA|bM6ElZ?qp^)AGr|bLP|JQYOU2{3ld7j^L-}mo+&b95NLeyB(~i z{U$5P>7j}@#3%HcrYkm$n|P<|t>ddeJvqzunqTE09e3SZ_4!^a`_dU=b{TF{8N-dm z{PceJr)RWI4CxTn&z&1e?ww!KecKA$|e()*7=kr=%5id&GB%@qate4Nx%w_J-7b z-Bs=mDg?xrkL-?jbsX^)^^ayXH#Z+hI-de71BemI0nLN{YjNAw8~MFE+?R1REiOje4%+2us#QQ*L#$8k9IDN~s;Y^hE}7B>USiI31b# z@QdvJtkK& z5iv2bQ*rI-mANFcvc`+dija4)@d`@{l!>6H%)<*tc|}gsVlQV z&@LHDZD~oNjh-`^aKe4PRWTOTD;{X{wD)t$RN-0hJ+=Ps*zZ` zS#3K3ISa>}y_pn+St2bhEoaW0IS`*I2ufkaf7PP~_?i^M}E;(rPyv(fU{b?JFmBP}R_w5lpSjatqM zqY*L8*^gl5;U`>3{vM9qV<;pH>*p85U|O&?X!c_s9NPD#>gKhsp0|KHq5&31$`+6n zX-uR_w+L2Thm!83)Z%=Ss%(IipTb{ff-C0Q|7J&tfmT@DuuFRZo%I)OYHBJgD{GrY zuptB{4S`8_Wf~&!oO6ajU*AXq0ED1?LbmsLBFy@Jeyw{H|KK>J>jP!Dy$(T{>_4Sr zaoHqx0C5H{**(LXeu;EgbeEdi*@fw0<8XCp9G>I>4)a>o)M+{B^J=~$<$NYo);EBw z4vmfWfju>J)^0N-5Md7Ge(262X&Fx~Q9e>5l)y%lg;ZtFMw(G%X>>aM>eZ_`#x^@) z$B;`kN*I;J3pfo%UKgUoHzIuxOt*$wOVEPhjKfhSc0{r?q$z`Qw^*VX0_T4`{g&Z$ znVn2&RDy(DxgH-w7CGKGG&I!LkC$H;AdxBLyHsgj8|}k7=>ddYL~@*=Q&E*j{Hwe*WWA2ZHql?AmUip}g z+Eb!&-j=a3AI{A#BovZ>(9huow=ofdln2f+)k$Zg*#old6U`1%%%Qe#IyH{)eur|m z6ofg*({xD3UWCkBc6YB4eg=>=lazD^Q$;vAIaO3tn&R>;5mXZ>A6PuT+05A)HeJss z79k+@kY@dpq(GdJqaB1OTdQ-}JpjK6G zZmyv05fI&f3%4u%54W~7)_R~sRnRuoSEsBICD=getVsd-ObSje{AOd?7mXw+IBE`V zSC@j8heFFk+dM?cxogTgH+Md>N-l|w)&}7h_&s9hk(O;7ooPtX!fgZj7D0#p<&kFs ztLmmecNryL1WVrsyIc%DDS_qzdGboslnDqs;NucCNUD)U;}lRBOsoCz##%mxjn)Ba zbZ9b;BW6TXbMw^wq0g?<>y3c4>)^pU>T}aIv`r}O;Q=W)1X56YPyaGQb?ytA0jvYKsCktQY*K4F|&9gKy;C zwNI>i9xA7bW9bK=IR`=x@kW?ZDy@2!mO|Y0(G0ir5g=67X`x$eG##T?<+}Y+hIp2Tjf}-I-!7MWdj}@uUo{8x*XM1neYjtmL7iU{E@I(De`jdQy;#_Y`BM z2-f!vF)=aG#``otcFFp?_1>g zo`HpZ=;f6%4-EL)W=h4P;qTG7uN5Dyh#hn<2tRC)?f`##yx?1g8pFIX{#%tuK(YYF zS-XU;V2@D{>9gK1ZA_>b3)Zpft%PWn?3)DybmYw(LAF64hmY0CCt?A#`T6p-rE8LHqNQ zw0(2pc9ls!55CGsTBuA^{O%dxT0RI^VnE#)gY_oqB-*3vfiCLwy{Wgfl;hiKB1Ewb@% zF-_M`5B-?I$Iq?w?a-s2R*gJJ1!=Hikke8~sC;zVIl8@JR7zKvSngO_)h{Q-i-rT- zfKx$GAwji8GSM+zo8FLY#u%y$>^I5Pi;9X$&`f;^K5JJd=%#q`XCf*aokpeG89&x} zpDMtLvz~v`;$+mKT32kXkE=k5(QvR4;fxY78Vl90ZpluFah>1hl*gqMm9YF?<->!A z5BG(YU6}a;=7NjnbZ$t4?A2 zC3lwbP#UelyV!Rft@EnDx=e06&5hM2zP%ef{k`e!kh&6_@B%#5i|9!l;U?WO&phK~ za$9w2rJ<(q_^o%QxxmpQz)|lL1^E@ofQ9e3#79FPB5p-Smf3dJ!YtJXUNVkwNEG4n z{(`uDM>PMShZ<>Yn&iunMv%ck@Cq6)I zejq1*-dk}?Q|G|F7Zwt3a9OqVh#8#7qThR9rbA_I2Q}HgD^;>F8C$@ot(!+RG*{o$ z<{l-brmc7BU{RvqOe->ixIHT|Tsh@llsEmu%81|FBSl^$vM0!UOL4fp`6p!fy=|;& zq&L@cm}|O8jja^62QOReRxiFjoTLzg#BL7w!uf z8bs+tw%X@D1utoYBXyv**xltjWxxpbVu6iGU1u;Z{QoEZECSCMCpJ$i)zx_5E z>mMI8qGCp{+$cI+WLGKn-fC!Nc~PgZ`!;k;S`SS1)g}tcLG;mEs&CeQx=_h(bIKO> zilb0!lkC?c7akQ;K6!(6t2yxzkAz#H6iKRzl&$y@LhaN6X_HxMgG_36i zx0FAZ6c?oPvR2i4&(jm}@$r(`8XE9kjBZ>2AKBC1dEXc55+<`vHEo_m{{9TwHzUSc zND}CXP~-ZvI9D+=a~OE_4SLtG17Q;qc&Gicw5;bmzYfml|0~#6nSa{2lB_1(ER zXjKCwpQ4SjT@i`?{B(=H$Yq;H!2{z5G|IL?hW}oUUb46C7YykpGpFO(MFJ0Sp~OMB zBfX|#g2?S04;=xm7->}(0B2e3Fd=Ri#L_1`A9L*>s2%`&{~yF$5ifdix60pTsB(26O~?8W zATR}ntmfyio13iq+GciM6m4 z+v>u_-ds<&*I;m*cY`|b*0{)>qeEG>VT_-4RHP;$A4y_Cs@5!5wzm1pS(E8#qp^x^PbBib4+DhPOe4 zU>ZnQq>3)!TJ=6^A*9Kn&++-Th2h!vMPXnAT?3biMJv0}?VlR#Vm;3eFyO zs$A7*GL(s9gKsPm%8nzV zg}&PSju{j0T~jNpc1-h^Exs~dB!Ad|ie&?T%^&xEaTVxT@woieR(AQX@iO5;$ET+y zf~047DUECGsj4q4Jw<#_=sw|O^s}(3$G*_z<$Qv|(-c$wqPK9(*hyLu__F@1j0t9ZdixeSlqt`$EmbuFjwt+yil+w|!EA0-0&aVVSll-Md4 z?L-V&Oc|Tis+kK^-jinjLE=8|W!uS$iKYHN;qDj^A2aBSSxr&`7Kv>7A-?H{+m6ck zTV|QOB{hRnZcDkD?Y-HTHa%RPT$mYM^y!m;cu}~MTsprAfhf>vI~_XY}8@o)WP@EU?bfF ihhqcK(}El2CRZul;xUCQnmX`+%}CE&_u)b3OaBKjjBcy| delta 5533 zcmZ8l2{@E(+cr;+*9S<(a3IN@O1u5{ihdQEFO{ zp_e3yL=h>wWaq!;`;PDbzvDj~j+tZb=f1DyJg@V*?gx(|q(u?pwi7PhP}AFh#$V#$ zGKYS_5U328MVb|eXCf6p1 zw~&Yb(%2N&LEVCgQ4>XViDYUpkt7+@CUBU#UQ$d;rS zygDI0Fy3tM^{Md(v@_M(hIPTi;SYY^vYxK;HeERR>sHU$)Dz#cgI}tHN}FPM2nb3f zm!6?!@nJ2d_|u!dzpDLkTi3JYqe=zzVs>3!-S@_n#uf1pUOpizgSrKG(aO1hfd}_Q z=a-fj7bDceKV5`Ff&`+}f&?5h7}t=iSFdhQ&XI$2H#;#B_dI4v6SGwU=6?_UzS%+m za~0_R4Y8{bXOZ2f2TUkk?dk-?SA^`*iS0h5Ef|u{Zf$Ly8vQl{ZW-t?5)IwW@?|Uv z*%N*B_i}Udo!@)I0#S;}L61}_;NyskJ3oDuD4A+;kx8?@n6-l_L+jmilD({ zvPUpuLK<-{&6QMR1tXP1R052m<&f+4wAwj;rt}frM zVI$X!0+n;2t#TL6L~2}`FHR!!U1>acKDwWkFH@*T)y7MYHRsxvT_KUF<7G_RRN3V* zVxVE~=c?1r-nK<3vCNv0czn<)=ztp?Nl*F|8^1uNlB<|B;W=iae&#{^;>C*%4GkN2 z6>C6Reqt1|$Io3AtMDjZt?j}S0f`5h>}O!>?Di^Qyn=tXb8cb62MRt)j8f}nm79@< zsZ`D|^P4_Ol#!)!WOgXm3f*^L3z!_b>JZC==ko4n<<^eqQ@7$nLqkefviU9GZLeZiEWi6n=led2mRd<^pY6a;}RxU#)JRnlyBJbAb?+83PF25Z_6 z56i}-v2%=5tQ75Qz*vV6*+XkB_mg(lWbUDl82n~n27vn=<(IR#%5H0G%d@`cZQq%N zVnp(N04&e?V<#FfG``W=s?Ta00m&dmscqp$i4k(+t;Q;ks9@<7SUM$bt%_HpT8h>w z1wo|$-kHi`+1uF}YW+ndhv>3Tn3%iG#`EABuqUDJmW^TN213#H9eq9& z1lrxNO57Z$KIU*BSIb4Zo$F9etv$=wi2%rJvS2tKYJfBFVY4#(0G+ z!;MU^^d#@7Cwo;gk9l-zyLJnDvA|3T?tPiTi^%KL94d~J1(~vV4(0g#kvO*;qgnqd zM{!XIXK#qFaEBv%NG*I}R*8U20FDz)y0WO05E=83+IYzdpaKv+CDf~s0uNYR7d;MAmd&JlA`Rqw`HlD1@OFu-EVJp-C(F80s;c+>go-N z4=fOLFNj9qso^xaVt;~p)9_7>IEhS@1|d0FsfgR4x0Dk&*=>fIrC{cme$h!}0l@rI z+iwQ^y8wW{{M5x_@N-4f+S*#dCjTIu2%rT9j(5rxo8wx|x0OrH70*aBs8Jv!7rJv% zD>%R^E!lw-XbygUX$mswWg-+67p-2wQ8Qr10BFcS!N`89C7zR$Gchr-|Eldy0#XP( zaiIUbMri%v%$~6f?Qkr(%P^>!;1;>39TgI$ zsU+^A(=+Veaby{pBM@1ad;uLv;>xj=*;PTR1r9_fWXTc(^?}v@66S26Teg})kWiE^ zyHw5c+CD*)s3l3|XyIRt5j0(%+Y4QqdtwH+&uJ5oHT<&)ZPbcFB9^V9x^rjnWPiiI z#cu>0?o>CvP4+t2+r(0T>vI&8f;bH7`R`6LewK^p7}lopVQSP;8aSW9Vm~2%ft4THNx@^kM|H`{p zb8qfo1@yGQl6w%n|OTS=Z6PfXMXd72I>M0 z@AQutQ!lW8?AO_T({xL7o$H%<_b$=?E?4cT+P-+T6Z0Wsk>xr+ z-xD#-da}T6A#Vk=Ca~veJ)f_GEJ~~D+{VN;Q?Rd%b3-b}5frlmMX<`qoYzz;o*vD1AFNRJrF;vXsDCE%w_`ZBM<_u3Dy;1x8yuvL z9jkA~!aPliLLY!apxD^WQ<*0bjrdl6~jxzChRJ@q4s={gg0wb-lnBWDn_ z8XgD?U#>Q{bX&YwxLG}HcgV<^8q2LA8T`Nq%nwL7I(Pp%TOGN4Y>I;IZfn@y+-BwC zko)nMub-dJ)af4ZBGZuIU*~7l<$_)kdLwV%f0{02cs=xU!|?F1h0>l>$gUm`Ur*S9 z$>5$-N_n0-{VhyxdN6XNGHvLKGIuC2g&H!gYbL#xu}GJ&e)dqr{<;E=t(DLb5l(O* zl7D1D52?-jwP!nCKW57xIFVXYsJ#5ceE2Nib6M#B-B1SH8A$aO<4bJ+l96V`yRn5$ zCDPXI`!(Zy!_@f4&KTiD~#%ed#8HA$MFzf{$;e?r3P@5}t z#;Pd;r=^5{Td5I{ZVh;&{v)YP_@q@H%&QER?OSR`rB`&|Zm6~d>$!Qu!QRTQ+*QVEb zN#<@6hxeB!ci_S_BU^Z&PjzAjzuXhi$-zw4WO3LI){z=DCkDf^%?(g);SxE57_^E94Umh z>9E7>t<1K#UVF9J@^DN$!zFIK&uj>FfMl1sMLBq#XXxxm@!k|~lAmC`3r~h&MQKC! z&UmpmAS^eIs%Y5C_!fmAXgm~7yu_Wq9HduAz&WXII4O)9wo=T**)ip|BKsT2m;*!OdrWXiRv)&;5O=T6By_ z73v@dLHJ~L8k$p8il%N(`nta=Ra0m-A3G z4uGWGhi04DGk~E&lyMes^g4?5d4cWy($dqz!z1b+aU&R8_@NW>la=e?`@{lp#mx~!DtDY04?tpdNR>0a znsf(8iA6Vkt_DhhFH#1Le%<$*NDkp2N=>B-}s>K6hx@JsaWMdAF$(V?DdV0PrwTT z@*TkPt3_g)A$Pubpy2E_5c6^6Zo=tSxZ=Kpmi;bf|GwLzw9(u^Rx&o*MIRf^)__~3 z4HP$K(`R8)$vb*BRdkafIdO3D%hOQxXe&{eDE}j4L&U_7i!~{A8`K*sALhKuw*r|` z2Bt)?JB^JqA5Xa)7Mb`Mv2y2hWM4)+KPm7448)x5Y1Sq7Poo_#>s{50xjO%?OipOC)&jxu&Mz8OF%&IV=MZXh#i*Lz5JXITewne z8#!kCv*-0%ZvQEQ+p?>|jC4yjxbSxEaZf z^Z1y%t-~j+SrsnFuMAW~PCfaqly*u)7kB{EU0oj&#=Tw76SSWS8%8%`itB3DXvW*q z&i@}vvP5V*ul(}&JTc==8&z59uXLyBtpWQXR*b|@t5+Yt~2a)ZFlW$hU3w&-A*d)gmo0SQCdqo`&W622m;D$C%>6 zkGD+h_&_-}Ef{}8TP9JD$-akVUE@RWqv(~SO|{ehm%?&}PPm?x-Joz0nyf7F-}rs| z2?2DpXRFTQ*g!M#Z29@hdqhi#Ol^T_HaE%rgUth8&Xngv=i{^Do!H{d=N;bGZw0Aw z#Y|el_;}(MXM9rI^y5Q~;OVE|ANcoFVZx2&zSWw6w_%cjOJu3QH3{J;r_7Ewei4dK zCW2Sp`)BckZ6y8F>hFTo77sAd`2J(~B;uIi6EHIEK-UYOzI*1OQ&s%aM7Z#K0mSdW zeEL}+jXY4Sy=+@D>0Y+$@UI`Uf4W6e67p8Hr-1^&35j+Vcz7K zP+vMhDOv8=BKrBseBqg%vu9#8XGLLzm4cg&)CScHYD5jG;y;;o3)CkIFWNn8-W5{P z>vAV2qz`O3$&vS&zia=cYhJ$5?@QH3gMB+CO9dqzq!Luo`PIgU;$z^w>Se?I`)m>j gXyk_VHoYqZA5AIG@VzcKVTH%=fZ6_sx^CzG2dMahiU0rr diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 5460a36248dced2571948d45b46db2211007a712..d9500e26249bb3b93b74d2b73f3d224fa0de9441 100644 GIT binary patch delta 9179 zcmYLPWmr_-*Hu6yR2ozyq#KbKI#dLtL`pzHknWbeq)1D{kOG2qcMb?BEgds-*U(4| zym#jJf1dXP-#F*)Tx;!pX7k7G_8+&u@!j@}dH(V~cI=m1cW>RgMGF2|#6xpT#fc(|dy69m#Ig(vk^Xt3yZo3%+V9qFEF3%mEqX>qld*Pxj~Ht& zIa+Gy`2?%kkevhG*u5l}YdUePZe-8P)g?(Sf}{-$ z=J1z$h8~{nl?eeUDfcJDKSyqkC?-Wv*VR;4AM@8CYON$c&4=TKPh zQZ9VbBwo<-^YhpH3RurnM@KxS7|_gR=Y_ov0wy)LZM|m@Aofy z6V%4kgD^$Y%t@bc;QY2uy3C+uP<%CBomJUtGC9>_J8C>RC)`lV;Rt(Dmf@0GT|oz| z1!8$a|JUM9v90pJroiFCooDLZQK%_7DN(zra$Vj)g!$%`hdpYO7z}E>KcTt4Tg-(5th%5qICSjALE*u?Wmft|4*F@;K6JLAg!3?as9{ ze72ZxUC(gpH3_Gb)z_23xTy}FhN^hqT%(3k`PYB(nFMHe1qB%mXT@f*in*;MBms3+ zh{op|bA5_Qb{sqp6wX`%(ednHU%=0E7*u@XC1^8MezZQA{@lf43Aq(%ropN0_DItE z5*FL1Rod)(ePvM* zgn#s5M0eE+!TSV-QXoJ#6L{wIshpJGbAJJ+hAP~w_4*`E@y#JaOl(a|LyWu{*NZS2 z^yTbq^a%Y+*ACUA(4decJ}b_aOp~5 zKzk%W>M(2nM%it@vPm_==Tu5LBP}7}-L@@arf!Jy=C#MlMLd){@G1IV--AVD{QY6T z!|Ca1b6_X~RhFNgxPYJ|<3DafW=yb;d>23LtNcArI-0G8Zm6su-~ruUrSZujcJ;gu zZWB5ZVs6D{Zxto$%#`!9F6SGr`>pr9Pul&lxjon}RM6)0QGVAiCmAuf!!IICJf=f0{(&Y7iM{>OP>jIdi@bUHh zteswbOvPF6tQ{xZbaV%InjGD5TZCZ5x}Ui3&Ya8~4H2EId^^Y!j~~FfpCD(@8~uSC zN!i7UPQ^YUP~T2|DndYMGMFs5O?`7f*V5!pV3HobY`D{>eSp&5>1TIIl{ac?q=`nb+ygdB1Jlof|tFSqxdxC*TzK2DuBV ziXHr-=54Z(CWPO6&X)&U-e?hZyE+>A5eitUaH7kVSOxSEf4Lb)k6qvOuk7gSsCy>s z9b@^B#1Bxa=?|1T^WQ9nS+{c5mwR9lPRV7+ZRUpzME!r1F$gvFqT6A$thLkdxge zo7G-hX6C;d8ehP&!+EC!8uwOdBQ1qr%4Rw;*_B=NS^m#`)*Nz>Xu)6-FN+|OGYUHj%MOi+p3juvwAXiUdT2XzdII;pq? zYC4>!aX((Y(#neTufa?mpYL#1J{522&!6&M__x4+mG=SRAHH6%oGN74wiK3L}^MVTE(lyWx)*-Xghu z*89tpK{b;yAengSd z!JqxK%KaMN0F!$3$i~EeHs~vLoo&J@oTf9NdKu`VUo~exORO~p zgF$}}i|1LF5oJJnY1`>JFr-Su$ADWA^tC{`95M>hxXkYz`LY!Lq}G;?FO@n8F>LgQ zs+?mKE`n#kbs5Q7CSD8Ntl_?svcsB3?eU9<-0kffx*DxCWzH}QORZ{0CqB$C-v-*1 zk(5N$-0yCaDLD|9?UapU;Bz!UO~s?6VLgLsf>(I;K>L`>s*&>#z1IC;z7{WtL=ZHT z0#9gQ?!$-Xr&3ZC16Gmf4p?FX^#IFKt?lfop5fKf&kTvw_l7l0_*d&9>p0Fyx!r<_ zXxgbj+etne=+Xam50Uy#Cq0({=S;|T4}M;zn8Z>;^%zra$)KQpto-PMHo=4mu)-Jl zg-|(IM#8JL#n(s1(b2*)mk|vS!!Ea}k{_E>!PhSbipmmu1XQ>`{Rm9D0VT(gvy;<{ z30?%oi9Edb36?>+4z?XfZzA^z9RIqN=-t)XjD^hKbEI>{Sf5P_rkq0kE!$_JxyU3# z$x{1y3r;pFJAZW+AR~=Rga__3CWfv%i}&rEhmVB_-r!u4egLLT$#5S;6js0BmRY#i zRz;FOYL+^R*xD*g95(b}Li0!Co#NiHU8jG47be1Z4V?bqcZSgU4zE*kJ1j?51zZ#D zOm8=f$0X{Ek{ zbgCrJDx$}=I~o$%-|9SqUu&eN=s1^O&I3CYhHd*V`ksUtfO?c30b2d!gw^P97>{2~Ff0F4PXnF2Bgew2oY1eo>qV-6tWb0I+ zA_o?CifVH8^8FJ_PMx%fRuMEfxzAnuouX+a?Ai`{-KH}%%CK2AiB9(BeALzczD_mE z-@>7$0;Lt_0+#x2gY9Igez_nZ69R|NuzIVy`eUcsg})Nbyo=jf@h>TXJ4cr-I)}+X@uId?3n?)nzWVub_wavPRX1V=9N)`fcqqL9jyq=Fdw3LOCupzAc8AmiUdW<*MGjPT z3=Kd-1B~;NZk?SEP6Y2JhePN6Du$Sy(7@8Z{$oSA#%tn-klR??Z`yB)850r`F3*tm zYE^kvjvx~W;APM62k&MkONd1c;3;ENAL+N=SA|7IRN{y7`^bke1aS;lH=Z+=*p2S0 zF|dcYXy>&-=l9^PRgO6b9QCshpqZVqsNm1us|{|6<=k^cCxsiWf*Kv}oxR86=d;T~R=n_Ks(JEqipFftebotnFp!Q**m z%flt^(Mt{xsvg}4uqd&4Zxwu8U7a>HEqNWy^BCf=z+cw9WLF3X+!Q?X1_ur<`h;^M zH-hl+Cf98ta3m*sa2}tC#cyWwd9MMsJ?VbW2`Y8%xYA(0V|cZ;R2~1!0?c~#t=k3t zByrW=AXDI9?g;4C2P(9A#G=;siozbnwIu}MwH8-mW%uTJt&yh@1XP^5?+7XNPrVvyF2SMGC%Jj zL@M&F6t^49s{b=^c-+cJNJtf;ff&E4Q%T>F+_kvCFV>$uU!)uS`bsxR?mEpk2V#uA zRl9xGcRM2$9FUD5fni}`jjCc$Gc1_1k~>0B@ZoXv-rjUoXSo1%%Oa?(=*O_M0MjT@ ziKaJ-Rh5<9L`UlrYM~?RzY??u#e}EwGieQKfWwU6Lkuy{<43JA=p+4DTBcj6>6sEo z{TCe_7jJ}s|InL2`rpU*_`(l$H;!@bx zt#5IZsNB`2Eo(%bM#{=s3IbMvXzCWe^TEi<$RNyp&t|?A1q!>zZ0jzf_&D0McaQg4 zEGiK9Ml2^H6K?sJ6TG*v_-y9;Z`+V6K3?F1N~jCzLx|0LgOvc6#}yT56LUcmA7>;N z78bfg1~SB_^QP~c{7??%EKQ!2eEz)>kwZbTYRPYbjsEUK;7;yywShMv?|`_4d$vcT zX&L-ZpB^0KpJo8pg@>`aNwL|yG#j*i9A>1W!c;%AGQdTpz(l%gM??i z0pN}Zzj3~lJ32gk=U)FBWPAT|!p{$z9Qj?2Op}x=A?nNh@7j(rtCD|Xcyx_ z2_a~w8=zdZu99vwy3<|o!*lbg&qZ^E*}%l^th!HDw;ZN)Kn6K3PUCl8GarVn(*-i;?)}E)ts+|}P0RaaMZ4LS%cLQxn@cJKJiE>N&Qd2BSzrZ< zQlN=ap0Sm37e4*{We5rC5|SO2y+}3%3Z3Xo@544)&yC&jqCiw_$E1t|2H`HiiEb=^ zbfLkBE-)A}HBXUHo4+jXH zuXNp(V^yxSJXMQ);bAY}+qBaiQKhF_&DQ0VJFWc`!o;dgO z(LFh2kH-SHG*`$>luc)??f0E@6^t5J2YnEbEzwHv?FD*wfBYEji7s=6PwwO4XvYUI ztzEWVt(K0A>>s~T#f*aI;Cf(V8WY9Z;zvnpBQFCgX%9Q&$;RHk0Tp@^~KyGOy5>O znTU4`eGoop6y#Kpg4#D|1BQ*k^5x4Y)ZCu9la#V!Mr<Rxr9U8_CK>w#y z7sCXt^HkHOzT3`Dg-}))__uhS%^kL5^R`@AwfsoK=)ri9fPeXl2A=qxq(U-9T?-NS zjHA7dzv|W6-y8E8xE+m*;e7o;(fp-> zf`S6g`V&mS#iL!f_6%gTP{|Ojv9+^neoTDk<1fXdf3X`(h*-I74MFS0Zw8>1p9kE_ zE#0nbZRyg*MGWqLy(IF_lsbcVWL{S{HbU=$^@x|74~~yfN3^-mvAvEr=7A>nC$sX) z!!j3#();BjNFcKt_X}o)L{gyS4d5k~>NsB<^sG1blUuJXboIJl{p%TOy>cqX4hIni zlqO0bB+la#p5B_Nu%DOfjtsZwx1?+-F5X$YL;|K=VFJ{3Xs+>-rUC5}eQWGxfmj zP~(NvZ!uLodKn5<4|+!jE(L#y@3C_d8vK;{IQH|EKw_gUIR%A}D=YwHmjba@j2KDw z8XWc?D^FAt*q0efneI&S(1;3@H0lNi2g?AoaY*LRzU1hfEXaduB8K+hiqZz^168g{ zWlGD;q+8(&ZgLa+UwM7&1-nxF5&#oESTGHeW`CiI_F2HYxSSjpM z9dGaNv#LteG}H@g=o5ID6A%!r@IB3BM1b~4JArzFX-w*~%I$E;Ggy-N*@_Jw^W#{|OM$x2;x;~yLGt8#8v)rYiqPpu5acQDE)4a(G28)y% zRMXjAfv4AJ2R-4ZI|?s4x%2;|i@D<>quHk#s#aZeRMF!Ey$DE)u?w`#CV5#uE!J`s z6fByU1#|#iUC%zSGHY$E@zclUz~B?hSArEHLRJp8HLS(+WP6-bWFVw|9OMna3vBjO zq2T)HM$B0HloTAeE#nSadT3{x-0er8M5diE+rRFp{gAR!J*wWhO>%TPgVfp!l}5~% zrv#dEi*3Nb&S(0A3gyP*_^&LWnAi-ogCqi)nDr6#xbyV^XNBEpp5x1q(~S{kU*+no z-_eqnJK$EkChNgA2j)TrOeb~z2~RYe5PjKj2Z(V@ z9vyB;33ni0pFo^GIbFo0XvWQ!HrlR3O&T@bH`EZVdagjzpkT?YT%~j1^`|inS#Xmw zT+vphbCiCGB;t9-oW=d$;y(=kQ~uOv-6aR#Yq&G33RIxlem=-mQeNwU%ckKtxadiZ zirGnPoGmFB{K?h_Z<6rRmN8TC?YUoI&Kl!cy$O;IxR|!~V)Cn|Cdy^R3jtUbAx@cNw(=9S4{JzxKOeeG_rTUT#L#SgS(C z&y$60jprhvZu-4FLq%Jn^s&?M&C)-XWK04ulz@&`IR66;f&8LbMa8nqvYVM!A1~*C zpO{qu4)oHT8ulI^2XbTAKa}Zubx?s2XmV-~LUl@#{;{Gc^2K!wpnw9{h%cYCcKYCx zza(M^vdWEeOmuW%GJSOw{eb7|>l?^Sfo7T8Soc8%{g~;Eh5u@i_7GI4M0rpx zgCne!cUok18Cch<3rF^y&G~#5tJ6P?QvQ3kZA4q;akREk#HN5gjk5*nm9fCMAaz*Y zG;F3nB9tt3+|8}%(9!uZD*0U;wAgaCpwab`tttiFd$8!q3We%2qqFl<9TX$1iqDDc zc(|U-|b<|#Jn0E zfvyigB7A}-waPNYT&+~%c=$CRWO!%w`AwY_%zYj2ap~his13cGk@Y&o@lcYH!QND9 zi4^{y)x@CH{;|g|VWmp2n&bkL9-&%R&Gbvndud}+x_s<(%@ui?uvs7mx zY}Pgef!1!Zt0P*i-4=TT?_AefjVqtc=@}RpsCwk1tHw{7AB0!|vS+R1bb8emt}bOp z1v`9PwPnY)GwlH-wM0DqT)LGjiFXJe12u(X)s*A^z2Sh{E3z#*KMJ|UQ$nLaA3%q`!z-vBo|<}&Ni;>*cf%of3gSX@lz1p8Ovie1*QbN)Ma4B?CMcYDeO7M!@2Tqc9o6pKM`JS zo_}(Pe38T2bpQE|1>TP@9VJ5+3^vW~jNsJpLT5#xUR}`^1nj(j`vpNX({(0&rbpc^ zN8P|SzO~v*Owbpy{0Nn}*H{;?GjF`Xxjv*xFK+TXOMyrKV25jF2IS+}9)fN!6|EmD7dcp|EnE9W?g*z@#ZRN(%^Eo z(zM3&QS+0siI^b3$7ds|sO_Xnk#r*M3&w%=5U}v+gz!&dn&^C7=B4JayniMM>WG9$ zvhKV*9}Ms+y*y*3MX7b@juB(x|5Lr)w#~IN=*+0x@^mjOjI29CWULEY|6;Ow*JZ;R zOR^Z)CDJy0=C#`kAsbF)`7cZ;^dx%*;4e>`Utg_5CW=MIsv_i<3<~u$b+3-RN^eH; zZe^}W02 z4Sg%--l?EP&n^ET5PL{_X^Oht5)#eK%%bQd#qr*c=9y^r%vFSaFi!(|C1z=x&hnb< zu48RWw~rmap&=(0{oKjmTLtxX-kkwAPO}9*m(U$}p0Mf_#*pmuc+s+?7@!;vOYt0aRT1xh2&OQ8c74;zVTF`7;xz*cW9xgX<0mkHisGv^a5d67QFf!RY<~Ulq{};ye#7^1m0B+gJx=UsnxRmnRitqFIP{9y%aZ zeGdnoE*Cc56V^>(i%udh_W5)Ta03^pvTUxhhZryL0zvI@uWGnhvibCM7Wlk2IU15a zRc);V8&%+yI3HG}ZLZ7B&DBA@O+m*AkLe{nhelQ~b}GHqe?YXL4I2(YThMecM3HVvPosXXWEJrGh`KldytfXZ&|kJL`-_9Z z{{g2l_)CP)67%OduApMQmbKag+NGLL*%n(AxL9%C=WtWgm_Bh_8lo2wrw;cOq@4d?2+p+gv2j?8l z?>)Z1-*x>im&-rT`@YX}-=ELt{@nMY={MfSZ@dTryuhD=FYXaU23{k&cJ112@YgjB z@7i_nFCO@F?dNAcg6xftFN@1+@ol}#qF0ii?f-i&`&p)H?*+>zBhrko+$^3C4BCxr z3$2W24Qfy9Mn`wyHs!QFc7zA(HinhuyEe6MhHkcyyuwkvd`{w%`5V`ji7D~F%*4-i zFH*@8i&?U}_@Wf!z2??1zKZ9lx=w1+bLP^6>!s9DW(2fAS}Q@#7ukD^W7T0Klq@oz zuHoSq(kQ8_CQABxRt~tezG&>x1Wp&VHLUH)x~Opr`~&gh$s5nqSXA(hYuzD!i*1_W z_oAd7jK=$t;dLKP1_q7E07FBU&D1$>TMK37Xw>LqJbVJ;hxMOy*f(CxyrsTLYZge( zAhPtA4F-fWO1KWB&D&}f zh@Vn^z%X}_Yn&^k(cp`Ti3zc*i@#lL!vI0lF2lnVZ(Qbtv%C2W1)bkHTX>y6BBPCt ziD4}H1wIJ|CGF=YoouS_@Iq>0!8qz{wRnj2(8OS-kf5P+eMu4D#gwfk`L|&~!A-@m z$HuitIFBv-gYlfof6;oW#J!|HKuWf>JA88?RcOJ+^{vBA_K`y`llZdq4%0q)Jz;4GPQRw^fOPX zCA(ZtXB)|aFraUw6$a&j3l%H>C z+(<}+@O5d5=K6{z4QyR-M7Z>C@#t5irLbNF$qfEUr(-Y8+8^)wjUMTPHZeu!78(p72ShW}Fj}gd?=C-|<*5 z?(P%JGKV(8J0HNlbbhtoch?n0cJ&lKUas100s%p7ViS9hDd)~)VOu>cqMmK|9gpE; zF&oEU^Uo>)-o8bev8nw0oF6Q%2jj9J!%eDWp(U{SWA(mWgex+L-SqS z6+-`p?LY*G`qDL1A;5We#O`dqWxomYwp+w2<9fTDsp}zPvN$#o(38H7vCjD^oBHh* zqxaxU^>9bHW=PxCTULCJvjyPybBXE7H61-L=#ySomyE9uP9qsTft4;epUKR8R``F2E&ik% z-@&SkxtI_z?}MC-%nxRHrYv=OsXh4{db~0F;*mwV;*wWW2cUr4BO{NQZnpV_`3M@; z5eC^S8&>#nmn!p;b+ETrw4aZ+_aAXg5ot0lX^dQkvOP{EWNw3pQf^5TNg5i=R512L zZJcK+2ut3@rUuO3M3R5(shSFbL03(I!>BXnQmWgyN4^nze$IU{VLrH#mHqPWe4^Z43Y?c$!3vxHX3!j79G z$DvsUj$4!Ewk9jQ^680@+y?9~J^|qAe^N$t$;k@bn)=(*Be@o>eR;VYWnPCix65vD zS~#A0d0IDs-6p;e7J+Pj#U=E%ybN~~=T94lq|W;;Jv3G+lM?*!YS|!Phu#FH=6$*u z*N&JupVEp7k)N=4+qisCz6D|X<+=IHOxp8+A`E7Y;}$d1lAlCYEs%cQ2QysfCGEQy zbZf&HcC|VcUn~ABv^!lfCOH{8IBAtCWv&22p3PBb60_1t$Xq%hlL^X=&?}p3U!bVNxK+Bkb^%Pm)wi z2w}PB$LoW@Qno5TWNkUhrKBW5JT<8k0j=y0V@ys3{MvG1MgO)Ckjy-r$r`tRV!EZQ zI6&%=dcxIsn#GGe|J-rz=!vK6O-+sN=VTl@Svtk?z25irp4#1_d;Hqea(Mei2^a2> zWx9y2hYF!Sr@I&MwPdzwxiZwuq~j1Q+dP#o9317MODAqvg!dfvFK z&S1i^ck5b35uNla2fc}vdD-mdvo-Y;mawmZmTr8UaHwb()FuTNf<@z|JT)W59DTUS zxT&vmu%zwml+yNO`Bo$$nfuPv=v44k?UE5Bc^wlp_aVg(bAa7D>?Yy><}D+N1$Ls7 zA*}p+?Y~vchm%cklFuSVSf|g<}}&qLoPGwCVbErElAO^IoCRo7Vw(6;}yTqu`#jwiNTD$BW_MU!`UHc+?0#Mm# z30=MQ8VLGZ#9I4VL*;8J5?gWR;%R#icRtP`)tc#-ng-evUq~wty-A=2vrYW;K?I^r z;9CY4<+l9(1#nL1rAF{W8e#bnbH;n>2h1q#DTc8WW#gE4il1T@Z2i3>jM1n2cdYx? zj97Mkwv5$tk)!%O$app2qW)yu0|7lnp?6jNl@nQUuivI|ns(gwBWk`e-i(N-PSFX* z_xfY)Zn^rc^6&A+u+4uH{!&rq`>KC?qYz1^fD5k?u8 z3}v2qlI-bW|5jckly8mq-;bF@vOQ1#Cb# zW_MJ-+dN~%C&FDwP^#|y6mt}dbvlUTPlaGv2pQRNCwxfd1Xt6+KaYw)B6qH~ze<^Ycy?Fu*V9U{yZ(x2>T^9K1W~ ziI$b&uk69Rv=x5hVWA>Z_x47So1V4I#0#;FWdyQ5bm~Z=_W5$iHWFuf#oY!1$xz04 zPXhjCG!Sd-UJ%2fA|9XU$B6hE7 z*a_xY7NANCb){}bs6A(*E=!4_)PD)C0G=`o`JP()}4elzweKswz zH{%3gFN!zSrEv_PB*W8uQf@3ZVPxH`-zaW5-&7%*ZfQq|LT;2=Y>kyCigs2Q)?a1( z4&e;Xu)++Vam~zmIeg`GZ>|HC=N8dqakNHMyp`7Hriw$+Pr5(c!?eTmdo6^f=&8RA z&}pqV$+u|!cJ(n0l&*EF#=5U_0^oEKz<40nLYA z4q?Bsk4Y|#iL=quN|&{|q@>1%BaQpvg9Er6rOD&#Ss1k40q&3@0#18*ik`jOpZ}%z zlCOajbr7kV9m;euhzGFOHHkI|aKyFFUtYg--pwVEzIWvc zZOA-<>AmzNZ23`ts*wFudNpepQ2?Y0Q-(GnbbG&@P zAM*52?2wfdTe$W6$aT|x$)k&K809TDriqn`mr5E~fglPT)1TmBZ#eRqS@p)dOio&f z>bF$4-O0%%aRBl(H7WKOE?XNhE1CR2eRQ0*fDIV7tQs(C0hjpM8p){k^CJrHsUSJp z^^x4&B}w+UPU$3Pg>WXwZ7rNX|MC1`AB& z>n6hNj%LXApH_#))r6wS$sd|$DcktE%#Vq`)^MO7_lAQw`NwXiYuWhS($eyVA$92b ztWRTR!^7I95{v7*RpzTjhfI^{c(pJ3AEI5amMcHPEq6jV4a!%rPiQ_QY{2Y#WcnRv z{zJ3Z?1#-*D+2d_;zxx6x$O=&<3hBH5R-O78V}W)g_@34)~EF z^9S~P0Oe*ni~Ops5%f+p;YvJ-Nt7&PPfkuwr4N<%u~e+1YLUb}(&x*AP&E5L;Jq5o z!~Nr7iJ4@WwqOvIG8b)LzKE%OYYEynOT97uK8b!t9s2=oob@cevmabXQ9fr-@7*WR z>H6zZ#g}P0>whvbif~Po6MIzTX6mAMDBKgHe2(P%zZK?3n`Mm z>_S7L(+wftfi%rq=h2b)LRJoE7ki2Yrm1wxPx&-poFCs zd{ju^Ta8=1A6a{Eak7KB=yz;Pj=aIHjw{f#5nWHIQbflJ#LQ90YCfbQZ=-6y1(M&d zvIJ&p<^%Ia9zAivq2xE%HV6ID(bLP;)ngL#^QB(9ucGTeKUfi*EOd#wbV+|{pGW`S z4qk!0a@Dy+HWPJ*-5V}d4A0h-NbttD9Y2l^Z(_KwlnA9^#N&G3m zH#cWU^hhO{THjdMaqc~ET03~6tKBUd{#ABiB)%aY6egAw!h)`+bjhhw=DPL23I-9!4mN+aIH``f0fb4PS4$4m4`Dn3t#ouO{1DJ;7RpL zH#+8*Mgc`(0coMeQ1z9Gye-pIFUSj!Yk7H{UvIp0Q&r`Ot;1JjuJ@W-MxF399(2U+ z)gOHU%l2||Q9M|7SrCMhp|dr_(6z&Ej)}xvH_LyfsP=Ia^kqMgf;oEn;y(nDsTGoS zM+)0&q>A)rPZnycB8OUu$PLOZdspqJBcT~ry5)HXEX|uaNuq%5*!f!0@TLv-!=Ey4 zx}(U&#SHuD3R*g>{nprGON&QHaNf>DjNAhe8c=bRK+tWP5r)6#uR%`z{KkyfOyGW4 zBD1kOyQNp;>#;Lsk*`NXL+d~`9l-Uo)f((QAL2$=yv_Hg8?hb^Rm~RJ6%&tilFb5+ z$q5Pf8ZVFl8IN88C#O|M?B=D!+?F)mW+e z_y15OC1*?pgSdE%5@re7V+aXq_h*1%^#Ig~tvPgbQ7f&a4s#-kI6LX)Z?sZ8uDTiV z99%swgO}Um&&~@GJl}71Y+R)WUEK&z1JG?hU40}9oV9B6*X!u&(#)|5i-UoroX`X< zD%!bhQx$aHd%LrWozi~xquk1BlJXs|L3j=*fL-kr@%SHuSUzJW6l%QI zb!%)tt>2k*9|3g5P)H&t(1sOl3llekaHp68Yizc?19~(sHgJ!^j91ndeX-hUv+psU>bI4X7qvHY;};+h)T`WzfWN?-$@^vHekZgdw~6H z&G(ADt0~HNGO+(>Yx!f@n?McI?CTfyPdFwOE>Hvbaro_OgN~KgRcgA z;4DF^5LIiiKB)J$IWMt-ZhMO3Phr8f;VA;$a=5Z%tAO+6l;3T^*FJ15eb*YevD9($ zw>#v0=9EoTQWNNC0hkutg~#dr*Df(@ z)6Cv~X$cAiw10tN2;ydm336Ii0dt=aBkaEXm$)*&`WFcS{D;@Pej{oUdkOdGD7&e~ zg;TUTaI$>(z^KNuw}4B##(m@Vd3O0kLDOzt-k#)sQ;1{3q#YLr&Wy9UO??W;ks2vOoHz&+;xAc(gBMhF#g@Hw=HWZbg4$W}wzf9A zsLB2m-;+@#(Es-)C!@c@b$3&6x+DAADeUga;%lbR2zeB;n1yfTbiub8P`XVdChaw{l zcdM-|lmX>KjlmwA&rcf#kF6I~Uf6PYvLy|9D{FGPYoA{7nCBM)DL#la%^n7P5{-#2lu(gi=b^_R|J4gpsTr+N|CR&y+-@MbzIWNlJl9~WPXxFoJu}{&DrWNpdTJOYZd?6T zP{BzPBO53J*Jo^PZ7Y98-v^Ed83#OAUEjPixeV~x_44WdC3m+#;Ho7d0v(z-`!$A; zkWTUmbYs|tRG3d;Zk~ZF_5$v^|NQ?NE|xF?BeTsvX1;5&jRh0+wXHSb-C+UKKn8He zj6LXd+gkkS`kyKuaDtz1%_#90-VP2nw@xLDUy_HP9Prxx(K!fXZT=10HBYwR?^(K zH+9^ZuG;U%twnyt7z1m`wUB?KHJ+TH1;xWpC6+ti>(dlJ%oX$JmGD@#unK1q$fyQn zcO>*B%!Rd!oQfr$F;`AFCok8FD8 zfLz7B5wzyT>l2#YV?~g+nnh*;6>ppFPuuTIHiI8 zpG;JkWQps^W>mIj|3C4++VZ^2U=y^GHM~g?vfF)%l&VXHzFtLA7)TXCz0{sRf8L$X zc{MR3NT32ee!wsu?6Z1lrQ@{wOf=WdrbMasN?UcIpIWZEabWFn&fZM<*U6r~t}ce% z2K049`HCRG;1TZYAGAQ<^H5chY38NQ)3*a@mlxR{5~Sdi<-6aJpz(F@Ys(2yg`SoHs{AWirQPJmc`ij<=s(FwfU(~p2-Q2$We{bG zN}bEsO|-PK)D4F`@cr|HI74_Sd`evv~X^e}u^MO7c-(y~0z}|I6|0@2J zRCp5RieCh{e{9b_t*xPfr{~{$RwPxcsuMU=qOes%L7ZZ3^V)qjkbE5BrvK*6o7MoS z|86*m!MmU!7RxV*Qw>&{f22>&1k4SF9QK(I_3#$kQ?#$)kiL+`W-YSsu|YMw$uM6J zdV%oa;o(W-+Peeb>KNq*7&!ZI$5*ICwf0Ll7F)p0ayMd`C-qSk_V1eYrqQ&PCI%(T zt3JEA4!wC=)80C`p&fyi5pL*@y#}gN#}9@zwNwPg;l{#oXu_*uSw^?mp35%_1xYaM zR86avzw=yR zWPnovX$%}Ies+>jRic=&^~PH;f2>?_;wWw!nFjgwsn z*k4~y%-{2UJsY8%{&LD6_Rc-4DC0N;77AU8p?f!$8FrQLCMj?XMRKHCwbqEO1qt1> z5a?3#P2y>I?o7>Y%cQyc?u^&`#C_cTA?_=nJu!UtZU?*|#+(&*-m>FM8a`djJU}n?s8u%K9K(4^1Qtg;f>y@WcVTIDQ1$%CV)n5vxO9$&1}cH z!Px#kW2>Y-uJQZ5seGn*ALhtvZcMTOh)QP>ArnsCRn{b8E{kQxHUgZUo8oNd~ zQKF^3j=mw2tDSV0!kX zkhO;2mFrFI_7s_hn|v;$Sqs>os9ZszP-rHvhiBN(oZo^#^GB3zw|o^yD{H z?AOBL)Kex+48%n5MjN&o02YHHcczCuXccX8%6(LTqoq9~?=fdWLc*Um+7ef*S-in! zJj0411o#y|ruOygR}M8uP0{JTJF-$V$m_#Gq3QXzN>iXZSv~j4?Ge0Vr~H?HTY#Y~ zjGnX(iSU6-o-U7ImZsf*aCz$Xl*Fdg?D8~P(=DY!j+X3$QfqJiEkwg^)km!wK>lMfUX{- zIw0!2(8!&k*sz@#{0P7F8F^l^Tand%WB%MU8B%bmV2&K`j$YQ g;vVI7e=NmE>`jqO0+sLJKlfZyme-Igk~RDMKRH6G-v9sr diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 8789c8efce86f4ba528ec02651ce5f2da01aea7f..9906864189ea35a803cb34703f123096e15c9a99 100644 GIT binary patch delta 5666 zcmX|Fc{tQ<)JBA4OXW>O+1_MLwydQ>mXhqtSVCS~h!Mt5i>0iQJ-f)h8;mW4#+H3I z*)j~qz7O9sHUGGtInO!wx$pa&XST!1o5RT?#K^rq-&UqS6B$58O-4p`0eoJ5 z4il!VWP;r{)P z)zAl_I`NNZE_|eo(o_)&y&~TFNj)SAt7<~=PxQl4xY3Xk)(Pt#cKeyCR2L4};F|)q zd+#RC!y7hp_gq%%7Ut&m!pqjr*j8dSC^y1grtBz~ z>CWTEcVlGSa_#Mhou}Acc&TWK2b44@j_EmAQqa^?8=IT9X0LuICL=udo@#50;k+?< z`S~A|{m95EC?mPd_4sJp9czQvn*)1L(*ZYU+ZN{M{nb}~rzxwc{eodUGeTjNPzSe? zAKBY2rU-OqoAe8ti`VVx-5@Nx6g@a-1xgU3;&<0x}?b`+%nE z#}}Hx{FiQhA86f#QTevo+GYAE$DU*vM^L6?P$J<2Jm=fC_F?9iu04Oj0as>K;W~}?A!`K*Kx1sz++oZvI#G$INLcW-|EB3c7tFVKE zLr7-rZAv1pT<6GhG%mh0)aT{lO_5pnny7HH?_1|pu>{Sa;ENYF;*ffU4!T1Ar&-TY zJd1BHC@r50IwmwX!8sMT2j0DV_b<&O#%o9DtIo?Mof;QCa!K8w1V?EiN-I>`O$%$V z=^|ZUM>D*4ztcCRzfq<>@3~q8h1O7AIt@z)&R%|1dN4rCtt=KYJepHK7C>{P_y;Gl)nd-*9F^b#y4 zm>=-rK7g3#gM{1FBsmw$5r6Zh-)SzLR_`96JXX$fz$a2zh0rEfJ`?d(@;aw#-REZV zj7fL6(6qsKJ~}o9f_yDr;Ej-yE%j|2NSDJMdgIFS%JdBqYN=G@QgKF&ztVLUl3iMt z=|q-~dJDeH_!2Dom+kRY+;UPZyD<`~#D*B&0(7$vJU$WeQ3}6?Nsz$JD$M)d#(38GGpCHY$D4POqaO_2( z-&KuKv2M*m*Ev6Osm`x5`LG5)+Y7$txYbE;``FmnhuzFG#DMJ-055od3QSVI`Qv}opS*^Z8I!)JhHAl=`ke$TV4Q!3pg1%0I8ts$fR*>E92jeTAJ z2LcWWgw76;@jJ5fR@cdhS=abI=j&GyP2bD@>PhUyQN;C}q!1aop_%mbfPes`e!<S9igcHtH{TP4-cs9en zN_%Z>$HJzvtE4yYpNP(^wREyN)k#b^9IW{%o@(a-g^l$M%g5vQ{r&x~e~fou|7juR zS5ALBvhi+({cxT4uKvmKuIjm4r{>2%t7^@l{bDLmTuWhp2v@`@_44U~pbsnE%_F-u`SGiO&h=qff6+v>9a!E*G1p1f9vtx`LaGMe)}MUy3TsIuo{VR>R-;FlOcsw zITsf#^Nfek0F!fH>KZna{SIr`VyviO4j6wi&9TAT5sUm^rxw)QF`5-Dp!}QV~B)i4Y8`T;T z4{xUDyeg6FSZhBifE7Ktm~8KRJg9H1t8PI`;66y;E`!%Xcan3QjJTlB@HeZr*C8S| z#r(71c`T13y_y3ux3XABXP=Yf(3z9%n)35sWPoP68i8|Y85$bO9Cph(CUC$F=z-Ct zLPE50(P|mJg2KYbLxcLz-w8L_*Vu?h7T<)wtbKoa()TRUCfkW}O9N2+JaI_<;%2R`i^5d!znz<1;_2`G{?Tf}SY} zu<)A94IW%(g$N1u3W%LiKq~f?m%QpV9tAl$hLtv5z2~I0h_{dOaYd{B2?{j)@FdB7 zNXoYB9IlqPI8o4kENj$I@{cglui8MD*w6gC)USNFRlQ$>>rRxbPEF-ndi2~ufJx3V zVe-pQW^nV10>JxYWr&l{iZnQ3F8==bnefAV!NA^k1q6m1CgM_eBuG|qg~!~ba*t8Z zV8dynQtGxX|L?9px|&EG~_tf_?FhrG=r1oJhtDqD@G_0jrJtXc1b$0=6vrx z`N{I+!-C9(!5R-mrZ~4zYcNng+#|su2;dmNa!S84KjseS$#q}x<$C(eK)M#D_z#IN zT@+5Dysx&9wYyV)%<9hWtRyxnlvXW*>BV`x5nD7KeG>|X&Il&~*zSS8j->pE)XmG% zOn8xmsk#-YR)iNeSVQ-Hb8$9&Pl{&<+S(GPZ(t4CTF-x>Ww|h}8B<}VtGk_ah=<(f z(Sj(aSQ27gV3A6=_Yt4AX1OzxrtH0oWyaW)RaO9_JjNk*3k(_*?5Rwo$tfU88g^l> zXc-f3?G%h*-9hb59l-=vlykdPJzrz-Qvbeqr-h zc1oNv^@$5Tv2Ep(%pTSt$)HDoPoYK^-SzT~;pi%tZ<93-we_R9m^R{G!R+R2HN;iphi z%j|cYMvB|>{=GuU@K+2!Mm=8i7GOu2z#^*r`|IPTF!`-OMTfBxZi_DDkAPk3*e3h9o;&FNAbgb#V)Na~g9-WI z3N&c+LP$-vB)ZI5b2r1$-wKGcTZ>T%tNm5OYtp?4M#ZC}^oMrr8NZ1ba)nY(m_Hsh zpCBY+J`9KG3?Ym>*f-*+!WEA@^NO;pU%w+Q2CpxQ-ij*KrVX!H&CN}a^YuaBP;Cg~ z^jXTm)UVx#4u109AQb`w*9x@Rth9#C1~TGxjS|pYeOY*w6z|pP>=doUSCsdOl#jpu z=d8}BwwrRSu74WeUZnnfc2aS_Po44Tc-M|8h>4U}GqG0BMuFVCW;$6Gm2fRh_BZ>X z95r`d3)?M^m8BUM1*9j_n?7cfy*v+Hd`+`HCpolmN0RS)@q!s7pZQu1tIA45;`a`v z>-ZF}^~5FrXb4}km^Do}5bNJ`EPJzrQEH+)(z;k+9& zapt4f{r#WFse|KpRMT%Uf16)@A(Diq{9mw1edaghSJ}d!yf5;VP*nT@MZKu5c5kE4 z!^vi)=^%$p4V4kT`<3rXqxVr#ZSds3(eSBitK%b4k--|zFMK2=aGvAY!*?^H)@?f| zPyzyXUt>i*u)kx1f4Wu{_FxHZDWT-TW}3QYgUJv!!L-b}Lu$3!_T-q*tE(SOf{OhD zF!6L%LSU&W>ha^pO>W}rw7eoGjU#SLUJM_f*sGLB#=>BCMfn@Jpq?S_ZuU*wSb?$f zH62}D_s+<3M1_;RwF1wLFtslO6c!>_H!a1wVuJRRz$dwwcRPptUopRVOM>xr(8-2J zpfD9m^3k>78prW^-`zF$%O(@QOa5(gut6Bc%kc@=L=*Y<=R2^LjK0jJmaNj!X~Mxaep0rE%++m^f&FCt zK~Ed|HRE<0*Nt-<9pNt7kX^Gk-e1lJQ;!H7-i{{~-vW5@ky-TmU~ImT`>|biMfj4W z&dTrb!DxoZss<{~-ZA?D1*6bcNBt>VEjE$a5l zXv0al%dFA3HsY6G!Vi9ArJe~o@RI$Qqv_}Vkxol^BIL0g(bhs`$W0>rb5QR9k5E|t zXt7085Ocx!9aJi!3tPq(UdenfUF4P&GVOc0`QaG%fs7|CC;f&(-V#S|RT1&KN{!tk zi-%WfWCe!?$}O+djMcYEy~ciR!sXzH_-p%Ldy?fzIUg@ELI!4LzI*plPSnl|In!#*d2z+J+(HA}M|smOYxfBT;fWSEbmsOf4e?D`vw6tk!Vvx)a{ zT?3obOj=yA~OxsP9Y0-+xK>^y4!bLZt!f|b#ekP|Nnn= z&w&~I&?&k!k`ajEZ<4GrR-yL&WYg%2WTT_(Hpsbp9sOeGy9lv&?XJKB&mNKeXK%lt zxY6FbAmfTdZ9fj|@Wf(H=DWcrs?1|aqV_WLtM@m>TfN4+ljOa&Xs)u|p+}}!#U7Be zxVpL~CO;ngobai%$n@@fg+HWb6n>ZJ#N-sz0PGi?A{IYp!}0m~4s1?V*^pD{%`U54 z565J;^_;6xP7Z8|Bq}Iz^#T?@pld52tXXan!WG8Kt6Q}mYBKVzw4;ZU>uknoFK=qH zRfQ3e`9njV)5*+bM{u8x^`|)f%V{R|2)=Di~1hJxvYWUtvUOjRg|CE2j>-@;JT!WUJw{RY=rIH6#xwNOnZl zQ#WUgUXqZAY7LXWXlc6G*xLOwu%2(a8C9gPIe$x`#ACjcXX_gsNeAHJY#1Md=v!Yd z6REEPj}zt3^M84q#cIat@Pvczs}1t-N1lq%(Ni)7VspvfUIcdWEQk{^%sF}oRd@+y ztAerC>K^+EB@li^C3_wH_`lZ;O^ZI*t@ph6aw;~^=-S#^T5b#`KeeRT8?jqYyRxUW zUr7zEh3;b-Mqg;$Pm&<5uG(NDN>@@37U&qtms$D1@+tTe8{ORjXxG6mFzcUY#Njvo z2G{v=c+9bUtH*Qj!wTrvM<&o^=G=6?SU!8c<>F0}g4o-qGwMkbBd+*DZiTE5TvVL&ipBc%| zIwkZ36$HMMbSH=Y_}~{ma&;_PU;A>A*{&02ZFBtZl;^y^T8KI>~ol6))^{L~S(aXm=FkN1{d z?b-yCqA(MJT@~|3Mjh||qP|AhCZHfVK1|}`_Ng26QMpD z7DjK8#W)kvEKoY2181R-8=-5-hn$OU=<=*@$_J(HU8Oom7-R=L`ebtpk`Mw!Ch5=w z=0k1Fl7D`z|X&AY7ey@6y7&~ F_dg#8zMlX9 delta 5724 zcmYLNcRZ92{6`8IkrgsR&X#%EDw5rhkuO(fcD5rP5(*h9dz8I1&)#H*Y}eh%p2ykS z@44&s`~6<`dS3U>=lP8H=lxkvdld0r6mhgTaY&5B0~XTHAw*mp3|?)cAT_DNi^LH}`DKTFsMUgy>6lXk|cd z>Uf&&nsAuCyv@&o;u@V?PdksCsttWbgW{@UMAf(;zaU~=i=6mAS9GxO=73sM3OTPO z+#cNpxMCWc7krhQy=Y*-0SY4~G5}QIE*Ud@CS?M+1*6l_IEMG79tIE*laSKzM8gx- zMCq3+TjqcNHenKe?c`+k?8)$o=GE(5Dgi`+92AGZG6ydIG)BeRdeLJk&G&pI*T&L% zd~DpzZ^G2W!{ZG(Ehwo%McUunK~x*>CGNLA*$G8^uaD-hu&}fiTQAL1iHM$~%B>nt z5dewW_M4|QCoJrnH#S2=q`RQMthd1}r zx*~3rnRkYX$a`@hvhZ?TD&LSK9CtjDMHd7kMFfoRdSTz==H!7TMiYea;ZNub6BBuu zng&61=r(bMgz2p2a9eAemtmUAPv##@K0`CDrri9?7-ax*)2zRs4BcP|m=Ym?m-@U#7Shzn;Hk+bgo62;WC_7PCS z1rYx|KK7dUv8lnc`Y*>1t{zTp-ZFlAZ0Y2LZ3`Eb^H}UB$5Pi;fB&0QkDXp|%=vO| zT?+@DP*rOw($+VMtsahzQaT=Ib54=+aVOnLP~j=mOUDIWY1~~*?h)c~#ryk68}z|E zQtM`hAU}W2f?In1VTv2zT2YZXd^U40r~u(;QV(~NVx8c_=OU{pfJ|FJemA`ax1+0o zOWGOSJn1R^qV$COI_ExVvNZ;7<( zEFj+gGo7~x5~>Om_}f09i^pPbEXCc28K5tuHgZWW+kz< zdTNn^{9~1_8Dge-e~yMJwMu;0nFl)UGx4>wpDKbWF>cUS)^E-ywV z19x8G+JZ;;^di^m$I15G66h_|BfK}y4hlUtXhkidV{H*t^gW&TclBbD)D(ffyj;BUO-fLeC{g2lxGAp-C_3m@P%+ANlt;S>q zuoM|}cr4&x@|@4F_RFqhnUpjl941RU8hT!ite3TiC|-0a^I3amfUyw;X3<9X@^;?w zknMiio(ZHXUlh^nQOPK;x-Q%>;1?A|o5C&(L4=3?GX%9RTLC2g`^dg4QX)J&JVuAu zHp#(=*40M3GD%G5mCd0c+8__1#>q-nBFoZ{Md4q?GSW zOFY^~$e4r)g3mAS7nR?qLGh36C~jz%ZlT)rfa8!pF)?BOM=e`&G`3qatzVx%UlSCP z7hM!22JpW`F-jdglIA-;My{9_HgNy3L{=&De1hHda<8rN3eXqAh<3 zFizAJzwPk7p`k%+F!iLCQk2~D(xXW(d3qy^lHV0(#Tz);X7vEV{A52(Dnk^nJSz$0yoT(onwlD{e*Sa*vs^x_ z-3_uvkFLnI&6@U{sSV27`}ic2T}}8Z`2X$Fqo|!ruH;ohr{tUlW=Lr5txs5kBT6*iE=HZMhSc1hRy`pw2_pbCM2e9F_pYt84G zA%5l>;Q@j+W|MxFFZpY^Q=|VpX0itER%+{c_a++nNlXCYwH{dZz7iDf=gZsMos`VK zQe;Fvg6)I?_R8NsJuc3w(C{FO&I3&?L8V-;t~H%Qc_Yq{%hmb@tSEt z_8kIP-@sl?N7e{y_JdDhpsA_pxiMAY+8&ayn?sM!!XpD>&FWwHe!r8_!!XYrGOSQNld!E#T>6hNJJhC3#uMhEfmgPwN4 zKwuTRxV-h2rrib`alJxXVc~Nu>gETj$*F#-_gXp8a7bA@13Mmp4s{hldd`Dy6SkQT zQFU+rx~Oxq7tOO(bA8U5+b%EvT4?B8Qlk6WJ6Hps-7i`pn}y^u$JRMN*d^=fx@Id)gTf^QJWO85Den=@F;F>LEJQBGd(^YlRKEM zeE3bPiUB{21I@sJ^$|r&Mu|DwS%`YeMzF* z@rs}D+X;A;rzR64gQbjgW23c>{6s3j{1;HMs!Ml}@q2h~%69fz3FSCE+#D(}z6;KX z+{q91rXW!6!l25@+!I3xHM>tAH8e%a-oVb@!S@J7495C#e0dX9e-Hynk1$x9K70I9#SEQQE^*`kHMs0T z9Z+&ETQk7#-@p0yW7>48rJ%~Yjhl_;hB*x9G&LSzgrp$Bm;I^HMpdf~iIT-TY%U8q zjlP@yyTh1<1G5g3aV`Q5@n9X+&5ezY>YErsck(hbkUC*iXd@WV_2AkWe4&@2-np6( zVkkHoZj#(C&oywKcJE&vR;Rjo1DLXYhNwD5)iWz^wIJ6fa*G1kAb98ypm7B2u$)<= z&)y0o<>hcg+Q}7oe=+9hS3M)6qO&F?p@{i5tfS5im_$X)?;4wmp=KF*xfzWc&v z<@1Vg?M-%bFL1LBbRvIbRTns(D%dwsEdQ$aOGaB8R^I?30yx4rc3LiNI!%)X;n_!C z$r#;RIragIJFc?VOO_Uz{4P+3+Wk4swz~T6C}Ckc0e!cIKvYEraEjBGQjS$`piQGw z6<_ryh>kvy5-Bb#!S3JILYG}bh+V>Kys6nj)%smO!xO8V?pxw|vY#i7p=6^$*Eemn zk^vZW>33#yvI${$J|a)}`tiMaE^@Nk)>ExhK+sl>sb~>L*}9fU5!UXEfg>oC_y+ zj;k0v-&%;L7ZatU!;i69aE#5$$`r~tDR{0=2Hw7nqvC#b?0?+J+8RI>d21)yHPrZ(y2n~rw zpEwDuVEK)X4g!Dpd(4-<5)W5+Wbi`sVI>VeJ$^)t-CP}>>-RmAEa3wgd*vxM=g5_*yn;UId18B@WSwhgnwqWYe8U+Eh5qk05TK%W;mC7CAaLeZ-y%41=77lX zb#RmB$=zW0y8psD{ZS`NC4K);Wl%WJlSB;H8eZ>BsR?c~9Ng>sET zn`5W6)t-M<$T8^`B=EQ%bllT+*5j4m^RX}G<+p$bA?{)UySovMvAW^=Z?%z=?;Q9< zc!b<0UZqJnqU-i@PO!)d8$D!Q=`aj)OO*!V;1=Z%+G4+xEee3C`prTNl{ z%z)Esn}qv7cIaA!tSS{)2@tE(6@*Vp=n6hB0`A7~>>90*@Buc=V7k~TT7q@df zJrDy}v>n`Yjuu);-B=V0Y5(g~{f!%JrvP{aHGo@x zYIT@Nk9wLPqM)uW2pi;8Dqq9er}b4RUbJC;)M!6X46px@3#k73TjHeTWKN&$p8X}+ z-zJn~kT%DL-HtO@&3dKk2URM(AJKB)nsB?z$WY%fp8BSdnStk{+07P%;}@k)2#m|PlI0;h(0fK zHsdlmrN<|#KS`t{qV_EuB7-=)PIqJKIS&_=J9Fe4{}Fp1ypZkm`0fpoC0j$Kq}k=+ z+vkU1AAWyAJ$WI1g{<{K_K}-)!&$DO#ZbKHzbtowH#;D=uhCIB*s$GOFjjb=9+$>c z-?{v*CxO|2t9a)7(=GqQbE2F7Oxy({_{igbP;XXYJ-||7ZDomNYZ`yoO17?!I`fn- zs xrxZ;(J1!LSFzQwNM~G)&#2$3%_s+~~hyp&ixf&iQi z8(M~m5g$j%?LzT5ao>(+%nzxgw+P&9KAgpkc>dy z;z30*ZJ8uGjda$U^E15IZs!bn@V!4TTcVXddoW$vbJ#{NrmF*9>Y<01-JtYt8Ew*O zgXK?G71qF-UE2Hh9m7)rmL3X(BI72o(L2Hk%%ej>Lf+fZa)FMJXH|l1nCCbTP51Xf zMzXV1J&&pT6nC7o#BmZ;s4R_qNE*XEMuV4BeUASM1kXf!ZzbfHmv4Wu$gQRk-h5?q z@A}TJ#R0l}`Lh+^lO3v)PjH*moR79A_qsUc3w1}}vf0H<5awb#(V{DLve8Dn6{(>G zob9$B2$$sN+ailB2slPFkgRCXWpl|hb@%o8o&qo@k<=lVGK66}wR9o+bnhC+@d2Zd zL8anm0v<$iUi?y8`?C=B`od{{eGLmBP<@;*^2L|?`o$-6oBk(_&X|?Ptf=R}DPc|G z1wYB~;%3-sj}I=Ef*V}A*xyJT-pCTGuXZ*o+pQ`nMsfmM$z;rLLjI}$0jh_0VOzSx z!@@ZHY;|f}Z1_crJxw}S7Xbg2rKle-s_ei%M#i+4NO%D($pDf>PHgX&fIteunM0r; z5pl(Tq}}vXZv?0TA|g_A4b^-46?CM=q%2uQEf<$^VAwmsReuw}55`0qYPzZ=_g}vI EAE^qnEdT%j diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 942e365387c4df865c8b5f0f5ef490773475b968..13b165c7b66e54198bd65930319da9842a4473a2 100644 GIT binary patch delta 10209 zcmYj%bySq!_q8GzAdM)EA|WY=ARwW%lqlWZNJ+;d28hH+Hz*27r_?YEB_%1HLw7d} z@!p5e_qW#j2Wzq5#<}P2v(MS*KAE|fH*+q3<-P0`&L>HDB_iO`wM&;Sk%1qp@Gx%N zG7`h*MYWYU(AxM7AAR-YCz}Tm{7NJI_Ep`!-wC2(-jB1HW88aGDM@!XKHq!KkxJE^ zHp3WJyBNav`qMqOA6K;QNKrnEAqp;5e2{K#yHwM|{8m1ZTRxKIC{>3md4y$yd4u_2 z+y}eG#IupIc5o`~_4HuEh3p1&orF>1-KEPoxEH^aoL}iAk9>!F1G2t8o2jayio-?8 z#>V!#8zZ;*yF*jH&$}<3xmWo8e1a~tO9-Gg@#n(WEYmQ;3=9X+<)~g9;el8 z`v+^P&!3}(+qZ7fNkGu5J7%+6kuN?@crR#%)t-}!%XhCsFflj%ziSu*{w#rF>jQ6I z51|*{4X`Z`J^S@z`)t?4ft_oAsbsArHMt~nw(#r}qh0*dXQoMwon7+c3g5rW3Nif9 ze*Nw0&{lRZPtE*vFfuyoXTJ%iW^_M>7%><{5eiN1EszIQp$w1zyLM_)HmlhL1>9E@ zaUQebgL-bmchl1!#6aR&MTRFPV@X23ZDez|f^>;An*MW#k>Tan@X3^QzbV9W8=f6D zBN)WR))F9f$lrzP@{k*YfsydivwXk9T6YMPEL{zzDnKPmDWebX;4XsfJr0 zS!sOL<;Ziv+>xv#6C+8Scb!4No;^Q*{iT|k_ps2y$;Mb^j)0w5E2mbR-v>3 zQpwi^hhd-q6I)PW(pc-?k(H#A{E5Ju)@>-&u*uJ5s{X<2%nS{c2QVtFGI2D_N35rN z36bBvO-3%HV7Gde2^jw6C2Aa@cH&;J6tyj%3T#Z?iD z!%av0x_2p}iP~uG{+b;q`85ao7xJJl=|L{!L3kKJJdh+)M z#XmX2Td;Le1jVQ99s}J8xDDB3&fF>rqBwTq0s_q9ygzju>zvPR@>}0xg<&{A-V%DwGvP(Q_9Jt8 zdOC*(wM)Q&)w0f179!m`IYJ3Ia3}DZE)HArERpUf4OhBM6q`dryv`r)!?W!OhBR45 z>%(o&hkhW=0NM3CdaUL04+p(({3g}o-H)8l)YE--0me)KUypfL%&)8at5gCt zG>=RgYYdwvcLwrw#i5;!DYPj`FbVA0Z^Ixo5|m=E+}hdR<`&v%N1wM_9@9C?5#1E* zxErAFca-(2a^o16LM!n-e^+8Vy17Yj9Yv;vMsS|L+L0c(}tMWrVvay;PZq@5mu?>hH)GX&y{hAnq;`?Hi6gS|r z{Nx1gZBy&v_81!~GQNVLPZp8&jvJ;P&)!=pI6XU9Sza#g_jkJiBRd=C%|q$Oj~*a$ zAlt!w%*k#hkH|uj@~2M^G{5N9ONS4l=;ct;??e}5HQ$@bTd~m-@AUX2mf2(=*K3+AkhVpYL9V z((vlb(TkwJd)eWkG#gbYF|(b|!8v#JCKGEi#Z2f1^=+$Jo45|>WHYdf;#PkEpUZsL z`M!Pm<5xrfF<|7oEX6lwE~_o!Q>}3|=1AgC`PG-k=s=;fCI8V8G{La3{TCy=KctR89rjos zU{XaI1Yf~tmD2^zw=d>YZ|Um7lx*pHED0;X2?gg27BEEsn$e3vQU<_#JQ;ZiRv*^i(>Mva>Dv)DIPt>1^Ct+s3{5W1WG?*!w- z3auhlrZ)XK#P^GM^u4ULW4Z15-VM1^6Lkyt3#GL@4%{>zy(X3|+VWWFlxR8{+I(Co>Y2 z^^j!>+9pPj`t-C^+TGD{>r@z!I@*A4IX-Nxum21pM<4beVq#*-o?+H<|Dsrx0xA%~ zM40RVh4UPRh1s*zaueUk-BkLmAa>%=?&{5NM509t%$U<6L z8hLODrdX{TTG7_&Zv$xLr(Ujbbc$n|+@ZYte&600qzY~@IG|4@G<{^@R8q-4NtiLv zGWOl~*k>g>r&H|A!@H^U>OmcLaWrSpn?@j4t7LvsTJJ8rDlK^-d*t}XPoW#!Ac%vN z#mDcnAiE!w8)$1&5p_(|_3NTii66D@Xj_BfIM_KDtQdzTaTuP-e5<}N3go~{7Cyw^^ zT=V)f{VuZa4j{kZeY+L#s*e!D7gq_pt* zSJTosm<7;;m7T>F`1L6zUo4jJSx0{IY$rya z?fC`LMz|^6y2tc274vn>l3LtmvD?zXJFlurnC}Wd$;AY95WMPHqI30~yysryuBTP* zDg_DudLYW{tY(DE{VPYqB|k(Je=xy_x^g|PjrP)MwAGR*17V8FqWOCe58m%e{t zdzsDYaWqj!=Qstm5C|}Jz*W{}XD$rqhjEv${QLFIO8LgBj;qVPsfwVaw)i?Ah#12k zM$7M&Pkv?!JKVg%`cp@zGKtJ%CbY)5xq2Jg{%#&b<7_i|RRIQm24B`G(MMecnB8Y< zRK8?rsAeq^JUyD-M6OdKEtAT9(Y;TQ0+6Nr5ygg26FbAPMPF(S`qPXpnN#}65N~h z(~gK>?1}4H`=|0b$4ddqVDbI?Ai9_A7+l(U=XsSL{YjT{bq(AjKKrH;V$G zhW0wCaN-SB*MbAb)Bk-Ef-&*($7JbkTbttH4`od3-QOk00sJ=_CfPYeC~x)K=b>iz zj&^dgv+oJ(H30c0AwpTm-m3*B*ev4{&~QxYjN!ddaHphPU+$^gS>UN+TQB+o*Be3) zz=^d+*f}_8kgGc0WKV3Tp~fO6*|N;fe_ntaA0q+R$*E$@UV_TcFCxqTP2*o!*IpE8 z7%NBdJMlB#|2Kw^kT6Fh&jvVcO~z>H-@1yZvzPY0c8ik>O1^$^2_e$Kl#jtBxG|Cl zoCE!YAoM@m9uV`b3bVRKN&x34AttC0JNp!(CLM~FOA_#O{%4;d6EaelUf?rVEvhPZ zG;8cC!fK~9mMXXEr#{L6N3xX*NPbE`jV7u+b*mBMxUWulRFaPyQy7u1IW?!v9NeNS zyNAu3&%xN)&+o?l5|#VZQ;+*(!-O)HoL`jE_;1FXX*yJz&mHfrv6w*qaD-ODWpihT z=_89i*TwF)Ld+wf5>NQC34gh51*h<%fTiJpV6BBkLA98CG@qyx`#MeZMhF#e+7l;F z?=M=pz^>1~Kq>Sh=xFW-VqkDEQ%&vr{@u+vp434>Cw=*#551$ZHPZ6$BtGI**G0W@ zt~c-$_1>oygN!6zq~H32l!$BoNFsjJD&2ppK~v)?&4J?|kO}?e2;$m)CZxE>a0;DP z%zP2$k7U=f``eK+?yx>4cDK5GVj1<@MA6k`D!*}hNQUDfIJA@jD2UWvm-=G&MHqY@ z_Wx`9qiJ+<_}KgX%D6XH5PdXFSDy4rPmdPkC@u32y5#?hz=KKzb#vTfuYX1sp@^L%^O!5H}g+l3jQ@j>lxU^36G?Yv&p?B!LLHP&D|K{t1Y6SS7@v4eqbCX~5)kdWJK zi-Ebh^`P3_8l3cG;r`h_5tk zob7gbdC6!i)zy3CaA>E&?YNV%TuimlQyM2wk#{&e?-;rfwY!F$JwH)dttb0$6i_9> z&MpTH#IX*;|I%30!eVO+*T=E@YcvIMQj*ZcB?I$Sy_T1kHDY5^`TVL!6}g;4ROig} z$G^0-{RvKPqxkU?|I;OZD}aVlesSA@0*1bv!O62z-I1@yPNYMshUeS4+D<_-v@i~< zfZ(EP>F&dvWE2is$L>xKnb@=HvWv*5@)Jpj~ z`UB3?XS)>_p*TTF770d8&_CgeuXIePQ&~s359aH~+o|bRvb9++wEnB>$f*#8cr4L4{LrDY^PwaG5yXX2xUI_ zggp7I5ZKoa-cqo{m+v{pYBtaca- zdKzdsGw3`uIEG8aWqY>zX>aqYZn*}ri)QZOLh4zc_F~>#9!&YP618gcm)NwAvrSe8 z>g(&70tx@V!Y0_8`wQ(XU_7vEpdXnxROypX4S4t4 zpTCe#z4X^Fs}c8_-W-Kk#mB#rHAt@8K16#jr5D~Y4gRLlJ%9(-zpris{Zggg^djII z=Rm$8w0*cmWveQwO{sNsh7}z5T-Y%5Daz01{;O{dm-@K>K!C}2alB5~jEyB;8+A9r z)W9NJo9y~d=d~2idBgP@q)%@Kr|WlZa7+>_xB;IYFeGg<;u4!BOxk3hcY4!eDUQ?ZapR2UVe&y#AA^s>Pi=j+L%l>ce zmd^m1GItV(TZ+|lnr<>oAY~C21DaeSI0apaf^++$$(AW}`gw4QZ2d#R@H;KZK3SAXnV2e#!1w#7SMasqwK)YN?a>W!l}Hgv>AEIY-O z63aY}wm%d1Z~hM*h|~3)9)Z+`PIS573{3-c?WWc5^#(I@O~f--^V1a(R|N_R`U*_I zv6+(kL|G`F?!dfyuKfC#-=s;=q1*U;hd64ZW^kbkAY^bvK0bYK3hGsh3U)YBPsk<)6C zAJAA(SI-!KexHNWuzVdWjvao@gSMMN%FQ=_184mj0Rk*4PUri}Kq5N7<2;AGuKH0$ zLT1ry*_3}TwUV;fwmBv^^q_s6V&Oo{tNgsY(+#Re8RC0U@`@RL9HpS==U+w!>Pn4m z>;cXC+g!bp`BKxc*46LBzdtmu(>m$hLpPkA{+?VYd|r|>SXiAv=P4pk{w-c0M;IC| zrU7am;j$n+Qq|%7%@p9!Oc{`nOAObWEbx-DK8_4ru+SO%mkLq0{KW_zhv=h-IuvcO zzawG|2TfnzLo-4t%Vyb4sDhVh{r|!H}g0&VE z5HGFJ6YF5=n}N3je#b{Ph1#le;L&9_Cq?etzUWvk{_djfs~Y>c%K~)b#Z$yiV-cbj zGe`?&ZMZ`wr2`J+yyWRHUNv=!C0^{!^khMXK<{Dk$#fEXUTabC;9qFcdWIlLG z_{emB6`UjD7RNqaNf!u9MiLqzX4s$%=g#Rr3-sQ0#wBka^%Er*<}^=ggk{&1YEJAc zsd~$M+e`l;Ua!HHKeG6E5UoQcl(K-vyNm;o0xtwvy^qP) z^X&Ufx(>4y{=e=gecyL59 z4uWi*mRL6)Xu;sFx134UM|9`)pr|B|v!DVq?Ha{Ihw{LxmB!jlHT*#0ljBy_^F2+p z4qAv+u)6hJ*6e?w9z3KaAg_%JlG}e*j7uuIjXA#l&ySYQ}9+OGx#P$ zVm&QeBQ}&0_orzbh7P%B%BVq8MI-^1U5_K_VKtxI8c8GG z@$D`T+U>9?sLbod{5e@bLkhkC=Q$c$XTM?mfsd;BaadJJWGCsIeDkPeDUZX_Z*sIR`+Nqb5c?suOQ1Y zHE;W~SNGD)VTNzH0gyy1_u3=NqkrUgrc#o}*HOjR1j%Mrd|?^3h~wi`I`cxWnI?cg zrYT_^Qf}*Zv?2nl&`E$#ICl zqpKIq2sxn7c#HNuJ4)QBvfaaOSXMg`!mqs&VfYlYixs0&g&RJbb)W%uusG(49-{ez z6p*r}ClAoXe6P5Rey9zem0s#@&qVa+KwcPM0*!n-d8TDDMhL8M0=gKpipu9cR{KLm z*wBg^@AIlRr~BCAP)Kq9mYr5iZeC~0LlA=pVq1-fvlX-exPGQ@)Ie%VRKs&$i5aFm zVy*V}X;++oxl0ff>>AjA{YCD z?3(8=@sGaY`JbhkrcioFz)nT7RZ3Dpw_WCRKIH|SKn>FOcvo}2v!m%-sXV;t7I{ms zpu#M}+yhBKjm`p^+cdQ0Sk1)APdYv#sD$QuG+RVQH@-?WI2Wn_?FF!e1K!_bAg>Dcn&M3fk zK8XW0!?nnux09&L51-$g7dE3ZV`1vMtad-f%H5eJE?r^wb)UxCGbG|qC!!li7o^W48V!DBDuX}x9K7OH3n`Af z?la8#%%E9!87K6*)ZzwOMrP?>9j@Qe;*Vx}-?s;C+g0=*h&3|y&|R47vMptdUd2N@ zpK_aqfTrL|(8mCER00LaZqR+zp9HHy{Mg+aGb>(1L2VK%EBLWp$^&$Vb+K^bVen#0 zVpXp+l~ucH2g@tQuYUpcRwkjuLw&~s2vJB1;si&Tdpu+X1A#~dycMGA-{TxE^Lpa; zBgo*}3s0*qUC<;SPHRg@qeKD^NhfawHidQPD#F^2JU$1~E}bOyq&W~rdz}vC^{%7O zA&du;%c)fmY~uA*(Agmm#C+JK-ogG|P!c}>B0$VStI=(Iv|?P6h+-Np*%}Y+&EPUb zd2Pgs!Gv@V;E>KV{pQ4~_Ml1mjkKc37X{G+eq(vG*m^FZ&UjfQ~oBNI~7VWclXe+GcTd%kNE41U-!5lSE;*J?oKfHye(`MegVKvfa$IUsAhd8<19|L+sw)*Tp|qYq zNHRKwWsz8Orij7y^PatP$F;V{w#7Xa7lrUUi7xAUB4jaKH> zC=}cj6D~0wpJwpcs>eJ%G1Ax9U$4Bw3*)9y0Td?&M`NrB54$`-h4PwJv1PQV(&#k8 z%)n1lD@DF+wWYOcWC4QCP|yiBXQ0NgMnzK$CiF}G0zhC`KRVV}^ss-z4<1FpGb8iL z%C=y^11&VCs^Q>EwSOVa{*nE<7-+6%!*{MYGsU|pg*Wf3h(3%!VDNslJSn?ouq*gZ zDSxGahl}f!@m=-%s(chEPkwQT{`~$+!E}XxZ_l$IiuO#L$T2M0oVd1rw6@cg*yvNc zNDd!CN)jA_+OuVzO*!Go7K7O7(v@8jWn1=Dwc8etr1h5xg30hc1xfL#8m8vvfC4>b ztcclRVW2U81XhKh%LwS~(jxqPLGt+>zz%N2*VDUW|7&uUr{?LFD}!LaVq$K#LUV(iR22BA~cN`sP0BaKu*JbMd?VXj!_JH17o>33P5nG~zaD>~x^Xv)yoh(sj+Ey>LsPUGKlITtK+%_8;u*~5)+t+UZ;jm9Ax_%8H8=M^G1$}O zUKyh`1K3XjN%>?GW3!fUzosrwvV;I1ABeCT!_S~M<~12_3G3atM1eK;R>Z~m7^i{B zp&sw~o>mv$f8^R0RfmGSH|)WnzmW3=>hD0_C^nS%zoCI}VtrZ6_plFq`~lQmXB_{7 zc*Ouq{|&#xsjS{iO%J69S4tBu08<8In&v=Y)%A3~Lm%ljUgo;8*PGGddoV6Q82rY=x9r*<0CLWRE)(MF=4?$tZho4$922H^<)2F^+L? zjNf&8e}2E;_b>Q9JbLKyaNYNHz1H*je7&xFGlw86m*C3-f}lu#+3Q46LFX=>J9my6 z{Ck^9aPB<#O#pt*MF#K^Wv)0sFDk7fbVQ>(O};K9KirIOfAqMC+?mDs)4S?-A9Zq? zbF^|@^&Q>ywe!|``=^HT+-xHZbMop8i6e&Ga%gK-v-7NCyh@pEV@MSrg_AhnQm7=X zvcP)Z<0&S2#vUJEyS*3fwZnEm3+M%*5}cuc@giD=V9$Aass^kcgzzm_R9hPan@{X{4p))~g`xb-WPw z{lK5H7T?vHEu17&x!sEJ`txNlCdPI+ANl5f<}I0b=lt79bUNfRzy462FMSqW?|HB= z_aux{^>%;^hpNH$Uz%Ixko}s|evcu&kB1zBS<=XF8~L1r^&!}hiX=xyBG>;D*G)p% z8rK?Z09ngt<~n1kcC*<~<0B_o+1gZAAv>Lg#H%G*9Ef^j1_U)Lydyg?w|T!!-`FhdLf~MuhSXs^%uhbo|69FNfbU(kd{)UmI zOloTCbiZYa=dSdTEN&f@$mr0Mn%5X-F}pQlUx5^J=*u)p7Dw!~FsSVH!A}yApsyBw z9aWlt)d&s0fE(udx`)vpkrnP(-J zS{T{wBbORa*c!p)-TZ-mZ+W6B9z!_cOj~_od$T1%G0BG8)C$J!+teZSNQqZZTVP!*-S&_9AN`;vIuBK6JnC zfG|rfP`RWFqnnEmB&a9FojO9O} z*1LH>*Be)0;I%SvPd!BJt<|&B^Sdj9woCu`DA9cx79C#~48`4ff8{0P_Qo&TEK8p4 zx|srY-O%+IWb~g-tUA4K-qG6q@aIF9&~K94%wAUJo3FPQ`&L)eqN1uJzWW{ib6MgY ztt=9KVJ-Xa97I5f(Q2Uc#FX#wJo8aT_%#MSki9@*_iSl#SGTsZwJj3zz2@CJ+dMq! zroVk?Ef0qeqtT!l@20o^wx5@qyB2+mCEly=8S#hFYSyt_mj=A_|&<~!yTi+Bx9PfPCXwW7N%F2J#EbTEG}%LjM3CR-VPDJuWhGF|8$hx(Jw>G;ju>({=)Wrcm&@RFVHU+idVE5$V@ zyz6lCX;FmvCT&3HlS<3dN5|EMel6EWuZ@0#5B~*0c)|dQK(=xIZGoYT-&4ePKi^ZQ zCO+8W)~8w}b6M;(t`j@o&JX~<`6e5P;O&l)wA<|AjGjw~kO@>=S7#9E48CXWr4{d zP`+U`?Z7~%emOVaP5Rq#wVLTS)bo$hQK0rYm|X`J=nZ-#mAmysTJMgWx~r8WI;JFl z+}Q6!ittUX_MjKr;4Jq42>Z3R0iQWs<+8;0-F@6jJ?{wc_1BAr3y@W5wswQDBj}F{cbMR$u&M|3j;!cNjFp#{*G}?WTwytU z?xe}?7@#W9ex!AFywS?&HX@Q=?=|u5 z0n|IGuzt9vHX4l;(&l9pNSlF{x~I6N&v3Jpi$LUCn)*7hv^&YA>k|#f6Ynb}Sth`@ ze&gj7!$$a*n`_HK?TFJkmQ#%vSCaKQ-owd~z%zY8z2tWj%BiFBOSM=6fi_;jA*^kk=BDKs<@b;AEojK4+`uUoK$7G_TLm^UG zpiU6*uZ#wfk2bq8mnim&Y<|^p+RUGz1zW=SaPQrO6iwR`0XlIGh)&7e%We(^VHpoV zolW|Qf%0?`uaAHCtBT_X)XB!Tm@IVv&&OzE~v-kqUQ z{kYizx%2QoDNLx^9m-NtTn)HF$8E5mA--<4^<3;MRsDdu^9ptBVK-a{<>_}<%kj^x zq*X|dmh15;UJEhoiPmRqb%m<%>m~0i;%;aFS?*1}3KhX&8~mD|r+d)Q;ZTJedL;5U zLD-oCe_Y+X?`EY`B3>BHp}Y@`2@QX~!T(o`Jx}a-w+HRH-M5~kHsND{KIQ#KS3;lYb-|76>+bjBFMH3NXG=U`Vv}dmg1<;_(seq0 zv0Hq*lOiMII*SDMJbUyfKA)K#N$a8Jp#r{?m-_lN2f25i!o&b^16--hmdXn{0ml z{_KhG;rp9Vf)#PmAnxN9lbC2f#P$IBtpI+fT(aGzncU1&=<_gXEoST zQ94l8tOwSvO48YTveOcUK6UTLHIzO)5Z z?TW`9EJZL0uxFK&yYqr_lKC@vWoLpSvbr;Y5hd7$?5Mzu$W#rqLOB_i8 zuGwK5&bR$itxsxA8eRJ{nU-lb&SG0n3Nn{IhG8(PJ51Z)&Fn=flkRT@PAop*vR=Dh z?sF{_!?~g!G~Z!?CX;fhk~10=K+G_8+`p0q?aW8>ye=j~p?h=6?a3&R`PtbvZj}GQ z=>TCq)L~X6F-IY}v!-qA+c|Y?nD)+vF1i0kb){@@J*ozA-`zp4PLLhu&K}Zz4q>j0 zm7X7egqD)FS6sTwwhXm=^hK5%qT(7uN5qO=JozejXQE_~r760>a*{nJQ`B$;^BSwHqLGiuAQTnXiuN8-r!&Ei?-MwQuBE$Gh-ER-`M+$4Lv>Ch)10IlFU+{(OxK<;;l#1W47Mz8!1L2d4G7QlD-I~X%~#T zEWQ_4K7*GA0wQj6ig(V16#?GVr&}NR7{{`@8*g|>L&RPz&TWU}46&ddA*H*{Ae=dX z9<4P_64ZQN@4O?he%m8hKxCse$rvi?aI?UMT?csk@9S88i>#BkG!UuY~ zt5+9{Z`=fF4as<4Ei6#(kD=skpK#qq`YI|VI%Lb=CaQNCZ(5?Cy7Ecm_9GDLWjNGt z!y-f%udRK{j8E%JtFE>hob7fGW4a#mejZ)|Yal_x>EG&^f~cpVzepyW{yZZuYI+mw zCmOkV%Jkx1wglj;(s!pt4r#TG?1iR5l#XdoWW=r8R7uuUX4`zj4u=EQS@L&ze#2U) z805LiSGS}6t!7P46+!G9TH-FL-GRtpM(XCyc{#A*Q<#chbQPJl3imd(zM!?T`bbYl zZ|u@5pVAQV+H*nfolH6iTf*I?nZS!8U`a3aH4z8n8yNo3H+KNkB92trj>K zU^r4~c$rxgaseN@vp!ZzBxAh^5K>U9 zEu2YQc(17YCg*b_$OwPDaVi{XMS@jgr=gVEKe#v7svxLs4KLpyCf$|_v}312bH9A9 zpNPEvndzl}TH1Vs-;NyBM7cfatD`hsmyS1@KGe8jNUbZIZo)JJKs&Q&c6exrJv5ZT zcBbSP+1qskbpwN`yj--vQWDLs!GZ#CUoCCnS8FKOZoJ&y7H#^yjKiOXLp4dzM#H;9 zCm$|9N-%P0S*7JR&yH$u(c8~Okq|L-GP;bE73n;yY1c{n|JMxk^b~q6bo{rd_Sg;A z$|6~%#;t?gKkGWE&I0v0M2Ww=!k@-u#w`zGFdx>i z9=&}ec#}#3@wRWi{Y^#ujR!UtS$x-BEYa=Qb`FJ1oAAjS-mG-v$G^&1z!OwfE(F>j z#Ek*OWaVDWHJ8$tA)j&klD*rbWwyH!&}P@2tYpUHBy*VUC9V{XJNAF5US&Dm>NE|v zu1= z0;<8r|H|T$hF_`&URt#ful?f9*qhdv{XS)v#+%~~T}gf}OM934ytk2)F)<41uS0EL zE8R-W`wO>|UO+@A=%;08XMdO4H*wLor8o4yL^FhbmhZli4e>z-AI2RIo?Ba6zwM8? z4(Ob8eZK{Qc#OU;nzdGVzIEWeG1f;V!?eEPR>kbZ@VAk z$}2_kwYy(Vj^i{maz{(=&3YFWSs2W$Lnkir6AOOncLoGw%xg=hk7r zeqLo+**{DI76ZeB87TGOr({XK*mlF}x1ZjIM8W5~Mr;sc{Y+DMedL9aj>rDy49`lg znjCS+>=uQ&dBNy4v9#S7I&i+L5U7<88CAG;4($Ttsiv-Kd5!6>rs4Pa_#&*F``~hV z3cu>rtAKIk9IgB$e(RT;G2L_&KlJB`NawaztE&%`V&!-R&dR$MBn3|avYJQV%WZ|< zrzbU*@`vR-Q3rm{ch(&dYM@6Azt96#s+#ueKJ$@b`cOr=4N3CPJ5E-(bzl5mn3ifW zC>fbw@TQucQSI|JYr~LJ6H!IH-omvXU0Y_@Wc%@0URO@__aU5DL9lpC?)8ry=1vsO z6dKhX?ruzt`R_!8$f$7c&gK+6xx?TIV<{T+IOeI3r#zQ`WZRGP-~YmPldT7uZg@Hm zv0VaAoezGOp?qPLmNCJ%+cd)5gjH7VajIp3QDmDe_5WMtodYYswT%w81Bm0V|GVqS z+cU!_?V0j^69~&MBEazLT!TCD0=C&X0>VlDSgq95{QS1j6;FLBe;U>=?q>@|$75nB zB~LtBf&`vRkr73dFStPaufc>yMhYN=S+Sl#3gkB`zG3DpXS(o1#{T9D%%K4@ooX96 z;csw4cCre54;sIrHZ*MM{M>XQu_3mjW15d`Dvgd0BraAHq|l!j{X#d4V~QvHpTHY{ zQ)5Lf1q)X10Mf-8f<5CDHn@BL6Y=3QS{7o9_N)pvgYUp93@vuEPa<}8AElwi|26J7&wPqE6E2MUM zx-jD<;=#SLw>JBYPsId_NZ=M2BM)q?i2(xvyxP%rtl?bY=aVsgM;@cvhupm%1K|}< zz>2jD$Pc>^M_<3IHI((>Q6i}w-`)MK&UpJ90qt}e~ZLO8r&m*5*P)(IM#djg*`{|;HXmXz! z$RXBE5dhRD?2!wn-cu#ryv9}h|DvzeL*zt@yoQJ8)ld}#wfDfDo7!NRGcIehtJs`F zB_-*pj?`k8#JB))w-JQd8(&NQkYaB&N5d*JJq^Njv7&v4gQ47U1N2EFg|Fh7M?CCr z6`IU6L5ae7<>lq}nIc@U9zTNpkp+=#Px|3y6K%_Ac@n}oKwGzy8Ko% z|DiA7pikAnQpp0Xa@CBNIobs$J?YQ3JZfWPS0=RxA6;PqQ)24~iP_EVa!!9waNfcK zFuAV3QRPwRw!JusswQW8c%MEx{#HL>V!1=zZi;t`2c}G!(oO=Lg39X0JKOU-?`>5o zqN1W|9BYfbp)A$B>vB_!WJJRzDSlhcD-jaZjI;@Jzg0jk5fFxv0NF*NS}sLeZ&LK; zPtY8Pa@!D53$nCf)qSdNcfE$ibLN8?fBZ^O*xC9#YQX7NjYGu?bP#@Tr%=*7%#T_p zNN%sKv3MV@oL1r{g%U-UES{8{DPTQ92k3rAb4+^+m@!x5f*2J4{Sy%VRm9KvyvXOE z6u)6y>0tOoLFgLnLbI1}mtvDCq*ch$zGqkyE;{?M)XO@F!9QVjb^1YuzGc@$p{as{ zg&-Ncyi^%5mAOOwO4-|mj-SDl8>2rJEP@0SgNWenE-q8Wd(d<7Iu;VV3|1F&n%s-l zoXosf@axw@bL@OTQ&VQ5O{A|sKr07m+FzW-x3s!i3||uO51c~8&kB|k{2jVnB>GTL zJm!YDjfU+7YtJdUN3y|K4P9JNSKUiPuPm`$#Ta+TyDPL@XfT0weA;s3hMts%(eCAx zGQX|eVaIP$zP%83!SiaR37qVca6f=KB@9T091-i)jpr}SbAL4{dO2nKyjcHKqcy*% zY4&{g_?M93bw%`uMy0ELnzKr6=4u1-=GJ-(kA7zNw=Hbbhsd@kFlJgAgY^9I3pYY= z-r*z;dPZw@a8p;p2x==l^b5{XtVxe^eY<*u>2933rrL4-CYT~bM+_xYIU zsLN9)HVz~0fTS6^y;wTik!-Mx=r=?BRy>QuEiqCt!HjkASc- zQo0@qzkk#!?Eaz@qK&iQ3=QbeZ2o5ys=c(O-m&ZI8i7nXE}1$Ry2-8%U2TEcNguS> z{aRRjH$zfIts63dCPI|GB-mn@oLO67wo>O&@5y%AEN+@q#kb(qr?KQu$@+Q^loMSI ztL;XS{h5-I?)g9S#&a*QpY!|7T+|)voe`Du;8T)WHZkWS`TM<4R{uhPE_IcCqPy;R}>Z1c{9sI{|e) zpD$S<3DWMb-t0!QJd3e(->ySZI=bKGo2V^5cvJT3pU6@-0$T49H`XuxMQ(au2u@ow z15o{Q>3KKqKNR%d&QxK!CS9-jla@}C<&D=^#Jw`Bj-X zaQt)h3IIHqZZ>b!mN<4A0aSY@V+Q=uq|>1t$D8bhE|TY|Mrw%cEFUrlx+H04yA;R` zE*46r*ZnBQ;&GGs2K1<_tE=dR2Tbtc=RwV}TMk$&YpAQMYZ^+vL5~O?9%Wb;;~U1< zX!^N>jfYQ1E_6)BKKK<+N4S^eEZ`(}jbeuZixfwze#O*!IMe zHN=p8`#*9==bvVDz#ci+o?2p5t&63T)b6)G6t=1#$(xzn9Y4fb%>`tN?Q$DbREiQ< z)*|~tLJ~98PEoa6pTzNX3cmV?L7M*ym!AE|n=CGJUD*)P`#aw^ z(JmHqp^*Smt3upYuX9(dCAL2VnjZfQT=-&IFKM=F-Z2XpNn3 zQ4_FwN;^OD6s~xGoWZenD`Ta9s&LDYE{q&ZVEms0ntop` z@yMz1$0$lkNr7=}5I&c?CkLk#A!+^3nlYDsnYuF z*~B&pJU*W!aeQe>OG8=oa2XrUL^)JefBH?s6Bp>4u!fu43P_Ft zhR0>A@sRRRayI;JVwjJxFSQ`h{uBTQL0N!v*#6 zlE2bxC7Q_yQG^&sL+0AYjvw123%Ilk>P=fdi)iIqAz2U4#zi|s1~Ki42^VncoFevO z;2P}srmE}4cl4MeLS%e@*dxtpuGquS^-0q&FoaDMTGL1De*lB$S!=5Q(nmAihjY24 z0O2>r;K=A{<|MvS_ua&$7Uy>fnKK=U9_fNE+f~(gv_}DnvoK4}@HjgD%y?Dz9|-@4hw>#TsU$)7EM>pK{Xa|c_V$qhHFfWgjosu6wLf2IEU7s z2TE$V4Gu9&T69iUu6bX!xw}@58~##zccNJAh34q2$s#h*9hK}}UsY)jb;(%08ToJ4 zz79lxd!47PrXKGxWIFXp`NiTCB2A@td|!VrcjbDi`y+5DJCJ!q}qHRhcRL%n-_gtP?;)I_;xjBpFLfMDW;y* zM8HX`PWM4aNx@FDhl5TA)LX+DL{g0r`}y{-LWNQkg(0SBMRrR`oPpEVmxhLhBh*(Y zVQhMD4CKS+@N&bI3l*kX`8p3^n)j%M@;82+7qR*2P9vn*oO3#n)IOzKwmR-Ud9crN zdJR24p0xMBd-r3Ij(g|Zvy6uwQDZ-I?fy2L)I>D%lJpXn2js1h3fK2PG&fX121ZEx zZTsT#rs%436vGtO)xq9WBr!4{PmalAlf6rZj+IewpIva7Z9flfnQLnfCR0lL z-~%ZgCaVOv2|{6EO@!42sP#Pu7dN8qIPvh8i!S1)TKJvETUNeK?!TQ-{uSkk_`@~h z1gYb0r8sC&vKscO467jFR*&4xFR^5A(IZ>+7gL_?iRUiJn&n%zr%iDPV<5lXMa=ky z-?aZ!cVb+X=Vveh1Dm;lxg5<0MBwy;MIJ7?@Y6PV`xIp-K*3mP;zSfW?6ybWYP_~l z)tkeekine)?}LQ4uVBIvA7zJ!-uOvvS1>6s_LskTtAl;q7_{JRs#Ei34yeEHbK!kP zSNP6O*AMqbboMn{)ovIs9J<1EOE|~6acGx`t}|Q+U4J_|x8VCOx&+smV2+zC$A{h* z0EZj<04AZm_gq|EcN>}x{pZxIqQB0P|E6%1WGTp@urRK7HirtgP`SfKC0mAx15N3` z6IR-U_It*g7)4Rb_LjLVV~Ha?MPSRKT}vI!RX*=BCrsK{LPD@5vU+3GGO(ZxecVO{ zN0{}CB$!sAN6V3IKM(inRvM)<6+2u+eP?Q}HY*wX+4iQ1TXg;!sTGY=PYi_}H&zAC z_5u%`AE`7|h_KHIX7;w}3g;);+fzAN`LseQ<43hjPLtE_JMTuIu!L?`oPTI-hR*G0Y5kvaDW`x|y?Rg64T5Ondtc2$Vz0~$^bK4;^Q>QZkIOlHx8cIPL&eH^>VfD^eQ+BtpDz+CT zw+4S}UqFaOPZTdIXhUdA0lySw_U&%@zp%5Yc!0C423@|}K>@BUhpuZ<9y`5&M@U69 z{?ZVWH!j3`tc52j43fF!O%%akr%9m2Kvq z+Ghq(00W!`*b-kP@xPg+mp*7e-$|7;q%rZ}dai|{SVoA_>krLEIufyp&Cz9B3EBHy zd7lz6MTgbIWb~@vMf+RaFP__hMI%J=9(>TlP9)#vM>189?I<#BNf+-1J@tm)PH&cx z=N(k|7%Yj-`6q$Ce*SYc569|I-nd#mqF&b8S|E_b;Wp9}Q>G8@R$9ZEkmxjcf(C*} zLtu=zr`a=5yTO=X_*~z6ez_MJ@6kV$Cw(;9ePX+3$1o)nti(VF!He~QG3C46DD<{= z?l&-i#W{r8$I%;n_FQeIN4O0e{u|0i8k$*v;Sor~JV5Ez#76b5hUIpn?a>?@q!b1_ zgKNW6UN}@l?PfsO1~#v7_P^Hr;U>ZclW@Db{bI0196cSgl+V83J@)^z8H7YOaFNDm z*(%cp8X%E*;tnSdaIx7BEQB^Dy<78i>V3wJaeM|94jbY|a0r1eq6YVZ5g{@D&jTI* z;G)5Gv6Gxxz2jUS8w?$(_OOmU`C7Tf@eYnx7>oEG<~Y_3 z21S0OO@J#4tm~onmrHXERxCGD^#xzY%@SQs5Bft4KHK~HOzx3J2>}r)HFN@rzm*|8 zckUeP^68ozk=Ud27cO7BZ_@CN;Qz{h7~svOC8?UeQziT@KS=`zIrmInRWARrNx=UD D{Vwkd diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 209b70322eac4e0ae2099c7d15f0ca28345765e8..35cce33971b40a147015b4b6c0b97fb1abe0172e 100644 GIT binary patch delta 8405 zcmX|Gc|27A_l{IbB5NpQE5?#NOQ@{bk|Z>i?8%aS8<$F?kbMg!RQ7!x!-wok_MJ(# zvdmZqGnU```h32>dAXXrLPMkS$;shP| zvx|xlrl_DbyHngyNA4VNFm+>%gt4_?Ih{y07KJ?ZTA4?Apet)oP+k0Pw{F&Um6h%` z!N;`RS6@80wRI_mUp@QLrw2B|Mou)sRj;YSs6OSrycu}%e3Mv{;0`+E29H(JylH{o z!i3__E|l5g4k;C%wAi%%WW+;9Z~-dSqo8Dr38b)wdhLSHs%C%hkVi57p=F!o_H+x# ztCA(SW>OhkCL^by%zVfh)o=ImVq2J!sm4nGbn%&%h?S#>BbL6enLG#;^?QLQ$jFT@ z774^KdTQwN-SGd_kSkohJ3q#Z^qQJgW8c#W2naYbt+T0HyVF5OsSF?QV+vKPl^?n| znyUm^Sj;3`)7kH8Rftz#!BlxqZT;2lN-7NkoRU zVv4NA6%V9HIm#ZjDo&SJce2-SUxgzr<0F$!G#;?4c#C0RWJJw=bQ4azS*%YY_$|BC zdan9Qxzmk#B4tG!QS;tC3~JcOQY~WAD`a_@9!nf+xBI+PDoB*0wUg zi`(M%hiQ%@BT{Ht8)FkIa@eVth7j^BZfy-g;rOA@i+H4aA4$YA-E*t2)XaM(JWG=e zI~jKzo(UY@uy^_AXPX-veChrhN!QXYRfO4VR2t?XN#PEK`Gtkf8nljQX*rcROUS3q z!&Z9B{x+&Y=ULt7o0*}Nq9z5jt(HErhJi8IGNd%#XLt&CT{pE!!iC4<0`@ zM54xH3NbYnO>ZQuFUG?mm2jCG@F7;BF(SPHu#oWR%7-29cgu~CUdNjtJo@WHs$qIw+H zURLFQ+uQ{|Z82N>>dg5zw5qlB{HYPs<=&O9&Q5i3O5*I}I1T!|B0nZdHf%6AB?g77 zzI9J?$6qB`ky^wrAwC{b*y%vn_i)R@FK4CR@R09rZ$Cv))T=XQKJC0}k?d0&;dtZU zEW@vvgw0GJnNCM@j?;n8P*SFtG!JW2D%%iiG;Z7bnjWz9`YiwCZUL`VEBSUQF|LHxlcC?aO?}z{4Yx+hSZHJ_+TnC4{S?$Uh z7HWs~5ODyddn+!>IFlDHKNEODS*Y$T=Z$B7dR@-luE$Ne8Ggyh5%_3xUr#Tp7I|L= z+^PTsF%Dvt=o)|Ez-M%1sN8`Wx9mvL|ngq3F`gQN-t7ZhVS&(gsJf& z!;QGNzdp^WYRsxdUSPvsk+M>Em%=!leUi?8>ro!EHX|>O_jcKF5M5MrwhCGAtA%MR z!uEcO|NMBvMytTfs*<>j-Ln@Y>^*)y>y%E4J8n$DUq(sSBXt-YbJ3=D*RdHw{o(VUd#5 z_hrO?)nS5zBs5WU^>Q-O#AG5G3j7z&EdWPis;Rke-)t{4PnWZg7n&7#e){WShH82# z<2*IEJ^50#{pHIQ{{-2@<(BZGv5*O+hmH3GtGh zx(e5+>>Ox7g!6?nM{T#yAYBP#s9KNC83l>lExiAtzJHg{i8>L)l%>0Cb+XUcGvSQn-a52LVUFBT=yzFzbfvw%W?RA``(Szg^R|*bR(|) zhWFrr_RW=|cbMH~r~GB)^1^3mCNm~2Rc>ds+#zM;3F2R(QQ{J}w|a|FTI@+Z!k&Vd z{g3uHyDb~dB_)0|$t}(>nQWcW2?kATH%m^4v!8`d;JuDABT*|kn?E@Q>lGu0a^><( zk^7Tw2a$>aMv?vf+a{I50VGF3cn;7Sk?AM3UC@4WZW&ugVq|`DV|~=pWUR%jdx1^d zW*d8?kXIvHpOa($BXmcVhIMNV+WhG}QIS7JCdu<}zPw-Y=cA)8u%|6iN51l3*H0IF zJLdRN%2^n_zd`TipO}y!Kkp{Jzl1Ye^$a&V9w%Jn*3dz1)iMui#lS_q6`m#G!p}Z|KG9!1+U2$Z(oOC zb`*(majWB%h1e&Cd)tV@yquhI=%nFs@oi4YBVvs3&u!&ae!VJ0@BUurR;%c~WtsdB zwg8E$Y90Li>62!Hu$w8WoaLg#e*j4A$c&S|^G{QY7qf2Kez``Btk=q=&n2#3V*xpI zL1s1X+ch55`X%?^u=wDxe%#=YwCOTPcp~nbowEYnyCG@0&^K_V zKe&QS$Hu54%&UZ4$9xvG753&L;Pwz4$uX)ab0@C0wmiL(V_E8!@3{?F>60Xt_gblB zB<^d(EkZn^VE50TzWnMKh~=-z=>86K{Y|k<;*^LVVM$DaTZZ>IcGa`qufFSP$$0ez zvfpaVCUJ+HZ?Eq7Vzic(kXBP#v^3i2WtslA`Eqhy%nw*Ov}FgfX^jo)s^QkcJVv*1 zM<2aR@h2~+tL;~?Y!2!(2MYz>#nyV}V{q3tSy`V=KQ761gdY5X+ctv_wCywQ{tVv> z3Tq8T>r=lJP{s@@vj=Af2M7H(uPGWCnPTdxP{(k$X9jA|^LG~y7;30(w{+6NTzgik zoGqWFNl$b_g7hb6Z0kaoeJ3SR2eeZt<(yjP1?w_p^RBBQudiA9Cd8KUgjQ(NU6OqM zI0PD%F(_uyem4IU?hf^8@CN+Xy6qYwuE+%(bcyR;mK%N}%Hk2(e7MKy(mbxy>O74W zZTgaQ~TF}X+r(F%Eb*`U=D~x~3&CfHNxoM;7?v_q0 znFR*WbAGr5CgoVEv+_b?W8 zMSson85YCRE!iHXWWl9pc7JW@5@XtX8LM9n3@r=-Cgfj8J2b8L9zJ|%m3zYHECh}YQ{;76x%~jb zxkqMuuxK3#NcH*fRj(xqy7sw@jSWV`qfFsUb@;YLA|S`4evM@>?$gxg-ie;{C1^P? z{Ua`N!*{f33@2(mjZ_75KPT|J7L%%i{16GqWgR)dT>GtB_jWB=BQcTQfqNzkebzimpQx!4JFJ}6K!Fm{UbKkL zwpa^iX^qid7Z&dM`Li%Lw^P+s1}3frcz#OE{ilg%II7Rj$ME@)`;?c@tO@ETt%L`5 z0cj@$IIAquD?K{eMa{lzZN~m5#BWaRl~K!Ao@qAh1@MN&PJr~C%K zU9Rh$;mOX)$@w#s#Rbbl9dHl#@a13AbxpOyXJ{^cD4_VQYVfI~L@Y^5wOrQYBH+4m zAdfNwl$)MvJ$@@QyF1%dn*Kdf`03oKNAU@27}ZlSA8u3f80#8BTmv!6T%t2YO6spv!4#D9Vz~MODbUx@b8-DaC8&OlG$LRfZ z8tphy0jc~KGUGY;%>AZ%Tj?nu+nS7bY#Ko>UiQOzMp1&=z%A3-m4h*7e|enQ6go&s zFui9<{+XMv$o!v|D({$7@~+Q=3H2!sR4daf5c1fW0tWS-^MyWcZd#mxYBp0a{O^29 z_}`h*&eQU%L1+3??rzyZlwAP_9-3l(m(Cf0GQwlAUtzj5%?}qBOf`_E`ZDiao+h6& zdNv|2p7U5!egjG2JFOspw45KWrDV&mHhR}?X|=L8CXqM5{w%HCGP#fA6;AF0>_Y>QWl;P0fFTEc$tB416sVF>>v?2&uW_Mn$#P zgS!)zytj$T_5)p(VyA^Kg7Opi%mqrNlgj+ramwhrXG3RaTU%$>%Xnxd4#+KNZ51}u zy%9o5TG@&z=Z{;H0|4{jMArUt`SM6YZ0z}Etm1sssjk3=_yJsq z7v#gISr=pbbH|}_2d_;U$Xn^fi+f+r#hV5dUD_P6O*<=s7~{HL*PB5PPQL~0mrNNS z*4gBzWsR@fSueKKzo_+Ii!$$VEnytHzkt_OT)k4J<%}aHV2mIL|CSp5i5F11M0b6F9kNNoG{W%*@P31V1c7K;YT! z^V$Q5BA2}lv!98@&$pMhvM;8>Bg~l7+#vguw~&LezJ6)I^6=R&s^x<3hwU$ZD=4@S z**xhof@G6&*3l%teazb`AldvcpFc{pMi@r0%9hzyI(0wG*Q=ob{ukPdF0b16bXYNnu~<=G9b?vA6@aHUlZoqI`ooIy%gHKKhVQJHZA?gKErCa2)-d<^nON z2eV)2<>j1{%fI5;c2(Oxw#I?phk8|(_uV}%SK51J!Iv{ zWlbWVoM-qvGfu|&u9&HQ+$dTYvNU?yRJ-nILhv`r-F5d+X%6?A6!rS&<)c2fRM;y_ z&;c*?!<%M#1W&{VNQ;Pczq4BRMn+!sK)typ=WcIY8M6)fuo*5E8pT#x7$8i(p4EIx zBY)st^@Sm=(JA?D?r{=X$?Xp-#aUC|2QV%p5KT-C+-GQX%)Wcn(R^oraR*gNMty2E z&O-w!nL9jef@XEZ)`9F`eah$l%d)avb%JWCiEr< zV_H53ZDv6e&+SDLn`ld_~C8DL(!EIOFp$Y_B=$HMn@sfVZPjM62j+Q01tlPR@C$gx%s;^2?*7n zc=YBDjlB76tqpaFDu^Mp48$<^W-(RL;SFJR0_<=W445LXRfOMEm|7OC9cHo-FIEni zZn-{fe@?dvk|*?Q0?)#Wc(h_%nhrny(-7!jwIMhaI!W|4|$CVEZs* z$}7k5VYEcynTUOYJ@$8r;OQ;jQa;#CR5&sY4XLsF+R=~i zZ}t=W4<%2wN|hVjE1;1ZW{4GfFUg}%mk$NsZd^9sxwZXpRNQ7LSyJeH7R>UL%1^)~ z&Nx}--W=A~^hR>EcV)jZxd`f-zQCpKr8c{6^`dfrCq7H=XTYoo0#UoLkiy}EtG@Ah z4Bsq#yf}Tr48V`0bW)Tcefz=+CEs?e*W1qG_l>GG5}2|3y5{7Gws9ymhQ5B%yXpU{ zCxo4tjy)(qb6t~0Ymf!ef<)T+fim=6+3d)lSft>P+5OcLJ9>Zg1?iFZTR|GTk}AK$ zrAk(PmySXo%l2pv?NoTXDuvOk&JTpEP@)unH2+Rao8ehXz5CzYzAu_`UenH^=X36bu(}kc3MSb6Zel!)EyfHp9E3EiijBr1_3a`=TqchjXvbk`%f> zU5zr7+iH7UkrutT^Rmf8cW=J)=%7^Mkg%Uy_$C}b4Wt`ucrZY?20Zxtvd8_0zI2{l2$M^?N4Z+dP^HsB^VVO;d+ndKM#G21f(yr0tb(i9 zo|3k<&8rz9^E(u%XI#EL5*yX7ljTQ}8c1)Q^)Zp=sc_OVn_iRX`XYkyp4XNZLRO2R zjFgWTgdb_aP9VtQoCsr~AVMx+vZ`*MA@o!W3K!%00RIO8xRu!pT>WLzEId2<2DnjsN+cAl3? zR$`8^W!b?K0hROC3mcU$0fB{{k9eR-TMlfDk^=*9c;a7JabNR{D#3iV^vu0!G+v2u zNS{*Z+9+IC4%jR4cJ#Q{0UTiW?gX~r=m0cg9R+utyd78o!kTBuuKY6DuatJ$xg~& zNPzZdh3n}v3u-mpWmLy^d1UK-$SHbe%)vl-_ft7Io zd){{zMc28Wn&p}+qxT<{Y>tHPe=sW+D7L;^`YuV_*>rT*=`cg3JL1z-2yV8ri$K*^ zOZUVXrIeY8lIW4mV#+A6Q^#`k9x#Kij+w!G48XmVVfi12k+TPfV zQj1^}w{b~VSkLMBwtY1%&v{=y8-r1J?!OaKe>lX$_iXkXJ9wkH*YLN8OWC&=Se#k~ zk;T6=&(d%FULT(hS(yo2LB)<%WVsBng}2v3yQy9U)q4v^sKdRY7-!1@FKRd=w4>T! zD=Pir2~qMni{ZnE5@-m!+WK@+#V$z5d3FiCQMsedV%nU1`0JHoy27o?2X%M^>Ue=k zLAXl$89E~9WfchyQpE{G*#0|yOv0w7NjwrTEMV&` zUEGEOK7YRZf{&`ltTxkS-EDK)f3rmn_qaR&#i!q?0$0iLNdtBEFyTzmi_Un$asr3z z;8!uv*xREMP^Bjh>s&>`nc%8z^$qBCnw%WBhcWqj-k-$W|_bv7`P z?%26t3XSan73HF>txYTm)j9?b0lErI2F=IIhdJnsj4De~)Rm>D#|pN;FNWFna6jy* z#=E%|pUqvB2)Y6kCkhP3@Yfx6ou($}-Kxll<&y2X9~Is0zfV5c{i8LM%zK3Dvpw?1 zPg!gxPP*xd!hw|u18XuYnO3{S*Cjq}R7a#&yBsxY9Cb7u4S91FUKRZL!^CsW3bh(j zX}WoU56FMMnv8s~cdWE2tw1O8Ic%}0KViDijT>`=o8Jm$+FZMb7kbCL%r@@ztYx-g zeMuXCFdLLBa`3w;Qt{c(#Pjb{8Q_R})yYU55_AHFBEVPF%!eUly>R=t&(Q5{BYlSx z2v9D5^2=w`I%e2Dx?S1B`ssmOyB9Wjz4b5T^ZRT}BF&|Pe?QrRV!Um+nOPcU*&0r& zUrenGIX_dD-59ZLAvJ)xX*8-3Z+7Wjvt_##8pDTi)bw9&`B9-=Tr-})ELiooM-Dqx zWI=P$|M*@MB|^YGtX^zWKg@+h&@L-&QQF$&_5<19ccylJe3a34{NoFA5GVL-uAlT? z&sJPdLqnsgdk}-7CnWk1!RJa~uuUqSx7$p)f`$QjJUHvU;sxEw|4nkS^Biq;7~yOjTDgDSksmcmc@TQ5qBK# zC@DcbSvX$UbJAV`LHv2IGLA4Rdp>q8QdS^pbaPk?FmgutDo&_g?sbpY)S} zy}-0vUMjdbSY5(E{!8xX zw^4cWZ&X?7`bm>>=Q&q{NheNFQ)?*Sepq^uf`Xc5Jgnj9%nnyhPt=#wBPYO5Lq$iq J;Ewsr{{#2#GK&BJ delta 8447 zcmXY1cRbbq_m32jk&=&+kx*vYn~GFsS&3_9W@TRUzC)#wy~&o_YAES4r=@qimqW#+VU)zjb7Q38^i8j9Y%v+>6*h(U z3S}4O`vo-I$Y}0rLN8zLMDOGRDS)tPa~YrYn(4H9DQ`D?+N2l+AeqdPzZQrIxU_wA(AV(Z4Vo?7P*- z!Q$)sylCuaK`}QCBHQ@vhR6$3CLISXd{IZdJUsQj{Yne1!O|W9T0j1cHXzVwmjtto z(b(@ZV`J~ORF0n$f%D`y(d8uStSn>6(_-RK|h=Yb2}E4Y$LiMX*Q zS>|TCP<)qhm|7al#-hiC1G=lw)uH(?gBcDna}|_u@hg>!98^jGCW!O}UEA0x?n&B% zw{6CLCUZ4;0<~MXXOTjaABueKJCff=${%G(LdOfqMRI3HtkB)(9h5wJ&|u)y^M%{! zdj5=oQF4~$@$pJ%Oq7WDkcRJKwBB#K@rW$GrnCF16EE-qHgG`pL|fbNf&IkTgEviA zJPE<#g$be%?{~wJvwbB1@$J@qwpTa@TwmIp?T`cVM0^xxHHg|;vH-48hyjP2U(y{T zP?1p~VPbCUUA<6!Ns05RmHDCdGn+N&eMd)*F+#G_^8+{0lVN0+Egt#DaM4M)|NCrP zSnE9S;nFQ*Tp>qtAnkv@e44)wi;CX5H2<2&;m@Dn-&DP__F3KN$*-+x21dhoBhR=k z-p$p7gbmwGdyUJVya69&|H>FZN3wJ_z(>luzII#O7H)gg2b;H`9Q+k(li->~k7TJ7 z6~a<{7lPH*i#);Bch3P9lCCaJi57bwppl7kgwm8_^NN9iDV^feE`oQc$FaQLXzK{( z)Jpjb4Gl67ACVy)e}#Hw*D_W7AF7UAuis#M_KWVFi{0A#zbMyaI@IO8B5iTe#1G8a!>Up34M=4EJ zQ7TVd=TACty7)5o6_8y;YgmL<015L9O*&2c1}7e<6=pV#DWsquC4NiI4*ANX3- zJ*{3D;qb0dgBUfnhwW>M=@xM5Q!MgiGnqTZ&LA;KjTqHN`NJMxJLY66?JQ8-!cW_u#`aEXZNE5BnLwCcZ^{8?P)6%OqvSiPzOF&e!x(!IX@ zlGe!Rh12#GU%cE&6BW69cM(hAeSR<*Wvcaz^&(n4ETYtvOLqk(l3-DIlT6vysQNeE;_f|z1K*vw> zLRZI!j-;G+{~Xi$cgyjN-@P5o^)Q=kcE7s1K3v)mglbUJtq5_4aeBLi>7k*a#yEkq zICux*pvkFcfp3AFHd*pbOJ8HaZY8xnAA$R{78k1GV7H*8eztIm!UH8Pj`j>M&&^#e z(eN?rSj$=IY=SerC@2y%s$1NsGt;mSu1g4i?|&BNy1*jyXz|bh4XOFhojH&O23Z4V zsYrumolYZt`Cd_ZtbVpStu%WgmE@pznenV%d4TfPRN51(3afEX@@%}2lj32v^6Gwe z0jg1gt8y_Bnai}_Ct1;(f*k2g(jfYRo$9}i50_c6uB@`lah-ApRbP9sg>Lx1UU@u9 zz797ZL49)qUX@2d2Z8Fz?58MUWB#RHA(Tb7ux4){PoV0b1lMYQOpM2iXYJ90Im!(S z+D`$Rq>R4`RsBv$YDbSXd~kn94V~L(*&n~lHyHJuuHLaqW^!&}>g46+g%1t$d*bc+ z&M2f0DAeYz_Vi;gdT@nRpl)wUOM{w={n2x#O_8hIP@(r?cs~}aRO8MNdaGu8b?)zq zJl^i{ns<|eX|`OzQEWjci<%*y=yyr4#s7uqI=lb+>B&h@|nX*7|g^0UwrpCylL zEQHn2`1%Qh08wBwBtQQ`tI{RaxoSn+zeA zqTLnWT_PT1uJcEeE3ftnB@3U=0TgUAcye^STLa&?o>sDx6P0$JTmbglt~*=pP^;uQ z$ZeW*K!IH7@6OTqjwA3EF$?=tw#T_2z!mfw2pqljSZltRw{SYfvr^M?;j)F0`i+A3 zvflg0jm?bE(UeP#*J$+{*-|_GB}cSGxyXo{73xQblC$WUDKDWv0V(lA-(KfyBzvvf z>lw?MHhsuth=F6tf=*B786yKq98Yo(kG(@WYK|CG0SDCiW~sA##;cU~HgED(%fDT0 z>lv@@Gr*P?sf|YpGkEWAj&Z+vkZ6fv-zK`E>Kaf6u}9-=w`$D@&7r<1N5T5aUeiDe z4uomTccCnZ=M|}hwRe#bR=ytbA~vQaHZPAV@=+eRIGre{a4$B(X@+;x{?ev^Ae{9( zFMX}KIgsA^t=;hmc2sY8hQuP1I?dc`V_EwXNnI_HP zlVE=i5s#(sIP&oW2rEvv>j&FMGAvkzy@?KURRR;Z~e{>7mIVz~an zgY{kwPm+D*b}JV^(hOmi8j|2zOr9h>8Z2AWDdtz^o7_=s8_(iUcn_WPJD{p;+(QJ{ z9!}-y6*)rtrOK?nt~1(GUnqU&J{~0hOcv(elCK6{voAhO%Jofn1TG+R^_QYivfC?7 z^va3Vob*DRA$H2CwXRg!tpQPvJlP){2npf=Zz$ng}Q#3Hkx;(o)C6ZM;`Gz*{gNyA#i`SJF>?=9xd3X=!exXRw=NTuM@s_n0?^aj8Ju$ ztlBML;B8XpV+HV1z5)?VpTvrixlhPoCG>E{US`J4JTD2qt8&Gj@or)Mac@rYTfAE# zV8=N>cgnHK)TDPzwW!<^0u%r*P^P&V(-`+?>AA^q7<=PydV>mwZXJ>x5Yef9MD1A8 z8riDA{7pUW2BZrXWQ$dTE3l~`t9x=ic zX9WbhazE-YNJ%0^okbkIV>9S=fMQaY(r;p1waw=Dab>zhch>|Y_AM!SK$V|Vf-;DK z%&nIlQB;2Dr6naLrS9(Ts;2_Pv`VQCz{wPlU-@@ckV+R*9{uv^OqFV+-CIhrUZE(c z-Ga4E?Y#o~rT;q2>ncD~_x}pHB>90R|9wsVg>FG!mmm@E~nkUm&;M~eE{9s0-+$rcGN zDOq}F&f(!(TYA6t^DuaRxE!>F6ls-(3P_Dm4-a0`rOhNXXG>I^@=PH@lZPnZw~U2D+^k z^avC(&AR}X@y)d)Y52){BYA1!<2`GN<5An9_=qo0^m6^r`8CmlL%g6~XgR*wu|6Iv zLcgeOq9R4ma9Lg~BWovCs#LItsq1lm4aQ+lSLMHfs`e}S?0b3A=&?CtXM&+!$g37vr{nHgw#EX_JFWik8$6{5!EAX+H@&#;JR zSJVIO1~DQ%kiP6(QD1=z$mBpjv3#CcpRlvPvt*U*<1Ax}i_=-w3~2c6yma7L*(f)b zmag&yY(@)s;nT;Fh!jJQ^=DM#+QK4wVImRTvpT<;;#Ew?mzxT z`%H{>>S*h)t?k>A_=l)4GJn0I2DgP^@L^wy{6t(wLp=T5{c%a=+Vv94?u0>gnFlPP zR@?!(`ivA(+b7Dbgyv~@O;MDa?Ii$jA0=?2E}O+(RC%KKL0;~E56quF$)LO*0sYCg zpF|)g?P2QM`p+ArX%gY8p#s zTUuJGfDUS4ogEV{#kP;GUEGUL+Qk|ZsBQ(xpP#pZ+Xu!LkMuiLNv)M*X9F`NU({Dv)~_^4`6 zT5ci}L^mYy@mAyd9$>Y(B1j?}-!^Gq!eG9j%j69DI>Fi$9e*`8Oc)?m0VPmEse&&eP3rs?r=rZJ#zLy`y7T9+)ZauiLS_ z(O>-ul*rcZ0nuKp}6BOx-kL~aLb7SFbX~&#`cpJ z*T79*bDHR8>!`AS*Vfi{9XzNXm8WtPE_cPipdlN6PMGlU*TtA&&d!w3(Uon3j&3zI zTA35vt%lROhpO2pJpb51J}M+ut>*|#ZKGHxhAh!C<=rDo@otA3QpKiw?ZxKXGlx=J zvTv zBnb~tS_;>}MkVhj{K5ovbdp>;?!i=OwsfHv8yt+$LmD`bYHxQ&r1!zn<^WnLQLntd zkU9Bq-L}Zrb|1BZ^x_Q^eR|ezGckfRC}9&wDkd)OI8nJB^(J&vGb(-lD5KYY*Y^45 z^YNFu;pmqv+x=ZVo0F$Y=o&}Qt%p>|^1MixB8b=z(-UJ^b!!yrSBj?(tIN z;w{+q;E!Cu_)O5O-G-)RA38N=k4M95LRb*L2;>8G7sU?bZ}ae9((N+9Tgu>zgzO9Q z@UhaET5MXNKVyCRfV_XJK6hsOvssajF?b$!CwH(ZT=zDbe zrie8%Zva-E^GWq{^F00}Dvx73eSVfjtEH6uakcWvRv_z?*Tzrkn?fvx3TNkQ`K3cL z1vTe7PhvfH>bx8N;#h|#HH4{I*rDdKa&}*)cbMku=;Ix!cBDbh7LSR z%72wOfOLHV8U@e6yhcJ4f1+5DjMKakLk=%_mKXKZhDk_6%T*=O&IL6r)4hxz#oIH1 zto8fi4f+lILA^-xOznrna>(Wbfi30|E94ja!ANN(T8~SW!hxHxlw#sk+u-x1D`z$u z^$K<|W(xLF$8F)eq>iX)xJ^7!Fe$LT2Ke`{hd$Qb%>W(O?-v=>wHW*CS=}D?BFfVb zA$mH-2Hum8X=V3@wB{HEf%XQNg@cKy=a4=o=0<*6NnY_b4su%9ADW)RZ^e{+l{Mzr zNweNaP~41hXDqeuPj~E2CcM_sso|(R8xK+{0#$V4wilow=Q&c`8gfI@8NRmtO6A&Q z-wf9unhR8nM(*9BEf40fk*Y*iK%nODl+cO~54)+|X<^w$TVCLw_EJJqXTQp~?!LL5 z4J&AcHP{UHBCvU$RM%hO3bEGzdj?SWL!IqI5XNCMLe-NcoVNb>ZNnxrMm~8+4IeqF zN=19&Xram_`IEnFTyF`vEJQ${kD$@2UCkz3W10%lFL$>1W#fLE#qq|wAw*BxbeiI0 z`}TxUgUHj(;XKH7P>mMMhJHmj+V;R{h`sK)s_mcXjXq> z?ayCj4=Y*T4A2r273uO`P20fK3XdJ(XRVIT=7|){ifoTg3DVHhXoOVQA5g^J9S6z{ zy}=)8W75c_xQ`dw3v6kV_X;>wET?@dK4NGJ7xjoIS_cDzNA4;ee4hq|&HdQvhuq)v zmljewCG#IWe%dr(Q9ldn)@HmNJg?-j_wB*c~OK7FjXsHi=x3O5$2(d#MN zXnCQT*XZ)~nsKdXTiL@`sTfZ%cf2$J`Zepr8E`TBJXB?2|B&6}zK_x7+M$co{hc{wcW1HtCHwG7fW3wD( zSZqDv&$&Qnn8ux4iJc4N_ld1(pok~-2y~b-9H zmc;b{6t8YUHDV-RIT@O1f=jMZ5g+(8HAyA$@;LvOG^NGx6ZDUPzpa7sPVmBrrCx>t z-@M|qIbw9XH2Tk8iQlpf3nvP6N=<-wsoPOF0dIVO>CLKfr7bm8y7^NjMs9c6z;Rl_ zN*%l0Z3<<_1f4B61l;Y}C;w%$(lawT`Yq1K-P(YO%od+*>|?%P>nT3tjfsKTk5Si@ z_S@2)#WI){axt#!_Z-eA2KBP^S8Rk87n?i9Xf^vQ`x>Y0mgZUZA~LF_CaeAt2gM|$ zc+iHRY$Fbo{a3Vgb@h^Dy%eg=AQKg*SX4Q8-4WK<2s_%4EZjZ5mX599dgiy zOH!z_oR&FkjL5-MLQxaUyqn1ad2LTrel8amp$QKhi*L49rBC}SJ@y?h*m&nw`NE1N)-7ju>SvR5%ao5>D9#YX$N9z48ON~6W{Egld_7Jm#QA5UlG1XfHm1O!v;@emWTUUJXXHSuzlxQiFp8XEZ*URAfgu{4fz2%#wi(aO!H) z2Q|N0bCntX&oMW6)Lumd^;uNBuGj-r9fKr66`0HphJUE*`o)93Yo;S*WZwq3Pb$Gh^$;zhrI zi^aNio(V=W8qV2bJ7#iv;oBX(n3k%^A=n6(@bdf4Q!vzrNVgPTcqwkiJ{uPV*YweHG&;SK2| zymdg#7Plfi@S7OBnc7i}dg3S<%t)!g!^ZrrsJWep_QiaRkCkQv!h2q~=+%Rb!kX(v z1{EuxY5#{rC{gaeu(C)U0)lloPd7=<(^-baF}Eni8|f=ww~`zz4Yy<~qx;`#Mo>1Z zG<$7&;Nsore)bI0l^)0L1ng9z<8s+`;uzr!1$#z`zQ_jq&_&A9?ensfmj^Ey&7M0) q&GSt0p?2xqd3I{1R86vzn<>VLrg2#%Nbo-m=bkC4D;7R7@&7-lA#k<; diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png index be06ca4e2249d22b2eb1201b8dc98579c49c0456..405c208e1459d6569d222124a1239a231a70c8a3 100644 GIT binary patch delta 39 ucmbP#h;i~E#tDXuRTGVzd6_4*F|ac*FbDy0#>757mTdy@P8$!zCjbEHwhN2^ delta 51 zcmbPyh;jZQ#tDXuZ4-^0C0Qo5F|ac*FbDy0$pS_OW+2T7gbWj#^jMa?EDG4TB|ZTF DQ&$Zs diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 5637c4d93eb42914232496188f472b46c6a2ed5a..1522242a18298fddf4e37f3d74857080b94a3d9d 100644 GIT binary patch literal 28109 zcmeIbWmJ^y+c&BRN+?JR3XG(5gER^V$NNH<7#cXtonxzFML z$NQ}Pyla2j-_+%ri@EANkB;Ag-`~lIKSC!)zjNo#BS{HS`8#)zw(s0Q;X*|M--wl@ z$$^i%*7D-7@8tD7TL(Wl>A#mWke0qf2aZwi+`Ef;2XQ3^_{Vk^=gvLwc^CY><70dW z`S$qE9YOGUM?u$0U*Fu?$ifEva4*#R&I9m?1O6E$BmMhIr0wK;|2{_H0_X0$QV@}p z1fT!CyY#X4Yp}*tW5xG2@1>!d zAUCqH`OM4AY;SMRWY5lIVP(k7%EQCM%)-XZ#>NP)V6=8Lx6yH6G`FVs=OO=oj;Ox1 zu9fj;8)FM|2;#XqA1!Qc1jxw|FZ$1)f7Yp|`=9rGwzV?7eT$whv%aamnZCJ=H8U#{ zEA#($pN+A>e^+R3{V#F=!Z0JwfZJI9*W2_RjQ=mUAUX^J22|$8|c`Y+K>zWm#h5D2-=t$|IhpX`SyQ4 zmY@0mx(2|gy%=>lUK=F-%7;73_;@mv7rCg$$#DW z&!YYhw%)G!pKpQC2%_^d|3?4?(d&{0i0|AHz9T96O3~r&)-+oDu+sEPHv=7Sl5u1` zamzoNY|x@17drT?g21N=e%erFgJcE|NL3DcVGP_LJ;wNKb6ql5W8kwi9`_Ws#yji? zO#|@f>I&)Zcy)j8O+`nK6++1#vT^C%NxV%M(D-Zd*Xo_XUt~CrB52?#KWIMx!~ud*fWE20s_q(a&H zI||3c#x3D5Y+4wSIJ51eSl$oCkA^%T!ISS}9el=j0%Qr|kNl8DUrygZQLZK6$zDb3 zkB&~0e(ukkBe{k$iScsF1PqA66yG^E`M&Jk($ZZTnST?odDD zobRyTexKd;F5+6fY+-*lnjU4m<+q9TT{|wTxIWK9>ksMa>3g3DOt2^*Nm2pwd353Q zg}3LNcgz_z21F+f*X(Lap&$3GbW>zNjU~~ysQ_6#y6{=*!z*l~SVMfU)6aBU-B!1X z)XU5h0+RWPjR!LoGFlT|xrn|?g&{Uf&J;|xcj_6nj`Wj#1(uzU$*vo!7JRc)zWDlE zyFcOzt9M`*n95Fme$OTdFqrFHKICgn;7iLM9k)O*}}#GYP;z9a((f`_pdA<61MU_eOB8pd3?_Uoqz zg?woOpMB!-H!@MmCprl8AY6!&Lmy^3&mb#g%dbv6kFcVm)PF&ZWVgZg+U>dt{4k9A9Z(I85b)shK)ggj7Q4aT zLf;Mc_<;F%;c`wVbC9V65(R=!qG083!}apgaGm%fWfPpPi44=Mx}y!|yrqkrk2a3_ zvXr{ZM;Q^C75oaxNuTXk;Q-l&m|@~sx^9s4V$p~+d4aKASh)riJnSJp5R5)QDcdnX zh6x_)W0@0h0psyx9G9!f#t>{~*$Kg>Hptz3P$ETDV+`AUU+l%Yquk@>W@G>43L+or zB0ih70;{8q73w*`Tb5P|ZDR|vDz5B7>TT`BoW4^Vm4dX@C|PHjcs546Nw^)kU^_Z#;djTj_# z)mt(71jHo0g~zV~O2o6TAWllu+!jhSNQ22k$uZY;BMtjqj0RRG5BNd5{6oTE-tg?` zV8u)SGX;*UUn5|w-yeN(K;ATTASHAbOE_48(e!TuiI2K12=MX6n93DV@J2evop)-; z-1gfQ^M{_(53n;oJB z@@mBLYXN6_)sEBFN2;6Ddyi%e$*PDrB)giyRlc!bXrfb;DHsC{H_B>lH}{uA%vw#S z#@x227S+|-JZIdeQ^XPt-v0X4gpdUJyMt~D9SK8S4ZVxAJxN04?l);G1EEcX45x=l zH!k(1X3ZIi=kihml3O)8gInLQUe4`c3B`};rN2QiKNA}WI`Qh#3Jx*rpm>m=O)^@ z4$9<`;c(ZpH$j3Wg;pmu&CSaqKITt&_P#TcOXz++WB3$>Un3eXkdzS|STxOq&?BH7BWHq(t;_Cv}Q-(s$lbZJF-1jSR5zh<* zCO)<);@V&Q>cm~Pa_*}Pd?H;ae1d$evc%}}l-W}z>eR>fsOp*9DWCl`=S%)LlFW>l z34DCpotdkUj6}cIo!i)!oau4D_9jJMWaRE^!>;_*-k^P?+EH!yrKNV7*3Z`mLfWVg zbgZD7%DL+9IZ3d*PI>q11938vUo}81gdqwDPm>CRX*=k=Kh0CWIPQOV@(pX^2eIu& zM?{T?LjrGvXJT}itQPK%y5o}74fNBNr&zh0sh_XUR_#wVhpbiE1ku$6JOi^sLkc;7 zRtrOjkYd@|-%0Dm{`8rPM8Obr9Mky@<>)uIUElB58^>UB_}CL)wb(9n`T@RqWHzf+ zY_%*nUnm)NO;q>N?XjAe(523B+MG>I!~J+2W!p-tC}xBO<&dn$ag<(oyWad0n|(1< z3Ul0|E_`KTs*8ZRwy2ClVT~M7}0~+_VU0ge2)#lp^dm?5YoO2c-!H`9FQDIX)gN>A z)PFaGCMo{tAW0=?gcb2PQBh#|o#@DIS}s>Q=qF1b-GF%hC?h74vC6ggPw@lNHA1fF zhtG7R;03cwhwC@KnBBXLSlnHB2^){mzyq>C#Fd#B;@_B#;Qx2m1vcNv0B)ve7IheetvdHI(ybe(P>fsyoR+c0T^B6 zPr%g--H{xb1a5zKzw^QIMnuj-HoED7fv=W}weKjFHs~+CH8Jw4_sc)OItt^=lf9bk z=;&a76Lid>u(7->0eIqsY)YewCDR~yeoSO|>9#HpkA7*CLxO9^j>;IZyIOyb8%P)Q zOylw^$964VkWTNtID;MIg@@qv156;#nW95Pxk;S#odmnmc;lf|_D!Lcrwig2N0Wt) z)_Vdc5B(A{a{`Rqb;dX@F7w?u<&XwIlzhh{62qvYc&eZW59Ak?orU@}Rh~p_S|zB8 z?!Lwip7`^AljeLcMtm!_3d2k)o)c#@z{Y=iZQOcyY;51Pom%XEW4kBw;?uU)D>fTL znd7v2CxJre{r%Z0Cu@`TolW1B7^6?K&+kpnF9i_Ytc{q7Uk;A_G0BGy*WJK~X%sGx zm$_%k_5OUtGo<4~DDW>O;e2Q=10yfbX4Pzs?U@xk+UYR}rnTn>xo6f64+jM{D@_N| z+*>ACvH1IhW}0F+)|ayLczE*C(?LNCYKOP>AB>q$Z>|^q)yESZ&a5(3`?k>U& zc}jORacudw07g9IlstXD%>VyGiq zzTAy#33A_ETu(pOl$31eS!{KVr5)bh2k~P)DUccGV2(}ECvue^x5mERfAnc&y24pu zEWmg;ORMg3ImGg-7sid2h6aKAQ$F954$rbMnwui~om@^PiO$IkEf>py{bl2JHQ23c z6TS!_<1s1~264#puu7Tvbmchh(pDgrz|f9;9UrCg<*tOCLwM%>W(#*`qjG9HHYhG8 zAL&pzB+vqD(*35-bC@-(g)W#A2*uYgS}_E!j~ZkP#S~x<*Hl+Mg9y?SpK|+i2o!TBv7|B(hbd)thpYtxrM9H)#cU6X#hl)1b>S#F0PgDNgJ0k5j%<*Wl{9dbN zCQ~&A(xU~k3YL3$#H$Kt$A@yM582akG_(-X0i<0)GgYp2@y!qZJzt6j;^V-|2{CEx z^6kphM2Ce`vn&}tvm-Q`oLtKjDP~*sQq$8ILqY2)%Y3CIjAsNGKNt&D7SD&OUF|nV zu)}CFsF@LR@=ycDyI;~Rq`mzMWjQs(#2s|$(`(F zL8R?&rR~M;4?b^T>t^P6Hdsx^<)^AnN_R8P+rvr9n3)hcW~!{X5sqv}2NMN?0-E*w zL2r~^88vBG_KOkJn&2s0Ggt1lWZ6OGi~AGaa6%Kl3yj!I+g2s5 z<+(|J`ce1N#o;v60UNQ*4HUuSZ_kg$4Ml&YOIsKN8W>ltDMU%u{HFKj*)9n3_-r?>oOcou&sc{k0?2m$ov`CQ zutbKJwyHlFuc=|OeCb8EDwdG;zPF%5hEFIk@F*r zD@P>}eT2+Gf^mQqsfuX~>NZ*J8n05wn9g{XE(xhM=<55qqh-@ymnFV8;|$NMIbM78 zbfQ&e+ws+r|DcuW;8t-#XZRhDc#t!n{o0@jdSW?MFfH4lw+~*uk`zMx#h=UkX&8sI`yC-4t zw{GjR%>9Rq24Z3}zx~cy`D6H=g~1ck6L~dw2yyi@W0y zfZMhXgBmqKD^3?EF!@;)vlt)T`+>#No#-mN3VbBS<%Wtl|M8fh&a2q*qMbZ9!J1Z3 z)DajozrUV)4}{afPmwCB@}SXij*AUm6Rbpg0@{U3sgI~dMx_GnpuRWN2g+V`x~}Z~ zv@&a}7^#u{-^D2rwnW)O!?eS*Cz=YE+S6;t&p-Prr)HS(L;X=xjAQ@?dR z4X#@j9`-g36BmTtGVF!NvriqKvqA1`u1;$KzrR?d)C%-VW>i;i#KlLc5G4eE4DmF) zsIwnYmGghgk60eHhlX*721v`x7}N>u+S_3roh5~e$|HhO0$dd3Am)vQWVmf8fILN& zGlg6j!iQW~M~my`ry*o-eroyg?rhgp2j;VKT4W*fsZ|ra=N#q>vRsT>E{1`!6#HB* zdkun5u+iQ;LA*-PgTbN$k*tp_cRIgkr*5pPSZon^^7fsw@?&BqgmGTh8_C}R)~59b zsq5Puy(rDn6esLOc-#ifpu@A{gRI!Hty>o?`S@2&s~{RtI&XVcPqgC!ugzNUM!<3u zhlkqJ7iVV?Qc*M5z1G87Gj*rUA@gRviIW-73T8J!_yewP8E1S{piQRuZ4I~b@RW0- z;OeAzgtv3q>S|r~!Hnz4T!(w_HhiY$Q}^uMds!`s4fKcO0VEe*W0t2N*1MUgv~|2R z!^6~iUuM45;3elP`tTo0ngV{AUurXG82yPhJ^#T4V{kv~ytbdI&<>696YWLhUygpKA;81alWu+?p^NNxKByxrG|g_e z?0z-6fh((_ewD{kM*Z-$bkcPYAf(%6lXb6j#qmV3&+|LLMC%0#w{Yu&wCLL{eB z3M3HQ;R23nz2ZV`rys>c))Q;HlW1=?F=UrtBRF;7GSNH6!3S4@fZGctAw_X)vqG!! z%$Q{6h;6^&DrDHph^f7pZp9RI5D~DSw(d=4K|LDI7u4?Mq`d|kpuHbOau;cwF*uWg z4LC=_gB21%hXOmOo%^dzy6N+yga(386%EVY8E1?9%~<%Pe@oy+!$I$f!_hDkqJW?o zm459K9XV4c9)vF|7;~O;8%u8CwzX-+7!zu+T<$g&<+Fvq@Id08`g{tKlC82! z&CIp&^-b!Diu(G?M5*H9mWSEP`eBILnLK^4j&w{P(Lwgv!DN|5k{VToerSD5Al$I$ z(lpSwF%>!po1QJ%{6V}rczT@B9Vadm6WVe|PZ~(l*5kJ5TS+1lIPZHdQrM_T8%oLI zew{JvQl!AJXW_CqF6-{3`O-XAW??LQq7NOHW_pUB1^01ZU`t!zOG^^0jF;WYE zzKSlcHR=f8=}y$7P_sZ})yaEEA?6(cmlLD4AYVN@Q>}HiUo`2AC_!(UWS!64+BLPy z9c_|TR&2teuw+R#?Co1O;{GO~=nVm{hI;sx;s7y{HSBr`PYWy7Wjk<=82F{D`$k3c z=)`G)RfnwOJyc9gZawt-Q_VUD%hLwuTW5KIobpn|;E=oS>?}2?^aJ$Z(~zb!dRM$- z1E7Dy*$)z_;|Y^h;|zhio!T9^qbhMPf1Mq?T+Dyq$(I~NiLU~wA7S*Q-PzpTxO>$M zw+ZP!hJeh!F3y~ngFw-Nw8~^OVW(Do7>1TF(S7r1599)~!wa`*xGLnebj&)T)@geq zrim5WlyEYphZOVGN5?a9kzeeFGm|Bo;`9U4PMd!~(w|&V@NsVe)hzaE-iItvQ0Fy? zm}M-2XB&9FO)V%ulwl|s_mAP~hS7qMK0FVh8^;@yJR&Vs)nR#|^)!{%AFjX5CW8_` zpc{H)x}JbQN>Oo%1k_$Y0fd8Uo>FVF(suLSgNNlOzwg5sX;_gYLHZ0~M;W+8RxoPn zsy-a}23Em-Buc{L(f0_KS9d`N;XxJ_rURo-&kAHUtW^w zaDOXS@w@|A#Nc5g_y;fX)fGHNY=SlBDDb;OvCeBKLxXjXBX5^?W(f%{6DmA<*Iq|Q zf5^{zI-W-kLlWU7e9MKM?ngJ|C;HP!pO0|Y%DoFtyhE-<-(d2+zS7oNI?hge%|4Sp z#3cVpZ_5Lgsj$QR!dKaZ+B9}LTP5E|IX8>mVf9wRyoHHjSBV$P%t@B}?F0dta=0_F zHxNO>(=YdCF0N0=B&A`}9nI$Vr5CIaiL(53c+ADs4w+m1-6^y*tq{}(CJeLr z?pNcBLE2I0Pl#g{|5by3qqW5b4BAq;@J5UMx$REv=LXN`SLe&+8P>D%^@7G3 zV@cljD(=gVEC+nEl3M5T)PPuTFeuYY`fFV;6l8g6Q#Fy3D3As-k{-WRd;gx*T*I@p zZ3g!SboloBk=&i#RuJ_9D|HC1bWE5GZ>Dv#_*1-9uKWRUwYfy+`!zRiRgV`In(g24 z_PIYW9NbzP4M9bFEa5LKAIqlAdpuT*_XdVc%v_)=-fvc>BrTOBbd#sHla&MAmPaWu zQ0r|kcz>L}nxN8)A_%A;Y>rOp_PzSgw+AO1!`Z{7W))~zLowtmmJ99AhkomleVVQ1 zAUzn#6$7zJ0jEi&r&(M>wusMw34mNgL$|xY0 zM3CT9SXY5gqnb&pp78Lhjjuw6a`*Dau*qcdD>6`(-#(u7K}4|5c8z#=ir!q(GdD*) zo}7mbnni}QgB1ro#k*bxL;I>lOwnA7j1?e42MrfI2^WPG-+P$826AFz5b9l_8|!+Q zv=X0j^_88^`%OwF1x3Z{OxYT@h3_U+wv*UGs*7{YgjFQ2hjE-Tur0G4CQYW7@ZzMK zc`VvaIYa@`x4i!5sx~H>rO_ggThrOBRrAq~eOU5CDv@L!q+xXyouKwX8FkkoIg8-F09_YWCkog~>RT<|2lj%_oYDPtWh;Lg!Gvos74_#?cPL@orTKyzTw7{A7;Tb=3wo)2M=) zNK#Rx2m765oE>%lifJmzGihcw2^e{ur(Vjk4~r~;RM$!r@@KQdvx1EWDI;2yc6Yl< z3D;v09*uC&27J_Wg!uT~z<}rEpm-sKBn%m6ie5opejsg26AIx)0d5& zM)Tx$_Fp>8x;C2MmkVO!#b>|)k(MIlPYoN$>uH(kI^^k_JJdXAlD0^Abw2`&Ev&E> zkU8_C;^x|;GYYE?)C5W`f~M>*MllHV%yM5m0rF2w6Fj$!SCFj_U$ANDJI5kADV3}dvt9i7{T=&@7!TE44i_<;z z;CxBPXPkqNclbHjjyyBsN$gI?@udofvcnd?PGlxE~H3o;@lE=Hv+SD_A{*Y4$4(5Zx~(J8>bKOUQqtO{Tjg84vu@vsWDkYU2$%6V+ zwxw5e9>TC*Y||3(LX`YIVh!&RYk2oXXx3K6&;L0eQakSHVTN}0ueK^sV>kF>Hm!Cf zVK-Z=9vY>4Aiq7BJK2`+n(KH{i9iv=&yYV zgZKxy3!sUY6;EWIIQS(GjLN+H!=iw^DJB6x6iUfi%mPID&ZI88TM9D2gI=+jrpG9H zl+AAM9#4{xIsKZIq^Tk9T?<#xLJuK4fmK ze)#K;M7DtmkVqGnchdieQhu|Zp`eQ6h%Mn;-v%ljj39?t!E%41Ch%vbrmT6Uh4dqO z>rWEgk!T{J>w{rtsf)c@*K<$lB1GRPm=CGSxj#&9Wwtko?6^bbO~(LbFu#Y{q#w03 zFHfY~t5;%1Ra-^9BfDG-v4lH8-)kPZ`-H&8hUUhO=PFsbF^^85K)_0nO2&6tiZjscMmh zJq1u5Hi_ho-vdiE4T&|q)tCFvFW^zt3~5l%gwiScnl6=1+Bj2~UM?LM+p8&?=wfBm z>WLXgCAAHGv&|@b08)YmW*n|=^NKV)U>s|Ot(^5=pEcHH)M*y~YCEs%zhUw%utZvH zb^n=`_Nmr|<3e}AYYcd{QMke5)z83I9HUcOPFOr-ywrMWHC4WaqF6YEQ@$MejXi|~ z_Y)OVA z&`_$!T$*atr_pnd$yC%T3Qx$cKYupen=S7ZBNX7WS@S-fECx=f6kaVEHbCkP!c7QZ zmL6k>3dMrv&GxpS(Of<8Y7JE|Hj+;0&fz+Z_d5~yu4((7D!uj$^E%hIETtMLDUAIC z`XHg}l0K>;YeSujV|~K}{M_YmL`M=naU|!@uitb64F>HqDTr7N+wU7WmiQu$D_a-mI=_@q+oR#9A$Xh}Z!?RmM&!VQ95 zXVacB$eFg$UJENgKlH@+de)Taw(2m>6_9_n(bEE1s1yN%uM9sq0xk3!S^jq(G*Gzl zuu(k8X(P;sM0v5g3^B0+ zb;YKyQd2nBemb z8VcvjA%A9>LTy*QZ1E334m2T;?m>S;8h5<1M{|fkC_1QX_rSvj-IH1>hjy@N_4uII zJq5r9)&pScj%1}b*YkvEB<#{r)(`F?VdG1v=RcLgpVsuj#)n@$!<7gW_Q5291w4z1tzJU(6!kFDifC~=2uNWLN4Z() zvw$5E>Gc^u_y=+>6u)-*-|<*NOSAq$tDLpFlPO&krt(Lg`dw6gm3rBaX#R>k*#sKE z1Q=?X&Ei)zxd!w5Bz$)KpXy5-2KzW-yb>84jfHwZ~yTeUaf^)7YAf@9fB-j2{NK^f08ros?*z*(%h^ zZ5T(kG?}GTQpx5XAXuR#)Ydpz6-3CYC`!s$I2wHy|H(!+HVQ6*#}@v}h5CElH{CNE zN^D4~^pBfi(E3EDqhWo0ebwx|56CHWyPXx*sl>76Kt9s#06XUEYX;ja;U2xh&^dj~p;}Kxoft*$WnxQ=je1n-(7DznQ!P;t0P1xc~?|~4H zxHBvCcHY+ukcAV4Er%Ajq0VK#L9?oXw;q(s(_SZAulA6VLQ#tOQ6C%S39oSKH?R!) zvg*dFYn=ePa(H|#EG%%B7~Hl*^aK8{4CH&aIwHQX)EqwmpuF_# z6sQ#&rZ)3OFA}J<-!%KC)wB2V8kH=wL&X>UGEM>4*Nk{4kFSXDLhU@GYt;m3CD4(msw z>L2H|5O{^D-KaW;hgH13z|yDC1`9~X%<-*v(3{1QbnnFcVw0I`wx4xf`{~2RWO^68 z;~SFda5!2cl&ibv5A$zw8V!hPrA>Dr_eQAX%N%5ooqG%Gu;EDS#sUfj20p#8+evJB z!bZD!`L!d(4!;?$j$e|?@UV((wInO2ZBi*# z-Zq4WJq8jHN*f&2)ifApH>sR!u3E~#%#7h5`J%6bi^Se`Q+p;|R;;Q2;s8$r<{oOXcean+I=AU%Zc#=SozsFz60Qmx6mI1lndpkqZ@)As2on*-F_=nw1aN zN0~4DMh0+}m*E=~+UwV?5l2GRAMg{b_d zch4|GQCCC5uDe-2nZbZZ7Zw&e|A=q9bBFE?;&%b~w$kzgv(p1GBUxO(z z5|ut08d z>cy_35^JO4Y-62b$xr!MEtI%fI=AC(4t=J{bL0jlsc+C=69X)Bo*mO9X6HF+?2KGuL5ad2SlRwtd%ZYQ@2J%X?%B(paM~cj6AkYu zkyBt@erZBfV%dHkW@|ajd(JoVm6)hhiFzkJda9wq=%vz|_?DLTgIMdfm&NJ~g)Z%H zvp3Y~>FKMQPU~j$%AT-lLa>8EB?!$*M-%-{C=q)@aqH=%DesF-9!hitr3U3moyB(3 zuiaCH{8s}~hCxpWsr?*59|2N@V)aXfT2b$Gy4YkiPvqQSvWP3<3`PHf1b3xFbiR|A z%b9MfoC`f<$@C#G)&T9xN9HJM2<>rufEO<;gSMoc5B)V^VqR^zSZX@?;CG63e#c?= zaqgZFv{~yyvDEYiPK@R>N$DY;=(4|=SYnP7E^KWlzJ{erY8d(~~Y zGkRumf$&DPz*5smx9!gqqf6Ach`csrRAi6%%?LDoO#c;u>Q71pO72>YtBE0WJ$X#y zT%LrqGXmex*y|fUhkETChg*6Bzx}uk#5BJ~>D~7HS&|@pbmb}17s_TSB^UPkH04j#i9m@U0FLI2gOA8@5*1>VT zv1s;@QBkwN{LLn^QrK`HAPW#f6m;v-rFrd}-Qta(BHntH!updxLg-(+z0$X=swPPf z!C>%5LU*>*DE;8^21=lmnF4@O%rkHM^3Uy4(B?k7DqlCvmd;_dTXYFo#e*c^ZSO* z*C_0Co3T#d>m)El_cA{C{b}(0R$%ejk6rqMontO}1-=M%7JLIHy5mjrDn^E$kVteRM;NehYKamH3NOCvDSl@*#8G&NF+07t+ekqz z%k?W9dXAE>*LJ9dZcAmk@%^<$iIY^O;iqx&B)g=9=sC7|;WfS6`qMh7cq1 z)QHX`Dh2r_6$L&~lD+(` zEwQ6>cCS6ZjLx^IE90y5roYN8u4>jVlv3NxkpqELyb?8RV8)2304gVPgV%ncNv-G2 znWDbf`&KIQyQN&kLu;`PAC^1VOLU35mtO-9I7IjO`|P4gAVVMxa6{~1gr?2yBAwM6 zW}ta`uApCYY_XwG$Hcu%I3%O>iAFR&L&uC3nEcKIrRC5JZQqvRJv03 z(@5t0gGE%gVC@3k)0o`-0p$!}W@)utRp^6sMz5wF%o@Fat7!aWhnYW5LiAeO!PvwG zC5B0OE&%553SJC3iVX?=dch357VtsoRC(1Mekm$9=_Cq-wO%_Lp_Yk&n2dNV^@?P3 zo{k4>CWBjF?X?*t1{EVlOjK6jV~R-znC{07m9_-6o%81^HXx><|6sUv?gB<^;FD!S zpa65u zspk9>#oJ-kXNX>@BN*c$Ho1sH!KgGCF~6UyrnEvB4tMU#BmG9K-Uibcb(&txoz<8Q z=PSIKu;;R63VPiHjlC~8kIu`4s!3C&sTkVf{Dnz zNC9pUR$(|}e&0Egxvrqvx_3X%wUZC7IS$ixz2d;fKjSO&gg?jN&g(9_oF)zJ&DZ49 z?28D0Rn7mST^J#^OaqH zN=iyNQ^w@di|JqvkBDADK6tB;jM_d9XzTrF7{kt%>mfU5##Dt{ZzQAo8_@H`PS?00 zeIW#@Hn>LP2MpNs(2fQ)3JjB3H$MIG_7`v@01K=lbH8MOM;DJGy4{chA7G#bB#7VO~nv zpq{Q51%UYe8|)9+^`~ItjC7yPr!#g%^og(d^O>|<2*{fgTfM5bI1FzO#Jl4Fn8$)B+nuJOoSVEV(4QRMFEutlE zO^8Hq0)<2WlfCi4IEeMYbdGdobF&<-!&ZzT>}HAPUl1=yUox0kBBuOw3QRVh@)r`j z-9L}CH~iMn0OH%pmYf_gh~pJ_A8x@$9Y-p6cV3;*__;AapyWl+M}hv@xSH@XqI$#x z0Em=1k)X!N(h*L1IP!ojAeO7UXoU7pBm$Ej`eF_Z<~i@Zo-#il|IpV&rJ|ZujiM$Fk}^xQ^^WMohJU*k{ZMA7JYtvF3I4> zH{dP8kgvcF;Aqq8M7P7nzZ;sF`|;Ury|iBtrFh$YWKD*Mk(q#LY#MzxCI)&oJr53d zl1|go$3#64RnP?#j4hLCQLZS2MF|7?NSR%l7k4{J+5T3u@(~)M3uVt#1vP|<$Os}T zcr5@;PMM9~$Pxx>d|v7nl_0{XyV}(5bUTQ1%HIdJ1<1vBuxdRey(dF!v#G6L1YJWT zJZwyG9S&+v-pItC)+@*kAOExl+k*sqfz^kqX9BERo<+%!F2<~;{iNA&4-dfW1Go`k zU$~?>iH{C-oB>VL>1<}*{z@F~YA-iCHOf#SqKZ6(ycLizMT{n-L8e`E4u&zq;-oP% zfR%Wb=OTt!Zp0U={m^TH=!;Y`_*4b^`uLLGec8H05O!J>uHYjOEmbHFS>UA-Ga=It zam=g0EW`l_FFq@yAqp4wP!2oNQU*Y^k~v2`$}irEBIq_Go?xwz8+|JYmUtgr!66YI zjnQy9JIko0@!rTv-k+b_eDZi&G7P$7hMf|JkOfV^ss4@z6r>WQt)&kli&lNEdUPB&8E;;8@Xs7x8 zM>xaf$<@`DK_@fLS4r23h*CD7r!a*2!{XO)H{6e4c)((Ct0lAUvgPS)0`JjpLM@}I ze#GYna5bL;`0hGn*t(-l;k07rz&BYYiV8 z1pv#`@`7qPcDDN#&6-A;5c6H|unD&)zlw=nmk?2+LQRtvR-iUJ^p06L+%cba>R)Pq z6TSe!M63h=wYZp!s+GRxKNPA^5x8;rirZbh)X-pW&wsfK5IoTAmwO0S@d7~_P$m4S zyVBx?;qUK1NF;i`@20!hB4yY!L=;v_9Lx^hRQL+KsJkROu8L@DqQs;rnl-UR<{(P; z7!a=w9kHjpmfgk6^igeD>&Kbhg5f7nc=S+gCjwin$^eRmR}oLWW32QBO)+NEZzidc z5YnCiws@#w9L<39Mty)xJd^^}I=&YGQbR+iWC69G;T5pkQPK%KWcT7JaILZr66N@g z`4P-_uR6b>?avyx9pH@y8uQBq7Zj=(J0q8u_09!JnU zk~*?qAJCVmbCW~+)of`xR=8Zfx45HOlGRKL1QJ2bA=+Rw*E_ZZ93q0{V0@7@rY`Ok zFmcB|q?O08I5ttu=v&qM1328DRn0KgJqHG`TeK{_ymnHKnsFW>=%i9#JZm&;W^~t6 z6JN-7DWo4g+p-@_v{}mhBSNC+4o~<(V$me&w=iqK1g9ruM29FqiS{(uXCg=4^1cZS zt%~i%1*?NQGA`=0tD3`MVB5EL0Z&(WjJ~}N_tb!R?A3xmNW$}UwGK}z9)d=c2(M$MfgAqmi9w}fd+67% z5)2*Bcy+H&JRZ2Y9CV}6#p@x+4jNU+ch{yIYZ}R$88N!#;C=1F?nkxhL8@HXWk5foRf!M`7nDPtSYlz|^CdXRic#!~vj! z$nk9XO?YT3z0xf~tAXS;#Zl8X_f2t(sm%Aofhh-U>OgI!4q zOB;mNB?;951Buo<50{Rs6|x0@WY5O8!3d!XWdtKirt`=dNxM2Q5KRc>zuy~FH<$dJ z9Ey-{5N~0Fc!^_hGQ>ZSJ9YR%rOhY`V zFJ+QmrFVq9%ddi$a?d*FUpIJr?Ob1v=@QgtYqw`0o(I620Mt{qokfn-2t6{5z4}j7 z5u@3f0-P0?5d%ve?!-OAYGhFWIG~fw!HhR)z`@>tf-!qu%}DJCr(X5#FJ@rC1t$Cv z{`uzFL6y>E_kW{`M@rcUh&E`i0A&$*9kc7`!u<`5DHbdRcVif3@JR7W~zMzgqBD l3;t@sUoH6m)&h@VVK3_ooo6OJUl5svq?n9o-s=xv{~wLfGiLw* literal 28185 zcmeIbg;$l|)<22}(v2WWNq2`d3P^|2-CfeX0i~7BO)Dj>bfa{4NDI>49d~WM=RN0l z#(jT(z`a8qj=kCYdDgSm?D?5~ zH{j)g(_6_`a3ur88{mf^Qw?b|d3iVn@E#fN;R8%K*p+DDAIAe+xQF2N0r(3SYzc>O z{~iub7`(zM8#|hs+BsR+JA)q(VuIk1z$-5J=bZWAKX+xq|Ic0UyO|IF=RG11xDM{6 zvY50qc>T{xxf_m3V82jBCiX_AEbg}NV7I^ty7Pm#wx-U8@_n4LWAoDJQX?VKq8S>%7# z5jS-*cC>uwY-w*t4qMmI$lk?Sh=Kz4pnw1QXP+j<|9;Lp7e|}>r&@~b+TI*Qrb!btpI8~T5J z`L7%Q+0_5R*84sG^C=J-VN^lZe+!^6YICO0Q#d$LIBD^hD((-q=a5pfD*5srQ5+i~S`^iiib~Y;dH7NRxy$&Z>g{gHdznNE87Yeg z@Xt{KhH#}6&1g7}(htfK4LLZWj=fHMH}hB-=d*dKM*<5+FPv6W*Z62z2ZP}rz#|}I zlZ*QOee;(He}3idD;Af!pIYyi7q45t*qSG45Y@4jCfLd*^L*3q7hDb zbt%BGny!}*N{i1|$Re|Ly^ug0tA&N?gqS$iP^!*cj2l;S8?RQlG@ z(0h8dU-&}aI2`DS-Sqh*1;wiGoe0vxo+gTkydK}}FB9R7BWB*$7m9avx;u9))!W;{ zLd?0^nydd??*55y!4sK^fA!H%tmxle649n7=_?J*MyWSkAFA>$%+403D#D(rNn_wg z-^;)i8-~YkY*({e=Tqlut2u;3=By<3vQw`2SVzJS&c6s}h%uI16>5k`OOXA}U zffVlkAknUd_$;-dGuFOC0ed6=44>O2OPAM{Opaamb~j1bXFc;}u}x<{(R5rKam`*6 zmdKe7UYQFj48JnT6D_=@8BoB@k;(nBO?LfzNDNEf0CRkNd@cNmVG_N6b_@kLtr?0% zkBOR(>AQ*#hli`Yf(N?Rs8tR}t4B2QR`RGN)BptY?|aPlBd*pZkv%#XCub__wa=RN z0)y2k4bjVL>lxP!g|u|YMve&;i#&`lAvL1q3x4759sz{h{zF7ZN6+kj`6`_+P(f}l z0`Oja=)Q;79F=~+DPyX6hJ_JN6O@-U)hU+h<#+GxoGmn;Uv4SKChEWdIC6gY)%Z{a zT@zI*%BF*GU%tfv0y*zVqd?)S-DjSVBC{bQQ9@jBofzVAkBLRQ1n>3`QMti%1!T&!-^h$yeqtgepveebCVlMdYoaF#dFw&%P3C_WYTRD^1u^INI1>*{T|>jnXglzE_&SZzE`EB0)(Z~0TgSIcB1$R zj%e}oean}1p1^ARsl)KX(t*$(B`I*tauwM6Y{}hfqVTaRQ@Y^AcByY;Ci#f61Yu$v z`uh0R!Aft6zPS3moYSBHInSP!@hVbJcN*B}B+KY;Ddrg5-|XBeSVy{ z`3)#S%f@JZ-j(jwDuGvPuKYrpU~R2BUk11efkO&x?2)S4^sC0*>m%0s{T5ehYRiwA zIYhw{<@Ijs`_+#bwUYg|?isoX9}Ss(zUj_~t-t>1;^Y%iYcST}Y+1U)Qs`%VVVE_E zDu#cQR90!GtzELe<-25>NaFn4zgV6QezN&yx1LZjgVbL%;2xR{B@Ef^tq5f;#7|+u zp08{&oNw62Rf;VQIS%#lJVB){vvAF3lzs;mwaE9=gC_KtZn%8rT)q5mjC8t{Xu2|^ z`{tnWO_p48qUm_S3vd}SJn5CoGj9s)I+k0XpYQE->mg-~+Eu8iXjmfanO7N}4BDvK z#IQ~7fA@PpQ~fGjbo~>Q(^O`URH!V;xGVEz5VWiY@;DsMKNY4j<*IkmKP4es+8H9d zHFD7N-PoFxX4NXEHTBL2+gq2Cdm&jzePR%1HwhG{f zOf6-MraZ7;&>RweajJCu;P1A*3FXYmX-3XdI6z}80hME3KkE(nu5ZrB?tkVq+a1eqtaDrF^{Dl% zDJa;iSJzpz&DAOPU7-;rmqp+*Ez71jW9nG9_q`f3j_i*{F+OF2;GGp|H{T+0n)4i( zeQvz2_01d@uems+lE{H%t z;Jj%;+tVteD%(lIcs4zleV2g@sj5VAXyiK3Cy zo!CSPM&)#&t2UP{{z{JM0DV0b9l@0z#!`(Mwnci`C>T-Sk^}vMU(PnidCDFu`RNId zv&D#$mE|M*rYVozSv({4vyJkcoNlVEy60DWWavV7DNOnsFlz?|aw?x|Co_T>>B*GD zTPiOtE#qh{O+)!QRa5t-JCK?>9*OMw7LkzY%0wNZ%5~?L!$ge(4zf5hOm>F^f#+KF zkl5TFie~eft+~ZXKeuZS^RcU4NqRBuk7P&3Qi3kvwpZdIWqp0#NjWZdbbU0ac)p?d zXL$NvWItgK#dU{R70`B(K~@;`1uvH0w}dD-YG`V{TZz{X=@hb_c(^l@*xB54X%+d* z{yJ~pr8`EX&F!_}pV?kk-ah=mFzknb0Ld)a!}DAc?nL67i}gVgyLo^@;`z^@OWlYUON~gRPl)IH zg^&6smuT~RvMOq76eCF$h3~wlDRE#7Lgsoy{>68Km;Ex8pV_%%R>)c{j&{HC<}`~& zK~PAI{%-BA;VDl`D?7&cn-rdm%{ zVCIl>ygU?AyBfwAgh>=LqNANW^&atz@Ud!p>A+wqRC;w-63KhVnzh}ngJ+_2?}d|D z|1_e6MEuoGb>-=><*4x4ycda)*Su$;Q!^yRnoNG>lu&_hh45^C%Qjafx%VS@6f@pF zhimU_3`DiR&FyHdnw~|$Q$f1dV5x1;eol+iVd0C!*<*ZfK}kJ#x|*WxKf(RhA~?kM z3vDg=K6I83cBvPu=OI@yn&qdCF|9Cpj^%8{^dfj5i3pG8c&60sntD{k@axxk z3g>ktVW(BvJwe5E@6BBKobQ~3EDg=y_G9PMJXsZ|2FP?FwG!pZxy+P+ycKey%DJX? z-F2Ii=4mb+Yr^W-<0!^jO50l{0xKV|gikilD=e{Gb{iNI#zqy-8n1RE$sTTwm`bq{ApIa77eB5QF3z0FG+d z%1+(Paf@PNOwPG$O-{F2-lR415}7tFg|Vus`gs3g*7Mfi*3C09iTldsX1f5HI6nxL zk>|gst_CRTZgVc zk!M);&Wg#$8qVq0;S%SwPJ{B-Q-fBL)+sPKqo4rcu+U>15LxkTw?XR+v_iK>E|qONGsRco(Z03MN?)Jq&XI$nFA|$ zbv*Uf%h$7-405^Q;kJ-A|72MK7T&u6&$H=AwPg(g0hg0;5O>I^JtB&?^YB<)A2uID zsXaw+HXMJ?=cIs4J2tPmc0J}>Tv8%z--c|@nWLu{0Z42i{=+dQ{5RG15G;{M`$%Bf z+4jkZxTAa71yrxBDWuh{LA-$Yi;{=KCv$YS zW2LcI>$}Yn3ZKP7P?%N7057mZLPW*yPQybKKDHt^Z~iM_)=GWls_)**+*zdYU2&mFyCb~N z@@Jb!rT#BWfjkADY_BttgRJl2ybIeS&3C!{0?OAeJ9RE1sX_wn(4GjGq-;JOVpL|8 zEp#1dT8^ntU>c6FL^v3cy(5h%(VpB0B=qEQo(@3q4XrdzcmLws5Xx!Tvf;Ew+U(_K zD5joccIVgRBSMNID-UtJ53z$lhQMVrdQG`FIde^%a$a&FMW+8oQl)$=oa+E>#Sj1z#K_oFwrLDPx zXQ`26ajPxVcVo`WIt_2+@MJumQIc6RCZWjmK2LS<|8kxXPJkA0+S~HXj?QW{x1D}@ zJ5W(*d@r$CkW1})U&hrvJ|DB)_qx+?G4s0l3NN~OrscKO}P`5*ZwqmDm)buPBc8zY4;)A&Q% z^c=!=Kpar1e>E*@D8SQ)pMT*irqY|pTz^?Lv)Q)C2s35bF#Ae91|qMY6_!H$Zg$=6 zC6+1%Cz~a_v-?xO=mP`C4=j&c()T-*-F@o^#=)MLi$+!6memj3sz8L!4tp^;b8vO^ zfY5zq&ZJk?&XS(NW2yNaKpBbd^ow`HhL&6MchyXUUe)#=(}Dqh>vsCQqI{rxl} zhVx%e10Dwfc=JU!Oz)DIUhs75r5!0BowOEf52n3f=l19hycav{^tt2&z<0G-VwRyQD z$~rn8b1qoCaj!Mr)}MYxM7w=LnikqP;XGrT(J-ugl{#jV;kx~Ok>=oDoX60-Hwncy z%LhYk61ZkxCM3GRsSp*UU$ zC^anF8xmW%1DyUuCUf(8{p;1X$jzv)(51y@c>*j?cGLmp)tFcGT;QsM-Plg2EIZqF zrenZ~2?zeeBOm;ji~ZIS-TKXep`)KjJFT(oks|XV35wg)xxmbYn9nrdt)9Eq@pzuC z<7hShlNuO37%bsICWSCFYkkZ7*2VN51DE>GkDmCsV2l|JffZ`m2n+#ZFP0jEsW+12 z4YfD=cP>9N?i%ivyCZk%?LE^Y^GOW7>mAQ_k0c|Gd6bFU9)WGb;%8+nM__q{JPsjO zyV`^7^>69+FSq6hnfZAV44**iR+A;)CV8NhYaLB!C-y@}TR!T(hMDbt!l4zV$?YDe z0X`u)?={`6I=1{ z67YAI$Gd6#K4raX$p}kM9mL-INQruvXVY(NW`FIjrbK$&-JE_;^Ga8`TCT83OpMpH z7~ZZJ>Wu@LA^uR@Ma^`nz}@v?tB8jH4Xu3;^5e@;+RI>5ntz$b%GRg6yBTiVU$2zj zy)e8yMiOvgDD*M&tk(B986W1IPx{CG_!HxRKpZWN{Z82Ub$tYhQ+5w$MUtxOg}Tl` zyT<-vE%azmf4+2Kb*>(*r13PaKSj3X_G@v!3HJx|#?b(IKhX}9WoKm?E+ZP?BVHnr zCOM2Je0b0qoSG}|_0?%jw-Rx;=1-S-`V_026sPUei*00(BgGeJbDVydh7jjp>pi6oie$p?V_u_NL-sRC?q=Fy} z^AN2;_}v%H{Jwv#1hrrXTgU$|ozhg*Y# zQ~K|bqZo!LCwb@)k<-@pcaB6w*e*4$V3qWwU8S!r4j_Xo0M1RssI8N;{}b7r z`wm6N3~!}du7g0)+GEm@!+WD(f;_fhobf63ZK@dnSGwD-V6?;eoc=Z*AO?WK$=dqLi?32l;h#hL!JRG`4z&v3Rq7*vp)( zo0sej07p?q#Iihk0GH;5KjY1`Ur-CH5iCcjvY`jz-C(B?<_CE+!g(?;YOl5sPO&w4 zLqpYHcxkwrTmt+q&v601@S0pUnFEF`n5TPMy)-nOQ2F$pOX4@J2+?f|nvk(MM zLC8gZr?91*+YsBTCr;7|xV+P_ME$1C)s#`vAV3K6C~kP7g*cFVI^`+sj5m5ssD#2+ znR646%ebxm1=X$JZP={~K5GE(d4xsRb+=Yz$BQXB?UVuLF~O;)JJnoX^N5>Kyhx4{liKQpFPDqC_B|kaM&YUsO}~NGT5j5%qLf^#v4f6 z<%5)iLW!3}D^fu6xAD5V^q@owcegl_#L+(SpT$w|Yp!-F(;;<=Oh0{ZCgb(bTeeW- z8cw8D)zve2joN~X6{S#thwbw4N4M@7bMEL%R-JD08BizodxXeiW8u3~Nju3$tY-?0 zYiFIy%o}sxr_FY=4Hpg4u_I6Xia^>EMFLMW8ix{|)J^#s{TV-(z-&B)G<>;P+WF~B zno($meJdJT_FsrIDD={8H6?*Kb4qQR#a0W!nfy3`Zop9c*FpqhVySk zHF>RUrj&GUcYDp#-tNU5iRY;HyPvl{Kox4#zBl96TjP+S6`$YGKv(s2C5GBJ1T)$& zcYQ2S3Y) zd`pP7sl0%pi6}H&Sja0#L9&oY{0j%dO8~EF3n|v@oKNxVQ_S$#(X@Pn{stf(HO3b2I$Z#s(2wi-Yt)=ptYL% zo|gG8#^di@8Qx_JtPlqcrmy?LuGMz+!snT}@_540p0~Vh#>3mbDj$7;JLr8!}xrBj>V11!Pv#Lk4+gysO4gyq9q zLO#%gT91y$O4QBSpo)ltq#U_(YrsH~yVB9>O@8)r>}xr1-z0alxjKdB^Y+ii9fCv) z(9>`+EIz7hkrt~*mZiuKz)G6%8{Qu*mGtHZ*9Ipex{Cy8mWG3njEry~S2L^ud4`*| zouF(XKy$6FqC!%+bnSk5{56n@6oj+lI^;qR$5{`tUib<4 zkD)PXInQpD24ntA9^`F07?Gwe4!4!uU%*J=@o_s`Mt(#Cs~?Cfk-Pj65c4`t0?F_X z$PL9;^6YJbuO$P|*q8dSM3}-K15bDL2>loS3I!D!=~@hIc7w5f_$e_>t0W&>l%KF_G$ z=Tw*qt8kgZu=GVo50LiIfT!)Y$_Xhwl!zNovrnaNbZRG`Jt69_8#}+qKYcYm9wYHu1;KQ%?pUmlJma)1z+LyMfgIQ(>HCc|Ocl4ytGoERsGV$4wVWL*6{NK(%wv=`h1mbElR-{l!DD}cH{XN(0Z$d zCPC7piQ@L^dH(Bg-nt;ul$JcP;6o^<$oLS-PRjMQFY!nH*p~3vAgsF$61OBXn!x2U z3Q_Wh2)GMzM8e+g&vuf~zh0(dIvg%1o7V#1hsR<_@)=LXRV!H#Ky@KruC4e4YQa46 zw|A!lZ3!=vcy>Myq`Bn;vT|j?O0h?!wKUANASizTKa4Nst-QMa5HrN=jf4a_%|0vx z$(843v~0Mt3!eVS6)4o+9-ncutqsen0D6JpP^>d`lVm(^w{B5(wl`Y26*<7a)v<6* zGesl~tBXM?_M3g}Y3Szn)&H>CH`TW8HkP`@W};JzWm3aeYO|&B#{=^nmK6!f`y7xf ze>Iye9T}KqJ?W}hS+Hf9S8TW5H@00`P;glS@JVB4Lcr{o26m?zD^3ckJd2CO(8rzp{MsC-7*^*Z$bTo|Btt8TkQxEk)FC57U-% zj#`Sm)G50CpGe$T@SQX4vI*DElXsE~^#3V_lz+#KgVj&^@uc3B%4uq99v!;z_;(@q zB{_@;>^Xl5blR!Ja@(rn>mdbe-*S@-@7Ofn`$sk5^<7l|2l|GZ=Y7Y?P%3%ILC^9D(Q23u||7+ zoOiGf>r5b@M8KV8#Cw!9f1woYRx&36)tzVcUOZ9q1M14^+|f3}em2L;1)_vffi$=I zLsp-S@n5|&piD!Q4^IX;Uz~sTNhKUP2+kiLjjBeSMqFg`%SU0~^F?}=plxI-rQdGN z0{i|*k2>+tf&Ds7<)dvlQCnlX#s`2D45N{psrCinWJU0EI2gJjLH@Im<1@NqMJpgMdze1lahV8(P{lcAwuK+c^VNv;Y9L z{}fR~>tG6|S!JVHdzLh!nsI1b`J*i~FOUn9-=ejfwvD69!VVaeVM=tMt z$i&xy?oML7Vn3_cDVT%eKm;pHRa#Ffvo^-{CFRJIhr%1me~7xo@;$HCApJ4 zv-I(RskhfUmkk+BvExkKkWy}g%YJi=PMqQq>^vIe2l7xQKQA(O^7i&95>O>ibze#W z(Sh5-%>kZ)cJ)L<%v|+e!t%Ar$XZbzD&nYIw$SH2F$*(jL=J`X&qnn=JmO!Rx*l9= zpJ11&DL{y2em7$)$*et5FPo@4seh-m)3CH>7MynAmfX_XY}k;WZQ{1WIa+@+F=MaK zVXfwA93=pQC+MT%^q?9K(mfXgRT9MY5O68p{Y2!pXSSUlJLPTWt8snb=w<)WC%mx@ zze-GirjMP|tgm@Yn0aUB=JJ>*?vxw><~s3FslKbz#4Yibn^oE_2nV)DKlVpJwwX~A z1c2xF093TXS2L!~H&sEUpN(ne$?&e8-eOS7f0z7Vye7H05O#Qo#*|DFm<%hp(c?(j zDbt|OEPzB)29*L2J`^8$k3DcVVZXwjX?U7B=~p@qWz!R{bhAOR&`()8mc`yXmC+vw ztB9Z45`og@xme3nis2%XH#p=`8ve_tJpmf6gQGL6gZ9R^3vClAV|Ui${(=u=dxgYOGZw9f0_Ew2wxp?x)}rQYAtCyT+^ zNm625WUmA%4#ZA1as-z=Di*$*#2_rXWT4l}`sNaHwUZdzf|@1-55rHtcRmU@9P=1( zYYjR@BP6Z&aGGJm=XTpA%ToSL8S$cpP1(6Xj0#lRq{%lP%6+c^TNEH*W}uVuq*aPZ zqLL+Ns;e;g>G1Qpa7l%;<=(>eYSgdgW`_(}R-e`19~L5g<*zz|XR6W!{tcbR&?<2* zXS^RAXlU5onZY7?!G`G3{a&;Mu|=#&IXCrb48{#RmT=8oOJK$g@LMeU^Ysl4-!d~V zvt#Gcbvf!?A6MGz9{^;+nm=b`X=#404!TUgSRH_Xf*lee8s=$ggm`;VIk`K->^yXi z-NoiVt$fq*3adB)26uK*E}-bI4T?belUa7AcR)c}5r;{`#pT3<^0j*k{XeZGxD-(% zxpEfmvzFW+0>)Cr-?Cs^_Xi+iSVCiZzS|K_ zsE{_Dt(+zfWK<4MY8X!W6E!;L$((tHzt5#fa*R95a+=4g^<|pT zc72O5C@fV62{ZN#rqhF9BL6JoO4qqMvVnAxfBD1!MO}fL0xFGi+@k!{s+iW?e%Cr8tJDDn00L97y`D_w6;sM>EOed2F=_)a}`-9cwktuIWux zHh4bTs1Xm-&GjHlv-I21i~+%;KK?6^7L(`0gLV`sL%$s6BPMENz)_|O_UJEul4S(_eXlDL%E*qJMdVg`E zcugTS>CJPr1P~9LAZI)I(Gw{2f0-f?z$9YJOkdvyai1kT3{nRW3zr@W9(fdpKNl8T zdr$51Wb^IstjM=&W<)sT*my5?ei>tM))Il--`QV^%M^a;t93~}rc>@E*x@Vx>+5Si z<=kvGF-@37JAi*<*%P42a(lR49?F`5kAiKXlIa#n`r)b2&zG>INr#+49{Pjp>U0LQ zTH!jdF5+|v`%9n^JTdEOvQnjhucgHPoGT~Vyu4ZH6s#wWA}*f!?dv*dF`~r%#0JZy zb^I;3O+U0u%+IMR=_lzxVTgRg_~I#J{%1{F;1>wyR2i-0T#F7;D-PhwaDz7mCJc& zFr0rJDlur6O23oOujvB{6(EYA!tq&0*}lfmO+tbKl@0?4&GY>|PBX4QHhHjK5_tFx z{hCbBvLjJ`cP{zSZ1STqE%3Az^z?jR%?E=MSrzpN-ojkE5hBGjBOWTz>!f+eUSnH= zniT@_w(9To#a0Z?9o23WKrBh}FnhLkmzF{0k7k|RHrP_oA05ov1DxH;JvFA{exh3l z6#PH%8C9N`Z4T$hN|bA>x0lcWv;p*s?eM#Jp5`Pb`{ba(us28lbxgK)I)b}}yL!W; z)wuGmDA5wM>n&;86@LFi!2IX%(Yx`XM8c;-V)_?Lgsr$uM6Vd+;HSv3Aza6sGiUT4 z8_dK^86NsG5r%k{%KgSFBobWo31e!;ddwOIpCPdMl`e>y25{J0i~OOd>8l@+tlFpc zwCPXGC~utin}ClJ_ix$UtRmrO0!eVFhg32kb}>O0=p)l0VosjhtV;mBCTPUIBhnqN zDaEVsz~3`Qv4e2A?>MFzQ`yU@D_nFiCg!e9lpJszmqA1pYIB zCOkSiR*}Ol%JL$mFrX<3C?;wu65w5>|wJKntZPc_$)AK0RIm zLy_5j$8;s1o_Uxec|5$&)tREH>mjU>(+QEIh#oJD*L1!C^gX6%E3Dy>4{`V_qzNRt zdwer>Edd%t4G*1+Lur*$|3nPMC58e*t82U&&R3?z2G%#ENOYCk6yEfSnbzIyRW$eG zC$KHDund&p(qrBun$aK}d+?{Exv8#>KgH^mCbGC%Cg|ra`;PJnPzO2}X;>Kv5(!N#F(48$Q$oA25uylE&aq~i?w2r~o%>!~Va9w75@mS!fh%)Jh_oWc^ zbp{fdPNkKq5E8Jsexk<+hgpPgttmGX65ZC8+Al6NH7X`XuJ=yHZ3T9B=bOu{GYVXF z53RJ=PJCXa*)=a=pvnMCHhEM+lC$gpxei|-LlbNdE{F7bH$X9>Edfw`%?UAwR3EL%J##7`jc)-ugY z?`DLgoG?xodGL5_$ntT+`+8m|uffbJcUTiXs@8P8;c20ED(-ZHtJ9sE0Vf%J8 z0_9Wu6(-9w&sPavW|jx(zPA_U6~<*1j*mZzDuBvWsodAzA$F&&29I`XYQDFn7Z-^2 zm>1q}JL6B|Y>rODc&x*@c2Mw!g+S+xu+g=98aqxgsUZ&ekeol$UOdXv#WfcNQ27zI za1Wbde^C2n@MrwoCC|~kljl=}7A=Ix>mB=r;RxvujZDBBCcuhp?{zf#4R-2MlTdTY zw41~9kf^rIEV<^&9x5nod%X=`T{VzJn4hURXuzRdop(`}WpTv-B>BP+b@Nb;{erE} zxw9n@F;MgB&_kZRHLFST+PeR}jO8W19VBl#2Wd2}*5(-=av}EY-Fg=9&1rRb+A#T9H}aQh}pv01tOR-(GqmFc2SVa?AgsdI_a-+vn@bFWeg%FOpmv8Y@|JC`}RwDIEY)i zPjti!?^h*oBCjKF?=m3#v-_}eeV3>I>}EX5INx2mm+)+Y?H0M3%dTo#Q=ZS(bw+kU zFJ!z}+qYifeG`u?z+BP=dg~|pDVzWtNyHMRJ=oL6z<8y`Nw;uUGba=*zNW^cd6C$H zVV#Ai7$I)7R8O35(M~0r86i4w(i3nPEPa%~s;`Gc5!<|~?z&5qf9~eiU-i5}>R^{h z&K-VxF)xGXU@o2G6qsl<_}X8<278_qSMSa?2?;RN6xd!hR1It0)Yrw*`YO9QHC{!A z?yb)GxNVlj;lY?r6_o{i79wFJ8Cg!4g$L7kPFms77j7ku@!X0!Hrzq->QrvLlA?kl znB8l5x2aM{Z~Dq5G#d2oqu^6BE_UTTgIoqzs|I6`JiU-Q!!>ZR>}*VpzHCJd9H-tn zbc@RqUYqS^FKTDEJhcf=n$35hrWVb7?r(9!y`U5)Jx!K=*Jul?T0!T0mDu;!!)`DY zaNm54W!5|}NV=$QPptoj;~!R57H!-Cir_dD!GQYo{<9T4 zzcKaH4L;Y`tv!U_Nn-jcB>_S=2l!fnANZW;zSB*$cDi?R>0G7!B4Inj*V}rvoDO$b zHl8wzFHiRU5^-{JKk~)Gfp&qACHtTFqxwQ#t{S{A+MzUT_16tjEbUR<*(gP6;?$Mi zQ)Q)=om4W)p!d%tQPhrEN6CIJuzRjPAp!Fl*Vp;1;RVg9d~<|EA*-XG&n^V2#r0V) zaUeA=TkXaj*N~~Q^Jke$RAbW+kKy9dtZT*U&epDI83wHJpa4^nuyBaDshODj{bEV zghQoI_(tj{h?G=cQHhd2z3<`(W;^lHh}w(YgO;gQF*h5jXTN9ub!+&9I8;RnirbeA zP-%Su8f7clR1n#Uk>}-g&6J~ZI1g_FLsPIVY%sW#ZLQc{&`i*D=2_=BIOGi`gkD40 zXv7#5e*t%|Dq7l?cMi<|5$LDO*M;uqtbd@xkW^76NLs6gR`)ZOHhF$_mE#;v1$~76 zNg%HD+znXhOP{OKt@p$fcU6b{jPSjDt!GG;?#(b;(PYvt7M&BOWgq}hU4OT@FKUcS z&qc|z5RQAsly!j}S09|G+bbwRJ*p9>`oR;FkgS6@$fA4cvXeyP#1{=qs!+w3_Rc*i zW&aF-7W%vu!yqPR7Vo_(fg#Na7%qc~lqdG}V&Km@jGAE78uxb4Uiisg zv&in{mP-fMJ&cTy%xvJ7|FUfV?U44?B!H%y9}R{~`xo@YOlB|vkEFHKQ(D5HcjLB` z6tpkzJsI^0R!+0X0aFqkaI^!dP|c5KgphA_Z1Rx>6`JK-zMZ)pTu$e|HY=^=y%Sqc zO4k9Pg9~}x|zrKG7w`M#e($Q5(?WqZy*B@W)U&-%dS3U$? z+<~gdw|}~6nWf|DRcXNR8HWPkv|hnBfc{r+usAoTfc%0l-X%T^yJ!?LvO9h~8~gfbUs;5hoOPQwckpUBZF^6P>sk{(Q&o~OjWROR}l0C%2zg;gI-{0W%5fsJ(rh; z?YEsf^X5v*Xj7?2(?vd2{WFZoP7p|CcDLysOpe;l$(`2o7qB;v# zI=z*q54}jlX+L(MAeOXM7ffw-a#G%L{M`WNS@LMpHLeTwS41@*!(?z6m+i;c*d_8G z#Ne~7$#V0-Ts>^)QHv401X)oJD_xP@0yk^33znz48h$Axce#@G$SqMK*LwqNdFO1t z-lio$QqPnA=5q+Fg{zh}Q$5$i<#vY8Nk%62mdWQ({nl-zHk)Q@`)9nHc)V-`>4Es%f=)R0;xIQ2a}A z)65^>89#0ONQ4N}qn~6|uw>`2l8}G#`Rb5k0SWC1WT=-s_X=6RV8z$9G z8g0ktcouV4OgM~kJuN7dX!gxl zc0FaWZ>AAj>L(&auC{qAj7RkF;6aA=gA~}rQ*z7ch~#l}0_GmpAp)CZb()My!_nO6 zSlX>m-OK#=0MwphGSGt(OxPuv<$85Sh&@8LSUh3rMeyG&C=j__Va2m+7-m3#D(M;l zA&~?fw-p6T_RasyJgVZdL==cV5hE7ysT)Z8IuTg|vL-M`l`B_h7bvI0Rj-OT5dP6k zT}6k_D$cFgDXS_z2G0Kz;r>_2!UDGIZqoEewy-slery%0`zi|17{yZ%16RXE0ptKD zHJ9|Vc{HzjZ!ACGT!6Iz-c-OXf>c1wTux<6QU_Rtulk_(6q0DR(?1KQd{q0kg)agL z^@;EH?$YN>RrtX|COLkAXt+wnU^cQ3&TIX7FcmqobheJcOagOaG8@gxHV5b>6a4}# zE|m-iYZhU@Lei_()c7<_SG^-S<$E=hSzid4K+u8gEhDPDs!Uvb!9j;c&~GPy!MQUH zW}qy_xF-Whx$oOMx-1^pvr!jaw*2{0sODwHWtiSA`xQ(zHA#~40k7lHb`QCr0t!;$ z)|(A)-**AiM2E^Seum$gD9vDV${k1w_#y+#yEi~Xq4c*V!(4<9G%U4qD=X>|q{RzN z4qx8FGB_#(bT;amr!Dq9)eBy%KSO0o#^Fm#wN8X~lGHfwyNz-1!SvF(%y$ddC3)@W zr4kYWmZ!V}A#WLxb;0-kQfL}P0te%GYHxOjYmcICZ}2iI@Kfmb!Ay|8pjVYkaZ2~9 zGmLyOz=xTPPmqk}20oH6dz-jBk01;hhuY2$_>E6jmtO$VaOi>g6-`yp3u)55%RC1H zfGpp;tJ(V9>6y?7P-6EJn2jrakTMlV%q$EytoW-oTk==I0(86=C^7+q=Z=a zimMmWUsxy58qcD^0^OGn7>#MGM_O4Y6V-}dEdTxxn<9^yr}surcqD+G&-)=AKX{T{ zbmFm>!31-4+|G3Tv5h0ghku3J3Tln7V0{5BV5C7Ix-I-AKdC=~8>WR;NK~X(&-0jX^r^QG;Qzx=XIUv8P+$tgVV7LLfZXiVd*_=3JEg3c(pS zyF>xR8<%Gb3-${>%88~`%f|qwSC>yK;H1&sROW*LYf`}OhmOr!YpHSmVtkVZ042t| z{r%SJ>s>dO)2n-VKrn(QV3FEL!v>dBJFVRSw2SMca%&fo-7!l+Ga|n;5v5KSiY_I()X>xQZQI zCCjiuK;$+A!PAr=f#=sTdUehl`-`okQEnCGv$eJ}?emopt|XzbL4Y@SfPz&|VY8w; z{H}7^r^CJOQ)Q;HcJ<7j4xzB7Y6J?<3?5sx$=-*bclT~kB^8cTnbi054An z%=Bn6uD98F|IHy|Dbwt#W*zX9d8#ba)FncM*6GU<(7AL4!LE zMQfE?Bw8clom=islA3+@9;OonC`lBoxbZ|m&=$^bjb6tg;#{GaMxYH>WIy8VKXUc} zo*nG}O)VZkn$tcKuC1AChYQv_Z!FK)M~0QX0GtO-8=>eENjw9_N3*l-X_t4+kT)n_ z#~o~D%T|y;>~45*sFp_s;|jpCGDWLS9&l+4q4T|i;U^rv+#i6iJ7O%N)O%ZDoHs;J;=o?yz||kMo6&zgDHbjxt^~niEq}EYHjBm zcyRQ%X4>5P3*VUEn;1vfoXel&5EUc|m*MUGl9Lhhbg1K6S}0u(7mQGTqJ;us2!`rKC`>BC6^@yh@AM?^tYp-Bug!Au01yWU}dX)cT zb-`&cm|my*XaSAUzvKT2eh&kTe8}12KUxs!4Ha}~ zKL0UUu`a~`ZKcnpn?UE-pFG7$qS${1ar{KJ07J3^8&#n}5l~odSy^W`S;bXdCBUt? zF>CCp*yO$B;se9;EKtir5B)QqDX1s+*RPJ6H{W(VTNsVFs0g)(DM8d{FwV?Tw)D!r zCuM8m#Qrwh!*Hp7nC4S=7e65vY)@mXAUlC1uI~Yjc4N{~Q5yOLsU*~MH@sGa;NE-* zy+0W;moC`3e0A6a)KJLZE+=Dk3n-rPprBJ#=-yLvM_9=Rfde)`-Jv!o0*}=y7tWM?^HY*6yl(X= z=<;+W`Irv7!cR&QrgXsmOd!IaoK09cv$;3!oOzZ`5&9E0gCBq2{3XI)N%*S;f3@JR7W~zMzgqBD o3;t@sUoH5n1^<85f*)4ou~CO!MGk}XuzW~bLP5ObmBIV}2O>F6@&Et; diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 02c112d28a8c3c2d92755c67b751c6f4a16d26e6..45c2924fabb638563eef0c8be3c17217a83e247b 100644 GIT binary patch delta 13100 zcmYLwcRba9_;!ghBP1n}jO@KGkTbKIc8J_jOEG*+{|gY`Gx!DyRSUbM7Lr>Zrr(X;|3}C`}S)T2X-xq z$;%&YO;{EwTEClxcDMV6edU<3@bAfHe`5co$Ts-k9yz!6=lim<3^=vNo*0vw zr1|)?&_f!=Z{F|?7*}Mxc|npBUf@1y*#~vUcm!H*vailw%&AJe+YxrN3OGom^733s zNlbYxS@i<@4teZrY*VP)G_b{}aaO`oF`uw3v*;eJ6m1b!gBlUc5PRY2m*iyM-mh{h zk40+7ql-G{4fk{9mPKgT1X{qhv+?4dgG810w4fj|8(0;63J;DYaIU;&Qpi1notfor$6$ap%r&%;~jUUUg)& zQtzd*aojB*DKk6`gz&d`ExH-<(-HYf@le|24$^L-O#GyA6UOThx^U|z7Iq9{#Ujl& z1y=b^Y@QbxIXO%18mi6zmSSh>9Gi>JKZaU`9~@Y>UhI^S3aP%oaT5!VoQow^3Z`dS z-?rv|(!5qZ(pYT+jU>EF-5eE z&)?zqR|it2P7l>dG3z|QhH>!^4jAdtJ0E6-BK#0Ox6Y63)Kh0_9SnX1TqV=^4duY; z8KuC8dH9rZHbn`Cdh|o(b`!haD8z?5g780iiAl<B`XMVT%U*Z}r}1u77M)L*(f z@j?u}8WP~!$ro(}{dZ|4tjgP3q(c$zd%v?oh>5F|@HBB7h-Mw?TK}Y_wuxEFVh)lY z%Rn${Pnxz5r@h@PwkuNfxa}6%Ojx1X)#a+h1A&3sD`l0 zZ2S34E(FJqs$titD!bCgvub@-{{0Oo@EixmHHKQWWh8K48!|gjw8o&#qt95w_Ose# zK&4$*)L_frD|1pMa631#A|G(EjCiv$^D6$E>>y6}yWA`FYK~Lqv9%l7vMA_{y%EUv zU?{FvU>uC4txI?7qzdTW*=Y}|%@J(hu+E=8PqWu7-wRB#Dil(Em=I?K6 zLscRdj5~DMF`vyAX%)B6&c?3}C@I+BKLr=AjY}EV6Kno-W;BnlPMYQLyXe>&Q2Pds^C!_Zv0UKRjs+27AW#B$1!o z*b-gZd=`j{>%R8wpww8HI7&3YA3JD@QAfn5d#vext|zVN8fa~x+L1aA0n_E?6yvKJ%9}c>&QZc( z?c@8d0IEQG1wU+&W!T0y-cl!Iie0&Xk>{wTC611f2TX|9ZX9Kou8MMzmHl0)k*{yX z*j$ zaJFjq@;`8gy-)pNkhO~Vgu4{Fo}(iC;4V$eaXSGOrF&t?XZik_b|L3qxXD&>#R@Xg z@1I$@Xa%+4_=3O+b3N`TkR2J7(x#)M304|e7RJzfJ2K&z@^#+ z3TaBIksnoBFewoH?qC2~pY@q3e%X5Zd$;w<;Up-i*B5mq{UI@j?dQf=G1Hv0bZ@u@ z2>=t%E83S%ZWf;l!|7H3?LxQi)*oFSoIK*nK+^?W%sfS+j|+;5O2vOVmC2?URfQ_0Bdf&zM=&UH}+$6W%mR?0?(U zUoM-kl4_OWe|(x9&B4J@mS1Z|xw7Ba$_E9kZtx*5SIpBdW_wddpjPy|=o-6-2b_-3 z?#~FXmZUP?ju1khrN45Ln{K(Rcb5V0(Q0EEcSdQsSz1<@QFT`o9_gqEJ^gIJhfDJ0 zQ=tm4&G6gqnn-WY~$>B^v>qT9SMm!OM$)$0vvubleVzcE2=9IKJ%hhs}Wae&ld zTD(e9we>YRvfLI*kZfGbda$W*J%@Ut>wgrZWL&_cr_XV;KC=2z`tDh4e!7a%Iz9uq z#m6{e$5dU~ec=fmiuGh&QPj|=8V*j_mh*%dftdSRSG@7LA0M-7BF}Hf+kfpJsokR` zc>zuXS0Mb0YX}h~$!&8_)Zb@;6emb%$QR|{8)DV|Q4k^)wO7O9Rkhc4`UBAIf5IT3 z+JITF1`g@jiDTZQLLC;Ic8%uM%gxphh3~tcU|GGTMZFDMm5a8nzW)N{+}xg>e1j=_ z&zqiRyfC5UUT|_-8I@R1!0^Tfx%{M4govA0j8rVPgrwi;Z!)IiwV_M}ud(#g+L^ls zElrS-N7DL8`o@U0!`1pT*P5oL%WBp#t^x9|DsM75sxnFNT6sEtMCfIk&Ymo+#9w(0ILOk5npdS&THrv8+KG3Jlx+0x>xXca(E zcwrZvR9$}i3Y-6z4_@-Xj2S(7<{`=hp=a;yeF#Cz{eMxo1r<78j31z8S>1je)9yUlp4D)g!9cyr$S5CsSlOnXa7%LeJt0nr@TKo=LxebF*r z8TuDEN}f*+IQZ8~DIG?N0rF(p4%BokhG!c+WGFrtlI;k#K5TsFQS5B=^w??0Ktw&2^13Z6v{~j+7m+xi2^rUaJIM6$V zP7`I^MRI0l)a7j3B+&NPWbM*NDy<#c3Rm8{0T&+Ud(o1e<`?F$hMEe zJl#w6Yw!t>{_7bZvW~`GBmVBRUT2&jHVsn;f*-W-L4K#b6xNBSgaY=H&f|yc2rI>= zlE(m>$x>o@w-)k}?(aoC;-+Kst3zeHNF0qmTg$E;$)`E$VPY6l76OU_?u8^7SYcyf zvDH!EZ#Qs~e@^jBJz>YSJ%Dh0oe z{>Bxf-JkpuQ&R|qFqex|bRtw_0=&=&gwikr&5`TLL!Kr0goz}&AbX+my;GKyb-xgu zMa_h9am#LS@CB+>Ot8*&xp%yVw;YjX^E1eSU~shMTjwH|Ywb4CKHu%kx8TL*NP`zk zz44c$Dkq2j(m2IuJ=ed{JBR2k#c=#e@I+O|b&++vEy4z$ z_&S6a2Pcl7dBm6GiT%b{>5N8s^LGfr^-TptJx!3LD&%^S_iaqnY*NP?2^@`T@Y>%* zS{bDK?u^%c*DiNF>N9Z^)EgxJ@| zH`i4tw-U=cH$Sybuqg`Qs~7PnM>jamHM&{#fBJb>U9F82Wc_;}y9!c?=fD+?l@MIRZ!66IsA|}3LnKCd@~tbxb*hH zR}qld<|IjTfy7o7+@?>hTE7o6Q<^hPh+Fi*X9Z1PShAuILXetpaD8MlQwJd&x2LH`&J4@j1|bQ*pFyS3W3QTM z$mj?MT~-|tm*?%lN7~)8nD}GdRsh>duHe2et1d85g!UvC7G6Rh>-P>PNH_5jKD#z#Ep zPSj+rxv7G)tO(;r0``^zUDY&AbrwC# zUWmkFE^Lgqv4m$or`)NUhyU33bUrp(vZdx>E`^}8w_af*4Oyr86UcAKDS)@y%6hA* zsi>mrXNs_RyE>o4w0)SSB;%;)mMs*}fAfym@j?WDFWXvKOX|M1^A+BOZ zRB0sMz9u+u@vV{4RkOMiK4d4nT`hP#=j9^=4T|64G@Sh5vAHjb^0N=MP&?Jt(-WTG z?_=jB3$7&rXz3vtic<)&6*(X**G+%d&uz9XN`PdjeOTnnOD*cyve(%P-6!{}Wj_Xj zXZNezF`+r{39{9Lc>687>HK(R!hcot?`LIAfSI?M05b$Pvpp_`<~)HGCfb@4)i<+r zxh14K#3%PZR_8DaKDt$V2`v5VuGuSBY*n4i_YmmN-0eo#sUj0j+rT&!s6tas&< zn6+>Cj9n!Fc~x$n*#zGap|||XmzPn@q*`m^X`YeuREpLU>&Zo=QS8{g-EQ#{Le%)<&k)M1Ykgb%}F{j&PxR(s+%2081E}f{!yhb!-ImujjX- zk7tZb0vdFb&oU)_h83KmA69XReBkE)p+z&v=X-~Q29Kos24GNKfj1y?0*7&DtJz~& zPZDCLNDzmgd?4O2fDi4yIX>NU0s^6On(3ME<&5Oj`bF!RvPB0OC4fc}3y95{KMwOH zfw9stK5nL(dl(v1dQUUD_i*jUe2Sa#I>=(SD7UOg;EZHkH?fXz*Lp`kkTob>`5xk; z^ilk#QcHAXuS>A)lc;5#d6U58*d6J{jHxQVE^QY2>kzC=OzTynOqoa;Zr^ zxb0%6AO(B4(|s8XOFjbb3P1T@@yTN7nu~Iz)p~F(MbM#^U;|u2I1DYgS5O% z=xFpRlsNh|>}3z+9(WZ(MBwW+H9$Kuqj!aCQ3-UPe6{20Vm+8kyHsD6j`oE`tdxz- zdXCMAfTc#xmlQD&qqu&yRBLqL*fLR zWxc=|dkYf8A@g2{d<51)0N70pFsehTfPLG0X6uw6 z1}8f<5mG;~ZShKs;K#F@WxNBL5x<@S{wDzwhj_AZa7bK#)8I(t$^_Y)&2!_g*c+s{ zCVg2=O~A7&-pq2t99tQfShH?mHI%s3d*Rm;YLT2q)8%`>s+uEw6iAN;R4OS12*Sws zV=yZ3GnLom%{w_|@(lOhX0?UiXZ@2bToop$fVp>f`~*|QujmA|Y~Xl)xI-wf<`e#O zh%Keo%eV1q1kqXoABqL| zQ1oiTYr-+buYC69WgxIT+0V}0 zkp6x(krXm&@69}h(<5EdBH(URP@dg-*XeS(OPVtWb9^EM7(@_QeSUH^My5*G zoG4@YoCfPFDdlK`Te^h*cMhXRg$39c^L`o#RV5Wk|MX1hWasD{>;=Xz zI@$tne&HG=*4HF^<5|Q5kTceKc|U>}o8J0wH>@77|IO|Zga-nnP|?jT{T=$OUY6Y$ zI2iIE>W)0}{r^dsGF;gf>&N{`|C2d$DtH^a7C6NXyLPo_uMFL% zZRVQnjA&x)5;-$y($5@9La-OuGFUadHvfP6<6+|YT}lAYe!7}Dxv01dyg8VZ4wM&l zTXf@#G{CjPbamlkk_L7qjMko|!E<|>t+;xFl$L}Eta@9&_i zoR%&Z#Ug=}fNAaRa(4OJnB|@byKL#I3U&UoWBo;r-vsw|!B^5C$M82;P*t40GqPpg zXjA2&`ns;ADG5?n_AlE;iJ*j3p5ojd-}U*&u2EBKcb00DlwPs` zXX~Y_>;89EFcj&tFJnfD{vm{TATF*CGjM-*efq@(tGxN(PZtf8;xeZG6d0X~v`TOu z1_p-JRKLn#6wd~{ghzqobP;)-_R-K%0l-BD-vTn%u{vzI2nj|tdQXUaHKF&h%xJr_ zIWF#`xsG?=*=%eR2l{Y(l)Bw`51e5Lx3MWI4C+Bmh@0=!x`f7CyD-B>J&c;0)E;j-7 zrPt{aHUd$`ds?N_ab8nUX_iKM?>Mzv?Xl^`Ii7z~rLxbaB!_O57;40_A(v`HamnyD zz8LT-B%#Ya!jc4*d$eCNu^Jxgl~*9<^se_gL0a3?)bzQ;l$SLw*#P1H+6UFlq9nq{ z9@-mz$wdlAp|=-nvL4enfH#l z=RGAYg^PX15Bt3WD-MRu5%eluuz#tt5gg5%9E;rCb^>5OyTpN?spASAk)ckl%?>lQ zdeh@RXSp3w1#gkhe8xBp1?rqLn{UV7Ny$o~Nf*CBKzC?hYG&aWQ!EPy+<9#C2nhh9c-N6>2RrbA z>M)c8PH^FR7j|a&&Ml=oG^8d)_EDF?!fu_LT^0v|Qge6~;Zh;dy&tXcZ}CcAtq4X$ zL_oZ`J&y|H932rW^Ky(H#x|?C`0;<(4K4C)tP?(^omnh)mT&x$sdo7&`(7#*hsRHo z(feay;z6FW9S_Xj={Z#e6n?=SXseWNc0cu^bDS*#i`e%bM$nu5jqb@3(9yz5@NtW%WKSsm#St|R_^OS z7c<1y$m?RxuXY##kT3P>+O~X*-YhY3x4s|5JiFAs>JE1Q6BQJj>f|&tbOi0sRz@Z= zQXQQ4n=~EwT)JdUp<@YH(Q-4Y(bPA}MV#R*ezwc%-|A_tuXUb4F_W zb#5X2=%SCMwX8E9ee{#3(D?F^&w)zoM$v9Y!hgT2l}ft)q{{7hI#cbo`sc%i)ihi{ z%jeACSm=3v#`E849uyA*d|7mBD*h;`fXO;SMjS@d6W!H=-5 zmD8W(hP(}6(_)}5a?Rpb)78^ekE02*{*pZSC{&b{g(YC`7nw=(I-_dmugr#D^6#Z8 zvGHT#T$T=qu8)YFig(1lotK_D(6$xDcoOBvUoAI5mqiQX4OD&VCa%ZdD32Es0#;K+ zxTi)IY!%l;jvATTTyT18;DP^%4-Ma@yv;xQ>@_0lU<(d zZ+Cqy0pZyMK1Wl)xF>78X73#nskmg~v2h48#Tan^O5_VHjV&li^IyIXl3jdEdEX*I z%G1H{we@1~?>Qw!MNo};y*jp*Uh)*3(c@{Yf?8t0;Kr)tef>< zf|)vVSZ^gL!=QnJnVIHl0VP=H-Dh=`bY2n2R`&D3ju=o{Z}&3mtDsn3u)_2|SOiMd zN*)`G{F;A&qA~F{+bU4ww_b4lLeuP%0$!(&fq>lpy)?>dTcEH?3N@PTs6XV9(gF2O z!o_l+C;BlL*%9KG?jO~swtha#p8V{-Jt9Q4eMvs&Ww;X80R^XomLwxbDT4|C($g_Q_Z}n6_JB!yGt&5wV*&>-b_(qZsR*7l&o=0-? z)vN{mcB?&nJUj{~O_Oly$oENSQ}C`$j5h;EMwgnPf#o;Ni}PBye#_gXH1aM@^~ z=&72A81jPR)}i5A=c#02Ep7x39gYJPV*7lKcTo3~17|cGs0Sr%G1jWSkezR_2SX)# z@TSK<34G3|ihx&33spg*0!XIv1iP8W+ABv{$5KxQs;p8_?NCiXr^H z7}L1I3uTn|X*#$!eBmUGnc7cYf1wvDEn<6jEqj$^%#nX-euPT~yZwyCdwpJckmk+r4^-0nBmf zrGeDHRdl`AXTovKr#*tDrB#YJHD{^g={9L@dW&`=60%xe)U9*V2XeBNa>sA0dF{h=lM^mqwv?>~A{%H+z@TvMG|6^yD~ z+zkY&RS!AW&(R9VG2zkLcoferK~$*JAm~<_-n0Q)s`?19(OTUzLlZwjP(y5xTk-?eEtSiWD@>yzkVmWM0RA z&}>-m?x?ZTeOXOYrsviJ+FvF_4h-~(Kz#+%u3~QR49ZKk3f<6hr_rsw2*7)s*28Xu zwB0dr6)d$WF`~!;PS+K3L>0Q853R~5D*_EWfp76Z>x#Ya$FrcPSLa6{ZTcZ7!)P=K z%lMal*5GrdrPC|VFOct?2m1D}?xGU7zk>!7jS(v6^9`zyskDURbj zT%YnTX8%t>Vv^U;7mK1vF$opjY?qg2%T3d>#JDQ;Piq1R+rRV>t%&QAuq|PE9V0!~rBV1;765 z5hKE%tj~Cn6+-sWCa(f(3*8Yan4%fJh6kSfI|n*CFb*H+c7u^sY^a_?eQ*>o{I#xZ z3?siufqL>{l{NAt{rchmI0`qhlu3u;Y)Ezd!?vwnr>h*6v0opKFq>RAdX;8R@X*nL zs~*8_`Z>y1{!Tr#OBQKdUy+8*AuB@*9G-5IYY{z3M!M~%gka8U7?`u~vbI%J?D|6T z1Yz*{A70KjWR!Ji^Rv=&Vm1K&GvRqnm@Xz4p#!L(*xUB2vdK$TrgP0JDcPDWF555j zkV%rJ{C~VEqu|)bbeU&(I3J4lN7Iv?!rKjfIdWvK-~Sxbd?)i7%(p-Vno+_89bQCH z2|9{_2s7}!=KWA97~)ek=$|4(LSkkQmMczCz|~gr8+WzOY9nx4F?3x`J+5)2OJ2f! z7z-RDi<EOx z3n%H80>swiFnZmvT?=dohC0HAYv~N&M$(Ay<6_#ONV-|TbV~wtHhD|uXx@`ErFdiC zF6y+;XZfKJrcfGzU80mc+j+Ou&?HUNtJSO{#sEgK3Yde<(N7B$8r!sQDlIWw`8v-) zCoMYq`bmdxDl6YltPWJ|8fn)yDS|Qn@d86FU5_5s?RXonZmz*{w8K zVBy>9isYZ75fr&uIa|*c&nAqMGiYL|9ZJ801-```{s);b1(LMu8XM0(W_G^`S7~7q zu(l3h5vxyRiL^!q1)h}VJ0??a*sRT+F zUwg0g?`+THXZ|}Sf2h46>@;1?#jqL*W&_WV6FkFKRrRvzfsBB0pk;mgO7zk`7n<~F zVU}G$f$?aaUM5&=AhWwbM^Jv|*4qaULDj4H1U(=80PE|2;(&ID@}?5B#Q%zU&&JVr z_ov6^ID-UIX!Uf9wH!RvKSa+x!Du;hpoQq&taM>sqV1Zbe%(&iL*D-9WABB)T5kk) z5`g&8q!pwQf}Ab4x`GP>At2R>mV1&8ye^X~tqydw`~&U=k3ZO2BF zodh7t*cOV{JYZKmFMtC*P@0eme+ca0KS?5E@NpaTJ~O5h>ETlV-hlzifYd>QCZT~B zIkl&+ks3$l>UL#(V2u_P;E5axIwn~4#wb^@`^BT3l5sILV;3N}hLJh*W?!qy>*=ms z=!JtO5I)v1IDWC8*?cj-gfhW1eD-(EEQwj{)=ZtGOu--ty2bTsTYA6Jeg; z%Zxeo`}_O5EZ>7sq(RJNnB{>++WAIOypWN=We?r$HE_~9Ei+?e z{%@f+7~B2TOy%c}HrOx0f!bZ8KK;o#%o@K?0;>2W#P}@!xl_{BQUoVrFE*V%dwicB zoW@NoNSSo^wnl8ddXi8R{vjfU!S`@Gd03r_WvheVj-vtn4y7-+v?PK-eGbM74$nx| zfq{hJwB=&|;MtGo7}xhYE#UluSaP#aX`N1#djY&9@ZHz@6*9&a$yV^TmaCEUfRy$*LaWxZ6{sG7BYD`<;UA`@9#=*k zyf*se+_DUC)|^%YMVpFrgQfqs1e5(uUc=`fnqbP4CCMqg=sRQ?9(=&yIkILI3wCrXr1K zl4W4@ODY5&PMxawk(1=LPei-xe*=-o#{%XW#zxM*>1gZOJXqNVNxU2H9x8#52$kKuX_4W0{%}1)$nwyL4+m#1I(xIrNnTOb_M;#SQC2GuGhZGeGj;Fx#BM= zDmoug;sl5fV9@z*<`zu1;EboiGvX20k$qD0<%nv&-_<6fCd!K{;y++)+~@kK$*H=; z3B*#F5q#&z$U!S$1e2?6Cqe%c_;F2?9~J(aFBlBz(}NUL*6&`!8I#xS-`sJ>+XUa) z;VZ{8M4d7@9v@#|0Hgp~#1&Ya!g}SVJ%a&|#uGR^-qENjux{ zzZQR;audnr?*`*FLV^#gxUmWE-9tm7?zFeSe|F*OMxH-0d1_I0&!Fzs1Hao?HS<3* V?it(>0RMJFK~_bk?4@bY{{ilgTwVYG delta 13156 zcmYLQWmr{Rw-sJc5K%%wL(9F1{P$vq`}bvh8$Y&_k%lnojlOv&B}1$# z9YWtaFJF6pohaS7veJV<$fHOb16RyoY)V@Xq^0ul-h8FVz6`$O%+taJ6EfPhpLg}> z7eZ|{j#tm`c4(B3&zmnVojomI_~*m@C>xwRkmOB*J8QG&SFFWriLLCTeAqa6(rPpU z`96WclA<_v_V&$Qv$iIokJR34HPjYvrJIhZchT7>k-+&7?|@qM^b@9c?)F zbp(E;c6jF{C6z02Sr5-q7;Hp434c^##KOiYQ*FtG#W3iUYcjoi_l|2}R7FwfWU}FM z8@hf{a~Z@cZmPe|!6@l{!{-&jS}$KhZy?SUT(2WdDmIjArpSKvTC5atU94NaGE8a3 zg{UA562QSDdXvy{0=;7>qQxcsh2t|kJj^;MwA#iyXfcyNHZZNCsx>Pvaossp?(FpW zFL<0LToNmT2fssA)t}&Tlv?itC3zI*4sf@a)YTt<@>85Q8&Pr5*YP9=eb%H_tN^Pgb6N=@Bei_-kHmM zUTrp#eO%{Q;(1{jRcyxtp2Xu*1G`u0OiGo({-@p{p3iA3&F7!QIpYx+0?M|33LbgH zoW?rxhAGD=R`UJX&LyeB*@i6R1F{E|naimj7wgNXvm=otT5{gt!-NEJdS;W|Ia5rf z*1Pl73P~=Lu}$Bb+Y`9um)9nn(9>CRCY;*SqXfx{n8!ozbFuvUWJ4(;D}(l*BdngM65tcjl-gsF!bGa~ zWwQw?tdUnShlAG}xk_2=?yx*=1^tn&Cse|h%gN4~0Mk0vL z&EOC{9*s=4&iejse40;$BER>UBuyW#gHhQfL3jv)x&pNm8$uZahh)A7BPb7g=bEEC5aPSQXywB;W-e*&CN%7En$*)OtP6b|<`Lgt~ zA9%AV#EYh@*tHukj&TVRY>VVD+tw5D`BF5{q+L5t65uU~vJyIV?sYx-rZOQz6KDBC z(7r!TVdn9k|Gyc^tf!hCZ^IE>BZ*2HaUOK7cxKX^lr$@3FMPcphQ25K$<`EFUs?Oo zhzx(K*mU;MVUZZKzmF_wtfV6y`1M#mR%p8&>u6rnEg^f(#-lM6g?L_z$+Fc{dQQ=KwW7a;jr;r9}!6p0!b4gPAlmxX@E0(GV_prwc}OV>wy||p%J&E-rAkI=5fNh zh6cTjQPTE^*H6x0&i!5YgtXN(TwB%P8paHb8t+7~ux-h0N?^0ROjkwNRQ4CgJ?CMO zNi*3xhbKrJ1@`w1HaoT9Fh9e9MhxC0MLoS+aY%rq*J+Y$ksSvW zj{_nrcIV^otb7()ZfimVq<;GaNQ@6c#DrMb+#u0O%fH||i$FeT)X}+w0E`n+yRqY* za?iQT;?#6|26<^*k}x476J2?W?qIR0e)oh=McVG^9c-LMrawCIg2wXwnm9p0K^#JV zzeR-#evBnR&be)J_r+G}f zku4~*vl%48Z$pq^3{kZ-jo3w+)lG|A>JtknuX=G`{09X2X{u0&p6 z4*nPvt2U968vEy-Xl36EWv8}jwSs}h_3ZCzIkeai4xZsR$uB7@zaFX(%BHmJ)4lR= zZ~KDf*`8cRh`!u$_H9DV`4$vZAC0~ayh`QjJ7Xu1{vPCkmN$O4H`_zp_v6k2p|KaDDo%q9|VLxW%L` zl^Y9k+m>{X%W0M;#&h4_lBWByUn9t+L|<|F{JEt`K)r_)Dj0d5xMUp<<9jOw4 zvXW^!K3~vPxz!H0@cCr_K&3xka#HbT+oMk8>+%<(teerl=;fxzlXyKZblp$1M)uB$ z&nwE(7u%RsV7xNVsM`pVNXA=yS9mxLzV7MmfCha>B!*}LXY zD#lO>Uc=qS3?j#eZR_)%bKfYSz!zNn+!oWN`fZLA@y0<4ucTjiOXA?dxMaAPHHejK z3Y=%TyJ6=m0`-p2CYj$&q|*#H<@5W+nsA3+X8Uv^Mfp3petz}UriT-!^T7&lXrF>a z9>oFwpPf#FcrbituD#9K^K_P%w#aiiI)z`v7->J4N=u?_`SA5)M^&Wm(n)2c2Ah@bHb2?H#S-(u2)otZ^;qFAHW zt{)-5m}k?8;aEH1zb~e=3U)%0&0f_Y7X$R$+x`j-1-kcsS9%&J?y2s1TtF?&m|quh zKQ_jxIQI=EKIqO z+aI4C5@o%E+dmVf4lJT!-jap5-+DWQJ@@gh0z6L%eYsJ09uif0^`~CXo*qxQ<8*v| z;>L6F{q-Hn6UrIbD+xtfP-&L|q%kuNX5XHvvULAD`7=XNqET&s+;Jm)|CEelmRt2TxRBupu-P&pRF5Sp_vJk*YGH0! z&9h>^p|5I&FYw{I=#R)I-BGeiC(EHQFNZ(f)XsNip8KUfx9r`m`MW-?*d7cWSzqmI z%w^=cX=`hHUM=;+#LGrkyMash)%)wuzY#euGE>beIkE=bJNzy-hV4!3*-*sODyzA1 zKd=6S<)W*-uRbMB({v6hRRn?(5)M!7%uXu{*U=uUB9Il*YsASUd&Tl0L!Ift67xNY zq=b#p7ohmO2ME_eEB_Op=dxIAchX=`boqPw8iHwz_UfeC7hc4IZyn2FsWKvxhi2;M zqjFpOY!H?_QG4h=?K{&5#fjA)1`r2ViNuvv``*0!xN=ZjvGGu$*G!cdgpih_6rK8m z_#WIviR|HYMtA1nz~)H3^LO9Ez#^-A3|ri;;~KkEHZ76Ke(2=g$ButPG@W?kB%0f2 zYpGs`!VcXWpCjfqvL7=_(uiQl5brL&l_Ox7i69yOhW7P^LERxsi2`543l0vPY$y5* z!nUayQn1x)3>anwqVy`s?w?MZm&_eSrBK~Un8eq_>ltm8N$#u`Q;8O zoVRb^wn1&q-BfmODpEeoKOM+^@*fz?RY9o5>RfF6{?V84;l=F%WQ`ZPb0o(-ff;u+ zAFN>!dq3`OfZ#_bf9IuAz-sG%piw5u5-W#;Gq6;z%iicLej(4L2#KvG)WxTj>eAd? zd((B~JS35@5Euk-lo^!&D4vb}JSH~C!+CDX`XVu<3~#ckXu(J*L9U9WKTXN2pz`P z6`}fcFB;B`C&r5eQtbysiZoxZtsse(@|XL%=iJQaJ~c7`JgLG5g;5jgN<_|MC5wFZ z+r4@3(dZ>Dt?ROiBHj23DHWn}~L7j95Pxw6?!<-C%s`Oh2xBTh7 z&FB5Qle@mTqJlZ0w=ctT^3D3LjoQTcDotK3_?c!i8A{#3!ON7BgfnO#{LFJs)oEl1 zp{zD3(fJuz)9G`yBH%8Dc4(gdxZ$L=(}dc?C7Av?^QjHNSIL-(?)=;t%-Q%iwBqa2 z8vuebWoS6rEA#otuyJv5Ne2c0hMJC)HJ#2+Fv^VW{_p6XwoHNi9=e-^7YiW*DNdLBwi+$f zN=0TJvb@IM?wD*ho*ra`r8xibk+ZWeh-EidJFQFDju*dj*=*x=lz97hGq@`xSY6Vz z;vIlU4F)KUHOdIzTH0PtPA(^u()n;X)_HTbmN>^`npt1b_jjgj)RoRi=~rqmo`#x( z-YD}K4V@;V5D9dkm>Sl`%;n{JVO^DwJK-aT#qg&+Od9dv4FP4~4d2V7h9{>h z&7DyK&92*L`&7KhSBs(6iDcn9Ie*a%}bNnW#aREkCY*V9rW((r5t(rNlg+ zin8AW1x&58&?0Kh%zT>p|I#$szm86Qc{s~5hQkuI7~BZx+^SOu#F#3|D4Ifbl^3s_ z@0ra!dr293|GuhBCh3b~!^H4hhiy z!1KjiLgKSkCO7D9zKZM@oVJkk)l{1{xS}w#1?=IEQd*+kI!BoaUC2Y8=KNgk$L?@O z_w?TL!c4fx6P3f~mm>XqW3!L6?7i+^HR^9q%Nh5VnmXOLdwLTaN0|nM6nlwD{U?ye zu_2Tb^(KQ%H_3hC+U#NaHCe`%NFs%~Cop*jurMuqNCFW`Ekkpew3_0&vZH$~d^;k* z2du7#?02+mmCaHwCmUPCS;fdJDtnkt%6g}^i3uWMF*33@Jw2!{XCThj!zGmY&Lm(l zDvn{VH!A1u&_>?{EI(Wnkjtg1S3j40Z{BNi=#K++T@RLiz->7#Yh@1|+WPqZ`Rx0+8IB|P_}Aj5XjhoI0Wu~$owgF1LY?91xzoCkb z`%ceLm;xUOb6xx4M%G)8+G$rIlQm$e+e$=Mm^%IIS4nOGu}xY3eT;Ok)qR`ac58A@ z*h*u!L$Mdfs%D|)-GtL$ZeEio4^GC51Amn9Y^hR3%a;=b2Q#KR&$BCvLJ>UF4~Ric zDqvW)@y^N`#-T=)SBc!uA`uI{OdyP@GM*eL_1e|8PDM6BZRB{rkLw_b9=ufAXg_v?moi zp|hzo6Yx7#^zS$5UZ{|Am@GlK$GVDfBBHqcAb{Db3Q^fA8UE|u^Xzul8L=f2)@ zFJ(3^bM>&-)N{>NSqrUO<%3?QE&q^Dh&`N|fUZVTcTn5bbd)JAm>{TvAY=z+U0>RV z37Z|6<~)dv$(HzT5t3%Cnr%LDj6Cbw6mu^Qr1G7+1AUC;!6K#k?G14EAHfLJr;=iA z#!yOuG;8E98HTj?_>+7h>gyzISlIPMrMxE`-n0j{fIp>2DK=eR9@abTJdOzeLagfp z;3tug(io9SjbsQ&0;f<3XpR|kI_l%KxMy3Nvsk@juvGwU@DFd(?1v2UzW_O>OJh~J z2fcN;`{2O?ZM!ZCMk-rrtCY?=8BsBJ0uARsd#vQ3}%t^oR}t@9s4;)z`JY_ zr!7HrlKlyVFy9MqeEJbw|GN~!0*u{OH{(-Gw&}uq0AHt#nA_{#kIM+X`0)O$IP)0^ z7Cvnv&hyx4Z9dECUJJ9bDj?Wo!Gpu8h{ifc)Hr?YW6w!_T-=*I!>OTc&^Maqw6y=4 z#bj05QTRl^ZUCEh9rn@7>q_8N$i=~a_g5tS`nQ*JICgd%&5O53U`=>9cpXZBo>*#7 z&R6et08(6w+A$55?8Mo?EBs%VSoLd%9I$JExyDTB%EH0Xv}LZF$mQlx|J-u;T_X$r zDC0X-Eohd5b2m%KegpNJ8-pxxfGqj;_LIMV#D-Fg)Ow;+;f3PZAw;E#Qgq=-Kt@)8 z)KrcQ1Glu&B>6atm`)jdT3-R_5aFLn7fie zr$a%{Ky7TIxPiw>pCArHy8vDOlS%!%1Zf$odzGc%j`5GRDi-lF-=%R02rx=qZ%>u0 zywAzUT)xL3dI+V0(*&^I4&Fi0tKTo%*?Epr{?Pci;9KMui<=)TxpcC906yxwmRtz- zG19A}3FY4!=Xvwa?=aarxyrOO@4^!`m9)`2uR&=hdx~*Aa{xSvWSK7AvYe@EG~JpU z%1PDP;Lds#Da0<0ZRvjGau9E_NLum+q|YlXN}g(_O|W@~%k`XUvzaQHteQAURon}w zjnQ(GL78AO8GcDy4CuXvL)r1VpRbaII0kZSM)=C z!xC@&Y=%|wS;E8P2)dGnwRF%Icz*tooG2hsQpRfMkhA%5b#AAewQ;P&(Ado7E9gjj0bcY9oM*L(5R|*=8 z<=htATvo>O&vZI)N(?grDrF42{qe?%>I&H$&yvd+a3b-!L&B7s`w7{E?u};U&6Q68 zS@{Zxi7nb>dV2b(0b%o{)?1of#^lQ416n?%mj)}(75V@$U=+ejW>LGz(qP~SMwt{* zzBtf;RtVO*O}s)Y!zGh|K-?JA0mS2C%4Ao8LFmst?f&MTHpL)G z*Uj;g#*+`qLF!E4O(f64$F$>#eQ(M?7O65{9l&o>z2gHTbwRa5_uz%^BTS2ipJ3zg z$kE8v^JHaZMGc4^z9&E$jaO@!lW^=U>0U+d@jQ3~?o7vRLj(BA8u~QVW~Y`)%shFr zByyNXL2qcfl5>VsW3`P2a(raJ(2Vw;&Xu*Et4b3;B0iV zJ%=i@{p)tR6|eB;&A7Jqa8>%@lV~b;^~#O#@~m4bNX500@7DdA)#-Z2kqw+=t1(3- zyU)6L-u=k}VFLG$w^Tnt+FR29_L8w-{lVcOC;A}vwV&Va!2wU155fXcwX*8mL9@DT zX{${)nbNDOt(53|*qchajSRSggc&!P3{sgtWL_RJyIXU5au8+rfNqW=tHhEKWvz@*Hr-KgYTyf}n{q?=K z(cQsf`WcXnS;~kX5>T?8-PLx)Yn}>Qd2@Bg#DT;U;q1EH3e(u_%Pp`5GS36Eooq)D ziB86S(ld{tbXI83cdP*3&FMf)N)tYZeI7r386qU~vd;RvXY}UtGVR3E$2!mVN{#P! zZDodT3_CrjKOSU7hF9y4DJa+q8e>xrrb0b)&Xi$+8q;6oAnz6l93CgtY=-7**KVpT z%V|T?;=_YJ|8IpB6a20SPOVFw1yOfGhI@iC^b!*0=hZs3+bz~`6*B3ovv8i5_M#?Ka-Gr3EI#y7iH`E+4zFpxRwW_6s+yV@ zbUbU1M3r2dtvyS{RD=|43UooE!rL|;UaFwrTgRL5)%V$rv$N*Nqk)4jMac(n_8xX* zxM>-;_26fj;NqJ7uI7eoul|{*9vH)wGn_qxhrj50h`rhY*a%*ManyvATGo%f+@^C} z+R|O=tuLG7G>sL`68rjcV9Addp!aA@OUM_uY*7bflD2&x`$ARsbanjENBapM)7@^I z!O}4&wV62{{O3)U@nR+eG8f$Yh}2*BM4u`ot6*GX-d{|!canvVx1&u5r&U*%t)JMu zsexw?xhgIO>X+wyUOtR`@00FKUuufJu!bdHY{u%|)pfy8#a}KyThOCu{Pl+290~1O zj*%0oLNjEMu%|Q1>J*C1zblrXSbcrsgzGxP<|6<+N=%u1D^LhQ2jqKndc zR4M?~H`{xnO*aPl2ZhEz_akSXr}D*j_C^o(ai-=_m(zvNf%4}g`P|9q#Ru!Np|CbI zPJ9G1KXP1%erOt9UzaQbshlofSab$~BGafS8d1kGCJpWc6TYD-oFP-M?Py);t@NQm z9*3oFGHFp@TYlT{#^sw?-&v1wK`jk6jq__D_|r8ZD_axT<6-(&KnBvk3orakr)zaq z0=ax}{N3Uc5#~USr5;-~*GZ9gEy&qiSEIJ$e=s_cB*yRGC}`EN$&nnl)l(BgLQ~4? z^9jx0T+S5b;L(>obvg%se}8x17vXt#a@OEYWI%$p#lHLCmuM)_vzCC8Xi`g^%;`<# zp7uYGH0YSp6yTIOe`d}Ux|E?De-Yx|lpz#NipX6|*(AFDT5h77Ngah1IEn-67-M)N zDU9ELL2;(^rTlJ+80qVTjmUiaj7e$)Vk=ohC3%Vc*ix(c>f<0&)=QmaKxnmSf|_wv zV9);!h_pAgG8gmtj4iK7+p+o!Mlo=S1VStuArrdSK#8aGMZffxG{!fa=)yf-;b7aD z|5@Si4%u|<^$6`7y``yY55OO? z(7(Pa@p?eE`bG}>gN~`rxql3K5eY(7{BAVUBAd2OX;v}3GEQMD#I2Tm|4 zm;i!<(!OGdH({@#d5r`{Mg_TVp$r4N32AF&3`vT*g|fBRlT-f1xATP2sdBl#%Ek> z%bn-Bc@Uib^0MJrcd1A#Zto*+Nej46(ZbyEv2b>fmsCK+Zor$5TW_)U_HACXMZRu- z3B&ChhH@+y7M<<$4H^ifZ`)C(Y+sBJaK1m|_>Vm=hcnAsJ|g`!^{Fi$x84`$rJg&|wtLKc5=aUJ}zW@y*PsY_+p;ET@?sr>f zu>0}a&k^4A6Dc*L{nwn|L4jhoT~>!b@H`0e>L1u+Wo0Q&;-P=5n6S;J+zpzWM7%&J z6Vh*8i0tHsj@=zRPlX~=vo+OjuOfQqO7ov0*sivs&JQ_9!+I7uy|Y09hg7jd3yBH*Js$}(e@uN)7^hpS*}U;NUA0TC)WT{Z$2zmDt7h~8SrdUT_w`j9K~-ju zTyKXgaNI!BZ_yed(aW3?*Neg0N%!-@3JT=r&v#X=jKZ7EWQS&BX^n^?^yb~KJ%J#9 zMvm#A`r-~d1%LVmYlp0EhDlu7SY24S)p<1qR(QE57~QxoLHGg**(2KtcO1f)&1SeYoLpx~%;wE}Q_YrOhr zh()if-=Qw6){%}%@vlpJc%hExScrhzxbPuwksrVmSxI>Id*?5Q8F(e&aWb-U>Itc`d7M9A4mPwg$dGA0R1BX zGuJf52!>lsg|18LcR4uZFB&W4gY$+=;N;cwvyOTXGxIpi4w{g)iQ{A(UL`aZ4@|j{ z!hgt`3VavnfgTtJ-pdHzsm+8e)bGSQ`P0)CH+s8X-DyK@%Ib6@vWhTdL{)9s@&jqr zSGM=!oP!6>B5wwg1$JXlJN731ce(CjYIYVdNj^Pte(!RX5hn5EcZu!@NAT#B+Jkdl zw%gYhQ$=~XYDQ~_?-fNXEIm_Y`8rlEg~HWkqSY3jJN{#?M&|Y`&tJ1ZCjmGrRi-1O z=EXk=q@;&26DDO^AUaXaHx{mtsk0CyrLP}V;vn@3uie>1#s`I2{xjeSew0k zysi52@dK7KcPH%!jdga*1o2eiYUtC$LEY=Q-Dy9XyXz!iqDq4WL`NnN+L}S`4F7l; z9Asmwire;h@hF~`uuGa>Vi9CBVnYCY!m2@^kGqP*ZHmJ71MxDDO_3vb#yLJdLvNmF&&^<633 z4Y{|!Th0m|Pn-YWrzFupa30t5t$*?mKv*gB@}icik*+2e_aG>4$`V5zFfFyjJ z3vplFd(LF-H_rF0fbRr_n&rmnxCY7t#V;`T<}HX!`ZN2h%@p?_xwj3 zFAFuC#ZKNI6e#}#(hKhyrgx@a=Ejc$MFvu%T{Z3{kQ_O$>W2)!L3v^s2K5HBZa302Zd9!VEy_<1QG+g)3-Aw6xhN?op zU~*lko9d+LDq>KgQLZ=qJ=iaZ|OmW>Tx)683Sj{**z(RuAMO_5XSX+-( zS=w#ByO6-i&(BYBJDJ)=Z@@)Cj$r7OgHdFn)d;NT7>yhzXZyeh*w7rsVe^wK2s2Hj z@k~#9dWk{g($4d=@f4Vxk+WuHHAjf$WfiuEQJuOs*xNicD;%DM4t zXtaWprv{Hf+^^gLtAC)uaP^q9ozC*8wQ*{}l&h>TNn+~9Oh z1#Xl*TH(+cWNua4Sw0>gskn_fVp?$LpDEkNLPntB+fD5yC_s+l$g4_u2bl3!lT>tT z2{boJkn|b@wHge1e?Na`5Zy8^bw{0`EfeMhkIzAi2aHdwN}Q$H=7Kg}49Z3MEQ(@GRJvB4b zynnGNaCz1}Lv<8`c>x|D8vw;pMG^^qglcfT-S#xANk3PHD#0S|zdzM!>B0@Ee!CaH zAaIJ(xVC^4z#+ziVS_p^*ym=gAUbe17#C00q^!NAKWwM5I=4nJSt_$+hrTaX*L}0- zronJ57C(lHl7g%nPU>vZs5Ny0rrCiS+;v)HvmojhG7FE@r?$j9b!dKAnlepa4JUkZ}}=?y;V zhH(##WM^9MOd|C5%Zw&GQ8QM%@-xnFI>DQM@nBV>U$HLyxV3k&S%Mk2_@K|vkr9j7ob7Nl`+1V%lOLe=@pDPX14A8jI@=R=Lbb!yM|yGe)M8UYvq zn0=Ytr9dSk#AK-6jOrbB}d$+UG_u^R2 z{gBM_WXcF=v#t8wY%vn7|E!=6!^q8~kB-BvYO^#~2i9*7v^lRw!a4uV?pJ`AalbVc zXN2))nU8GM98upP4hT&M*%i? zArj=QYg)f4ds5i^3(o2PiASD5_U}fQLxjL4nn~$aYAU~Z#edZ%_zuKO3~C(7N{8A( zpU5to>720bvL(CedA^#8Rb7uBtW6(dmi}Kq{&Sz=8J~)y+8A_wToQJ!yv30CXKHLWDkmO&GtEyt#Ee_glwq1=i3!>`WEfX! z?MFic!?%J)^nl0+bk?p7ecv?r_o@-GVo{QJuxLAeCPd*ou&`52R$C_Kc$m}&GS(E!l?W(cI zHR_}?)Z;Ghzad~J0l!H=+Z3R6*^%t*R9K$a3KUvN^WVF^+{8VhT>l0k5AZD6WqGBA z%6bab@3Ff4j`=1st(vfZqP#V)$oDAlKKf)w+&qR6fS=!xl~Ir`exdjI Fe*iv Date: Sat, 1 Aug 2026 16:15:09 -0700 Subject: [PATCH 012/330] [Flutter GPU] Raise Dart errors for invalid render pipelines and memoize per-draw pipeline state (#189899) Invalid `gpu.RenderPipeline`s currently crash the process at first draw with no Dart-visible error (observed on macOS/Metal with `impeller::PipelineDescriptor::GetHash()` on top of the stack). This change surfaces those failures as Dart exceptions instead: `createRenderPipeline` rejects wrong-stage shaders up front, and a failed stage-function lookup or backend pipeline build logs the offending shader and fails the draw rather than dereferencing null. Since draw rates reach tens of thousands per frame, the draw path also gets cheaper rather than more expensive: `RenderPass` now memoizes the built pipeline behind a dirty flag, so consecutive draws with unchanged state skip the per-draw descriptor copy, hash, and pipeline-library lookup entirely. Includes unit tests for the dirty tracking and the stage validation. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- engine/src/flutter/lib/gpu/BUILD.gn | 1 + engine/src/flutter/lib/gpu/render_pass.cc | 62 +++++++++++++++++-- engine/src/flutter/lib/gpu/render_pass.h | 28 ++++++++- .../flutter/lib/gpu/render_pass_unittests.cc | 26 ++++++++ engine/src/flutter/lib/gpu/render_pipeline.cc | 47 ++++++++++++-- engine/src/flutter/lib/gpu/render_pipeline.h | 16 ++++- .../lib/gpu/render_pipeline_unittests.cc | 40 ++++++++++++ engine/src/flutter/lib/gpu/shader.cc | 4 ++ engine/src/flutter/lib/gpu/shader.h | 3 + 9 files changed, 214 insertions(+), 13 deletions(-) create mode 100644 engine/src/flutter/lib/gpu/render_pipeline_unittests.cc diff --git a/engine/src/flutter/lib/gpu/BUILD.gn b/engine/src/flutter/lib/gpu/BUILD.gn index 50e6f07b24cc2..18a0ea4edb18e 100644 --- a/engine/src/flutter/lib/gpu/BUILD.gn +++ b/engine/src/flutter/lib/gpu/BUILD.gn @@ -89,6 +89,7 @@ if (enable_unittests && !is_fuchsia) { sources = [ "command_buffer_unittests.cc", "render_pass_unittests.cc", + "render_pipeline_unittests.cc", "shader_library_unittests.cc", ] diff --git a/engine/src/flutter/lib/gpu/render_pass.cc b/engine/src/flutter/lib/gpu/render_pass.cc index cbe5bbc12dd6f..f085424e27f07 100644 --- a/engine/src/flutter/lib/gpu/render_pass.cc +++ b/engine/src/flutter/lib/gpu/render_pass.cc @@ -12,6 +12,7 @@ #include "flutter/lib/gpu/shader.h" #include "fml/make_copyable.h" #include "fml/memory/ref_ptr.h" +#include "impeller/base/validation.h" #include "impeller/core/buffer_view.h" #include "impeller/core/formats.h" #include "impeller/core/sampler_descriptor.h" @@ -39,6 +40,7 @@ const std::shared_ptr& RenderPass::GetContext() const { } impeller::RenderTarget& RenderPass::GetRenderTarget() { + pipeline_state_dirty_ = true; return render_target_; } @@ -46,7 +48,7 @@ const impeller::RenderTarget& RenderPass::GetRenderTarget() const { return render_target_; } -impeller::ColorAttachmentDescriptor& RenderPass::GetColorAttachmentDescriptor( +impeller::ColorAttachmentDescriptor& RenderPass::ColorAttachmentDescriptorAt( size_t color_attachment_index) { auto color = color_descriptors_.find(color_attachment_index); if (color == color_descriptors_.end()) { @@ -55,25 +57,43 @@ impeller::ColorAttachmentDescriptor& RenderPass::GetColorAttachmentDescriptor( return color->second; } +impeller::ColorAttachmentDescriptor& RenderPass::GetColorAttachmentDescriptor( + size_t color_attachment_index) { + pipeline_state_dirty_ = true; + return ColorAttachmentDescriptorAt(color_attachment_index); +} + impeller::DepthAttachmentDescriptor& RenderPass::GetDepthAttachmentDescriptor() { + pipeline_state_dirty_ = true; return depth_desc_; } impeller::StencilAttachmentDescriptor& RenderPass::GetStencilFrontAttachmentDescriptor() { + pipeline_state_dirty_ = true; return stencil_front_desc_; } impeller::StencilAttachmentDescriptor& RenderPass::GetStencilBackAttachmentDescriptor() { + pipeline_state_dirty_ = true; return stencil_back_desc_; } impeller::PipelineDescriptor& RenderPass::GetPipelineDescriptor() { + pipeline_state_dirty_ = true; return pipeline_descriptor_; } +bool RenderPass::IsPipelineStateDirtyForTesting() const { + return pipeline_state_dirty_; +} + +void RenderPass::ClearPipelineStateDirtyForTesting() { + pipeline_state_dirty_ = false; +} + bool RenderPass::Begin(flutter::gpu::CommandBuffer& command_buffer) { render_pass_ = command_buffer.GetCommandBuffer()->CreateRenderPass(render_target_); @@ -85,6 +105,7 @@ bool RenderPass::Begin(flutter::gpu::CommandBuffer& command_buffer) { } void RenderPass::SetPipeline(fml::RefPtr pipeline) { + pipeline_state_dirty_ = true; // On debug this makes a difference, but not on release builds. // NOLINTNEXTLINE(performance-move-const-arg) render_pipeline_ = std::move(pipeline); @@ -105,6 +126,19 @@ void RenderPass::ClearBindings() { std::shared_ptr> RenderPass::GetOrCreatePipeline() { + // Consecutive draws overwhelmingly reuse the same pipeline state; skip the + // descriptor rebuild, hash, and pipeline-library lookup entirely until a + // state mutation marks it dirty. Draw rates reach tens of thousands per + // frame, so this path stays free of per-draw allocation and hashing. A + // memoized null is a build failure that was already reported for this + // exact state; returning it without retrying keeps a broken pipeline from + // re-logging on every draw. + if (!pipeline_state_dirty_) { + return memoized_pipeline_; + } + pipeline_state_dirty_ = false; + memoized_pipeline_ = nullptr; + // Infer the pipeline layout based on the shape of the RenderTarget. auto pipeline_desc = pipeline_descriptor_; @@ -112,7 +146,7 @@ RenderPass::GetOrCreatePipeline() { render_target_.IterateAllColorAttachments( [&](size_t index, const impeller::ColorAttachment& attachment) -> bool { - auto& color = GetColorAttachmentDescriptor(index); + auto& color = ColorAttachmentDescriptorAt(index); color.format = render_target_.GetRenderTargetPixelFormat(); return true; }); @@ -146,8 +180,10 @@ RenderPass::GetOrCreatePipeline() { auto& context = *GetContext(); - render_pipeline_->BindToPipelineDescriptor(*context.GetShaderLibrary(), - pipeline_desc); + if (!render_pipeline_->BindToPipelineDescriptor(*context.GetShaderLibrary(), + pipeline_desc)) { + return nullptr; + } std::shared_ptr> pipeline; @@ -179,7 +215,14 @@ RenderPass::GetOrCreatePipeline() { pipeline = context.GetPipelineLibrary()->GetPipeline(pipeline_desc).Get(); } - FML_DCHECK(pipeline) << "Couldn't resolve render pipeline"; + if (!pipeline) { + VALIDATION_LOG << "Failed to build the render pipeline. The vertex and " + "fragment shaders may be incompatible (for example, a " + "fragment input with no matching vertex output)."; + return nullptr; + } + + memoized_pipeline_ = pipeline; return pipeline; } @@ -195,7 +238,14 @@ bool RenderPass::Draw(size_t element_count, return false; } - render_pass_->SetPipeline(impeller::PipelineRef(GetOrCreatePipeline())); + auto pipeline = GetOrCreatePipeline(); + if (!pipeline) { + // The failure was already validation-logged with the specifics; failing + // the draw surfaces a Dart exception instead of crashing on a null + // pipeline in the backend. + return false; + } + render_pass_->SetPipeline(impeller::PipelineRef(pipeline)); for (const auto& [_, buffer] : vertex_uniform_bindings) { render_pass_->BindDynamicResource( diff --git a/engine/src/flutter/lib/gpu/render_pass.h b/engine/src/flutter/lib/gpu/render_pass.h index ae72c0b5e6b6c..154558ab906ad 100644 --- a/engine/src/flutter/lib/gpu/render_pass.h +++ b/engine/src/flutter/lib/gpu/render_pass.h @@ -61,6 +61,14 @@ class RenderPass : public RefCountedDartWrappable { /// [indexed] is true. [instance_count] is the number of instances to draw. bool Draw(size_t element_count, size_t instance_count, bool indexed); + /// Whether the next draw must rebuild its backend pipeline. Exposed for + /// testing the memoization's dirty tracking. + bool IsPipelineStateDirtyForTesting() const; + + /// Clears the pipeline dirty flag without building a pipeline, so tests + /// can observe which mutations re-dirty it. + void ClearPipelineStateDirtyForTesting(); + struct BufferAndUniformSlot { impeller::ShaderUniformSlot slot; impeller::BufferResource view; @@ -97,10 +105,28 @@ class RenderPass : public RefCountedDartWrappable { private: /// Lookup an Impeller pipeline by building a descriptor based on the current - /// command state. + /// command state, or return the memoized pipeline when that state is + /// unchanged since the last draw. Returns null (after a validation log) + /// when a stage function cannot be resolved or the backend fails to build + /// the pipeline. std::shared_ptr> GetOrCreatePipeline(); + // The non-dirtying counterpart of GetColorAttachmentDescriptor, for the + // pipeline rebuild itself. + impeller::ColorAttachmentDescriptor& ColorAttachmentDescriptorAt( + size_t color_attachment_index); + + // The result of building a pipeline for the current pipeline-affecting + // state, null when that build failed (memoized so a broken pipeline is + // reported once, not per draw). Every mutable-state accessor above marks + // the state dirty; consecutive draws with unchanged state reuse this + // directly, skipping the descriptor rebuild, hash, and pipeline-library + // lookup. + std::shared_ptr> + memoized_pipeline_; + bool pipeline_state_dirty_ = true; + impeller::RenderTarget render_target_; std::shared_ptr render_pass_; diff --git a/engine/src/flutter/lib/gpu/render_pass_unittests.cc b/engine/src/flutter/lib/gpu/render_pass_unittests.cc index be4a75f4b56ea..5a5b22045aea6 100644 --- a/engine/src/flutter/lib/gpu/render_pass_unittests.cc +++ b/engine/src/flutter/lib/gpu/render_pass_unittests.cc @@ -26,5 +26,31 @@ TEST(FlutterGpuRenderPassTest, SetDepthWriteEnableHonorsArgument) { EXPECT_FALSE(render_pass->GetDepthAttachmentDescriptor().depth_write_enabled); } +// Draws memoize the built pipeline until the pipeline-affecting state +// changes, so every state mutation must mark the state dirty (a missed +// mutation would silently draw with a stale pipeline). +TEST(FlutterGpuRenderPassTest, PipelineStateMutationsMarkStateDirty) { + auto render_pass = fml::MakeRefCounted(); + + // A fresh pass must build a pipeline on first draw. + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + InternalFlutterGpu_RenderPass_SetDepthWriteEnable(render_pass.get(), true); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + InternalFlutterGpu_RenderPass_SetColorBlendEnable(render_pass.get(), 0, true); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + InternalFlutterGpu_RenderPass_SetDepthCompareOperation(render_pass.get(), 0); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + render_pass->SetPipeline(nullptr); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); +} + } // namespace } // namespace flutter::gpu diff --git a/engine/src/flutter/lib/gpu/render_pipeline.cc b/engine/src/flutter/lib/gpu/render_pipeline.cc index 8402ea8fa5100..342a5a1d3c253 100644 --- a/engine/src/flutter/lib/gpu/render_pipeline.cc +++ b/engine/src/flutter/lib/gpu/render_pipeline.cc @@ -11,6 +11,7 @@ #include #include "flutter/lib/gpu/shader.h" +#include "impeller/base/validation.h" #include "impeller/core/shader_types.h" #include "impeller/renderer/pipeline_descriptor.h" #include "impeller/renderer/vertex_descriptor.h" @@ -43,17 +44,47 @@ RenderPipeline::RenderPipeline( fragment_shader_->GetDescriptorSetLayouts().size()); } -void RenderPipeline::BindToPipelineDescriptor( +bool RenderPipeline::BindToPipelineDescriptor( impeller::ShaderLibrary& library, impeller::PipelineDescriptor& desc) { - desc.SetVertexDescriptor(vertex_descriptor_); + // A failed lookup previously flowed into `AddStageEntrypoint`, which + // dereferences its argument, crashing with no signal about which shader + // was at fault. + auto vertex_function = vertex_shader_->GetFunctionFromLibrary(library); + if (!vertex_function) { + VALIDATION_LOG << "Unable to resolve the vertex shader function '" + << vertex_shader_->GetEntrypoint() + << "' from the shader library."; + return false; + } + auto fragment_function = fragment_shader_->GetFunctionFromLibrary(library); + if (!fragment_function) { + VALIDATION_LOG << "Unable to resolve the fragment shader function '" + << fragment_shader_->GetEntrypoint() + << "' from the shader library."; + return false; + } - desc.AddStageEntrypoint(vertex_shader_->GetFunctionFromLibrary(library)); - desc.AddStageEntrypoint(fragment_shader_->GetFunctionFromLibrary(library)); + desc.SetVertexDescriptor(vertex_descriptor_); + desc.AddStageEntrypoint(std::move(vertex_function)); + desc.AddStageEntrypoint(std::move(fragment_function)); + return true; } RenderPipeline::~RenderPipeline() = default; +const char* ValidateRenderPipelineShaderStages(const Shader& vertex_shader, + const Shader& fragment_shader) { + if (vertex_shader.GetShaderStage() != impeller::ShaderStage::kVertex) { + return "The shader given for the vertex stage is not a vertex shader."; + } + if (fragment_shader.GetShaderStage() != impeller::ShaderStage::kFragment) { + return "The shader given for the fragment stage is not a fragment " + "shader."; + } + return nullptr; +} + namespace { // Translation table from the Dart-side `VertexFormat` enum (encoded as the @@ -297,6 +328,14 @@ Dart_Handle InternalFlutterGpu_RenderPipeline_Initialize( Dart_Handle buffer_layouts_handle, Dart_Handle attributes_handle, Dart_Handle attribute_names_handle) { + // Swapped or mismatched stages would otherwise only fail at first draw, + // deep inside backend pipeline compilation. + if (const char* stage_error = + flutter::gpu::ValidateRenderPipelineShaderStages(*vertex_shader, + *fragment_shader)) { + return tonic::ToDart(stage_error); + } + // Lazily register the shaders synchronously if they haven't been already. vertex_shader->RegisterSync(*gpu_context); fragment_shader->RegisterSync(*gpu_context); diff --git a/engine/src/flutter/lib/gpu/render_pipeline.h b/engine/src/flutter/lib/gpu/render_pipeline.h index a8d1e19032ee2..295574936044e 100644 --- a/engine/src/flutter/lib/gpu/render_pipeline.h +++ b/engine/src/flutter/lib/gpu/render_pipeline.h @@ -28,8 +28,13 @@ class RenderPipeline : public RefCountedDartWrappable { ~RenderPipeline() override; - void BindToPipelineDescriptor(impeller::ShaderLibrary& library, - impeller::PipelineDescriptor& desc); + /// Sets this pipeline's vertex descriptor and stage entrypoints on [desc]. + /// Returns false (after a validation log naming the shader) when either + /// stage's function cannot be resolved from [library], in which case the + /// descriptor must not be used to build a backend pipeline. + [[nodiscard]] bool BindToPipelineDescriptor( + impeller::ShaderLibrary& library, + impeller::PipelineDescriptor& desc); private: fml::RefPtr vertex_shader_; @@ -49,6 +54,13 @@ class RenderPipeline : public RefCountedDartWrappable { FML_DISALLOW_COPY_AND_ASSIGN(RenderPipeline); }; +/// Checks that [vertex_shader] and [fragment_shader] are actually a vertex +/// and a fragment shader, returning an error message suitable for a Dart +/// exception when they are not (pairing the wrong stages otherwise fails +/// deep inside backend pipeline compilation with no useful signal). +const char* ValidateRenderPipelineShaderStages(const Shader& vertex_shader, + const Shader& fragment_shader); + } // namespace gpu } // namespace flutter diff --git a/engine/src/flutter/lib/gpu/render_pipeline_unittests.cc b/engine/src/flutter/lib/gpu/render_pipeline_unittests.cc new file mode 100644 index 0000000000000..f2b205b7607c0 --- /dev/null +++ b/engine/src/flutter/lib/gpu/render_pipeline_unittests.cc @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/lib/gpu/render_pipeline.h" + +#include "gtest/gtest.h" + +#include "flutter/lib/gpu/shader.h" + +namespace flutter::gpu { +namespace { + +fml::RefPtr MakeShader(impeller::ShaderStage stage) { + return Shader::Make("library", "Entrypoint", stage, + /*code_mapping=*/nullptr, /*inputs=*/{}, /*layouts=*/{}, + /*uniform_structs=*/{}, /*uniform_textures=*/{}, + /*descriptor_set_layouts=*/{}); +} + +// Pairing shaders of the wrong stages previously constructed a pipeline that +// only failed at first draw, deep inside backend pipeline compilation; the +// stages must be rejected with an error at creation instead. +TEST(FlutterGpuRenderPipelineTest, ValidatesShaderStages) { + auto vertex = MakeShader(impeller::ShaderStage::kVertex); + auto fragment = MakeShader(impeller::ShaderStage::kFragment); + + EXPECT_EQ(ValidateRenderPipelineShaderStages(*vertex, *fragment), nullptr); + + const char* swapped = ValidateRenderPipelineShaderStages(*fragment, *vertex); + ASSERT_NE(swapped, nullptr); + EXPECT_NE(std::string(swapped).find("vertex"), std::string::npos); + + const char* two_vertex = ValidateRenderPipelineShaderStages(*vertex, *vertex); + ASSERT_NE(two_vertex, nullptr); + EXPECT_NE(std::string(two_vertex).find("fragment"), std::string::npos); +} + +} // namespace +} // namespace flutter::gpu diff --git a/engine/src/flutter/lib/gpu/shader.cc b/engine/src/flutter/lib/gpu/shader.cc index c2ff9954f501a..7923e2caa0343 100644 --- a/engine/src/flutter/lib/gpu/shader.cc +++ b/engine/src/flutter/lib/gpu/shader.cc @@ -170,6 +170,10 @@ impeller::ShaderStage Shader::GetShaderStage() const { return stage_; } +const std::string& Shader::GetEntrypoint() const { + return entrypoint_; +} + const std::vector& Shader::GetDescriptorSetLayouts() const { return descriptor_set_layouts_; diff --git a/engine/src/flutter/lib/gpu/shader.h b/engine/src/flutter/lib/gpu/shader.h index b9429bc5f61ba..49f18966a49aa 100644 --- a/engine/src/flutter/lib/gpu/shader.h +++ b/engine/src/flutter/lib/gpu/shader.h @@ -85,6 +85,9 @@ class Shader : public RefCountedDartWrappable { impeller::ShaderStage GetShaderStage() const; + /// The shader's entrypoint name, for identifying it in error messages. + const std::string& GetEntrypoint() const; + const Shader::UniformBinding* GetUniformStruct(const std::string& name) const; const Shader::TextureBinding* GetUniformTexture( From 6c071c210a8135ac5daa785ac7e7551ed9e20b5e Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Sat, 1 Aug 2026 18:05:39 -0700 Subject: [PATCH 013/330] [Impeller] Skip binding dead-code-eliminated resources on Metal (#190040) Fixes https://github.com/flutter/flutter/issues/190034. When the shader compiler dead-code-eliminates a fragment sampler or uniform block, reflection still lists it but stamps its Metal binding index with the out-of-range sentinel `uint32_t(-1)`. The Metal backend forwarded that index straight to `setFragmentTexture:atIndex:`, which has no bounds check, so it crashed in the AGX driver. The GLES backend already skips optimized-out bindings and Vulkan binds by the stable SPIR-V decoration, so only Metal was affected. This drops the optimized-out binding at flutter_gpu shader-library load so it is never registered, and guards the Metal bind cache so an out-of-range index can never reach the encoder. A shared `kOptimizedOutBinding` constant names the sentinel. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [C++, Objective-C, Java style guides]: https://github.com/flutter/flutter/blob/main/engine/src/flutter/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ --- .../src/flutter/impeller/core/shader_types.h | 12 +++ .../flutter_gpu_optimized_out_sampler.frag | 15 ++++ .../backend/metal/pass_bindings_cache_mtl.mm | 14 ++++ engine/src/flutter/lib/gpu/shader_library.cc | 20 +++++ engine/src/flutter/lib/gpu/shader_library.h | 6 ++ .../lib/gpu/shader_library_unittests.cc | 83 +++++++++++++++++++ .../flutter/lib/ui/fixtures/shaders/BUILD.gn | 3 +- engine/src/flutter/testing/dart/gpu_test.dart | 39 +++++++++ 8 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 engine/src/flutter/impeller/fixtures/flutter_gpu_optimized_out_sampler.frag diff --git a/engine/src/flutter/impeller/core/shader_types.h b/engine/src/flutter/impeller/core/shader_types.h index f765c8801bc6f..de954bbb6ccaa 100644 --- a/engine/src/flutter/impeller/core/shader_types.h +++ b/engine/src/flutter/impeller/core/shader_types.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -130,6 +131,17 @@ struct ShaderMetadata { std::vector members; }; +/// @brief The `ext_res_0` / `texture_index` stamped on a resource slot whose +/// backing the shader compiler dead-code-eliminated. +/// +/// SPIRV-Cross returns `~0u` from `get_automatic_msl_resource_binding` for a +/// resource dropped from the compiled Metal shader, so a slot carrying this +/// value has no argument-table index. It must be skipped, never bound: Metal's +/// `setFragment/VertexTexture:atIndex:` has no bounds check and crashes on an +/// out-of-range index. The GLES backend already skips optimized-out bindings. +inline constexpr uint32_t kOptimizedOutBinding = + std::numeric_limits::max(); + /// @brief Metadata required to bind a buffer. /// /// OpenGL binding requires the usage of the separate shader metadata struct. diff --git a/engine/src/flutter/impeller/fixtures/flutter_gpu_optimized_out_sampler.frag b/engine/src/flutter/impeller/fixtures/flutter_gpu_optimized_out_sampler.frag new file mode 100644 index 0000000000000..d44e9f4b992ba --- /dev/null +++ b/engine/src/flutter/impeller/fixtures/flutter_gpu_optimized_out_sampler.frag @@ -0,0 +1,15 @@ +// 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. + +// `tex` is sampled then overwritten, so the compiler drops it. Backs the test +// for binding an optimized-out sampler. +uniform sampler2D tex; + +in vec4 v_color; +out vec4 frag_color; + +void main() { + frag_color = texture(tex, v_color.xy); + frag_color = v_color; +} diff --git a/engine/src/flutter/impeller/renderer/backend/metal/pass_bindings_cache_mtl.mm b/engine/src/flutter/impeller/renderer/backend/metal/pass_bindings_cache_mtl.mm index 295967784ebaf..8539e602137bb 100644 --- a/engine/src/flutter/impeller/renderer/backend/metal/pass_bindings_cache_mtl.mm +++ b/engine/src/flutter/impeller/renderer/backend/metal/pass_bindings_cache_mtl.mm @@ -32,6 +32,12 @@ uint64_t index, uint64_t offset, id buffer) { + if (index == kOptimizedOutBinding) { + // The shader compiler dead-code-eliminated this resource, so it has no + // argument-table slot. Skip the bind rather than forward an out-of-range + // index to Metal, which has no bounds check and would crash. + return true; + } auto& buffers_map = buffers_[stage]; auto found = buffers_map.find(index); if (found != buffers_map.end() && found->second.buffer == buffer) { @@ -75,6 +81,10 @@ bool PassBindingsCacheMTL::SetTexture(ShaderStage stage, uint64_t index, id texture) { + if (index == kOptimizedOutBinding) { + // See SetBuffer: a dead-code-eliminated sampler has no argument-table slot. + return true; + } auto& texture_map = textures_[stage]; auto found = texture_map.find(index); if (found != texture_map.end() && found->second == texture) { @@ -99,6 +109,10 @@ bool PassBindingsCacheMTL::SetSampler(ShaderStage stage, uint64_t index, id sampler) { + if (index == kOptimizedOutBinding) { + // See SetBuffer: a dead-code-eliminated sampler has no argument-table slot. + return true; + } auto& sampler_map = samplers_[stage]; auto found = sampler_map.find(index); if (found != sampler_map.end() && found->second == sampler) { diff --git a/engine/src/flutter/lib/gpu/shader_library.cc b/engine/src/flutter/lib/gpu/shader_library.cc index 7960a5cf44f9e..8aeb3269a7805 100644 --- a/engine/src/flutter/lib/gpu/shader_library.cc +++ b/engine/src/flutter/lib/gpu/shader_library.cc @@ -242,6 +242,11 @@ static ShaderLibrary::ShaderMap ParseShaderBundle( std::unordered_map uniform_structs; if (backend_shader->uniform_structs() != nullptr) { for (const auto& uniform : *backend_shader->uniform_structs()) { + if (uniform->ext_res_0() == impeller::kOptimizedOutBinding) { + // A dead-code-eliminated uniform block, dropped for the same reason + // as the optimized-out samplers below. + continue; + } std::vector members; if (uniform->fields() != nullptr) { for (const auto& struct_member : *uniform->fields()) { @@ -293,6 +298,12 @@ static ShaderLibrary::ShaderMap ParseShaderBundle( std::unordered_map uniform_textures; if (backend_shader->uniform_textures() != nullptr) { for (const auto& uniform : *backend_shader->uniform_textures()) { + if (uniform->ext_res_0() == impeller::kOptimizedOutBinding) { + // The shader compiler dead-code-eliminated this sampler. Reflection + // still lists it but stamps the out-of-range binding sentinel, so + // drop it here rather than register a binding that cannot be bound. + continue; + } Shader::TextureBinding texture_binding; texture_binding.slot = impeller::SampledImageSlot{ .name = uniform->name()->c_str(), @@ -438,6 +449,15 @@ fml::RefPtr ShaderLibrary::GetShader(const std::string& shader_name, return shader; } +fml::RefPtr ShaderLibrary::FindShaderForTesting( + const std::string& shader_name) const { + auto it = shaders_.find(shader_name); + if (it == shaders_.end()) { + return nullptr; + } + return it->second; +} + ShaderLibrary::ShaderLibrary(std::shared_ptr payload, ShaderMap shaders, std::string library_id) diff --git a/engine/src/flutter/lib/gpu/shader_library.h b/engine/src/flutter/lib/gpu/shader_library.h index 8c69fcba904ce..05f72cd6ee939 100644 --- a/engine/src/flutter/lib/gpu/shader_library.h +++ b/engine/src/flutter/lib/gpu/shader_library.h @@ -62,6 +62,12 @@ class ShaderLibrary : public RefCountedDartWrappable { fml::RefPtr GetShader(const std::string& shader_name, Dart_Handle shader_wrapper); + // Looks up a registered shader by name without creating a Dart wrapper, for + // tests and tooling that run without a UI isolate. Production code uses + // `GetShader`. + fml::RefPtr FindShaderForTesting( + const std::string& shader_name) const; + const std::string& GetLibraryId() const { return library_id_; } ~ShaderLibrary() override; diff --git a/engine/src/flutter/lib/gpu/shader_library_unittests.cc b/engine/src/flutter/lib/gpu/shader_library_unittests.cc index ddb72fa9009dd..75d9486ca7ed8 100644 --- a/engine/src/flutter/lib/gpu/shader_library_unittests.cc +++ b/engine/src/flutter/lib/gpu/shader_library_unittests.cc @@ -6,10 +6,14 @@ #include #include +#include +#include #include +#include "flutter/lib/gpu/shader.h" #include "fml/mapping.h" #include "gtest/gtest.h" +#include "impeller/core/shader_types.h" #include "impeller/renderer/context.h" // Pulls in flatbuffers/flatbuffers.h (FlatBufferBuilder, Verifier) and the // generated impeller::fb::shaderbundle:: symbols. @@ -162,6 +166,85 @@ TEST(FlutterGpuShaderLibraryTest, EXPECT_FALSE(library); } +// Serializes a single-fragment-shader bundle (Metal desktop variant) carrying +// the given reflected textures and uniform structs, each as a +// (name, ext_res_0) pair. A texture/struct whose ext_res_0 is the optimized-out +// sentinel stands in for a resource the shader compiler dead-code-eliminated. +static std::shared_ptr> BuildFragmentBundle( + const std::vector>& textures, + const std::vector>& structs) { + namespace fbs = impeller::fb::shaderbundle; + + auto metal = std::make_unique(); + metal->stage = fbs::ShaderStage::kFragment; + metal->entrypoint = "main"; + // The bytes are ignored at parse time (no GPU compile), but the field must be + // present and non-empty for the loader to build a code mapping. + metal->shader = {0}; + for (const auto& [name, ext_res_0] : textures) { + auto texture = std::make_unique(); + texture->name = name; + texture->ext_res_0 = ext_res_0; + metal->uniform_textures.push_back(std::move(texture)); + } + for (const auto& [name, ext_res_0] : structs) { + auto uniform = std::make_unique(); + uniform->name = name; + uniform->ext_res_0 = ext_res_0; + uniform->size_in_bytes = 16; + metal->uniform_structs.push_back(std::move(uniform)); + } + + auto shader = std::make_unique(); + shader->name = "test"; + shader->metal_desktop = std::move(metal); + + fbs::ShaderBundleT bundle; + bundle.format_version = + static_cast(fbs::ShaderBundleFormatVersion::kVersion); + bundle.shaders.push_back(std::move(shader)); + + flatbuffers::FlatBufferBuilder builder; + builder.Finish(fbs::ShaderBundle::Pack(builder, &bundle), + fbs::ShaderBundleIdentifier()); + return std::make_shared>( + builder.GetBufferPointer(), + builder.GetBufferPointer() + builder.GetSize()); +} + +// A sampler the shader compiler dead-code-eliminated is still listed in +// reflection but carries the out-of-range binding sentinel. It must not be +// registered as a bindable uniform texture (binding an out-of-range index +// crashes the Metal backend), while a live sampler alongside it survives. +TEST(FlutterGpuShaderLibraryTest, MakeFromFlatbufferSkipsOptimizedOutTexture) { + const uint64_t sentinel = impeller::kOptimizedOutBinding; + auto bundle = BuildFragmentBundle( + /*textures=*/{{"u_live", 0}, {"u_dced", sentinel}}, /*structs=*/{}); + auto library = ShaderLibrary::MakeFromFlatbuffer( + impeller::Context::BackendType::kMetal, CreateMappingFromVector(bundle), + "test_bundle"); + ASSERT_TRUE(library); + auto shader = library->FindShaderForTesting("test"); + ASSERT_TRUE(shader); + EXPECT_NE(shader->GetUniformTexture("u_live"), nullptr); + EXPECT_EQ(shader->GetUniformTexture("u_dced"), nullptr); +} + +// The same skip applies to a dead-code-eliminated uniform block. +TEST(FlutterGpuShaderLibraryTest, MakeFromFlatbufferSkipsOptimizedOutStruct) { + const uint64_t sentinel = impeller::kOptimizedOutBinding; + auto bundle = BuildFragmentBundle( + /*textures=*/{}, /*structs=*/{{"Live", 0}, {"Dced", sentinel}}); + auto library = ShaderLibrary::MakeFromFlatbuffer( + impeller::Context::BackendType::kMetal, CreateMappingFromVector(bundle), + "test_bundle"); + ASSERT_TRUE(library); + auto shader = library->FindShaderForTesting("test"); + ASSERT_TRUE(shader); + EXPECT_NE(shader->GetUniformStruct("Live"), nullptr); + EXPECT_EQ(shader->GetUniformStruct("Dced"), nullptr); +} + } // namespace testing } // namespace gpu } // namespace flutter diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn index e6f60718fc3b3..137ef1b01e009 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn +++ b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn @@ -62,12 +62,13 @@ if (enable_unittests) { "//flutter/impeller/fixtures/flutter_gpu_unlit.frag", "//flutter/impeller/fixtures/flutter_gpu_unlit.vert", "//flutter/impeller/fixtures/flutter_gpu_unlit_alt_instance.frag", + "//flutter/impeller/fixtures/flutter_gpu_optimized_out_sampler.frag", "//flutter/impeller/fixtures/flutter_gpu_texture.frag", "//flutter/impeller/fixtures/flutter_gpu_texture.vert", ] fixtures = rebase_path("//flutter/impeller/fixtures") - shader_bundle = "{\"InstancedFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_instanced.frag\"}, \"InstancedVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_instanced.vert\"}, \"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"UnlitFragmentAltInstance\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit_alt_instance.frag\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}}" + shader_bundle = "{\"InstancedFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_instanced.frag\"}, \"InstancedVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_instanced.vert\"}, \"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"UnlitFragmentAltInstance\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit_alt_instance.frag\"}, \"OptimizedOutSamplerFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_optimized_out_sampler.frag\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}}" shader_bundle_output = "test.shaderbundle" } diff --git a/engine/src/flutter/testing/dart/gpu_test.dart b/engine/src/flutter/testing/dart/gpu_test.dart index b31d0ce8c4687..311eada0e940f 100644 --- a/engine/src/flutter/testing/dart/gpu_test.dart +++ b/engine/src/flutter/testing/dart/gpu_test.dart @@ -73,6 +73,16 @@ Future createUnlitRenderPipeline() async { return gpu.gpuContext.createRenderPipeline(vertex!, fragment!); } +Future createOptimizedOutSamplerRenderPipeline() async { + final gpu.ShaderLibrary? library = await gpu.ShaderLibrary.fromAsset('test.shaderbundle'); + assert(library != null); + final gpu.Shader? vertex = library!['UnlitVertex']; + assert(vertex != null); + final gpu.Shader? fragment = library['OptimizedOutSamplerFragment']; + assert(fragment != null); + return gpu.gpuContext.createRenderPipeline(vertex!, fragment!); +} + Future createTextureRenderPipeline() async { final gpu.ShaderLibrary? library = await gpu.ShaderLibrary.fromAsset('test.shaderbundle'); assert(library != null); @@ -1198,6 +1208,35 @@ void main() async { } }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('Binding a dead-code-eliminated sampler does not crash', () async { + final RenderPassState state = createSimpleRenderPass(); + final gpu.RenderPipeline pipeline = await createOptimizedOutSamplerRenderPipeline(); + state.renderPass.bindPipeline(pipeline); + + final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer(); + final gpu.BufferView vertices = transients.emplace( + float32([-0.5, -0.5, 0.5, -0.5, 0.0, 0.5]), + ); + state.renderPass.bindVertexBuffer(vertices); + state.renderPass.bindUniform( + pipeline.vertexShader.getUniformSlot('VertInfo'), + transients.emplace(unlitUBO(Matrix4.identity(), Colors.lime)), + ); + + // `tex` is optimized out. Binding it used to crash; now it either binds or + // is skipped, and either way the pass must draw. + final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate, 1, 1); + try { + state.renderPass.bindTexture(pipeline.fragmentShader.getUniformSlot('tex'), texture); + } on Exception { + // Optimized out; binding it is a no-op. + } + + state.renderPass.draw(3); + state.commandBuffer.submit(); + expect(state.renderTexture.asImage(), isNotNull); + }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('RenderPass.bindTexture throws for deviceTransient Textures', () async { final RenderPassState state = createSimpleRenderPass(); From e9af081867ebf16d8bec30ae81cb5b6dbec1280a Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 1 Aug 2026 22:38:25 -0400 Subject: [PATCH 014/330] Roll Skia from 32329e5643b5 to df13bfb5a54e (1 revision) (#190394) https://skia.googlesource.com/skia.git/+log/32329e5643b5..df13bfb5a54e 2026-08-02 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from ac9d03694598 to 84f40b5d1039 (1 revision) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,bwils@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 0859055df5a11..6d24916b3d072 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '32329e5643b53a27b503d8cb03f27272dedcbab1', + 'skia_revision': 'df13bfb5a54eaec4720db17e9276f56bc1d5491a', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From de01d5daa62dcb2fd0378d55206c91e4cf008923 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Sun, 2 Aug 2026 14:43:26 +0900 Subject: [PATCH 015/330] iOS: Serialise CADisplayLink access in VSyncClient tests (#190335) VSyncClientTests were reaching into the client's CADisplayLink from the test thread, reading properties like isPaused, calling onDisplayLink directly, inspecting the frame rate range, etc. However, VSyncClient registration, vsync callbacks, and invalidation run on the dedicated worker thread it was handed as a task runner. CADisplayLink is not thread-safe, so this was an unsynchronised data race between the code running on the test thread and the code running on the worker. VSyncClient is built around the assumption that its display link is only ever referenced on its task runner's thread. Right now tests violate that invariant. While this isn't actively buggy at the moment, it's not safe to write the tests this way. This forces every interaction with the CADsiaplyLink through a new run() helper that dispatches onto the runner and returns the result. Test assertions remain on the test thread. I've moved the object-lifetime tests off the autoreleasepool and onto the sam async model. We also migrate to AsyncStream for the vsync signal and a run {} barrier for registration/flushing. It scopes the client's only strong reference to a nested func so we release deterministically on return. It also means we get rid of the blocking code altogether. Issue: https://github.com/flutter/flutter/issues/181684 Issue: https://github.com/flutter/flutter/issues/112232 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../framework/Source/VSyncClientTests.swift | 163 ++++++++++++------ 1 file changed, 110 insertions(+), 53 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClientTests.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClientTests.swift index ea09d333686bd..00ebf51a42179 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClientTests.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClientTests.swift @@ -2,10 +2,45 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import Foundation import Testing @testable import InternalFlutterSwift +extension TaskRunner { + /// Runs `work` on this runner's thread and returns its result once complete. + /// + /// `VSyncClient` owns its `CADisplayLink` on the runner's thread. To prevent races, all + /// interactions with it to happen via this call, and are dispatched to the owning thread, never + /// invoked directly from the test thread. + fileprivate func run(_ work: @escaping () -> T) async -> T { + await withCheckedContinuation { continuation in + postTask { + continuation.resume(returning: work()) + } + } + } +} + +extension AsyncStream where Element: Sendable { + /// Returns the stream's next element, or `nil` if `timeout` elapses first. + fileprivate func next(timeout: TimeInterval) async -> Element? { + await withTaskGroup(of: Element?.self) { group in + group.addTask { + var iterator = self.makeAsyncIterator() + return await iterator.next() + } + group.addTask { + try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + return nil + } + let result = await group.next() ?? nil + group.cancelAll() + return result + } + } +} + struct VSyncClientTests { let threadTaskRunner = TaskRunnerTestHelper.makeTaskRunner(withLabel: "VSyncClientTest") @@ -27,7 +62,7 @@ struct VSyncClientTests { /// This test passes a newly created, paused `CADisplayLink` (whose properties both evaluate to /// 0.0) and asserts that the client intercepts the invalid state and synthesizes a safe, positive /// next-frame target timestamp based on the display's maximum refresh rate. - @Test func realDisplayLinkVsyncTimestampsCorrect() throws { + @Test func realDisplayLinkVsyncTimestampsCorrect() async throws { var callbackStartTime: CFTimeInterval = -1 var callbackTargetTime: CFTimeInterval = -1 let vsyncClient = VSyncClient( @@ -40,7 +75,7 @@ struct VSyncClientTests { } let link = try #require(vsyncClient.displayLink) - vsyncClient.onDisplayLink(link) + await threadTaskRunner.run { vsyncClient.onDisplayLink(link) } // Since the display link is paused and has not delivered a frame yet, both timestamp and // targetTimestamp are 0.0. Verify the client synthesizes a valid target timestamp using the max @@ -49,7 +84,7 @@ struct VSyncClientTests { #expect(abs((callbackTargetTime - callbackStartTime) - 1.0 / 60.0) <= 0.0001) } - @Test func vsyncClientPreventsZeroRefreshRateDivision() throws { + @Test func vsyncClientPreventsZeroRefreshRateDivision() async throws { var callbackStartTime: CFTimeInterval = -1 var callbackTargetTime: CFTimeInterval = -1 // Initialize with maxRefreshRate = 0.0 to simulate uninitialized/zero max refresh rate. @@ -63,7 +98,7 @@ struct VSyncClientTests { } let link = try #require(vsyncClient.displayLink) - vsyncClient.onDisplayLink(link) + await threadTaskRunner.run { vsyncClient.onDisplayLink(link) } #expect(callbackStartTime > 0.0) // Should fallback to effectiveRefreshRate of 60.0. @@ -84,7 +119,7 @@ struct VSyncClientTests { #expect(vsyncClient.refreshRate == 60.0) } - @Test func setAllowPauseAfterVsyncCorrect() throws { + @Test func setAllowPauseAfterVsyncCorrect() async throws { let vsyncClient = VSyncClient( taskRunner: threadTaskRunner, isVariableRefreshRateEnabled: false, @@ -92,18 +127,24 @@ struct VSyncClientTests { ) { _, _ in } let link = try #require(vsyncClient.displayLink) - vsyncClient.allowPauseAfterVsync = false - vsyncClient.await() - vsyncClient.onDisplayLink(link) - #expect(link.isPaused == false) + let pausedWhenDisallowed = await threadTaskRunner.run { () -> Bool in + vsyncClient.allowPauseAfterVsync = false + vsyncClient.await() + vsyncClient.onDisplayLink(link) + return link.isPaused + } + #expect(pausedWhenDisallowed == false) - vsyncClient.allowPauseAfterVsync = true - vsyncClient.await() - vsyncClient.onDisplayLink(link) - #expect(link.isPaused) + let pausedWhenAllowed = await threadTaskRunner.run { () -> Bool in + vsyncClient.allowPauseAfterVsync = true + vsyncClient.await() + vsyncClient.onDisplayLink(link) + return link.isPaused + } + #expect(pausedWhenAllowed) } - @Test func setCorrectVariableRefreshRates() throws { + @Test func setCorrectVariableRefreshRates() async throws { let maxFrameRate: Double = 120.0 let vsyncClient = VSyncClient( taskRunner: threadTaskRunner, @@ -113,15 +154,19 @@ struct VSyncClientTests { let link = try #require(vsyncClient.displayLink) if #available(iOS 15.0, *) { - #expect(abs(Double(link.preferredFrameRateRange.maximum) - maxFrameRate) <= 0.1) - #expect(abs(Double(link.preferredFrameRateRange.preferred ?? 0) - maxFrameRate) <= 0.1) - #expect(abs(Double(link.preferredFrameRateRange.minimum) - maxFrameRate / 2) <= 0.1) + let range = await threadTaskRunner.run { link.preferredFrameRateRange } + #expect(abs(Double(range.maximum) - maxFrameRate) <= 0.1) + #expect(abs(Double(range.preferred ?? 0) - maxFrameRate) <= 0.1) + #expect(abs(Double(range.minimum) - maxFrameRate / 2) <= 0.1) } else { - #expect(abs(Double(link.preferredFramesPerSecond) - maxFrameRate) <= 0.1) + let framesPerSecond = await threadTaskRunner.run { link.preferredFramesPerSecond } + #expect(abs(Double(framesPerSecond) - maxFrameRate) <= 0.1) } } - @Test func doNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIsNotOn() throws { + @Test func doNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIsNotOn() + async throws + { let maxFrameRate: Double = 120.0 let vsyncClient = VSyncClient( taskRunner: threadTaskRunner, @@ -131,15 +176,17 @@ struct VSyncClientTests { let link = try #require(vsyncClient.displayLink) if #available(iOS 15.0, *) { - #expect(abs(Double(link.preferredFrameRateRange.maximum)) <= 0.1) - #expect(abs(Double(link.preferredFrameRateRange.preferred ?? 0)) <= 0.1) - #expect(abs(Double(link.preferredFrameRateRange.minimum)) <= 0.1) + let range = await threadTaskRunner.run { link.preferredFrameRateRange } + #expect(abs(Double(range.maximum)) <= 0.1) + #expect(abs(Double(range.preferred ?? 0)) <= 0.1) + #expect(abs(Double(range.minimum)) <= 0.1) } else { - #expect(abs(Double(link.preferredFramesPerSecond)) <= 0.1) + let framesPerSecond = await threadTaskRunner.run { link.preferredFramesPerSecond } + #expect(abs(Double(framesPerSecond)) <= 0.1) } } - @Test func awaitAndPauseWillWorkCorrectly() throws { + @Test func awaitAndPauseWillWorkCorrectly() async throws { let vsyncClient = VSyncClient( taskRunner: threadTaskRunner, isVariableRefreshRateEnabled: false, @@ -147,49 +194,56 @@ struct VSyncClientTests { ) { _, _ in } let link = try #require(vsyncClient.displayLink) - #expect(link.isPaused) - vsyncClient.await() - #expect(link.isPaused == false) - vsyncClient.pause() - #expect(link.isPaused) + let (initiallyPaused, pausedAfterAwait, pausedAfterPause) = await threadTaskRunner.run { + () -> (Bool, Bool, Bool) in + let initiallyPaused = link.isPaused + vsyncClient.await() + let pausedAfterAwait = link.isPaused + vsyncClient.pause() + let pausedAfterPause = link.isPaused + return (initiallyPaused, pausedAfterAwait, pausedAfterPause) + } + + #expect(initiallyPaused) + #expect(pausedAfterAwait == false) + #expect(pausedAfterPause) } - @Test func releasesLinkOnInvalidation() { + @Test func releasesLinkOnInvalidation() async { weak var weakClient: VSyncClient? + let (vsyncSignals, vsyncContinuation) = AsyncStream.makeStream(of: Void.self) - autoreleasepool { - let vsyncSignal = DispatchSemaphore(value: 0) + // Scope the VSyncClient to a nested function so it is released on return. + func awaitFirstVsyncThenInvalidate() async { let client = VSyncClient( taskRunner: threadTaskRunner, isVariableRefreshRateEnabled: false, maxRefreshRate: 60.0 ) { _, _ in - vsyncSignal.signal() + vsyncContinuation.yield() } weakClient = client - threadTaskRunner.postTask { - client.await() - } + await threadTaskRunner.run { client.await() } - #expect(vsyncSignal.wait(timeout: .now() + 1.0) == .success) + // Wait for the display link to deliver its first vsync on the runner's thread. + let vsync = await vsyncSignals.next(timeout: 1.0) + #expect(vsync != nil) client.invalidate() } + await awaitFirstVsyncThenInvalidate() - let backgroundThreadFlushed = DispatchSemaphore(value: 0) - threadTaskRunner.postTask { - backgroundThreadFlushed.signal() - } - - #expect(backgroundThreadFlushed.wait(timeout: .now() + 1.0) == .success) + // Flush the task queue to ensure the invalidate() dispatched to the runner on dealloc has run. + await threadTaskRunner.run {} #expect(weakClient == nil) } - @Test func deallocatesWithoutExplicitInvalidation() { + @Test func deallocatesWithoutExplicitInvalidation() async { weak var weakClient: VSyncClient? - autoreleasepool { + // Scope the VSyncClient to a nested function so it is released on return. + func create() { let client = VSyncClient( taskRunner: threadTaskRunner, isVariableRefreshRateEnabled: false, @@ -197,7 +251,11 @@ struct VSyncClientTests { ) { _, _ in } weakClient = client } + create() + // Flush the task queue to run the registration task dispatched in init. That task holds only a + // weak reference to the client, so the client can dealloc. + await threadTaskRunner.run {} #expect(weakClient == nil) } @@ -205,24 +263,23 @@ struct VSyncClientTests { /// the display server has taken ownership of the link. On iOS 27+, QuartzCore holds a /// `_CADisplayLinkAssertion` on registered links; a never-unpaused link may therefore /// outlive `VSyncClient` itself, which is expected. - @Test func deallocatesAfterRegistrationCompletes() { + @Test func deallocatesAfterRegistrationCompletes() async { weak var weakClient: VSyncClient? - autoreleasepool { + // Scope the VSyncClient to a nested function so it is released on return. + func createAndAwaitRegistration() async { let client = VSyncClient( taskRunner: threadTaskRunner, isVariableRefreshRateEnabled: false, maxRefreshRate: 60.0 ) { _, _ in } - weakClient = client - // Registration is dispatched to the task runner in init. Post a barrier task after it - // so we know registration has completed before deinit fires. - let registered = DispatchSemaphore(value: 0) - threadTaskRunner.postTask { registered.signal() } - #expect(registered.wait(timeout: .now() + 1.0) == .success) + // Flush the task queue to run the registration dispatched in init. This ensures registration + // has completed before the strong reference is released. + await threadTaskRunner.run {} } + await createAndAwaitRegistration() #expect(weakClient == nil) } From 11c38ba0eae425184027fb524e7b3c3df0acedc9 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 01:59:32 -0400 Subject: [PATCH 016/330] Roll Skia from df13bfb5a54e to 39cda9d6d7d2 (2 revisions) (#190425) https://skia.googlesource.com/skia.git/+log/df13bfb5a54e..39cda9d6d7d2 2026-08-03 skia-autoroll@skia-public.iam.gserviceaccount.com Roll shaders-base from 61415213cc70 to 9278a7788a7e 2026-08-02 skia-autoroll@skia-public.iam.gserviceaccount.com Roll SKP CIPD package from 570 to 571 If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,alexisdavidc@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 6d24916b3d072..7eb11a6af1af6 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'df13bfb5a54eaec4720db17e9276f56bc1d5491a', + 'skia_revision': '39cda9d6d7d2c4940524b4ee7766a5cdb160ae02', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 7c902ab0f060af313db041d46dc2909f5ebadcc9 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 04:12:24 -0400 Subject: [PATCH 017/330] Roll Skia from 39cda9d6d7d2 to 4c9f8b4805e2 (6 revisions) (#190426) https://skia.googlesource.com/skia.git/+log/39cda9d6d7d2..4c9f8b4805e2 2026-08-03 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-03 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from 2d7debe40c61 to 72e9d968d26a 2026-08-03 skia-autoroll@skia-public.iam.gserviceaccount.com Roll ANGLE from 0e5d32f40590 to 272d37f4cc0c (20 revisions) 2026-08-03 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Skia Infra from e5a5eb98eb1c to 9c262f334dfc (8 revisions) 2026-08-03 skia-autoroll@skia-public.iam.gserviceaccount.com Roll skottie-base from e34c0f483d40 to 0517426ca15b 2026-08-03 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,alexisdavidc@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 7eb11a6af1af6..78e873270952e 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '39cda9d6d7d2c4940524b4ee7766a5cdb160ae02', + 'skia_revision': '4c9f8b4805e20b4885ccf517db878d4f5ca382f0', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 272fe23e0e32d63fa6c34dbf9659411dcfabb970 Mon Sep 17 00:00:00 2001 From: ellie-ya Date: Mon, 3 Aug 2026 17:20:32 +0900 Subject: [PATCH 018/330] [macOS] Resume app lifecycle on becomeActive to avoid frozen UI after occlusion (#188772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/flutter/flutter/issues/155977 ## Problem `FlutterEngine` drives the framework's `AppLifecycleState` from two inputs: - `_active` — toggled by `applicationWillBecomeActive` / `applicationWillResignActive` - `_visible` — toggled **only** by `applicationDidChangeOcclusionState` `handleWillBecomeActive` resumed only when `_visible` was already `true`, otherwise it sent `kHidden`: ```objc - (void)handleWillBecomeActive:(NSNotification*)notification { _active = YES; if (!_visible) { [self setApplicationState:flutter::AppLifecycleState::kHidden]; } else { [self setApplicationState:flutter::AppLifecycleState::kResumed]; } } ``` macOS does **not** reliably deliver an `NSApplicationDidChangeOcclusionState` (visible) notification on every occlusion→visible transition. Returning to the app **on the same screen** (Cmd-Tab, Mission Control, clicking away and back) frequently delivers `visible=NO` when leaving but **never delivers `visible=YES` when returning**. When that happens: 1. App is occluded → `handleDidChangeOcclusionState` sets `_visible = NO`, sends `kHidden` (framework disables frames — correct). 2. App is brought back to the foreground → `applicationWillBecomeActive` fires, but `_visible` is still stale-`NO`, so `handleWillBecomeActive` sends `kHidden` **again**. 3. The framework stays `hidden`: animations are muted, `scheduleFrame` is gated, no `OnVsync` is requested — **the UI is frozen even though the window is fully on-screen and frontmost.** It only recovers if the app is later occluded and revealed in a way that does fire the `visible` notification (e.g. a real Dock-icon activation). Reproduces reliably on low-spec **Intel** Macs launched via Finder/LaunchServices (rarely on Apple Silicon). ## Fix An application receiving `applicationWillBecomeActive` is, by definition, frontmost and visible to the user, so treat becoming-active as an authoritative "visible" signal and resume. `handleDidChangeOcclusionState` still authoritatively drives `kHidden` when the app is genuinely occluded, so this does not prevent the app from going hidden — it only ensures that *returning to the foreground always resumes*, instead of depending on a notification macOS may never send. ## Diagnosis A local engine build with tracing in `FlutterEngine` (lifecycle) and `FlutterVSyncWaiter`/`FlutterDisplayLink` (vsync) showed, on the freezing return: ``` handleDidChangeOcclusionState visible=0 -> AppLifecycleState.hidden handleWillBecomeActive _visible(prev)=0 occlusionState&Visible=0 -> AppLifecycleState.hidden (stuck) (no further waitForVSync / onDisplayLink — frames were never requested again) ``` i.e. `applicationWillBecomeActive` **does** fire on the return, but `[[NSApplication sharedApplication] occlusionState]` still reports not-visible at that instant and the `visible` notification never arrives, so the old code re-sent `kHidden`. With the fix the same transition logs `-> AppLifecycleState.resumed` and frame production resumes immediately. The `FlutterVSyncWaiter` / `FlutterDisplayLink` layer was verified healthy — it was never the cause; the engine simply was never asked to render because the framework believed it was hidden. ## Tests Updated `HandleLifecycleStates` in `FlutterEngineTest.mm`: with the occlusion state reading not-visible, `handleWillBecomeActive` now expects `kResumed` (previously `kHidden`), directly exercising the missed-`visible`-notification scenario, and the following `handleWillResignActive` now expects `kInactive`. ## Pre-launch checklist - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes. - [x] I added new tests to check the change I am making (updated the existing lifecycle test to cover the regression). - [x] I updated/added relevant documentation (inline rationale comment). --- .../macos/framework/Source/FlutterEngine.mm | 19 ++++++++---- .../framework/Source/FlutterEngineTest.mm | 30 ++++++++++++++++++- 2 files changed, 42 insertions(+), 7 deletions(-) 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 ccc14b11ec931..36578fb1347a9 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 @@ -1658,11 +1658,18 @@ - (void)setApplicationState:(flutter::AppLifecycleState)state { */ - (void)handleWillBecomeActive:(NSNotification*)notification { _active = YES; - if (!_visible) { - [self setApplicationState:flutter::AppLifecycleState::kHidden]; - } else { - [self setApplicationState:flutter::AppLifecycleState::kResumed]; + // occlusionState can latch stale on an occlusion->visible transition (same-screen + // Cmd-Tab / Mission Control), so `_visible` is unreliable here. Resume from + // NSWindow.isVisible instead — NO for a minimized window, so it won't resume hidden. + // https://github.com/flutter/flutter/issues/155977 + for (NSWindow* window in [NSApplication sharedApplication].windows) { + if (window.isVisible) { + _visible = YES; + break; + } } + [self setApplicationState:_visible ? flutter::AppLifecycleState::kResumed + : flutter::AppLifecycleState::kHidden]; } /** @@ -1679,8 +1686,8 @@ - (void)handleWillResignActive:(NSNotification*)notification { } /** - * Called when the |FlutterAppDelegate| gets the applicationDidUnhide - * notification. + * Called when the application's occlusion state changes + * (NSApplicationDidChangeOcclusionStateNotification). */ - (void)handleDidChangeOcclusionState:(NSNotification*)notification { NSApplicationOcclusionState occlusionState = [[NSApplication sharedApplication] occlusionState]; diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index c43c3025945ed..6d49a588958f4 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -1220,6 +1220,16 @@ @implementation MockableFlutterEngine [invocation setReturnValue:&visibility]; }); + // handleWillBecomeActive derives visibility from the windows (not from the + // occlusion state, which can latch stale), so provide a window whose visibility + // we can toggle independently. + __block BOOL windowVisible = YES; + id mockWindow = OCMClassMock([NSWindow class]); + OCMStub([mockWindow isVisible]).andDo(^(NSInvocation* invocation) { + [invocation setReturnValue:&windowVisible]; + }); + OCMStub([mockApplication windows]).andReturn(@[ mockWindow ]); + NSNotification* willBecomeActive = [[NSNotification alloc] initWithName:NSApplicationWillBecomeActiveNotification object:nil @@ -1248,12 +1258,30 @@ @implementation MockableFlutterEngine [engineMock handleDidChangeOcclusionState:didChangeOcclusionState]; EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden); + // Becoming active with an on-screen window resumes even though occlusionState still + // reads not-visible (the stale-latch case). Regression test for #155977. [engineMock handleWillBecomeActive:willBecomeActive]; - EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden); + EXPECT_EQ(sentState, flutter::AppLifecycleState::kResumed); + // The app is now active and considered visible, so resigning active makes it + // inactive (not hidden) until a real occlusion notification hides it. [engineMock handleWillResignActive:willResignActive]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kInactive); + + // Occlusion stays authoritative after a becomeActive resume: a genuine + // not-visible occlusion notification still hides the app. + visibility = 0; + [engineMock handleDidChangeOcclusionState:didChangeOcclusionState]; + EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden); + + // Becoming active while no window is visible (e.g. activated with all windows + // minimized, which does not deminiaturize) must not resume: visibility is + // derived from the windows, not from the activation itself. + windowVisible = NO; + [engineMock handleWillBecomeActive:willBecomeActive]; EXPECT_EQ(sentState, flutter::AppLifecycleState::kHidden); + [mockWindow stopMocking]; [mockApplication stopMocking]; } From 7bb14cd567b80610b6637a51bea617d1ea13c582 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 08:39:38 -0400 Subject: [PATCH 019/330] Roll Skia from 4c9f8b4805e2 to 5a761eb826c1 (1 revision) (#190437) https://skia.googlesource.com/skia.git/+log/4c9f8b4805e2..5a761eb826c1 2026-08-03 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,alexisdavidc@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 78e873270952e..b1ca771e1bbc4 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '4c9f8b4805e20b4885ccf517db878d4f5ca382f0', + 'skia_revision': '5a761eb826c12d673b0c2a32facebdb83f20779f', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From fa8988b33d9d424222f07f700da4da20320a8d71 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 10:37:39 -0400 Subject: [PATCH 020/330] Roll Skia from 5a761eb826c1 to 68efb3f2ad16 (1 revision) (#190440) https://skia.googlesource.com/skia.git/+log/5a761eb826c1..68efb3f2ad16 2026-08-03 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 84f40b5d1039 to af17ae0ac3a5 (1 revision) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,alexisdavidc@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index b1ca771e1bbc4..f526eecc5f7b6 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '5a761eb826c12d673b0c2a32facebdb83f20779f', + 'skia_revision': '68efb3f2ad161c475a4494322c442e3bb9598bfd', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 5a2a94a5a971471ad940709c75463b0798df7e5c Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 11:40:05 -0400 Subject: [PATCH 021/330] Roll Packages from 5351d8c0f8df to ac87e65333e1 (4 revisions) (#190441) https://github.com/flutter/packages/compare/5351d8c0f8df...ac87e65333e1 2026-07-31 developeryusuf@icloud.com [google_fonts] Remove failed loads from pendingFonts (flutter/packages#12240) 2026-07-31 34871572+gmackall@users.noreply.github.com [google_sign_in_android] Keep `default_web_client_id` resource from being stripped by resource shrinker (flutter/packages#12075) 2026-07-31 brunocorona.alcantar@gmail.com [vector_graphics] Provide textDirection for semantics label to avoid crash without Directionality (flutter/packages#11962) 2026-07-31 43054281+camsim99@users.noreply.github.com [camera_android_camerax] Update `AGENTS.md` to improve video recording integration tests (flutter/packages#12301) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- bin/internal/flutter_packages.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index a6ebf2a65b6f0..0b3f00dce77db 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -5351d8c0f8df210f09d79aea5960a5d48ceb44af +ac87e65333e1159022267053c009f747667f6f50 From a6cfc81a426120c2538600e5fa5b475da1238102 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 12:33:09 -0400 Subject: [PATCH 022/330] Roll Skia from 68efb3f2ad16 to abecb0dc02c1 (1 revision) (#190443) https://skia.googlesource.com/skia.git/+log/68efb3f2ad16..abecb0dc02c1 2026-08-03 kjlubick@google.com Manually Roll Dawn from e832cc409215 to 36cf1fae0cd8 (38 revisions) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,alexisdavidc@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index f526eecc5f7b6..1ebc1060ba5d8 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '68efb3f2ad161c475a4494322c442e3bb9598bfd', + 'skia_revision': 'abecb0dc02c1d1bf8085783eb465df828c4063b1', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 0b2198ab8a14bacd1535688681e6086442b07614 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 14:26:49 -0400 Subject: [PATCH 023/330] Roll Dart SDK from 65b163be2485 to 2a799a2404e9 (3 revisions) (#190454) https://dart.googlesource.com/sdk.git/+log/65b163be2485..2a799a2404e9 2026-08-03 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-85.0.dev 2026-08-03 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-84.0.dev 2026-07-31 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-83.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC aaclarke@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPS b/DEPS index 1ebc1060ba5d8..dbc1ca5cb7d6d 100644 --- a/DEPS +++ b/DEPS @@ -55,12 +55,12 @@ 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': '65b163be2485aa0aa3fd0ab7d5333f2fa317a986', + 'dart_revision': '2a799a2404e9c2cf468e0142b2d9a47e4d609a5f', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py 'dart_binaryen_rev': '9926156a583cec3d22d521232b31c70fa9a87dc1', - 'dart_boringssl_rev': '22a0079b189c391b95689813a41982ce11876f0a', + 'dart_boringssl_rev': 'f1f2556a5dfa59e147d9d47279cc3f7f8a18b433', 'dart_core_rev': 'fe516ee1b38cc60e7a8c6e082c337037a043d782', 'dart_devtools_rev': '21f1838f3a9b138ac377efb953ca5a53c8832e75', 'dart_ecosystem_rev': 'edfdb3b4063b9034b708144633a700204f865f43', From fd004496f4086dd92650aa256505acc377654918 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 14:44:06 -0400 Subject: [PATCH 024/330] Roll Skia from abecb0dc02c1 to 958c1c1921a1 (4 revisions) (#190459) https://skia.googlesource.com/skia.git/+log/abecb0dc02c1..958c1c1921a1 2026-08-03 nscobie@google.com Optimize VulkanAMDMemoryAllocator::totalAllocatedAndUsedMemory() 2026-08-03 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-03 michaelludwig@google.com [graphite] Use ComplementRect for faster bounds intersection tests in Layer 2026-08-03 alexisdavidc@google.com [Formatting] Replace tabs with spaces If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,alexisdavidc@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index dbc1ca5cb7d6d..fac4f866e63b2 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'abecb0dc02c1d1bf8085783eb465df828c4063b1', + 'skia_revision': '958c1c1921a154d6a6f0551fa26948f18aa8d9ff', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 12b8592cd84bb66b99f4dce7937c87ed2d5607e5 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 3 Aug 2026 13:16:30 -0700 Subject: [PATCH 025/330] [Infra] Replace defunct umbrella template with Wasm issue form (#190471) Replaces `09_feature.yml` ("Create an umbrella issue"), which currently breaks schema validation and fails to render in `/issues/new/choose` due to its attachment to deleted Classic Project 82 (`projects: ["flutter/82"]`), with a dedicated Wasm community issue form (`09_wasm.yml`). - Automatically tags Wasm bug reports with `platform-web` and `e: wasm` labels. - Captures compilation logs, browser specifications, and dependency blocker fields. - Frees up slot 09 to improve Wasm community feedback intake velocity. Fixes #190315 --- .github/ISSUE_TEMPLATE/09_feature.yml | 25 ----------- .github/ISSUE_TEMPLATE/09_wasm.yml | 60 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 25 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/09_feature.yml create mode 100644 .github/ISSUE_TEMPLATE/09_wasm.yml diff --git a/.github/ISSUE_TEMPLATE/09_feature.yml b/.github/ISSUE_TEMPLATE/09_feature.yml deleted file mode 100644 index ccce3f7890a3d..0000000000000 --- a/.github/ISSUE_TEMPLATE/09_feature.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Create an umbrella issue -description: As a contributor, I would like to create an umbrella issue for a -feature to be added to the feature tracker. -title: '☂️ ' -projects: ["flutter/82"] -body: - - type: input - id: design_doc - attributes: - label: Design Doc - description: Add a design doc if there is one for this feature. - - type: textarea - id: description - attributes: - label: Description - description: Please describe the feature you are adding. - validations: - required: true - - type: textarea - id: tracking_issues - attributes: - label: Tracking Issues - description: List of issues associated with this feature. - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/09_wasm.yml b/.github/ISSUE_TEMPLATE/09_wasm.yml new file mode 100644 index 0000000000000..fa4a2dc526f58 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/09_wasm.yml @@ -0,0 +1,60 @@ +name: Report a Flutter WebAssembly (Wasm) issue +description: | + You hit a compilation, packaging, or runtime blocker while targeting WebAssembly (Wasm) in Flutter Web. +labels: ['platform-web', 'e: wasm'] +body: + - type: markdown + attributes: + value: | + Thank you for testing Flutter Web with WebAssembly! 🚀 + Please provide relevant build logs, browser details, and package dependencies so engineering leads can rapidly triage your issue. + - type: textarea + id: build_command_output + attributes: + label: Build Command & Compile Logs + description: | + Paste the exact build command (e.g., `flutter build web --wasm`) and any resulting terminal errors or stack traces. **Note: If reporting a runtime bug where compilation succeeded cleanly, simply list your build command without full terminal output.** + placeholder: | + $ flutter build web --wasm + Target wasm_module failed: ... + render: shell + validations: + required: true + - type: input + id: browser_version + attributes: + label: Target Browser & Version + description: Specify the browser and version where the Wasm runtime or visual bug occurred (e.g., Chrome 145.0.0.0, Safari 18.2). **Optional if reporting a compilation or build failure.** + placeholder: Chrome 145.0 + validations: + required: false + - type: textarea + id: blocking_packages + attributes: + label: Blocking Packages / Dependencies + description: | + List any third-party packages from your `pubspec.yaml` causing Wasm compilation failures or missing FFI bindings. Leave blank if standard SDK code. + placeholder: | + package:sqlite3_web 2.1.0 + package:js_bindings 1.4.2 + validations: + required: false + - type: textarea + id: reproduction_steps + attributes: + label: Steps to reproduce & Actual Behavior + description: Provide specific code snippets or minimal reproduction steps describing what went wrong versus expected results. If reporting a runtime exception, include console DevTools stack traces here. + placeholder: | + 1. Create default demo app with `--wasm` flag. + 2. Import package X and call `init()`. + 3. Observe console exception in browser DevTools. + validations: + required: true + - type: textarea + id: flutter_doctor + attributes: + label: Flutter Doctor Output + description: Paste the complete output of `flutter doctor -v` from your local terminal. + render: shell + validations: + required: true From 30c289b8cde9cfe1a7f2dcf345fc26afdba7cb47 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 05:31:06 +0900 Subject: [PATCH 026/330] a11y: Map disabled/read-only semantics to AX node restriction (#190353) This is a partial re-land of https://github.com/flutter/flutter/pull/184501, which just lands the common bridge code and tests as well as a macOS test for behaviour landed in landed in https://github.com/flutter/flutter/pull/190330. This sets AXNodeData::SetRestriction() from the Flutter semantics flags: kDisabled when is_enabled is false, and kReadOnly for read-only text fields. Previously the restriction was never populated, so disabled and read-only nodes were indistinguishable from editable/enabled ones at the platform layer. We also add a macOS regression test that checks that a disabled, editable text field is exposed as static text rather than a FlutterTextField. This guards the !GetData().IsReadOnlyOrDisabled() check in FlutterPlatformNodeDelegateMac::Init. The GetData() override rewrites a disabled field's role to kStaticText but leaves kEditableRoot set, so IsTextField() stays true and the restriction check is what prevents an editable FlutterTextField from being created. I've added this in response to a bad comment by the Gemini review bot to ensure that this doesn't regress. Issue: https://github.com/flutter/flutter/issues/184559 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../platform/common/accessibility_bridge.cc | 5 ++ .../common/accessibility_bridge_unittests.cc | 54 +++++++++++++ .../FlutterPlatformNodeDelegateMacTest.mm | 79 +++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/engine/src/flutter/shell/platform/common/accessibility_bridge.cc b/engine/src/flutter/shell/platform/common/accessibility_bridge.cc index d2524d35c1d34..41c954c6c2a7a 100644 --- a/engine/src/flutter/shell/platform/common/accessibility_bridge.cc +++ b/engine/src/flutter/shell/platform/common/accessibility_bridge.cc @@ -373,6 +373,11 @@ void AccessibilityBridge::SetStateFromFlutterUpdate(ui::AXNodeData& node_data, if (flags->is_text_field && !flags->is_read_only) { node_data.AddState(ax::mojom::State::kEditable); } + if (flags->is_enabled == FlutterTristate::kFlutterTristateFalse) { + node_data.SetRestriction(ax::mojom::Restriction::kDisabled); + } else if (flags->is_read_only) { + node_data.SetRestriction(ax::mojom::Restriction::kReadOnly); + } if (node_data.role == ax::mojom::Role::kStaticText && (actions & kHasScrollingAction) == 0 && node.value.empty() && node.label.empty() && node.hint.empty()) { diff --git a/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc b/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc index 4a1e1143a47e5..4f013b1cc3318 100644 --- a/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc +++ b/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc @@ -302,6 +302,60 @@ TEST(AccessibilityBridgeTest, SliderHasSliderRole) { EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kSlider); } +TEST(AccessibilityBridgeTest, DisabledButtonHasDisabledRestriction) { + std::shared_ptr bridge = + std::make_shared(); + FlutterSemanticsNode2 root = CreateSemanticsNode(0, "root"); + auto flags = FlutterSemanticsFlags{ + .is_enabled = FlutterTristate::kFlutterTristateFalse, + .is_button = true, + }; + root.flags2 = &flags; + bridge->AddFlutterSemanticsNodeUpdate(root); + bridge->CommitUpdates(); + + auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); + EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kButton); + EXPECT_EQ(root_node->GetData().GetRestriction(), + ax::mojom::Restriction::kDisabled); +} + +TEST(AccessibilityBridgeTest, EnabledButtonHasNoRestriction) { + std::shared_ptr bridge = + std::make_shared(); + FlutterSemanticsNode2 root = CreateSemanticsNode(0, "root"); + auto flags = FlutterSemanticsFlags{ + .is_enabled = FlutterTristate::kFlutterTristateTrue, + .is_button = true, + }; + root.flags2 = &flags; + bridge->AddFlutterSemanticsNodeUpdate(root); + bridge->CommitUpdates(); + + auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); + EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kButton); + EXPECT_EQ(root_node->GetData().GetRestriction(), + ax::mojom::Restriction::kNone); +} + +TEST(AccessibilityBridgeTest, ReadOnlyTextFieldHasReadOnlyRestriction) { + std::shared_ptr bridge = + std::make_shared(); + FlutterSemanticsNode2 root = CreateSemanticsNode(0, "root"); + auto flags = FlutterSemanticsFlags{ + .is_enabled = FlutterTristate::kFlutterTristateTrue, + .is_text_field = true, + .is_read_only = true, + }; + root.flags2 = &flags; + bridge->AddFlutterSemanticsNodeUpdate(root); + bridge->CommitUpdates(); + + auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); + EXPECT_EQ(root_node->GetData().GetRestriction(), + ax::mojom::Restriction::kReadOnly); +} + // Ensure that checkboxes have their checked status set apropriately // Previously, only Radios could have this flag updated // Resulted in the issue seen at diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm index 24c84818044b4..bb3a4bf913568 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm @@ -320,6 +320,85 @@ EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES); } +// A disabled (but otherwise editable) text field must be exposed as static text +// rather than a `FlutterTextField`. This exercises the `!IsReadOnlyOrDisabled()` +// guard in `Init`: the `GetData()` override rewrites the role to `kStaticText`, +// but the node retains its `kEditableRoot` attribute, so `GetData().IsTextField()` +// still returns true. The restriction check is what keeps a disabled field from +// being instantiated as an editable `FlutterTextField`. +TEST(FlutterPlatformNodeDelegateMac, DisabledTextFieldDoesNotUseFlutterTextField) { + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; + [viewController loadView]; + + // Creates a NSWindow so that the native accessibility element has a hosting view. + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO]; + window.contentView = viewController.view; + engine.semanticsEnabled = YES; + + auto bridge = viewController.accessibilityBridge.lock(); + // Initialize ax node data. + FlutterSemanticsNode2 root = {}; + FlutterSemanticsFlags flags = FlutterSemanticsFlags{0}; + // A text field that is editable (not read-only) but disabled. + FlutterSemanticsFlags child_flags = + FlutterSemanticsFlags{.is_enabled = FlutterTristate::kFlutterTristateFalse, + .is_text_field = true, + .is_read_only = false}; + root.id = 0; + root.flags2 = &flags; + // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) + root.actions = static_cast(0); + root.label = "root"; + root.hint = ""; + root.value = ""; + root.increased_value = ""; + root.decreased_value = ""; + root.tooltip = ""; + root.child_count = 1; + int32_t children[] = {1}; + root.children_in_traversal_order = children; + root.custom_accessibility_actions_count = 0; + root.identifier = ""; + root.rect = {0, 0, 100, 100}; // LTRB + root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + bridge->AddFlutterSemanticsNodeUpdate(root); + + FlutterSemanticsNode2 child1 = {}; + child1.id = 1; + child1.flags2 = &child_flags; + // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) + child1.actions = static_cast(0); + child1.label = ""; + child1.hint = ""; + child1.value = "disabled textfield"; + child1.increased_value = ""; + child1.decreased_value = ""; + child1.tooltip = ""; + child1.text_selection_base = -1; + child1.text_selection_extent = -1; + child1.child_count = 0; + child1.custom_accessibility_actions_count = 0; + child1.identifier = ""; + child1.rect = {0, 0, 50, 50}; // LTRB + child1.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + bridge->AddFlutterSemanticsNodeUpdate(child1); + + bridge->CommitUpdates(); + + auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock(); + // The disabled text field must not be backed by a `FlutterTextField`. + id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible(); + EXPECT_FALSE([native_accessibility isKindOfClass:[FlutterTextField class]]); + EXPECT_TRUE( + [[native_accessibility accessibilityRole] isEqualToString:NSAccessibilityStaticTextRole]); + + [engine shutDownEngine]; +} + TEST(FlutterPlatformNodeDelegateMac, ChangingFlagsUpdatesNativeViewAccessible) { FlutterViewController* viewController = CreateTestViewController(); FlutterEngine* engine = viewController.engine; From 326fd6f2fa38b307448d711f44ed3fe0cbd9e3a8 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 05:31:49 +0900 Subject: [PATCH 027/330] iOS: Hardcode rendering API to Metal in tests (no-op) (#190422) Some of our tests were selecting a rendering API between kMetal and kSoftware. On iOS we no longer support any backend other than Metal. `enable_impeller` is a `static constexpr const bool` that's hardcoded to `true`, so these expressions always evaluate to `kMetal`; the `kSoftware` arm is dead code. This replaces the ternaries with `kMetal` to clean up the code and save the compiler a few microseconds. This removes the last references to `IOSRenderingAPI::kSoftware` outside the no-Metal fallback itself. I'll send a follow-up to delete that. No changes to behaviour, and really no change to the compiled tests for that matter. Issue: https://github.com/flutter/flutter/issues/190041 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Source/FlutterEnginePlatformViewTest.mm | 4 +- .../Source/FlutterTextInputPluginTest.mm | 4 +- .../Source/accessibility_bridge_test.mm | 156 +++++------------- 3 files changed, 41 insertions(+), 123 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm index e4b3a64564ff2..a0d1245aedcb8 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm @@ -86,9 +86,7 @@ - (void)setUp { /*io=*/thread_task_runner); platform_view = std::make_unique( /*delegate=*/fake_delegate, - /*rendering_api=*/fake_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/sync_switch); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index 907e2ff8a35d2..56ab4eb1750f5 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -891,9 +891,7 @@ - (void)testHotRestart { thread_task_runner->PostTask([&] { auto platform_view = std::make_unique( /*delegate=*/mock_platform_view_delegate, - /*rendering_api=*/mock_platform_view_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index ff0b5ba433f54..4db876e4250dd 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -162,9 +162,7 @@ - (void)testCreate { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -185,9 +183,7 @@ - (void)testUpdateSemanticsEmpty { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -215,9 +211,7 @@ - (void)testUpdateSemanticsOneNode { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -262,9 +256,7 @@ - (void)testIsVoiceOverRunning { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -297,9 +289,7 @@ - (void)testSemanticsDeallocated { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -362,9 +352,7 @@ - (void)testSemanticsDeallocatedWithoutLoadingView { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -413,9 +401,7 @@ - (void)testReplacedSemanticsDoesNotCleanupChildren { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -511,9 +497,7 @@ - (void)testScrollableSemanticsDeallocated { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -585,9 +569,7 @@ - (void)testBridgeReplacesSemanticsNode { [[FlutterPlatformViewsController alloc] init]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -658,9 +640,7 @@ - (void)testAnnouncesRouteChanges { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -727,9 +707,7 @@ - (void)testRadioButtonIsNotSwitchButton { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -774,9 +752,7 @@ - (void)testSemanticObjectWithNoAccessibilityFlagNotMarkedAsResponsiveToUserInte /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -818,9 +794,7 @@ - (void)testSemanticObjectWithAccessibilityFlagsMarkedAsResponsiveToUserInteract /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -868,9 +842,7 @@ - (void)testLabeledParentAndChildNotInteractive { [[FlutterPlatformViewsController alloc] init]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -927,9 +899,7 @@ - (void)testLayoutChangeWithNonAccessibilityElement { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1012,9 +982,7 @@ - (void)testLayoutChangeDoesCallNativeAccessibility { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1088,9 +1056,7 @@ - (void)testLayoutChangeDoesCallNativeAccessibilityWhenFocusChanged { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1163,9 +1129,7 @@ - (void)testScrollableSemanticsContainerReturnsCorrectChildren { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1221,9 +1185,7 @@ - (void)testAnnouncesRouteChangesAndLayoutChangeInOneUpdate { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1319,9 +1281,7 @@ - (void)testAnnouncesRouteChangesWhenAddAdditionalRoute { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1406,9 +1366,7 @@ - (void)testAnnouncesRouteChangesRemoveRouteInMiddle { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1499,9 +1457,7 @@ - (void)testHandleEvent { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1556,9 +1512,7 @@ - (void)testAccessibilityObjectDidBecomeFocused { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1596,9 +1550,7 @@ - (void)testAnnouncesRouteChangesWhenNoNamesRoute { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1667,9 +1619,7 @@ - (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1737,9 +1687,7 @@ - (void)testAnnouncesLayoutChangeWithTheSameItemFocused { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1813,9 +1761,7 @@ - (void)testAnnouncesLayoutChangeWhenFocusMovedOutside { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1891,9 +1837,7 @@ - (void)testAnnouncesScrollChangeWithLastFocused { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1965,9 +1909,7 @@ - (void)testAnnouncesScrollChangeDoesCallNativeAccessibility { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2041,9 +1983,7 @@ - (void)testAnnouncesIgnoresRouteChangesWhenModal { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2099,9 +2039,7 @@ - (void)testAnnouncesIgnoresLayoutChangeWhenModal { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2162,9 +2100,7 @@ - (void)testAnnouncesIgnoresScrollChangeWhenModal { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2233,9 +2169,7 @@ - (void)testAccessibilityMessageAfterDeletion { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2267,9 +2201,7 @@ - (void)testFlutterSemanticsScrollViewManagedObjectLifecycleCorrectly { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2317,9 +2249,7 @@ - (void)testPlatformViewDestructorDoesNotCallSemanticsAPIs { thread_task_runner->PostTask([&] { auto platform_view = std::make_unique( /*delegate=*/test_delegate, - /*rendering_api=*/test_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2364,9 +2294,7 @@ - (void)testResetsAccessibilityElementsOnHotRestart { thread_task_runner->PostTask([&] { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2394,9 +2322,7 @@ - (void)testWeakViewController { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2428,9 +2354,7 @@ - (void)testSemanticsObjectAndContainerAccessAfterBridgeDestruction { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2499,9 +2423,7 @@ - (void)testAccessibilityChannelCallbackAfterBridgeDestruction { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/mock_delegate.settings_.enable_impeller - ? flutter::IOSRenderingAPI::kMetal - : flutter::IOSRenderingAPI::kSoftware, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); From 86abb606b59e6fc47cfb9063677be4bad7cad17f Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 05:31:58 +0900 Subject: [PATCH 028/330] tools: Support FLUTTER_HOST_ARCH in update_dart_sdk scripts (#190421) Adds support for overriding the host CPU architecture via a `FLUTTER_HOST_ARCH` environment variable when pre-caching binaries. Updates `update_dart_sdk.sh` and `update_dart_sdk.ps1`, to make use of this to pull down the specified Dart SDK. This is required to allow arm64 macOS CI hosts to download and cache the x64 Dart SDK when cross-packaging x64 Flutter SDK release archives in the `packaging/packaging` recipe in `packaging.py`. See: https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/packaging/packaging.py This is pre-factoring prior to updating the tool's precache code. No test changes since by default, this behaves exactly as today and this is "tested" by the build itself on CI. The followup that updates the tool's precache code will exercise this and add tests for it. Issue: https://github.com/flutter/flutter/issues/189144 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- bin/internal/update_dart_sdk.ps1 | 8 +++++++- bin/internal/update_dart_sdk.sh | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bin/internal/update_dart_sdk.ps1 b/bin/internal/update_dart_sdk.ps1 index 318ef62add853..111a34e92210a 100644 --- a/bin/internal/update_dart_sdk.ps1 +++ b/bin/internal/update_dart_sdk.ps1 @@ -49,10 +49,16 @@ if ($engineRealm) { # It's important to use the native Dart SDK as the default target architecture # for Flutter Windows builds depend on the Dart executable's architecture. +# FLUTTER_HOST_ARCH can be set as an override to force download for the specified architecture. +# PROCESSOR_ARCHITECTURE is a standard Windows env var indicating host CPU architecture. $dartZipNameX64 = "dart-sdk-windows-x64.zip" $dartZipNameArm64 = "dart-sdk-windows-arm64.zip" $dartZipName = $dartZipNameX64 -if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { +if ($env:FLUTTER_HOST_ARCH -eq "arm64") { + $dartZipName = $dartZipNameArm64 +} elseif ($env:FLUTTER_HOST_ARCH -eq "x64") { + $dartZipName = $dartZipNameX64 +} elseif ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { $dartSdkArm64Url = "$dartSdkBaseUrl/flutter_infra_release/flutter/$engineVersion/$dartZipNameArm64" Try { Invoke-WebRequest -Uri $dartSdkArm64Url -UseBasicParsing -Method Head | Out-Null diff --git a/bin/internal/update_dart_sdk.sh b/bin/internal/update_dart_sdk.sh index 41f419112e1f4..cf306f0c1cd11 100755 --- a/bin/internal/update_dart_sdk.sh +++ b/bin/internal/update_dart_sdk.sh @@ -61,8 +61,11 @@ if [ ! -f "$ENGINE_STAMP" ] || [ "$ENGINE_VERSION" != "$(< "$ENGINE_STAMP")" ]; exit 1 } - # `uname -m` may be running in Rosetta mode, instead query sysctl - if [ "$OS" = 'Darwin' ]; then + if [ -n "$FLUTTER_HOST_ARCH" ]; then + # FLUTTER_HOST_ARCH can be set to override the host architecture detection. + ARCH="$FLUTTER_HOST_ARCH" + elif [ "$OS" = 'Darwin' ]; then + # `uname -m` may be running in Rosetta mode, instead query sysctl # Allow non-zero exit so we can do control flow set +e # -n means only print value, not key From 7fb5a40d78678b57bb2a4490305e99507d477f39 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 05:32:04 +0900 Subject: [PATCH 029/330] iOS,macOS: Use @autoclosure in Logger (#190417) This updates `Logger` to use `@autoclosure` for its log message parameter, which enables lazy evaluation *after* checking the log level, similar to our `fml::Logger` macros in C++. Messages that are expensive to construct, e.g. "Semantics tree: \(expensiveFunction())". Adds `objc@` wrappers taking string arguments to ensure this remains backwards compatible with the previous Obj-C API. Issue: https://github.com/flutter/flutter/issues/44030 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../common/framework/Source/Logger.swift | 45 +++++++++++++--- .../common/framework/Source/LoggerTests.swift | 54 ++++++++++++++++++- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift index 089b2068a3bcf..c396bc97d6d73 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift @@ -59,9 +59,9 @@ import Foundation #endif } - public func log(level: LogLevel, _ message: String) { + public func log(level: LogLevel, _ message: @autoclosure () -> String) { if level.rawValue >= logLevel.rawValue { - outputWriter.writeLine(level: level, message) + outputWriter.writeLine(level: level, message()) } } } @@ -80,31 +80,62 @@ extension Logger { } /// Logs a message at `LogLevel.info`. - @objc public static func logInfo(_ message: String) { + @available(swift, obsoleted: 1.0) + @objc(logInfo:) public static func objcLogInfo(_ message: String) { shared.log(level: .info, message) } + /// Logs a message at `LogLevel.info`. + public static func logInfo(_ message: @autoclosure () -> String) { + shared.log(level: .info, message()) + } + /// Logs a message at `LogLevel.important`. - @objc public static func logImportant(_ message: String) { + @available(swift, obsoleted: 1.0) + @objc(logImportant:) public static func objcLogImportant(_ message: String) { shared.log(level: .important, message) } + /// Logs a message at `LogLevel.important`. + public static func logImportant(_ message: @autoclosure () -> String) { + shared.log(level: .important, message()) + } + /// Logs a message at `LogLevel.warning`. - @objc public static func logWarning(_ message: String) { + @available(swift, obsoleted: 1.0) + @objc(logWarning:) public static func objcLogWarning(_ message: String) { shared.log(level: .warning, message) } + /// Logs a message at `LogLevel.warning`. + public static func logWarning(_ message: @autoclosure () -> String) { + shared.log(level: .warning, message()) + } + /// Logs a message at `LogLevel.error`. - @objc public static func logError(_ message: String) { + @available(swift, obsoleted: 1.0) + @objc(logError:) public static func objcLogError(_ message: String) { shared.log(level: .error, message) } + /// Logs a message at `LogLevel.error`. + public static func logError(_ message: @autoclosure () -> String) { + shared.log(level: .error, message()) + } + /// Logs a message at `LogLevel.fatal` and immediately terminates the application. - @objc public static func logFatal(_ message: String) { + @available(swift, obsoleted: 1.0) + @objc(logFatal:) public static func objcLogFatal(_ message: String) { shared.log(level: .fatal, message) abort() } + /// Logs a message at `LogLevel.fatal` and immediately terminates the application. + public static func logFatal(_ message: @autoclosure () -> String) { + shared.log(level: .fatal, message()) + abort() + } + /// Logs a message unconditionally. @objc public static func logDirect(_ message: String) { shared.outputWriter.writeLine(level: .important, message) diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift index 9872233df543d..20181fae40d1a 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift @@ -3,9 +3,8 @@ // found in the LICENSE file. import Foundation -import Testing - import InternalFlutterSwiftCommon +import Testing import test_utils_swift @Suite struct LoggerTests { @@ -55,4 +54,55 @@ import test_utils_swift #expect(writer.lastLine == "Hello world") } + @Test func testAutoclosureDoesNotEvaluateMessageBelowLogLevel() { + let writer = StringOutputWriter() + let logger = Logger(outputWriter: writer, logLevel: .warning) + var wasEvaluated = false + + logger.log( + level: .info, + { + wasEvaluated = true + return "Hello world" + }()) + #expect(!writer.didLog) + #expect(!wasEvaluated) + } + + @Test func testAutoclosureEvaluatesMessageAtOrAboveLogLevel() { + let writer = StringOutputWriter() + let logger = Logger(outputWriter: writer, logLevel: .info) + var wasEvaluated = false + + logger.log( + level: .info, + { + wasEvaluated = true + return "Hello world" + }()) + #expect(writer.didLog) + #expect(wasEvaluated) + } + + @Test func testStaticLogInfoDoesNotEvaluateMessageBelowLogLevel() { + let writer = StringOutputWriter() + let oldWriter = Logger.outputWriter + let oldLevel = Logger.logLevel + defer { + Logger.outputWriter = oldWriter + Logger.logLevel = oldLevel + } + Logger.outputWriter = writer + Logger.logLevel = .warning + var wasEvaluated = false + + Logger.logInfo( + { + wasEvaluated = true + return "Hello world" + }()) + #expect(!writer.didLog) + #expect(!wasEvaluated) + } + } From 47e519e29245bb87f006dbda27ea517735c12a31 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 05:39:41 +0900 Subject: [PATCH 030/330] iOS: Eliminate the Impeller/Skia backend selection params (#190416) iOS has been Impeller for a while now. Skia is gone. There are still a few remnants like `IOSRenderingBackend` enum and `IOSContext::GetBackend()` that offer the illusion of a choice, when in fact every Skia arm leads to an abort(). The base `GetBackend()` still returned a `kSkia` value that can no longer be constructed. This removes the remaining dead code. We still need to keep the cross-platform bits around: the `enable_impeller` setting itself stays. iOS simply stops consulting it here. The `IOSContextNoop` unit test used to assert on `GetBackend`, but we removed that; it now asserts the noop context has no Impeller context, which is equivalent. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../shell/platform/darwin/ios/ios_context.h | 10 ---------- .../shell/platform/darwin/ios/ios_context.mm | 16 +--------------- .../darwin/ios/ios_context_metal_impeller.h | 2 -- .../darwin/ios/ios_context_metal_impeller.mm | 4 ---- .../shell/platform/darwin/ios/ios_context_noop.h | 2 -- .../platform/darwin/ios/ios_context_noop.mm | 5 ----- .../darwin/ios/ios_context_noop_unittests.mm | 2 +- .../shell/platform/darwin/ios/ios_surface.mm | 14 ++++---------- .../platform/darwin/ios/platform_view_ios.mm | 3 --- .../darwin/ios/rendering_api_selection.h | 5 ----- 10 files changed, 6 insertions(+), 57 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context.h b/engine/src/flutter/shell/platform/darwin/ios/ios_context.h index da8df41347cf9..88a96742412e0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context.h @@ -47,14 +47,11 @@ class IOSContext { /// /// @param[in] api A client rendering API supported by the /// engine/platform. - /// @param[in] backend A client rendering backend supported by the - /// engine/platform. /// /// @return A valid context on success. `nullptr` on failure. /// static std::unique_ptr Create( IOSRenderingAPI api, - IOSRenderingBackend backend, const std::shared_ptr& is_gpu_disabled_sync_switch, const Settings& settings); @@ -64,13 +61,6 @@ class IOSContext { /// virtual ~IOSContext(); - //---------------------------------------------------------------------------- - /// @brief Get the rendering backend used by this context. - /// - /// @return The rendering backend. - /// - virtual IOSRenderingBackend GetBackend() const; - //---------------------------------------------------------------------------- /// @brief Creates an external texture proxy of the appropriate client /// rendering API. diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm index fe89967cde340..25db4909ed6fa 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm @@ -22,7 +22,6 @@ std::unique_ptr IOSContext::Create( IOSRenderingAPI api, - IOSRenderingBackend backend, const std::shared_ptr& is_gpu_disabled_sync_switch, const Settings& settings) { switch (api) { @@ -34,25 +33,12 @@ "this."]; return std::make_unique(); case IOSRenderingAPI::kMetal: - switch (backend) { - case IOSRenderingBackend::kSkia: - [FlutterLogger logFatal:@"Impeller opt-out unavailable."]; - return nullptr; - case IOSRenderingBackend::kImpeller: - return std::make_unique(settings, is_gpu_disabled_sync_switch); - } - default: - break; + return std::make_unique(settings, is_gpu_disabled_sync_switch); } FML_CHECK(false); return nullptr; } -IOSRenderingBackend IOSContext::GetBackend() const { - // Overridden by Impeller subclasses. - return IOSRenderingBackend::kSkia; -} - std::shared_ptr IOSContext::GetImpellerContext() const { return nullptr; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.h b/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.h index 00f489199e9b6..a54b3c898e2c4 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.h @@ -26,8 +26,6 @@ class IOSContextMetalImpeller final : public IOSContext { ~IOSContextMetalImpeller(); - IOSRenderingBackend GetBackend() const override; - private: FlutterDarwinContextMetalImpeller* darwin_context_metal_impeller_; std::shared_ptr aiks_context_; diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.mm index d5c3deb1edd9b..116ba7d070902 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context_metal_impeller.mm @@ -34,10 +34,6 @@ IOSContextMetalImpeller::~IOSContextMetalImpeller() = default; -IOSRenderingBackend IOSContextMetalImpeller::GetBackend() const { - return IOSRenderingBackend::kImpeller; -} - // |IOSContext| std::shared_ptr IOSContextMetalImpeller::GetImpellerContext() const { return darwin_context_metal_impeller_.context; diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h index 71ee138705adf..912b69ba1e48b 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h @@ -21,8 +21,6 @@ class IOSContextNoop final : public IOSContext { std::unique_ptr CreateExternalTexture(int64_t texture_id, NSObject* texture) override; - IOSRenderingBackend GetBackend() const override; - private: IOSContextNoop(const IOSContextNoop&) = delete; diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm index 8c50308924a54..f5d2b64d07a8f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm @@ -15,11 +15,6 @@ // |IOSContext| IOSContextNoop::~IOSContextNoop() = default; -// |IOSContext| -IOSRenderingBackend IOSContextNoop::GetBackend() const { - return IOSRenderingBackend::kImpeller; -} - // |IOSContext| std::unique_ptr IOSContextNoop::CreateExternalTexture(int64_t texture_id, NSObject* texture) { diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm index f5c882f150140..2878bea423b17 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm @@ -18,7 +18,7 @@ @implementation IOSContextNoopTest - (void)testCreateNoop { flutter::IOSContextNoop noop; - XCTAssertTrue(noop.GetBackend() == flutter::IOSRenderingBackend::kImpeller); + XCTAssertTrue(noop.GetImpellerContext() == nullptr); } @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm index b03b9b4600dfe..647fb77b9f9a3 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm @@ -22,16 +22,10 @@ if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { if ([layer isKindOfClass:[CAMetalLayer class]]) { - switch (context->GetBackend()) { - case IOSRenderingBackend::kSkia: - [FlutterLogger logFatal:@"Impeller opt-out unavailable."]; - return nullptr; - case IOSRenderingBackend::kImpeller: - return std::make_unique( - static_cast(layer), // Metal layer - std::move(context) // context - ); - } + return std::make_unique( + static_cast(layer), // Metal layer + std::move(context) // context + ); } } return std::make_unique(std::move(context)); diff --git a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm index 814b380f0e213..311c6fdfd903a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm @@ -38,9 +38,6 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} const std::shared_ptr& is_gpu_disabled_sync_switch) : PlatformViewIOS(delegate, IOSContext::Create(rendering_api, - delegate.OnPlatformViewGetSettings().enable_impeller - ? IOSRenderingBackend::kImpeller - : IOSRenderingBackend::kSkia, is_gpu_disabled_sync_switch, delegate.OnPlatformViewGetSettings()), platform_views_controller, diff --git a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h index a285657dca883..8af497b41212d 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h +++ b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h @@ -16,11 +16,6 @@ enum class IOSRenderingAPI { kMetal, }; -enum class IOSRenderingBackend { - kSkia, - kImpeller, -}; - IOSRenderingAPI GetRenderingAPIForProcess(); Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api); From 162eaf1998c207dd4b700229be45ba9520fd44c3 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 05:40:13 +0900 Subject: [PATCH 031/330] iOS: Remove the synchronous first-frame wait (#190432) `-waitForFirstFrameSync:` is a no-op on iOS. The iOS embedder now only supports merged platform/UI thread task runners on the iOS main thread. `FlutterDartProject.mm` passes `require_merged_platform_ui_thread` to `SettingsFromCommandLine`, and flutter/flutter#190051 prevents the `mergeAfterLaunch` configuration (which was never implemented for iOS. Because of this, the thread calling this method at first layout is the thread responsible for producing the frame, and `Shell::WaitForFirstFrame` always ends up taking the `kFailedPrecondition` bailout without waiting. This removes the method and its only call site in FlutterViewController's first layout. This *was* the main-thread wait introduced in flutter/engine#9506 to avoid presenting a black first frame when transitioning into a FlutterViewController with a prewarmed engine (flutter/flutter#32937). It has been dead code since the platform and UI threads merged. Maybe more intuitive evidence of this is that the main thread can't block waiting for a frame that it itself has to produce. Because I'm paranoid, I tested locally on a physical device (since the original issue mentioned this doesn't repro on simulators) on both debug and release builds using https://github.com/cbracken/flutter_flashofblack just to be 100% sure this didn't sneak back in at some point during or after the thread merge. It still looks good to me. No test changes: the wait and its call site had no coverage, and regardless the removed code has been dead code for a couple years. Issue: https://github.com/flutter/flutter/issues/112232 (prework) ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/framework/Source/FlutterEngine.mm | 7 ------- .../framework/Source/FlutterEngine_Internal.h | 4 ---- .../framework/Source/FlutterViewController.mm | 19 ++----------------- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index b07f3c32c4844..b86edb4252d71 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -1534,13 +1534,6 @@ - (void)onStatusBarTap { [self.statusBarChannel invokeMethod:@"handleScrollToTop" arguments:nil]; } -- (void)waitForFirstFrameSync:(NSTimeInterval)timeout - callback:(NS_NOESCAPE void (^_Nonnull)(BOOL didTimeout))callback { - fml::TimeDelta waitTime = fml::TimeDelta::FromMilliseconds(timeout * 1000); - fml::Status status = self.shell.WaitForFirstFrame(waitTime); - callback(status.code() == fml::StatusCode::kDeadlineExceeded); -} - - (void)waitForFirstFrame:(NSTimeInterval)timeout callback:(void (^_Nonnull)(BOOL didTimeout))callback { dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 1dc1a840ceeca..a3c0d8b4ef7da 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -65,10 +65,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)attachView; - (void)notifyLowMemory; -/// Blocks until the first frame is presented or the timeout is exceeded, then invokes callback. -- (void)waitForFirstFrameSync:(NSTimeInterval)timeout - callback:(NS_NOESCAPE void (^)(BOOL didTimeout))callback; - /// Asynchronously waits until the first frame is presented or the timeout is exceeded, then invokes /// callback. - (void)waitForFirstFrame:(NSTimeInterval)timeout callback:(void (^)(BOOL didTimeout))callback; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 042f078c22f7f..5c787776e9ba0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -1393,27 +1393,12 @@ - (void)viewDidLayoutSubviews { [self updateViewportMetricsIfNeeded]; // There is no guarantee that UIKit will layout subviews when the application/scene is active. - // Creating the surface when inactive will cause GPU accesses from the background. Only wait for - // the first frame to render when the application/scene is actually active. + // Creating the surface when inactive will cause GPU accesses from the background, so only + // create the surface when the application/scene is actually active. // This must run after updateViewportMetrics so that the surface creation tasks are queued after // the viewport metrics update tasks. if (firstViewBoundsUpdate && self.stateIsActive && self.engine) { [self surfaceUpdated:YES]; -#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG - NSTimeInterval timeout = 0.2; -#else - NSTimeInterval timeout = 0.1; -#endif - [self.engine - waitForFirstFrameSync:timeout - callback:^(BOOL didTimeout) { - if (didTimeout) { - [FlutterLogger logInfo:@"Timeout waiting for the first frame to render. " - "This may happen in unoptimized builds. If this is" - "a release build, you should load a less complex " - "frame to avoid the timeout."]; - } - }]; } } From 7f8c4a8db70ee8e35420620c974bdc1f65ad3d91 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 05:43:13 +0900 Subject: [PATCH 032/330] tests: add --ios-runtime param (#190414) By default, when creating the simulator to run iOS unit tests, we use the newest installed runtime, even if it's a beta release of iOS. During the annual window between WWDC and the final release, we may want to test using either current stable or the beta OS version. This adds an --ios-runtime param. By default, the code behaves as it does today, selecting the newest installed runtime. If you want to override, you can specify the simulator runtime to use explicitly, such as: --ios-runtime com.apple.CoreSimulator.SimRuntime.iOS-26-5 the returend simulator UUID is then passed to xcodebuild. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- engine/src/flutter/testing/run_tests.py | 29 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/engine/src/flutter/testing/run_tests.py b/engine/src/flutter/testing/run_tests.py index df89a48384fef..b4d9c6c634620 100755 --- a/engine/src/flutter/testing/run_tests.py +++ b/engine/src/flutter/testing/run_tests.py @@ -856,7 +856,9 @@ def run_android_tests( def run_objc_tests( - ios_variant: str = 'ios_debug_sim_unopt', test_filter: typing.Optional[str] = None + ios_variant: str = 'ios_debug_sim_unopt', + test_filter: typing.Optional[str] = None, + ios_runtime: typing.Optional[str] = None, ) -> None: """Runs Objective-C XCTest unit tests for the iOS embedding""" assert_expected_xcode_version() @@ -870,12 +872,15 @@ def run_objc_tests( delete_simulator(new_simulator_name) create_simulator = [ - 'xcrun ' - 'simctl ' - 'create ' - f'{new_simulator_name} com.apple.CoreSimulator.SimDeviceType.iPhone-11' + 'xcrun', + 'simctl', + 'create', + new_simulator_name, + 'com.apple.CoreSimulator.SimDeviceType.iPhone-11', ] - run_cmd(create_simulator, shell=True) + if ios_runtime is not None: + create_simulator.append(ios_runtime) + simulator_id = subprocess.check_output(create_simulator, text=True).strip() try: ios_unit_test_dir = os.path.join(BUILDROOT_DIR, 'flutter', 'testing', 'ios', 'IosUnitTests') @@ -892,7 +897,7 @@ def run_objc_tests( '-sdk iphonesimulator ' '-scheme IosUnitTests ' '-resultBundlePath ' + result_bundle_path + ' ' - '-destination name=' + new_simulator_name + ' ' + f'-destination id={simulator_id} ' 'test ' 'FLUTTER_ENGINE=' + ios_variant ] @@ -1324,6 +1329,14 @@ def main() -> int: default='ios_debug_sim_unopt', help='The engine build variant to run objective-c tests for' ) + parser.add_argument( + '--ios-runtime', + dest='ios_runtime', + action='store', + default=None, + help='The iOS simulator runtime to run tests on ' + '(example: "com.apple.CoreSimulator.SimRuntime.iOS-26-5")' + ) parser.add_argument( '--verbose-dart-snapshot', dest='verbose_dart_snapshot', @@ -1490,7 +1503,7 @@ def main() -> int: if 'objc' in types: assert is_mac(), 'iOS embedding tests can only be run on macOS.' - run_objc_tests(args.ios_variant, args.objc_filter) + run_objc_tests(args.ios_variant, args.objc_filter, args.ios_runtime) # https://github.com/flutter/flutter/issues/36300 if 'benchmarks' in types and not is_windows(): From 1cc78881468e624e2e4f45f097b0963d1d68be48 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 3 Aug 2026 16:58:26 -0400 Subject: [PATCH 033/330] Roll Skia from 958c1c1921a1 to a08d918ebd6a (3 revisions) (#190467) https://skia.googlesource.com/skia.git/+log/958c1c1921a1..a08d918ebd6a 2026-08-03 nathanasanchez@google.com [Graphite] Add explicit root nodes struct 2026-08-03 kjlubick@google.com Avoid integer overflow in SkRasterPipeline 2026-08-03 jmbetancourt@google.com Add exemption to mutex Presubmit check If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC aaclarke@google.com,alexisdavidc@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index fac4f866e63b2..c56eb335c3657 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '958c1c1921a154d6a6f0551fa26948f18aa8d9ff', + 'skia_revision': 'a08d918ebd6a85b03538040ad80da26dc78e387f', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 1204b7ae5b489b45805ed65076bcf4aa2dc2ee77 Mon Sep 17 00:00:00 2001 From: b-luk <97480502+b-luk@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:52:05 -0700 Subject: [PATCH 034/330] Add windows platform support for primitive_shape_test integration test (#190464) Without this fix, windows_primitive_shape_golden_test is currently failing with: `[2026-08-03 12:47:06.236940] [STDOUT] stdout: Failed to load "C:/b/s/w/ir/x/w/rc/tmpr_xvq3iy/flutter sdk/dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart": No Windows desktop project configured. See https://flutter.dev/to/add-desktop-support to learn about adding Windows support to a project.` e.g. https://ci.chromium.org/ui/p/flutter/builders/staging/Windows%20windows_primitive_shape_golden_test/43/overview ## Run at head: Ran at head with: ``` led get-builder "luci.flutter.try:Windows windows_primitive_shape_golden_test" | \ led edit -pa git_ref='refs/pull/190464/head' | \ led edit -pa git_url='https://github.com/flutter/flutter' | \ led launch ``` #### Run results: https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Windows%20windows_primitive_shape_golden_test/2/overview #### Generated new golden: https://flutter-gold.skia.org/detail?grouping=name%3Dprimitive_shape.integration_test.primitive_shape_canvas_snapshot%26source_type%3Dflutter&digest=b978f6619a26f754ee1a2a2307504cbd&changelist_id=190464&crs=github image #### 500% zoom: image Shows AA on all shapes. ## Run prior to Windows AA fixes: Ran it at commit `24debf8bbfc44134b71dca9739abda02e5e73b93`, which is prior to both the MSAA and UberSDF Windows AA fixes: ``` led get-builder "luci.flutter.try:Windows windows_primitive_shape_golden_test" | \ led edit -pa git_ref='refs/pull/190464/head' | \ led edit -pa git_url='https://github.com/flutter/flutter' | \ led edit -pa flutter_prebuilt_engine_version='24debf8bbfc44134b71dca9739abda02e5e73b93' | \ led launch ``` #### Run results: https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Windows%20windows_primitive_shape_golden_test/3/overview #### Generated new golden: https://flutter-gold.skia.org/detail?grouping=name%3Dprimitive_shape.integration_test.primitive_shape_canvas_snapshot%26source_type%3Dflutter&digest=a1688d28bfb5359fdc31f698b916c5a3&changelist_id=190464&crs=github image #### 500% zoom: image Shows no AA on UberSDF shapes (everything except for the stroked rsuperellipse in the bottom right), which is to be expected without the UberSDF AA fix. The stroked rsuperellipse is rendered with tessellation, which depends on MSAA. This does show AA in the golden result, even without the Windows MSAA fix. This is an unusual finding, which I have brought up to @gaaclarke previously. From manually running the test app and examining the output, the app does not have AA for this case, but the golden screenshot seems to differ. I do not know what causes this difference in real app behavior vs in the golden output. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- dev/devicelab/lib/tasks/integration_tests.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index 929e9d5aec4e2..78a56b8a14965 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -234,6 +234,7 @@ TaskFunction createPrimitiveShapeTest() { return IntegrationTest( '${flutterDirectory.path}/dev/integration_tests/primitive_shape_test', 'integration_test/primitive_shape_test.dart', + createPlatforms: ['windows'], ).call; } From b29df1dcd1c064c2c0727cab610ae1dd733594e8 Mon Sep 17 00:00:00 2001 From: walley892 Date: Mon, 3 Aug 2026 17:59:03 -0400 Subject: [PATCH 035/330] Add path rendering benchmarks (#188654) Adds primitive rendering benchmarks for path drawing operations. Pre work for implementing a solution to [185117](https://github.com/flutter/flutter/issues/185117) ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> --- .../benchmarking/dl_benchmarks.cc | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/display_list/benchmarking/dl_benchmarks.cc b/engine/src/flutter/display_list/benchmarking/dl_benchmarks.cc index 4068314510af8..4b4883aa7b44a 100644 --- a/engine/src/flutter/display_list/benchmarking/dl_benchmarks.cc +++ b/engine/src/flutter/display_list/benchmarking/dl_benchmarks.cc @@ -159,6 +159,7 @@ constexpr size_t kRRectsToDraw = 5000; constexpr size_t kRSEsToDraw = 5000; constexpr size_t kDRRectsToDraw = 2000; constexpr size_t kArcSweepSetsToDraw = 1000; +constexpr size_t kPathsToDraw = 2000; constexpr size_t kImagesToDraw = 500; constexpr size_t kFixedCanvasSize = 1024; @@ -940,6 +941,98 @@ void BM_DrawPath(benchmark::State& state, "DrawPath-" + label); } +void BM_DrawPathPrimitives(benchmark::State& state, + BackendType backend_type, + unsigned attributes, + PathVerb type) { + auto surface_provider = DlSurfaceProvider::Create(backend_type); + DisplayListBuilder builder; + DlPaint paint = GetPaintForRun(attributes); + + CheckAttributes(attributes, state, DisplayListOpFlags::kDrawPathFlags); + + size_t length = state.range(0); + surface_provider->InitializeSurface(length * 2, length * 2); + auto surface = surface_provider->GetPrimarySurface(); + surface->Clear(DlColor::kTransparent()); + surface->FlushSubmitCpuSync(); + + DlPathBuilder path_builder; + DlPoint center = DlPoint(length / 2.0f, length / 2.0f); + float radius = length / 2.0f; + + switch (type) { + case PathVerb::kLine: + GetLinesPath(path_builder, 10, center, radius); + break; + case PathVerb::kQuad: + GetQuadsPath(path_builder, 10, center, radius); + break; + case PathVerb::kConic: + GetConicsPath(path_builder, 10, center, radius); + break; + case PathVerb::kCubic: + GetCubicsPath(path_builder, 10, center, radius); + break; + } + DlPath path = path_builder.TakePath(); + + const DlPoint delta(0.5f, 0.5f); + RectAnimator animator(DlRect::MakeWH(length, length), delta, surface); + + state.counters["DrawCallCount"] = kPathsToDraw; + for (size_t i = 0; i < kPathsToDraw; i++) { + builder.Save(); + builder.Translate(animator.GetPoint().x, animator.GetPoint().y); + builder.DrawPath(path, paint); + builder.Restore(); + animator.Animate(); + } + + auto display_list = builder.Build(); + + // Prime any path conversions + surface->RenderDisplayList(display_list); + surface->FlushSubmitCpuSync(); + + // We only want to time the actual rasterization. + size_t items_processed = 0; + for ([[maybe_unused]] auto _ : state) { + surface->RenderDisplayList(display_list); + items_processed += kPathsToDraw; + surface->FlushSubmitCpuSync(); + } + state.SetItemsProcessed(items_processed); + + std::string label = VerbToString(type); + SaveSnapshotIfNecessary(surface_provider, surface, state, + "DrawPathPrimitives-" + label); +} + +void BM_DrawPathLine(benchmark::State& state, + BackendType backend_type, + unsigned attributes) { + BM_DrawPathPrimitives(state, backend_type, attributes, PathVerb::kLine); +} + +void BM_DrawPathQuad(benchmark::State& state, + BackendType backend_type, + unsigned attributes) { + BM_DrawPathPrimitives(state, backend_type, attributes, PathVerb::kQuad); +} + +void BM_DrawPathConic(benchmark::State& state, + BackendType backend_type, + unsigned attributes) { + BM_DrawPathPrimitives(state, backend_type, attributes, PathVerb::kConic); +} + +void BM_DrawPathCubic(benchmark::State& state, + BackendType backend_type, + unsigned attributes) { + BM_DrawPathPrimitives(state, backend_type, attributes, PathVerb::kCubic); +} + // Returns a set of vertices that describe a circle that has a // radius of `radius` and outer vertex count of approximately // `vertex_count`. The final number of vertices will differ as we @@ -1676,6 +1769,12 @@ constexpr int kFilledShadow10Primitive = DRAW_BENCHMARK_SHADOW_PRIMITIVE(BACKEND, TYPE, FilledShadow5) \ DRAW_BENCHMARK_SHADOW_PRIMITIVE(BACKEND, TYPE, FilledShadow10) \ +#define DRAW_BENCHMARK_PRIMITIVES_PATH(BACKEND) \ + DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, PathLine) \ + DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, PathQuad) \ + DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, PathConic) \ + DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, PathCubic) + #define DRAW_BENCHMARK_PRIMITIVE_SUITE(BACKEND) \ BENCHMARK_OVERHEAD(SyncOverhead, BACKEND) \ BENCHMARK_OVERHEAD(EmptyDisplayList, BACKEND) \ @@ -1687,7 +1786,8 @@ constexpr int kFilledShadow10Primitive = DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, SimpleRRect) \ DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, ComplexRRect) \ DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, SimpleRSE) \ - DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, ComplexRSE) + DRAW_BENCHMARK_PRIMITIVES_TYPE(BACKEND, ComplexRSE) \ + DRAW_BENCHMARK_PRIMITIVES_PATH(BACKEND) // clang-format on From ca7bea93c72dfe9bde825a9f039ca994ff9bfa4a Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 4 Aug 2026 07:18:34 +0900 Subject: [PATCH 036/330] iOS: Eliminate use of IOSContextNoop in platform view tests (#190419) `FlutterPlatformViewsTest` used the internal engine class `flutter::IOSContextNoop` in a dozen places purely as a no-op context to hand to `submitFrame:withIosContext:`. This replaces those with a local `FakeIOSContext` that replicates the behaviour the tests rely on so that we can delete `IOSContextNoop` in a follow-up. Like `IOSContextNoop` `FakeIOSContext` reports the Impeller backend and returns no external texture, and inherits the null Impeller/Aiks contexts from the base class. The one place that uses the engine's real context (`GetIosContext`) is left untouched. I suspect we can eventually move this code to use the real Metal backend but for now, this is just refactoring with no semantic change. Back in ancient times, before the simulator required Metal, and when iOS still had a Skia software renderer, we ran that on the Simulator due to some issues with our OpenGL implementation. Later, Flutter on iOS migrated from Skia to Impeller. Skia supports a software backend but Impeller did not, and so when running on Impeller, we stubbed out the software backend to no-op context/surfaces, hence IOSContextNoop and IOSSurfaceNoop. The Simulator now *requires* Metal and Flutter has eliminated Skia support altogether so there's no longer a software mode at all, nor a need for a no-op path in case you're using Impeller and ask for a software backend. This is part of a series of changes to remove the dead no-op fallback. No behavioural change; just a test refactoring. Issue: https://github.com/flutter/flutter/issues/190041 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Source/FlutterPlatformViewsTest.mm | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 1fdcb42de2585..aa676568e4ef0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -26,11 +26,32 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTestHelper.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView+Test.h" -#include "flutter/shell/platform/darwin/ios/ios_context_noop.h" +#include "flutter/shell/platform/darwin/ios/ios_context.h" #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" FLUTTER_ASSERT_ARC +namespace { +// An IOSContext fake for tests that do not need a real GPU context. +class FakeIOSContext : public flutter::IOSContext { + public: + FakeIOSContext() = default; + ~FakeIOSContext() override = default; + + // |IOSContext| + flutter::IOSRenderingBackend GetBackend() const override { + return flutter::IOSRenderingBackend::kImpeller; + } + + // |IOSContext| + std::unique_ptr CreateExternalTexture( + int64_t texture_id, + NSObject* texture) override { + return nullptr; + } +}; +} // namespace + @class FlutterPlatformViewsTestMockPlatformView; __weak static UIView* gMockPlatformView = nil; const float kFloatCompareEpsilon = 0.001; @@ -4225,9 +4246,8 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return false; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600)); - XCTAssertFalse([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertFalse([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); auto embeddedViewParams_2 = std::make_unique(finalMatrix, flutter::DlSize(300, 300), stack); @@ -4242,9 +4262,8 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600)); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface_submit_true) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface_submit_true) + withIosContext:std::make_shared()]); } - (void) @@ -4431,9 +4450,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // platform view is wrapped by touch interceptor, which itself is wrapped by clipping view. UIView* clippingView1 = view1.superview.superview; @@ -4460,9 +4478,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] > [flutterView.subviews indexOfObject:clippingView2], @@ -4535,9 +4552,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // platform view is wrapped by touch interceptor, which itself is wrapped by clipping view. UIView* clippingView1 = view1.superview.superview; @@ -4564,9 +4580,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] < [flutterView.subviews indexOfObject:clippingView2], @@ -5015,9 +5030,8 @@ - (void)testDisposingViewInCompositionOrderDoNotCrash { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // Disposing won't remove embedded views until the view is removed from the composition_order_ XCTAssertEqual(flutterPlatformViewsController.embeddedViewCount, 2UL); @@ -5042,9 +5056,8 @@ - (void)testDisposingViewInCompositionOrderDoNotCrash { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // Disposing won't remove embedded views until the view is removed from the composition_order_ XCTAssertEqual(flutterPlatformViewsController.embeddedViewCount, 1UL); @@ -5108,7 +5121,7 @@ - (void)testOnlyPlatformViewsAreRemovedWhenReset { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; UIView* someView = [[UIView alloc] init]; [flutterView addSubview:someView]; @@ -5174,7 +5187,7 @@ - (void)testResetClearsPreviousCompositionOrder { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; // The above code should result in previousCompositionOrder having one viewId in it XCTAssertEqual(flutterPlatformViewsController.previousCompositionOrder.count, 1ul); @@ -5243,7 +5256,7 @@ - (void)testNilPlatformViewDoesntCrash { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; XCTAssertEqual(flutterView.subviews.count, 1u); } @@ -5354,7 +5367,7 @@ - (void)testFlutterPlatformViewControllerSubmitFramePreservingFrameDamage { }); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; XCTAssertTrue(submit_info.has_value()); XCTAssertEqual(*submit_info->frame_damage, flutter::DlIRect::MakeWH(800, 600)); From a09e37c28f9317e646f1e937ba9f73523a0c5dc5 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:54:04 -0700 Subject: [PATCH 037/330] [web] Unify image decoding and codecs on CanvasKit and Skwasm (#188573) Consolidates all image decoding, tiered network routing, progressive stream duplication, and DOM-based resizing logic into a unified shared frontend coordinator (EngineCodec). Both CanvasKit and Skwasm backends are reduced to dumb native adapters, and garbage-collection finalization is centralized exclusively in the frontend wrappers. More work towards CanvasKit/Skwasm unification: https://github.com/flutter/flutter/issues/175630 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: zhongliugo --- .../src/flutter/lib/web_ui/lib/painting.dart | 10 +- .../flutter/lib/web_ui/lib/src/engine.dart | 6 +- .../web_ui/lib/src/engine/backend/codec.dart | 50 ++ .../web_ui/lib/src/engine/backend/image.dart | 6 + .../src/engine/canvaskit/animated_image.dart | 109 +++++ .../lib/src/engine/canvaskit/image.dart | 447 +---------------- .../engine/canvaskit/image_wasm_codecs.dart | 144 ------ .../engine/canvaskit/image_web_codecs.dart | 77 --- .../lib/src/engine/canvaskit/renderer.dart | 166 +++---- .../lib/web_ui/lib/src/engine/dom.dart | 141 +++++- .../src/engine/html_image_element_codec.dart | 275 ----------- .../web_ui/lib/src/engine/image_decoder.dart | 450 +++++++++++++++--- .../lib/src/engine/primitives/codec.dart | 351 ++++++++++++++ .../lib/src/engine/primitives/image.dart | 4 +- .../src/engine/primitives/image_source.dart | 21 + .../lib/web_ui/lib/src/engine/renderer.dart | 133 +++++- .../src/engine/skwasm/skwasm_impl/codecs.dart | 88 ++-- .../src/engine/skwasm/skwasm_impl/image.dart | 6 +- .../engine/skwasm/skwasm_impl/renderer.dart | 157 +----- .../engine/skwasm/skwasm_stub/renderer.dart | 48 +- .../web_ui/lib/ui_web/src/ui_web/images.dart | 2 +- .../test/canvaskit/image_golden_test.dart | 4 +- .../lib/web_ui/test/canvaskit/image_test.dart | 33 -- .../compositing/render_canvas_test.dart | 10 +- .../test/skwasm/native_memory_test.dart | 8 +- .../lib/web_ui/test/ui/codecs_test.dart | 36 +- .../image/html_image_element_codec_test.dart | 243 ---------- .../web_ui/test/ui/image_decoder_test.dart | 273 ++++++++++- .../lib/web_ui/test/ui/image_golden_test.dart | 2 +- .../test/ui/image_texture_source_test.dart | 5 +- engine/src/flutter/skwasm/animated_image.cc | 9 +- 31 files changed, 1655 insertions(+), 1659 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/backend/codec.dart create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/animated_image.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/primitives/codec.dart delete mode 100644 engine/src/flutter/lib/web_ui/test/ui/image/html_image_element_codec_test.dart diff --git a/engine/src/flutter/lib/web_ui/lib/painting.dart b/engine/src/flutter/lib/web_ui/lib/painting.dart index 94ddea19142fe..1b2783bb9dcb7 100644 --- a/engine/src/flutter/lib/web_ui/lib/painting.dart +++ b/engine/src/flutter/lib/web_ui/lib/painting.dart @@ -733,7 +733,7 @@ Future instantiateImageCodec( int? targetWidth, int? targetHeight, bool allowUpscaling = true, -}) => engine.renderer.instantiateImageCodec( +}) => engine.engineInstantiateImageCodec( list, targetWidth: targetWidth, targetHeight: targetHeight, @@ -747,7 +747,7 @@ Future instantiateImageCodecFromBuffer( bool allowUpscaling = true, }) async { try { - return await engine.renderer.instantiateImageCodec( + return await engine.engineInstantiateImageCodec( buffer._list!, targetWidth: targetWidth, targetHeight: targetHeight, @@ -766,14 +766,14 @@ Future instantiateImageCodecWithSize( FrameInfo? info; try { if (getTargetSize == null) { - return await engine.renderer.instantiateImageCodec(buffer._list!); + return await engine.engineInstantiateImageCodec(buffer._list!); } else { - codec = await engine.renderer.instantiateImageCodec(buffer._list!); + codec = await engine.engineInstantiateImageCodec(buffer._list!); info = await codec.getNextFrame(); final int width = info.image.width; final int height = info.image.height; final TargetImageSize targetSize = getTargetSize(width, height); - return await engine.renderer.instantiateImageCodec( + return await engine.engineInstantiateImageCodec( buffer._list!, targetWidth: targetSize.width, targetHeight: targetSize.height, diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index 54823c096c99a..700434a00228a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -18,17 +18,17 @@ library engine; export 'engine/alarm_clock.dart'; export 'engine/app_bootstrap.dart'; export 'engine/arena.dart'; +export 'engine/backend/codec.dart'; export 'engine/backend/image.dart'; export 'engine/backend/path.dart'; export 'engine/browser_detection.dart'; +export 'engine/canvaskit/animated_image.dart'; export 'engine/canvaskit/canvas.dart'; export 'engine/canvaskit/canvaskit_api.dart'; export 'engine/canvaskit/color_filter.dart'; export 'engine/canvaskit/fonts.dart'; export 'engine/canvaskit/image.dart'; export 'engine/canvaskit/image_filter.dart'; -export 'engine/canvaskit/image_wasm_codecs.dart'; -export 'engine/canvaskit/image_web_codecs.dart'; export 'engine/canvaskit/mask_filter.dart'; export 'engine/canvaskit/native_memory.dart'; export 'engine/canvaskit/painting.dart'; @@ -63,7 +63,6 @@ export 'engine/font_fallbacks.dart'; export 'engine/fonts.dart'; export 'engine/frame_service.dart'; export 'engine/frame_timing_recorder.dart'; -export 'engine/html_image_element_codec.dart'; export 'engine/image_decoder.dart'; export 'engine/image_decoding_manager.dart'; export 'engine/image_format_detector.dart'; @@ -102,6 +101,7 @@ export 'engine/plugins.dart'; export 'engine/pointer_binding.dart'; export 'engine/pointer_binding/event_position_helper.dart'; export 'engine/pointer_converter.dart'; +export 'engine/primitives/codec.dart'; export 'engine/primitives/image.dart'; export 'engine/primitives/image_source.dart'; export 'engine/primitives/path.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/backend/codec.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/backend/codec.dart new file mode 100644 index 0000000000000..dc9468f9212fe --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/backend/codec.dart @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:ui/src/engine.dart'; + +/// Holds the decoding result for a single frame of an animated image processed +/// by a native backend (CanvasKit or Skwasm). +/// +/// This is a backend-agnostic equivalent of [ui.FrameInfo], containing a +/// [BackendImage] representing the GPU texture or Skia image, and the [duration] +/// for which this frame should be displayed. +class BackendFrameInfo { + BackendFrameInfo({required this.duration, required this.image}); + + /// The duration this frame should be shown. + final Duration duration; + + /// The underlying backend-specific representation of the image frame. + final BackendImage image; +} + +/// An abstract contract defining the interface for a backend-specific, multi-frame +/// animated image decoder (e.g., CkAnimatedImage for CanvasKit and +/// SkwasmAnimatedImageDecoder for Skwasm). +/// +/// The shared frontend (`EngineCodec`) is responsible for high-level codec state, +/// while the concrete backends act as frame extractors that implement this contract. +abstract class BackendAnimatedImage { + /// The total number of frames in the animated image. + int get frameCount; + + /// The number of times this animation should repeat. + /// + /// A value of -1 indicates infinite repetition, while 0 indicates the animation + /// should play once (no repetitions). + int get repetitionCount; + + /// Decodes and returns the next frame of the animation. + /// + /// When called, the implementation should extract the current frame, prepare + /// the decoder to advance to the next frame, and return a [BackendFrameInfo] + /// containing the frame's image and duration. + Future getNextFrame(); + + /// Releases all native resources associated with the animated decoder. + void dispose(); +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/backend/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/backend/image.dart index 91609c7daa971..06124659eb7d9 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/backend/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/backend/image.dart @@ -4,6 +4,12 @@ /// Represents an image managed by a specific graphics backend. abstract class BackendImage { + /// The width of this image in pixels. + int get width; + + /// The height of this image in pixels. + int get height; + /// Disposes resources held by the backend image. void dispose(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/animated_image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/animated_image.dart new file mode 100644 index 0000000000000..919cd89fbf328 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/animated_image.dart @@ -0,0 +1,109 @@ +// 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. + +/// Uses image codecs supplied by the CanvasKit WASM bundle. +/// +/// See also: +/// +/// * `image_web_codecs.dart`, which uses the `ImageDecoder` supplied by the browser. +library animated_image; + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:ui/src/engine.dart'; + +/// The CanvasKit implementation of [BackendAnimatedImage]. +/// +/// This class acts as a simple, backend-specific adapter around CanvasKit's +/// native [SkAnimatedImage]. It delegates frame extraction and metadata queries +/// directly to the native object, and deletes the native WebAssembly object when +/// explicitly disposed. +class CkAnimatedImage implements BackendAnimatedImage { + /// Decodes an animated image from a list of encoded bytes. + CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight}) { + skAnimatedImage = createSkAnimatedImage(); + } + + /// The underlying native Skia animated image object. + late final SkAnimatedImage skAnimatedImage; + final String src; + final Uint8List _bytes; + int _frameCount = 0; + int _repetitionCount = -1; + + /// The target width requested by the user. + /// + /// Note: CanvasKit's WASM animated image decoder does not support native resizing + /// during decode. The shared frontend (`_SkiaEngineCodec`) will apply a canvas-based + /// scaling fallback if these dimensions are specified. + final int? targetWidth; + + /// The target height requested by the user. + final int? targetHeight; + + /// Invokes CanvasKit to instantiate the native decoder and read initial metadata. + SkAnimatedImage createSkAnimatedImage() { + final SkAnimatedImage? animatedImage = canvasKit.MakeAnimatedImageFromEncoded(_bytes); + if (animatedImage == null) { + throw ImageCodecException( + 'Failed to decode image data.\n' + 'Image source: $src', + ); + } + + _frameCount = animatedImage.getFrameCount().toInt(); + _repetitionCount = animatedImage.getRepetitionCount().toInt(); + + return animatedImage; + } + + bool _disposed = false; + bool get debugDisposed => _disposed; + + bool _debugCheckIsNotDisposed() { + assert(!_disposed, 'This image has been disposed.'); + return true; + } + + @override + void dispose() { + assert(!_disposed, 'Cannot dispose a codec that has already been disposed.'); + _disposed = true; + skAnimatedImage.delete(); + } + + @override + int get frameCount { + assert(_debugCheckIsNotDisposed()); + return _frameCount; + } + + @override + int get repetitionCount { + assert(_debugCheckIsNotDisposed()); + return _repetitionCount; + } + + @override + Future getNextFrame() { + assert(_debugCheckIsNotDisposed()); + + // Query the display duration for the current frame. + final int frameDurationMs = skAnimatedImage.currentFrameDuration().toInt(); + + // Extract the current frame as a static SkImage and wrap it. + final SkImage skImage = skAnimatedImage.makeImageAtCurrentFrame(); + + final currentFrame = BackendFrameInfo( + duration: Duration(milliseconds: frameDurationMs), + image: CkImageDelegate(skImage), + ); + + // Advance the native decoder so the next frame is ready on the next call. + skAnimatedImage.decodeNextFrame(); + + return Future.value(currentFrame); + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart index 8e15014e11b89..7bb24c4ea84db 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -2,430 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:math' as math; -import 'dart:typed_data'; - import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; -import 'package:ui/ui_web/src/ui_web.dart' as ui_web; - -/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia. -Future skiaInstantiateImageCodec( - Uint8List list, [ - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, -]) async { - ui.Codec codec; - // ImageDecoder does not detect image type automatically. It requires us to - // tell it what the image type is. - final ImageType imageType = tryDetectImageType(list, 'encoded image bytes'); - - if (browserSupportsImageDecoder) { - codec = await CkBrowserImageDecoder.create( - data: list, - contentType: imageType.mimeType, - debugSource: 'encoded image bytes', - ); - } else { - if (imageType.isAnimated) { - codec = CkAnimatedImage.decodeFromBytes( - list, - 'encoded image bytes', - targetWidth: targetWidth, - targetHeight: targetHeight, - ); - } else { - final DomBlob blob = createDomBlob([list.buffer]); - codec = await decodeBlobToCkImage(blob); - } - } - return CkResizingCodec( - codec, - targetWidth: targetWidth, - targetHeight: targetHeight, - allowUpscaling: allowUpscaling, - ); -} - -/// A resizing codec which uses an HTML element to scale the image if -/// it is backed by an HTML Image element. -class CkResizingCodec extends ResizingCodec { - CkResizingCodec(super.delegate, {super.targetWidth, super.targetHeight, super.allowUpscaling}); - - @override - ui.Image scaleImage( - ui.Image image, { - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) { - final ckImage = image as EngineImage; - if (ckImage.imageSource == null) { - return scaleImageIfNeeded( - image, - targetWidth: targetWidth, - targetHeight: targetHeight, - allowUpscaling: allowUpscaling, - ); - } else { - return _scaleImageUsingDomCanvas( - ckImage, - targetWidth: targetWidth, - targetHeight: targetHeight, - allowUpscaling: allowUpscaling, - ); - } - } - - EngineImage _scaleImageUsingDomCanvas( - EngineImage image, { - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) { - assert(image.imageSource != null); - final int width = image.width; - final int height = image.height; - // Calculate the target scaled dimensions while maintaining the aspect ratio if needed. - final BitmapSize? scaledSize = scaledImageSize(width, height, targetWidth, targetHeight); - if (scaledSize == null) { - return image; - } - // If upscaling is disabled and the target dimensions exceed the original size, - // do not perform scaling and return the original image. - if (!allowUpscaling && (scaledSize.width > width || scaledSize.height > height)) { - return image; - } - - final int scaledWidth = scaledSize.width; - final int scaledHeight = scaledSize.height; - - final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(scaledWidth, scaledHeight); - final ctx = offscreenCanvas.getContext('2d')! as DomCanvasRenderingContext2D; - ctx.drawImage( - image.imageSource!.canvasImageSource, - 0, - 0, - width, - height, - 0, - 0, - scaledWidth, - scaledHeight, - ); - final DomImageBitmap bitmap = offscreenCanvas.transferToImageBitmap(); - SkImage? skImage; - if (CanvasKitRenderer.instance.isSoftware) { - skImage = canvasKit.MakeImageFromCanvasImageSource(bitmap); - } else { - skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true); - } - - // Resize the canvas to 0x0 to cause the browser to eagerly reclaim its - // memory. - offscreenCanvas.width = 0; - offscreenCanvas.height = 0; - - if (skImage == null) { - domWindow.console.warn('Failed to scale image.'); - return image; - } - - image.dispose(); - return EngineImage( - CkImageDelegate(skImage), - skImage.width().toInt(), - skImage.height().toInt(), - imageSource: ImageBitmapImageSource(bitmap), - ); - } -} - -ui.Image createCkImageFromImageElement( - DomHTMLImageElement image, - int naturalWidth, - int naturalHeight, -) { - SkImage? skImage; - // If software rendering is active, make the SkImage directly from the canvas source. - if (CanvasKitRenderer.instance.isSoftware) { - skImage = canvasKit.MakeImageFromCanvasImageSource(image); - } else { - // If GPU-accelerated CanvasKit is active, create a lazy image from the HTML Image Element, - // which uploads the texture dynamically when painted. Specify pre-multiplied alpha and sRGB. - skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( - image, - SkPartialImageInfo( - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - colorSpace: SkColorSpaceSRGB, - width: naturalWidth.toDouble(), - height: naturalHeight.toDouble(), - ), - ); - } - if (skImage == null) { - throw ImageCodecException('Failed to create image from Image.decode'); - } - - return EngineImage( - CkImageDelegate(skImage), - skImage.width().toInt(), - skImage.height().toInt(), - imageSource: ImageElementImageSource(image), - ); -} - -class CkImageElementCodec extends HtmlImageElementCodec { - CkImageElementCodec(super.src, {super.chunkCallback}); - - @override - ui.Image createImageFromHTMLImageElement( - DomHTMLImageElement image, - int naturalWidth, - int naturalHeight, - ) => createCkImageFromImageElement(image, naturalWidth, naturalHeight); -} - -class CkImageBlobCodec extends HtmlBlobCodec { - CkImageBlobCodec(super.blob, {super.chunkCallback}); - - @override - ui.Image createImageFromHTMLImageElement( - DomHTMLImageElement image, - int naturalWidth, - int naturalHeight, - ) => createCkImageFromImageElement(image, naturalWidth, naturalHeight); -} - -/// Creates and decodes an image using HtmlImageElement. -Future decodeBlobToCkImage(DomBlob blob) async { - final codec = CkImageBlobCodec(blob); - await codec.decode(); - return codec; -} - -Future decodeUrlToCkImage(String src) async { - final codec = CkImageElementCodec(src); - await codec.decode(); - return codec; -} - -void skiaDecodeImageFromPixels( - Uint8List pixels, - int width, - int height, - ui.PixelFormat format, - ui.ImageDecoderCallback callback, { - int? rowBytes, - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, -}) { - if (targetWidth != null) { - assert(allowUpscaling || targetWidth <= width); - } - if (targetHeight != null) { - assert(allowUpscaling || targetHeight <= height); - } - - // Run in a timer to avoid janking the current frame by moving the decoding - // work outside the frame event. - Timer.run(() { - final SkImage? skImage = canvasKit.MakeImage( - SkImageInfo( - width: width.toDouble(), - height: height.toDouble(), - colorType: format == ui.PixelFormat.rgba8888 - ? canvasKit.ColorType.RGBA_8888 - : canvasKit.ColorType.BGRA_8888, - alphaType: canvasKit.AlphaType.Premul, - colorSpace: SkColorSpaceSRGB, - ), - pixels, - rowBytes ?? 4 * width, - ); - - if (skImage == null) { - domWindow.console.warn('Failed to create image from pixels.'); - return; - } - - if (targetWidth != null || targetHeight != null) { - if (validUpscale(allowUpscaling, targetWidth, targetHeight, width, height)) { - return callback(scaleImage(skImage, targetWidth, targetHeight)); - } - } - return callback( - EngineImage(CkImageDelegate(skImage), skImage.width().toInt(), skImage.height().toInt()), - ); - }); -} - -// An invalid upscale happens when allowUpscaling is false AND either the given -// targetWidth is larger than the originalWidth OR the targetHeight is larger than originalHeight. -bool validUpscale( - bool allowUpscaling, - int? targetWidth, - int? targetHeight, - int originalWidth, - int originalHeight, -) { - if (allowUpscaling) { - return true; - } - final bool targetWidthFits; - final bool targetHeightFits; - if (targetWidth != null) { - targetWidthFits = targetWidth <= originalWidth; - } else { - targetWidthFits = true; - } - - if (targetHeight != null) { - targetHeightFits = targetHeight <= originalHeight; - } else { - targetHeightFits = true; - } - return targetWidthFits && targetHeightFits; -} - -/// Creates a scaled [CkImage] from an [SkImage] by drawing the [SkImage] to a canvas. -/// -/// This function will only be called if either a targetWidth or targetHeight is not null -/// -/// If only one of targetWidth or targetHeight are specified, the other -/// dimension will be scaled according to the aspect ratio of the supplied -/// dimension. -/// -/// If either targetWidth or targetHeight is less than or equal to zero, it -/// will be treated as if it is null. -EngineImage scaleImage(SkImage image, int? targetWidth, int? targetHeight) { - final temporaryImage = EngineImage( - CkImageDelegate(image), - image.width().toInt(), - image.height().toInt(), - ); - try { - assert(targetWidth != null || targetHeight != null); - final int width = temporaryImage.width; - final int height = temporaryImage.height; - - var adjustedWidth = targetWidth; - var adjustedHeight = targetHeight; - if (adjustedWidth != null && adjustedWidth <= 0) { - adjustedWidth = null; - } - if (adjustedHeight != null && adjustedHeight <= 0) { - adjustedHeight = null; - } - - final int finalTargetWidth = - adjustedWidth ?? - (adjustedHeight != null ? (adjustedHeight * width / height).round() : width); - final int finalTargetHeight = - adjustedHeight ?? - (adjustedWidth != null ? (adjustedWidth * height / width).round() : height); - - final recorder = CkPictureRecorder(); - final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest); - - final paint = CkPaint(); - canvas.drawImageRect( - temporaryImage, - ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()), - ui.Rect.fromLTWH(0, 0, finalTargetWidth.toDouble(), finalTargetHeight.toDouble()), - paint, - ); - - final CkPicture picture = recorder.endRecording(); - final ui.Image finalImage = picture.toImageSync(finalTargetWidth, finalTargetHeight); - - return finalImage as EngineImage; - } finally { - temporaryImage.dispose(); - } -} - -const String _kNetworkImageMessage = 'Failed to load network image.'; - -/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after -/// requesting from URI. -Future skiaInstantiateWebImageCodec( - String url, - ui_web.ImageCodecChunkCallback? chunkCallback, -) async { - final imageElementCodec = CkImageElementCodec(url, chunkCallback: chunkCallback); - try { - await imageElementCodec.decode(); - return imageElementCodec; - } on ImageCodecException { - imageElementCodec.dispose(); - final Uint8List list = await fetchImage(url, chunkCallback); - final ImageType imageType = tryDetectImageType(list, url); - if (browserSupportsImageDecoder) { - return CkBrowserImageDecoder.create( - data: list, - contentType: imageType.mimeType, - debugSource: url, - ); - } else { - return CkAnimatedImage.decodeFromBytes(list, url); - } - } -} - -/// Sends a request to fetch image data. -Future fetchImage(String url, ui_web.ImageCodecChunkCallback? chunkCallback) async { - try { - final HttpFetchResponse response = await httpFetch(url); - final int? contentLength = response.contentLength; - - if (!response.hasPayload) { - throw ImageCodecException( - '$_kNetworkImageMessage\n' - 'Image URL: $url\n' - 'Server response code: ${response.status}', - ); - } - - if (chunkCallback != null && contentLength != null) { - return await readChunked(response.payload, contentLength, chunkCallback); - } else { - return await response.asUint8List(); - } - } on HttpFetchError catch (_) { - throw ImageCodecException( - '$_kNetworkImageMessage\n' - 'Image URL: $url\n' - 'Trying to load an image from another domain? Find answers at:\n' - 'https://docs.flutter.dev/development/platform-integration/web-images', - ); - } -} - -/// Reads the [payload] in chunks using the browser's Streams API -/// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API -Future readChunked( - HttpFetchPayload payload, - int contentLength, - ui_web.ImageCodecChunkCallback chunkCallback, -) async { - final result = JSUint8Array.withLength(contentLength); - var position = 0; - var cumulativeBytesLoaded = 0; - await payload.read((JSUint8Array chunk) { - cumulativeBytesLoaded += chunk.length; - chunkCallback(cumulativeBytesLoaded, contentLength); - result.set(chunk, position); - position += chunk.length; - }); - return result.toDart; -} /// A [BackendImage] backed by an `SkImage` from Skia. class CkImageDelegate implements BackendImage { @@ -435,9 +12,11 @@ class CkImageDelegate implements BackendImage { final SkImage skImage; /// Returns the width of the image in pixels. + @override int get width => skImage.width().toInt(); /// Returns the height of the image in pixels. + @override int get height => skImage.height().toInt(); /// Releases the native memory allocated for the Skia image. @@ -452,25 +31,3 @@ class CkImageDelegate implements BackendImage { return other is CkImageDelegate && other.skImage.isAliasOf(skImage); } } - -/// Detect the image type or throw an error if image type can't be detected. -ImageType tryDetectImageType(Uint8List data, String debugSource) { - // ImageDecoder does not detect image type automatically. It requires us to - // tell it what the image type is. - final ImageType? imageType = detectImageType(data); - - if (imageType == null) { - final String fileHeader; - if (data.isNotEmpty) { - fileHeader = '[${bytesToHexString(data.sublist(0, math.min(10, data.length)))}]'; - } else { - fileHeader = 'empty'; - } - throw ImageCodecException( - 'Failed to detect image file format using the file header.\n' - 'File header was $fileHeader.\n' - 'Image source: $debugSource', - ); - } - return imageType; -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart deleted file mode 100644 index 4de3ffc1a8d01..0000000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// Uses image codecs supplied by the CanvasKit WASM bundle. -/// -/// See also: -/// -/// * `image_web_codecs.dart`, which uses the `ImageDecoder` supplied by the browser. -library image_wasm_codecs; - -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -/// The CanvasKit implementation of [ui.Codec]. -/// -/// Wraps `SkAnimatedImage`. -class CkAnimatedImage implements ui.Codec { - /// Decodes an image from a list of encoded bytes. - CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight}) { - final SkAnimatedImage skAnimatedImage = createSkAnimatedImage(); - _ref = CkUniqueRef(this, skAnimatedImage, 'Codec'); - } - - late final CkUniqueRef _ref; - final String src; - final Uint8List _bytes; - int _frameCount = 0; - int _repetitionCount = -1; - - final int? targetWidth; - final int? targetHeight; - - SkAnimatedImage createSkAnimatedImage() { - SkAnimatedImage? animatedImage = canvasKit.MakeAnimatedImageFromEncoded(_bytes); - if (animatedImage == null) { - throw ImageCodecException( - 'Failed to decode image data.\n' - 'Image source: $src', - ); - } - - if (targetWidth != null || targetHeight != null) { - if (animatedImage.getFrameCount() > 1) { - printWarning('targetWidth and targetHeight for multi-frame images not supported'); - } else { - animatedImage = _resizeAnimatedImage(animatedImage, targetWidth, targetHeight); - if (animatedImage == null) { - throw ImageCodecException( - 'Failed to decode re-sized image data.\n' - 'Image source: $src', - ); - } - } - } - - _frameCount = animatedImage.getFrameCount().toInt(); - _repetitionCount = animatedImage.getRepetitionCount().toInt(); - - return animatedImage; - } - - SkAnimatedImage? _resizeAnimatedImage( - SkAnimatedImage animatedImage, - int? targetWidth, - int? targetHeight, - ) { - final SkImage image = animatedImage.makeImageAtCurrentFrame(); - final EngineImage ckImage = scaleImage(image, targetWidth, targetHeight); - try { - if (ckImage.backendImage case CkImageDelegate(:final skImage)) { - final Uint8List? resizedBytes = skImage.encodeToBytes(); - if (resizedBytes == null) { - throw ImageCodecException('Failed to re-size image'); - } - final SkAnimatedImage? resizedAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded( - resizedBytes, - ); - return resizedAnimatedImage; - } else { - throw StateError('The resized image must be a CanvasKit image.'); - } - } finally { - ckImage.dispose(); - } - } - - bool _disposed = false; - bool get debugDisposed => _disposed; - - bool _debugCheckIsNotDisposed() { - assert(!_disposed, 'This image has been disposed.'); - return true; - } - - @override - void dispose() { - assert(!_disposed, 'Cannot dispose a codec that has already been disposed.'); - _disposed = true; - _ref.dispose(); - } - - @override - int get frameCount { - assert(_debugCheckIsNotDisposed()); - return _frameCount; - } - - @override - int get repetitionCount { - assert(_debugCheckIsNotDisposed()); - return _repetitionCount; - } - - @override - Future getNextFrame() { - assert(_debugCheckIsNotDisposed()); - final SkAnimatedImage animatedImage = _ref.nativeObject; - - // SkAnimatedImage comes pre-initialized to point to the current frame (by - // default the first frame, and, with some special resurrection logic in - // `createDefault`, to a subsequent frame if resurrection happens in the - // middle of animation). Flutter's `Codec` semantics is to initialize to - // point to "just before the first frame", i.e. the first invocation of - // `getNextFrame` returns the first frame. Therefore, we have to read the - // current Skia frame, then advance SkAnimatedImage to the next frame, and - // return the current frame. - - final int frameDurationMs = animatedImage.currentFrameDuration().toInt(); - final SkImage skImage = animatedImage.makeImageAtCurrentFrame(); - - final ui.FrameInfo currentFrame = AnimatedImageFrameInfo( - Duration(milliseconds: frameDurationMs), - EngineImage(CkImageDelegate(skImage), skImage.width().toInt(), skImage.height().toInt()), - ); - - animatedImage.decodeNextFrame(); - - return Future.value(currentFrame); - } -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart deleted file mode 100644 index 9309d7f44beb9..0000000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Uses the `ImageDecoder` class supplied by the browser. -// -// See also: -// -// * `image_wasm_codecs.dart`, which uses codecs supplied by the CanvasKit WASM bundle. - -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:typed_data'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -/// Image decoder backed by the browser's `ImageDecoder`. -class CkBrowserImageDecoder extends BrowserImageDecoder { - CkBrowserImageDecoder._({ - required super.contentType, - required super.dataSource, - required super.debugSource, - }); - - static Future create({ - required Uint8List data, - required String contentType, - required String debugSource, - }) async { - final decoder = CkBrowserImageDecoder._( - contentType: contentType, - dataSource: data.toJS, - debugSource: debugSource, - ); - - // Call once to initialize the decoder and populate late fields. - await decoder.initialize(); - return decoder; - } - - @override - ui.Image generateImageFromVideoFrame(VideoFrame frame) { - SkImage? skImage; - if (CanvasKitRenderer.instance.isSoftware) { - final int width = frame.displayWidth.toInt(); - final int height = frame.displayHeight.toInt(); - final DomHTMLCanvasElement canvas = createDomCanvasElement(width: width, height: height); - final DomCanvasRenderingContext2D ctx = canvas.context2D; - ctx.drawImage(frame, 0, 0); - skImage = canvasKit.MakeImageFromCanvasImageSource(canvas); - } else { - skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( - frame, - SkPartialImageInfo( - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - colorSpace: SkColorSpaceSRGB, - width: frame.displayWidth, - height: frame.displayHeight, - ), - ); - } - if (skImage == null) { - throw ImageCodecException( - "Failed to create image from pixel data decoded using the browser's ImageDecoder.", - ); - } - - return EngineImage( - CkImageDelegate(skImage), - skImage.width().toInt(), - skImage.height().toInt(), - imageSource: VideoFrameImageSource(frame), - ); - } -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 2739d04ce50c8..aa729276f8bc3 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -198,111 +198,101 @@ class CanvasKitRenderer extends Renderer { } @override - Future instantiateImageCodec( - Uint8List list, { - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) async => skiaInstantiateImageCodec(list, targetWidth, targetHeight, allowUpscaling); - - @override - Future instantiateImageCodecFromUrl( - Uri uri, { - ui_web.ImageCodecChunkCallback? chunkCallback, - }) => skiaInstantiateWebImageCodec(uri.toString(), chunkCallback); + BackendAnimatedImage createAnimatedImage(Uint8List bytes, {int? targetWidth, int? targetHeight}) { + return CkAnimatedImage.decodeFromBytes( + bytes, + 'encoded image bytes', + targetWidth: targetWidth, + targetHeight: targetHeight, + ); + } @override - ui.Image createImageFromImageBitmap(DomImageBitmap imageBitmap) { + /// Converts a normalized [ImageSource] into a CanvasKit-specific [BackendImage]. + /// + /// This method implements a highly optimized resource allocation strategy that + /// behaves differently depending on the active rendering mode: + /// + /// - **Software Rendering Fallback (`isSoftware`):** If the CanvasKit backend is running + /// without GPU acceleration, we call `MakeImageFromCanvasImageSource`. This eagerly + /// rasterizes the DOM source on the CPU and copies its pixels into a C++ WASM-allocated + /// heap buffer. + /// - **Hardware-Accelerated WebGL Path (`!isSoftware`):** To avoid blocking the main + /// thread and prevent massive GPU memory spikes, we use "lazy" texture uploads: + /// - **ImageBitmap Source:** We call `MakeLazyImageFromImageBitmap`. The second argument + /// (`true`) transfers ownership of the bitmap to CanvasKit, allowing CanvasKit to + /// automatically close and release the browser-allocated bitmap once it has been + /// successfully uploaded to a GPU texture. + /// - **Other Texture Sources:** We call `MakeLazyImageFromTextureSourceWithInfo` to register + /// the texture source (e.g. canvas or video frame) with WebGL. + /// In both cases, the actual upload of the texture to the GPU is deferred until the + /// image is drawn on the screen for the first time, ensuring smooth animations. + /// + /// Additionally, lazy texture uploads allow a single texture source to be uploaded + /// to multiple WebGL contexts. This is critical in "MultiSurfaceRasterizer" mode, + /// where multiple WebGL contexts are active on-screen concurrently, and the same + /// image may need to be rendered across different surfaces. + BackendImage createImageFromImageSource(ImageSource source) { SkImage? skImage; - // For software rendering, instantiate an SkImage immediately from the canvas source. + final DomCanvasImageSource canvasImageSource = source.canvasImageSource; if (isSoftware) { - skImage = canvasKit.MakeImageFromCanvasImageSource(imageBitmap); + skImage = canvasKit.MakeImageFromCanvasImageSource(canvasImageSource); } else { - // For GPU-accelerated CanvasKit, create a lazy image from the ImageBitmap, which - // defers uploading the image texture to the WebGL context until render time. - skImage = canvasKit.MakeLazyImageFromImageBitmap(imageBitmap, true); + if (canvasImageSource.isA()) { + skImage = canvasKit.MakeLazyImageFromImageBitmap(canvasImageSource as DomImageBitmap, true); + } else { + skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( + canvasImageSource, + SkPartialImageInfo( + width: source.width.toDouble(), + height: source.height.toDouble(), + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + ), + ); + } } if (skImage == null) { - throw Exception('Failed to convert image bitmap to an SkImage.'); + throw Exception('Failed to convert image source to an SkImage.'); } - return EngineImage( - CkImageDelegate(skImage), - skImage.width().toInt(), - skImage.height().toInt(), - imageSource: ImageBitmapImageSource(imageBitmap), - ); + return CkImageDelegate(skImage); } @override - FutureOr createImageFromTextureSource( - JSAny object, { + bool get isMultiThreaded => false; + + @override + bool get supportsResizingAnimatedImages => false; + + @override + BackendImage decodeBackendImageFromPixels( + Uint8List pixels, { required int width, required int height, - required bool transferOwnership, - }) async { - if (!transferOwnership) { - final DomImageBitmap bitmap = await createImageBitmap(object, ( - x: 0, - y: 0, - width: width, - height: height, - )); - return createImageFromImageBitmap(bitmap); - } - SkImage? skImage; - if (isSoftware) { - if (object.isA()) { - // If the object is a VideoFrame, we need to draw it to a canvas first to - // avoid a bug in CanvasKit where MakeImageFromCanvasImageSource doesn't - // work with VideoFrames. - final DomHTMLCanvasElement canvas = createDomCanvasElement(width: width, height: height); - final DomCanvasRenderingContext2D ctx = canvas.context2D; - ctx.drawImage(object as VideoFrame, 0, 0); - skImage = canvasKit.MakeImageFromCanvasImageSource(canvas); - } else { - skImage = canvasKit.MakeImageFromCanvasImageSource(object); - } - } else { - skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( - object, - SkPartialImageInfo( - width: width.toDouble(), - height: height.toDouble(), - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - colorSpace: SkColorSpaceSRGB, - ), - ); - } + required ui.PixelFormat format, + int? rowBytes, + }) { + final SkImage? skImage = canvasKit.MakeImage( + SkImageInfo( + width: width.toDouble(), + height: height.toDouble(), + colorType: format == ui.PixelFormat.rgba8888 + ? canvasKit.ColorType.RGBA_8888 + : canvasKit.ColorType.BGRA_8888, + alphaType: canvasKit.AlphaType.Premul, + colorSpace: SkColorSpaceSRGB, + ), + pixels, + rowBytes ?? 4 * width, + ); if (skImage == null) { - throw Exception('Failed to convert image bitmap to an SkImage.'); + throw Exception('Failed to create image from pixels.'); } - return EngineImage(CkImageDelegate(skImage), skImage.width().toInt(), skImage.height().toInt()); - } - @override - void decodeImageFromPixels( - Uint8List pixels, - int width, - int height, - ui.PixelFormat format, - ui.ImageDecoderCallback callback, { - int? rowBytes, - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) => skiaDecodeImageFromPixels( - pixels, - width, - height, - format, - callback, - rowBytes: rowBytes, - targetWidth: targetWidth, - targetHeight: targetHeight, - allowUpscaling: allowUpscaling, - ); + return CkImageDelegate(skImage); + } @override ui.ImageShader createImageShader( diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart index 489581eaaea57..113ba0b43dabf 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart @@ -258,24 +258,105 @@ external DomIntl get domIntl; @JS('Symbol') external DomSymbol get domSymbol; -@JS('createImageBitmap') -external JSPromise _createImageBitmap(JSAny source, [int x, int y, int width, int height]); +extension CreateImageBitmapExtension on JSObject { + @JS('createImageBitmap') + external JSPromise createImageBitmap(JSAny source); + + @JS('createImageBitmap') + external JSPromise createImageBitmapWithOptions(JSAny source, ImageBitmapOptions options); + + @JS('createImageBitmap') + external JSPromise createImageBitmapWithBounds( + JSAny source, + int x, + int y, + int width, + int height, + ); + + @JS('createImageBitmap') + external JSPromise createImageBitmapWithBoundsAndOptions( + JSAny source, + int x, + int y, + int width, + int height, + ImageBitmapOptions options, + ); +} + Future createImageBitmap( - JSAny source, [ + JSAny source, { ({int x, int y, int width, int height})? bounds, -]) { + ImageBitmapOptions? options, +}) { if (debugThrowOnCreateImageBitmapIfDisabled && !browserSupportsCreateImageBitmap) { throw UnsupportedError('createImageBitmap is not supported in this browser'); } - JSPromise jsPromise; + final JSPromise jsPromise; if (bounds != null) { - jsPromise = _createImageBitmap(source, bounds.x, bounds.y, bounds.width, bounds.height); + if (options != null) { + jsPromise = globalContext.createImageBitmapWithBoundsAndOptions( + source, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + options, + ); + } else { + jsPromise = globalContext.createImageBitmapWithBounds( + source, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + } } else { - jsPromise = _createImageBitmap(source); + if (options != null) { + jsPromise = globalContext.createImageBitmapWithOptions(source, options); + } else { + jsPromise = globalContext.createImageBitmap(source); + } } return jsPromise.toDart.then((JSAny? value) => value! as DomImageBitmap); } +@JS() +@anonymous +extension type ImageBitmapOptions._primary(JSObject _) implements JSObject { + factory ImageBitmapOptions({ + String? imageOrientation, + String? premultiplyAlpha, + String? colorSpaceConversion, + int? resizeWidth, + int? resizeHeight, + String? resizeQuality, + }) { + final obj = JSObject(); + if (imageOrientation != null) { + obj['imageOrientation'] = imageOrientation.toJS; + } + if (premultiplyAlpha != null) { + obj['premultiplyAlpha'] = premultiplyAlpha.toJS; + } + if (colorSpaceConversion != null) { + obj['colorSpaceConversion'] = colorSpaceConversion.toJS; + } + if (resizeWidth != null) { + obj['resizeWidth'] = resizeWidth.toJS; + } + if (resizeHeight != null) { + obj['resizeHeight'] = resizeHeight.toJS; + } + if (resizeQuality != null) { + obj['resizeQuality'] = resizeQuality.toJS; + } + return ImageBitmapOptions._primary(obj); + } +} + @JS('Navigator') extension type DomNavigator._(JSObject _) implements JSObject { external DomClipboard? get clipboard; @@ -1264,6 +1345,10 @@ abstract class HttpFetchResponse { /// Returns null if "Content-Length" is missing. int? get contentLength; + /// Returns the value of the HTTP header with the given [name], or null if + /// the header is not present. + String? header(String name); + /// Return true if this response has a [payload]. /// /// Returns false if this response does not have a payload and therefore it is @@ -1322,13 +1407,16 @@ class HttpFetchResponseImpl implements HttpFetchResponse { @override int? get contentLength { - final String? header = _domResponse.headers.get('Content-Length'); + final String? header = this.header('Content-Length'); if (header == null) { return null; } return int.tryParse(header); } + @override + String? header(String name) => _domResponse.headers.get(name); + @override bool get hasPayload { final bool accepted = status >= 200 && status < 300; @@ -1367,6 +1455,14 @@ class MockHttpFetchResponse implements HttpFetchResponse { @override final int? contentLength; + @override + String? header(String name) { + if (name.toLowerCase() == 'content-length' && contentLength != null) { + return contentLength.toString(); + } + return null; + } + @override bool get hasPayload => _payload != null; @@ -1392,6 +1488,9 @@ abstract class HttpFetchPayload { /// Return the data as a string. Future text(); + + /// Returns the raw DOM readable stream. + DomReadableStream get stream; } class HttpFetchPayloadImpl implements HttpFetchPayload { @@ -1399,13 +1498,16 @@ class HttpFetchPayloadImpl implements HttpFetchPayload { final DomResponse _domResponse; + @override + DomReadableStream get stream => _domResponse.body; + @override Future read(HttpFetchReader callback) async { final DomReadableStream stream = _domResponse.body; - final _DomStreamReader reader = stream._getReader(); + final DomStreamReader reader = stream.getReader(); while (true) { - final _DomStreamChunk chunk = await reader.read(); + final DomStreamChunk chunk = await reader.read(); if (chunk.done) { break; } @@ -1460,6 +1562,9 @@ class MockHttpFetchPayload implements HttpFetchPayload { @override Future text() async => throw AssertionError('text not supported by mock'); + + @override + DomReadableStream get stream => throw AssertionError('stream not supported by mock'); } /// Indicates a missing HTTP payload when one was expected, such as when @@ -1541,17 +1646,23 @@ extension type DomHeaders._(JSObject _) implements JSObject { extension type DomReadableStream._(JSObject _) implements JSObject { @JS('getReader') - external _DomStreamReader _getReader(); + external DomStreamReader getReader(); + + @JS('tee') + external JSArray tee(); } -extension type _DomStreamReader._(JSObject _) implements JSObject { +extension type DomStreamReader._(JSObject _) implements JSObject { @JS('read') external JSPromise _read(); - Future<_DomStreamChunk> read() => - _read().toDart.then((JSAny? value) => value! as _DomStreamChunk); + Future read() => _read().toDart.then((JSAny? value) => value! as DomStreamChunk); + + @JS('cancel') + external JSPromise _cancel(); + Future cancel() => _cancel().toDart; } -extension type _DomStreamChunk._(JSObject _) implements JSObject { +extension type DomStreamChunk._(JSObject _) implements JSObject { external JSAny? get value; external bool get done; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart deleted file mode 100644 index 57d7e5fb495d3..0000000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; -import 'package:ui/ui_web/src/ui_web.dart' as ui_web; - -/// The lifecycle status of an [HtmlImageElementCodec]. -enum _HtmlCodecStatus { - /// The codec has been created but has not yet started loading. - initial, - - /// The codec is waiting for the browser to load the image metadata (width/height). - loadingMetadata, - - /// The codec has loaded metadata and is now waiting for an available slot in - /// the [ImageDecodingManager] to begin the heavy decoding process. - waitingForSlot, - - /// The codec has been granted a slot and is currently executing the - /// [HTMLImageElement.decode] operation. - decoding, - - /// The image has been successfully loaded and decoded. - success, - - /// An error occurred during the loading or decoding process. - failed, - - /// The codec has been disposed and should no longer be used. - disposed, -} - -/// Exception thrown when a codec is disposed while it is still decoding. -class _HtmlCodecDisposedException implements Exception { - const _HtmlCodecDisposedException(); - - @override - String toString() => 'HtmlCodec was disposed.'; -} - -abstract class HtmlImageElementCodec implements ui.Codec { - HtmlImageElementCodec(this.src, {this.chunkCallback, this.debugSource}); - - final String src; - final ui_web.ImageCodecChunkCallback? chunkCallback; - final String? debugSource; - - @override - int get frameCount => 1; - - @override - int get repetitionCount => 0; - - /// The Image() element backing this codec. - DomHTMLImageElement? imgElement; - - /// A Future which completes when the Image element backing this codec has - /// been loaded and decoded. - Future? decodeFuture; - - ImageDecodingRequest? _decodingRequest; - _HtmlCodecStatus _status = _HtmlCodecStatus.initial; - Completer? _loadCompleter; - - /// Whether a [ui.Image] has been created and returned by [getNextFrame]. - /// - /// This is used during [dispose] to determine if it is safe to clear the - /// `src` attribute of [imgElement]. If the image has been handed out, the - /// [ui.Image] might still be using the element for rendering, and clearing - /// the `src` could disrupt the browser's internal image state. - bool _imageHandedOut = false; - - Future decode() { - decodeFuture ??= _performDecode(); - return decodeFuture!; - } - - void _checkDisposed() { - if (_status == _HtmlCodecStatus.disposed) { - throw const _HtmlCodecDisposedException(); - } - } - - Future _performDecode() async { - try { - _checkDisposed(); - await _waitForMetadata(); - await _executeThrottledDecode(); - - _status = _HtmlCodecStatus.success; - chunkCallback?.call(100, 100); - } on _HtmlCodecDisposedException { - _status = _HtmlCodecStatus.disposed; - } on ImageDecodingCancelledException { - _status = _HtmlCodecStatus.disposed; - } catch (e) { - _status = _HtmlCodecStatus.failed; - if (!_imageHandedOut) { - imgElement?.src = ''; - } - rethrow; - } finally { - _cleanupDecodingSlot(); - } - } - - Future _waitForMetadata() async { - _status = _HtmlCodecStatus.loadingMetadata; - // Currently there is no way to watch decode progress, so - // we add 0/100 , 100/100 progress callbacks to enable loading progress - // builders to create UI. - chunkCallback?.call(0, 100); - - imgElement = createDomHTMLImageElement(); - - // The 'anonymous' cross-origin setting is required for CanvasKit-based - // rendering. Without it, the browser would "taint" the image when it's - // drawn to a canvas, preventing us from reading the pixels back or - // converting it into a texture. - imgElement!.crossOrigin = 'anonymous'; - - // We set decoding to 'async' to hint to the browser that it should perform - // image decompression off the main thread. This helps prevent jank - // during the loading process. - imgElement!.decoding = 'async'; - - _loadCompleter = Completer(); - - // We use a local listener to ensure we can properly remove it in the - // finally block. This prevents potential memory leaks or multiple - // resolutions of the completer. - final DomEventListener loadListener = createDomEventListener((DomEvent event) { - _loadCompleter?.complete(); - }); - final DomEventListener errorListener = createDomEventListener((DomEvent event) { - _loadCompleter?.completeError(ImageCodecException('Failed to load image: $src')); - }); - - imgElement!.addEventListener('load', loadListener); - imgElement!.addEventListener('error', errorListener); - - // Setting the src attribute triggers the browser's image loading process. - imgElement!.src = src; - - try { - await _loadCompleter!.future; - } finally { - // It's critical to remove the listeners to avoid leaks, as the - // HTMLImageElement might persist if it's cached by the browser or - // referenced elsewhere. - imgElement!.removeEventListener('load', loadListener); - imgElement!.removeEventListener('error', errorListener); - _loadCompleter = null; - } - _checkDisposed(); - } - - Future _executeThrottledDecode() async { - _status = _HtmlCodecStatus.waitingForSlot; - final int width = imgElement!.naturalWidth.toInt(); - final int height = imgElement!.naturalHeight.toInt(); - - _decodingRequest = ImageDecodingManager.instance.requestDecodingSlot(width, height); - await _decodingRequest!.future; - _checkDisposed(); - - _status = _HtmlCodecStatus.decoding; - // We use a timeout to prevent the decoder from hanging indefinitely and - // blocking the queue. - try { - await imgElement!.decode().timeout(const Duration(seconds: 30)); - } on TimeoutException { - throw ImageCodecException('Timed out decoding image: $src'); - } catch (e) { - throw ImageCodecException('Failed to decode image: $src. Error: $e'); - } - _checkDisposed(); - } - - void _cleanupDecodingSlot() { - if (_decodingRequest != null) { - ImageDecodingManager.instance.releaseDecodingSlot(_decodingRequest!); - _decodingRequest = null; - } - } - - @override - Future getNextFrame() async { - await decode(); - if (_status == _HtmlCodecStatus.disposed) { - throw StateError('Codec has been disposed'); - } - int naturalWidth = imgElement!.naturalWidth.toInt(); - int naturalHeight = imgElement!.naturalHeight.toInt(); - - // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=700533. - // - // In some versions of Firefox, certain image formats (like SVG or - // very large JPEGs) may report a natural size of 0x0 even after the - // 'load' event has fired if the browser hasn't fully computed the - // intrinsic dimensions. - // - // Since Flutter requires a non-zero size to create a [ui.Image], we fall - // back to a default size (300x300) to allow the image to be processed - // and rendered, albeit potentially at a scaled size. - if (naturalWidth == 0 && - naturalHeight == 0 && - ui_web.browser.browserEngine == ui_web.BrowserEngine.firefox) { - const kDefaultImageSizeFallback = 300; - naturalWidth = kDefaultImageSizeFallback; - naturalHeight = kDefaultImageSizeFallback; - } - final ui.Image image = await createImageFromHTMLImageElement( - imgElement!, - naturalWidth, - naturalHeight, - ); - _imageHandedOut = true; - return SingleFrameInfo(image); - } - - /// Creates a [ui.Image] from an [HTMLImageElement] that has been loaded. - FutureOr createImageFromHTMLImageElement( - DomHTMLImageElement image, - int naturalWidth, - int naturalHeight, - ); - - @override - void dispose() { - if (_status == _HtmlCodecStatus.disposed) { - return; - } - final _HtmlCodecStatus oldStatus = _status; - _status = _HtmlCodecStatus.disposed; - - if (oldStatus == _HtmlCodecStatus.loadingMetadata) { - _loadCompleter?.completeError(const _HtmlCodecDisposedException()); - } else if (oldStatus == _HtmlCodecStatus.waitingForSlot) { - if (_decodingRequest != null) { - ImageDecodingManager.instance.cancel(_decodingRequest!); - } - } - if (!_imageHandedOut) { - imgElement?.src = ''; - } - } -} - -abstract class HtmlBlobCodec extends HtmlImageElementCodec { - HtmlBlobCodec(this.blob, {super.chunkCallback}) - : super(domWindow.URL.createObjectURL(blob), debugSource: 'encoded image bytes'); - - final DomBlob blob; - - @override - void dispose() { - super.dispose(); - domWindow.URL.revokeObjectURL(src); - } -} - -class SingleFrameInfo implements ui.FrameInfo { - SingleFrameInfo(this.image); - - @override - Duration get duration => Duration.zero; - - @override - final ui.Image image; -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart index 35cb5e83cf77d..f5b3c1067cf9c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart @@ -2,14 +2,25 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:js_interop'; +import 'dart:math' as math; +import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; - -/// Image decoder backed by the browser's `ImageDecoder`. -abstract class BrowserImageDecoder implements ui.Codec { +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; + +/// An image decoder that delegates to the browser's modern native `ImageDecoder` API. +/// +/// This decoder is highly efficient as it offloads the decoding work (including +/// frame extraction and progressive stream processing) to the browser's underlying +/// image decoding subsystem, running off the main thread where possible. +/// +/// Under the hood, it configures the native decoder to use premultiplied alpha and +/// default color space conversion, aligning with Flutter's rendering expectations. +class BrowserImageDecoder { BrowserImageDecoder({ required this.contentType, required this.dataSource, @@ -20,11 +31,8 @@ abstract class BrowserImageDecoder implements ui.Codec { final JSAny dataSource; final String debugSource; - @override - late int frameCount; - - @override - late int repetitionCount; + int frameCount = 0; + int repetitionCount = 0; /// Whether this decoder has been disposed of. /// @@ -32,13 +40,39 @@ abstract class BrowserImageDecoder implements ui.Codec { /// unusable. bool _isDisposed = false; - @override + final List _onDisposeCallbacks = []; + + void addDisposeCallback(void Function() callback) { + if (_isDisposed) { + callback(); + } else { + _onDisposeCallbacks.add(callback); + } + } + void dispose() { _isDisposed = true; - - // This releases all resources, including any currently running decoding work. - _cachedWebDecoder?.close(); - _cachedWebDecoder = null; + final errors = []; + try { + for (final void Function() callback in _onDisposeCallbacks) { + try { + callback(); + } catch (e) { + errors.add(e); + } + } + } finally { + _onDisposeCallbacks.clear(); + _cachedWebDecoder?.close(); + _cachedWebDecoder = null; + + if (errors.isNotEmpty) { + printWarning( + 'Failed to execute ${errors.length} dispose callback(s) in BrowserImageDecoder: ' + '${errors.join(', ')}', + ); + } + } } /// The index of the frame that will be decoded on the next call of [getNextFrame]; @@ -60,12 +94,18 @@ abstract class BrowserImageDecoder implements ui.Codec { ImageDecoder? get debugCachedWebDecoder => _cachedWebDecoder; Future initialize() async { - _cachedWebDecoder = await _createWebDecoder(); + final ImageDecoder webDecoder = await _createWebDecoder(); + if (_isDisposed) { + webDecoder.close(); + } else { + _cachedWebDecoder = webDecoder; + } } Future _createWebDecoder() async { + ImageDecoder? webDecoder; try { - final webDecoder = ImageDecoder( + webDecoder = ImageDecoder( ImageDecoderOptions( type: contentType, data: dataSource, @@ -97,6 +137,7 @@ abstract class BrowserImageDecoder implements ui.Codec { return webDecoder; } catch (error) { + webDecoder?.close(); // TODO(srujzs): Replace this with `error.isJSAny` when we have that API // in `dart:js_interop`. // https://github.com/dart-lang/sdk/issues/56905 @@ -117,8 +158,7 @@ abstract class BrowserImageDecoder implements ui.Codec { } } - @override - Future getNextFrame() async { + Future getNextFrame() async { if (_isDisposed) { throw ImageCodecException( 'Cannot decode image. The image decoder has been disposed.\n' @@ -145,21 +185,17 @@ abstract class BrowserImageDecoder implements ui.Codec { // For more details, see: https://issues.chromium.org/issues/456445108 .decode(DecodeOptions(frameIndex: _nextFrameIndex, completeFramesOnly: false)) .toDart; + if (_isDisposed) { + result.image.close(); + throw ImageCodecException( + 'Cannot decode image. The image decoder has been disposed.\n' + 'Image source: $debugSource', + ); + } final VideoFrame frame = result.image; _nextFrameIndex = (_nextFrameIndex + 1) % frameCount; - - // Duration can be null if the image is not animated. However, Flutter - // requires a non-null value. 0 indicates that the frame is meant to be - // displayed indefinitely, which is fine for a static image. - final duration = Duration(microseconds: frame.duration?.toInt() ?? 0); - final ui.Image image = generateImageFromVideoFrame(frame); - return AnimatedImageFrameInfo(duration, image); + return frame; } - - /// Creates a [ui.Image] from a [VideoFrame]. Implementers of this class - /// should override this method to create a [ui.Image] that is appropriate - /// for their associated renderer. - ui.Image generateImageFromVideoFrame(VideoFrame frame); } /// Data for a single frame of an animated image. @@ -173,65 +209,322 @@ class AnimatedImageFrameInfo implements ui.FrameInfo { final ui.Image image; } -// Wraps another codec and resizes each output image. -class ResizingCodec implements ui.Codec { - ResizingCodec(this.delegate, {this.targetWidth, this.targetHeight, this.allowUpscaling = true}); +ImageType tryDetectImageType(Uint8List data, String debugSource) { + // ImageDecoder does not detect image type automatically. It requires us to + // tell it what the image type is. + final ImageType? imageType = detectImageType(data); + + if (imageType == null) { + final String fileHeader; + if (data.isNotEmpty) { + fileHeader = '[${bytesToHexString(data.sublist(0, math.min(10, data.length)))}]'; + } else { + fileHeader = 'empty'; + } + throw ImageCodecException( + 'Failed to detect image file format using the file header.\n' + 'File header was $fileHeader.\n' + 'Image source: $debugSource', + ); + } + return imageType; +} - final ui.Codec delegate; - final int? targetWidth; - final int? targetHeight; - final bool allowUpscaling; +/// Duplicates the network response stream to enable parallel progress tracking +/// and native image decoding. +/// +/// In the web platform, a `ReadableStream` (like the HTTP response body) can only +/// have a single active reader at a time. If we read the stream in Dart to track +/// download progress (triggering [chunkCallback]), we lock the stream and prevent +/// the browser's native `ImageDecoder` from reading and decoding it. +/// +/// To solve this, we use `body.tee()` to duplicate the stream at the browser level +/// into two independent, concurrent branches: +/// 1. `progressStream`: Read chunk-by-chunk in Dart to calculate cumulative bytes loaded +/// and invoke the progress callback. +/// 2. `dataStream`: Passed directly to the native `BrowserImageDecoder` for streaming decode. +/// +/// We register a cancel callback in [onDisposeCallbacks] so that if the decoder is +/// disposed before the download completes, the progress reader is cancelled to prevent +/// dangling resource locks. +Future handleProgressAndGetStream( + HttpFetchResponse response, + ui_web.ImageCodecChunkCallback? chunkCallback, [ + List? onDisposeCallbacks, +]) async { + if (!response.hasPayload) { + throw ImageCodecException('Failed to load network image.'); + } + final DomReadableStream body = response.payload.stream; + final int? contentLength = response.contentLength; - @override - void dispose() => delegate.dispose(); + if (chunkCallback == null || contentLength == null) { + return body; + } - @override - int get frameCount => delegate.frameCount; + final List streams = body.tee().toDart.cast(); + final DomReadableStream progressStream = streams[0]; + final DomReadableStream dataStream = streams[1]; - @override - Future getNextFrame() async { - final ui.FrameInfo frameInfo = await delegate.getNextFrame(); - return AnimatedImageFrameInfo( - frameInfo.duration, - scaleImage( - frameInfo.image, + final DomStreamReader reader = progressStream.getReader(); + onDisposeCallbacks?.add(() { + reader.cancel(); + }); + + unawaited(() async { + try { + var cumulativeBytesLoaded = 0; + while (true) { + final DomStreamChunk chunk = await reader.read(); + if (chunk.done) { + break; + } + final JSAny? value = chunk.value; + if (value != null) { + final array = value as JSUint8Array; + cumulativeBytesLoaded += array.length; + chunkCallback(cumulativeBytesLoaded, contentLength); + } + } + } catch (e) { + // Ignore progress stream reading errors. + } + }()); + + return dataStream; +} + +/// Consolidates the image decoding and routing strategy for in-memory byte arrays. +/// +/// This function implements a tiered routing strategy to select the most efficient +/// decoding pipeline: +/// +/// - **Modern Browser Path (`BrowserImageDecoder`):** If the browser supports the +/// native `ImageDecoder` API, we sniff the byte header to identify the format's +/// MIME type and delegate decoding to `BrowserImageDecoder`. +/// - **Legacy Browser Path (`createImageBitmap`):** If the native `ImageDecoder` +/// is unsupported (e.g. older browsers or Safari/Firefox fallback), but the image is +/// static (non-animated) and `createImageBitmap` is available, we load the bytes +/// as a Blob and decode/resize natively via the browser's asynchronous bitmap APIs. +/// - **Skia Fallback (`BackendAnimatedImage`):** If the browser APIs are unsupported or +/// disabled in tests, we route the raw bytes to the active backend renderer +/// (CanvasKit or Skwasm) to be decoded using Skia's C++ WASM or FFI image codecs. +/// *Note:* We aim to compile Skia without built-in image decoders where possible to +/// minimize the WebAssembly bundle size. Therefore, we prioritize native browser +/// decoders and only route to the Skia/Skwasm backend when necessary. +Future engineInstantiateImageCodec( + Uint8List list, { + int? targetWidth, + int? targetHeight, + bool allowUpscaling = true, +}) async { + final ImageType imageType = tryDetectImageType(list, 'encoded image bytes'); + + if (browserSupportsImageDecoder) { + final decoder = BrowserImageDecoder( + contentType: imageType.mimeType, + dataSource: list.toJS, + debugSource: 'encoded image bytes', + ); + try { + await decoder.initialize(); + } catch (e) { + decoder.dispose(); + rethrow; + } + return EngineCodec.browser( + decoder, + targetWidth: targetWidth, + targetHeight: targetHeight, + allowUpscaling: allowUpscaling, + ); + } else { + if (!imageType.isAnimated && browserSupportsCreateImageBitmap) { + final DomBlob blob = createDomBlob([list.buffer]); + final DomImageBitmap originalBitmap = await createImageBitmap(blob); + final int originalWidth = originalBitmap.width; + final int originalHeight = originalBitmap.height; + final BitmapSize? scaledSize = scaledImageSize( + originalWidth, + originalHeight, + targetWidth, + targetHeight, + ); + + final int destWidth = scaledSize?.width ?? originalWidth; + final int destHeight = scaledSize?.height ?? originalHeight; + + var bitmap = originalBitmap; + if (scaledSize != null) { + if (allowUpscaling || (destWidth <= originalWidth && destHeight <= originalHeight)) { + bitmap = await scaleImageSource( + originalBitmap, + originalWidth, + originalHeight, + destWidth, + destHeight, + ); + originalBitmap.close(); + } + } + + final ImageSource source = ImageBitmapImageSource(bitmap); + return EngineCodec.staticImage( + source, targetWidth: targetWidth, targetHeight: targetHeight, allowUpscaling: allowUpscaling, - ), + ); + } else { + final BackendAnimatedImage backendAnimated = renderer.createAnimatedImage( + list, + targetWidth: targetWidth, + targetHeight: targetHeight, + ); + return EngineCodec.skia( + backendAnimated, + targetWidth: targetWidth, + targetHeight: targetHeight, + allowUpscaling: allowUpscaling, + ); + } + } +} + +const Set _knownImageMimeTypes = { + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/webp', + 'image/bmp', + 'image/x-icon', + 'image/vnd.microsoft.icon', + 'image/apng', + 'image/avif', +}; + +/// Parses and cleans the HTTP Content-Type header, returning the MIME type without parameters. +/// +/// Returns null if [contentTypeHeader] is null. +String? parseMimeType(String? contentTypeHeader) { + if (contentTypeHeader == null) { + return null; + } + final int semicolonIndex = contentTypeHeader.indexOf(';'); + if (semicolonIndex != -1) { + return contentTypeHeader.substring(0, semicolonIndex).trim().toLowerCase(); + } + return contentTypeHeader.trim().toLowerCase(); +} + +/// Consolidates the progressive network image decoding and routing strategy. +/// +/// This function implements the tiered network routing strategy designed to maximize +/// streaming performance and minimize memory overhead: +/// +/// - **Streaming Decode (Fast Path):** We inspect the HTTP `Content-Type` header +/// from the response. If it is a known static or animated image format, and the +/// browser supports `ImageDecoder`, we stream the response body directly to the +/// native decoder without waiting for the full download. If a progress [chunkCallback] +/// is provided, we use `handleProgressAndGetStream` (`ReadableStream.tee()`) to +/// concurrently track progress and stream decode. +/// - **Buffered Decode (Fallback Path):** If the `Content-Type` header is missing, +/// generic (e.g. `application/octet-stream`), or if the browser lacks native +/// `ImageDecoder` support: +/// - We download the entire response as an `arrayBuffer`. +/// - We sniff the binary headers to detect the image format. +/// - We then fall back to the tiered in-memory routing strategy (using the browser's +/// `ImageDecoder` with the buffer, `createImageBitmap`, or Skia C++/WASM decoders). +Future engineInstantiateImageCodecFromUrl( + Uri uri, { + ui_web.ImageCodecChunkCallback? chunkCallback, +}) async { + final url = uri.toString(); + final HttpFetchResponse response; + try { + response = await httpFetch(url); + } catch (e) { + throw ImageCodecException('Failed to load network image: $e'); + } + + if (response.status < 200 || response.status >= 300) { + throw ImageCodecException( + 'Failed to load network image.\n' + 'Image URL: $url\n' + 'Server response code: ${response.status}', ); } - ui.Image scaleImage( - ui.Image image, { - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) => scaleImageIfNeeded( - image, - targetWidth: targetWidth, - targetHeight: targetHeight, - allowUpscaling: allowUpscaling, - ); + final String? cleanContentType = parseMimeType(response.header('Content-Type')); + final bool isKnownImageMimeType = + cleanContentType != null && _knownImageMimeTypes.contains(cleanContentType); - @override - int get repetitionCount => delegate.repetitionCount; + if (browserSupportsImageDecoder && isKnownImageMimeType) { + final List onDisposeCallbacks = []; + final DomReadableStream stream = await handleProgressAndGetStream( + response, + chunkCallback, + onDisposeCallbacks, + ); + final decoder = BrowserImageDecoder( + contentType: cleanContentType, + dataSource: stream, + debugSource: url, + ); + onDisposeCallbacks.forEach(decoder.addDisposeCallback); + try { + await decoder.initialize(); + } catch (e) { + decoder.dispose(); + rethrow; + } + return EngineCodec.browser(decoder); + } else { + final ByteBuffer buffer = await response.payload.asByteBuffer(); + final Uint8List list = buffer.asUint8List(); + final ImageType imageType = tryDetectImageType(list, url); + + if (chunkCallback != null) { + chunkCallback(list.length, list.length); + } + + if (browserSupportsImageDecoder) { + final decoder = BrowserImageDecoder( + contentType: imageType.mimeType, + dataSource: list.toJS, + debugSource: url, + ); + try { + await decoder.initialize(); + } catch (e) { + decoder.dispose(); + rethrow; + } + return EngineCodec.browser(decoder); + } else if (!imageType.isAnimated && browserSupportsCreateImageBitmap) { + final DomBlob blob = createDomBlob([buffer]); + final DomImageBitmap bitmap = await createImageBitmap(blob); + final ImageSource source = ImageBitmapImageSource(bitmap); + return EngineCodec.staticImage(source); + } else { + final BackendAnimatedImage backendAnimated = renderer.createAnimatedImage(list); + return EngineCodec.skia(backendAnimated); + } + } } BitmapSize? scaledImageSize(int width, int height, int? targetWidth, int? targetHeight) { if (targetWidth == width && targetHeight == height) { - // Not scaled return null; } if (targetWidth == null) { if (targetHeight == null || targetHeight == height) { - // Not scaled. return null; } targetWidth = (width * targetHeight / height).round(); } else if (targetHeight == null) { if (targetWidth == width) { - // Not scaled. return null; } targetHeight = (height * targetWidth / width).round(); @@ -239,6 +532,16 @@ BitmapSize? scaledImageSize(int width, int height, int? targetWidth, int? target return BitmapSize(targetWidth, targetHeight); } +/// Performs a fallback image scaling operation on the frontend using a canvas. +/// +/// This is used as a fallback when the native backend decoder (specifically +/// CanvasKit's WASM animated image decoder) does not support resizing/scaling during +/// the decode phase. +/// +/// It draws the original [image] onto a temporary [ui.Canvas] at the [scaledSize] +/// using [ui.PictureRecorder], and compiles the recording into a new scaled [ui.Image] +/// via `toImageSync`. The original full-size [image] is eagerly disposed of immediately +/// after to prevent memory spikes. ui.Image scaleImageIfNeeded( ui.Image image, { int? targetWidth, @@ -277,8 +580,6 @@ ui.Image scaleImageIfNeeded( return finalImage; } -/// Thrown when the web engine fails to decode an image, either due to a -/// network issue, corrupted image contents, or missing codec. class ImageCodecException implements Exception { ImageCodecException(this._message); @@ -287,3 +588,20 @@ class ImageCodecException implements Exception { @override String toString() => 'ImageCodecException: $_message'; } + +Future scaleImageSource( + DomCanvasImageSource source, + int originalWidth, + int originalHeight, + int destWidth, + int destHeight, +) async { + return createImageBitmap( + source, + options: ImageBitmapOptions( + resizeWidth: destWidth, + resizeHeight: destHeight, + resizeQuality: 'high', + ), + ); +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/codec.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/codec.dart new file mode 100644 index 0000000000000..6127edba5998e --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/codec.dart @@ -0,0 +1,351 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +/// An engine implementation of the [ui.Codec] interface. +/// +/// This codec delegates to a concrete sub-codec depending on how the image +/// source is loaded (e.g. browser image decoder, static image, or a Skia/Skwasm backend animated image). +abstract class EngineCodec implements ui.Codec { + /// Creates an [EngineCodec] that uses a browser-provided [BrowserImageDecoder] + /// to decode frames. + /// + /// This is the preferred path for web browsers supporting the modern native + /// `ImageDecoder` API, allowing highly efficient progressive and animated + /// decoding offloaded to the browser. + factory EngineCodec.browser( + BrowserImageDecoder decoder, { + int? targetWidth, + int? targetHeight, + bool allowUpscaling, + }) = _BrowserEngineCodec; + + /// Creates an [EngineCodec] for a single static [ImageSource]. + /// + /// Used for static images that have already been decoded or represent a static + /// DOM asset (like an `HTMLImageElement` or `ImageBitmap`), bypassing multi-frame + /// animation machinery. + factory EngineCodec.staticImage( + ImageSource source, { + int? targetWidth, + int? targetHeight, + bool allowUpscaling, + }) = _StaticEngineCodec; + + /// Creates an [EngineCodec] that wraps a Skia/Skwasm [BackendAnimatedImage]. + /// + /// Used for animated formats (like GIF or WebP) or unsupported formats where + /// native browser decoders are unavailable, falling back to C++ Skia or WASM + /// C++ decoding. + factory EngineCodec.skia( + BackendAnimatedImage backendAnimatedImage, { + int? targetWidth, + int? targetHeight, + bool allowUpscaling, + }) = _SkiaEngineCodec; + + /// Protected constructor for subclasses. + EngineCodec._(); +} + +/// An [EngineCodec] that decodes frames using the browser's [BrowserImageDecoder]. +/// +/// This class orchestrates a multi-step asynchronous pipeline that decodes frames +/// using the browser's native `ImageDecoder` and resizes them natively on the CPU +/// using `createImageBitmap` if target dimensions are requested. +class _BrowserEngineCodec extends EngineCodec { + _BrowserEngineCodec( + BrowserImageDecoder browserDecoder, { + this.targetWidth, + this.targetHeight, + this.allowUpscaling = true, + }) : super._() { + _ref = UniqueRef( + this, + browserDecoder, + 'BrowserCodec', + onDispose: (BrowserImageDecoder decoder) => decoder.dispose(), + ); + } + + late final UniqueRef _ref; + final int? targetWidth; + final int? targetHeight; + final bool allowUpscaling; + bool _disposed = false; + + @override + int get frameCount => _ref.nativeObject.frameCount; + + @override + int get repetitionCount => _ref.nativeObject.repetitionCount; + + @override + void dispose() { + _disposed = true; + _ref.dispose(); + } + + @override + Future getNextFrame() async { + if (_disposed) { + throw StateError('Cannot call getNextFrame() after dispose()'); + } + try { + // Decode the next raw frame from the browser decoder. + // This is an asynchronous operation, during which the codec could be disposed. + final VideoFrame frame = await _ref.nativeObject.getNextFrame(); + + // Async Safety Check: If the codec was disposed while awaiting the frame decode, + // we MUST close the native VideoFrame immediately. Failing to do so would + // leak the underlying browser-allocated GPU/system memory. + if (_disposed) { + frame.close(); + throw StateError('Codec disposed during getNextFrame()'); + } + final duration = Duration(microseconds: frame.duration?.toInt() ?? 0); + + final int originalWidth = frame.displayWidth.toInt(); + final int originalHeight = frame.displayHeight.toInt(); + BitmapSize? scaledSize = scaledImageSize( + originalWidth, + originalHeight, + targetWidth, + targetHeight, + ); + if (scaledSize != null && + !allowUpscaling && + (scaledSize.width > originalWidth || scaledSize.height > originalHeight)) { + scaledSize = null; + } + + final int destWidth = scaledSize?.width ?? originalWidth; + final int destHeight = scaledSize?.height ?? originalHeight; + + final ui.Image image; + if (scaledSize != null) { + // Scale the frame natively. + // We use the browser's high-performance native scale API which creates a + // scaled ImageBitmap directly. This is asynchronous and yields the thread. + final DomImageBitmap bitmap; + try { + bitmap = await scaleImageSource( + frame, + originalWidth, + originalHeight, + destWidth, + destHeight, + ); + } finally { + frame.close(); + } + + // Async Safety Check: Check if disposed during the scale operation. + // If so, close the newly created scaled bitmap to prevent leaks. + if (_disposed) { + bitmap.close(); + throw StateError('Codec disposed during getNextFrame()'); + } + + // Upload the scaled bitmap to the GPU. + try { + image = await renderer.createImageFromTextureSource( + bitmap, + width: destWidth, + height: destHeight, + transferOwnership: true, + ); + } catch (e) { + bitmap.close(); + rethrow; + } + } else { + // Upload the original unscaled frame directly to the GPU. + try { + image = await renderer.createImageFromTextureSource( + frame, + width: destWidth, + height: destHeight, + transferOwnership: true, + ); + } catch (e) { + frame.close(); + rethrow; + } + } + + // Async Safety Check: Check if disposed during the GPU texture upload. + // If so, dispose of the wrapped image. + if (_disposed) { + image.dispose(); + throw StateError('Codec disposed during getNextFrame()'); + } + return AnimatedImageFrameInfo(duration, image); + } catch (e) { + if (_disposed) { + throw StateError('Codec disposed during getNextFrame()'); + } + rethrow; + } + } +} + +/// An [EngineCodec] that wraps a single static [ImageSource]. +/// +/// This codec is optimized for static images. Since static images do not animate, +/// [frameCount] is always 1, and the single frame is resolved synchronously +/// by querying the backend factory. +class _StaticEngineCodec extends EngineCodec { + _StaticEngineCodec( + this._staticImageSource, { + this.targetWidth, + this.targetHeight, + this.allowUpscaling = true, + }) : super._() { + // Retain the underlying image source to increment its reference count. + // This guarantees that the native image data (such as an ImageBitmap) remains + // alive in memory as long as this codec exists. + _staticImageSource.retain(); + } + + final ImageSource _staticImageSource; + final int? targetWidth; + final int? targetHeight; + final bool allowUpscaling; + bool _disposed = false; + + @override + int get frameCount => 1; + + @override + int get repetitionCount => 0; + + @override + void dispose() { + _disposed = true; + // Release our reference to the image source. If the reference count drops to 0, + // the native resources (like the ImageBitmap) will be closed and freed. + _staticImageSource.release(); + } + + @override + Future getNextFrame() { + if (_disposed) { + return Future.error(StateError('Cannot call getNextFrame() after dispose()')); + } + // Synchronously instruct the backend renderer to convert the normalized + // DOM asset into a backend-specific representation (e.g., uploading to a GPU texture). + final BackendImage backendImage = renderer.createImageFromImageSource(_staticImageSource); + final ui.Image image = EngineImage( + backendImage, + _staticImageSource.width, + _staticImageSource.height, + imageSource: _staticImageSource, + ); + return Future.value(AnimatedImageFrameInfo(Duration.zero, image)); + } +} + +/// An [EngineCodec] that wraps a Skia/Skwasm [BackendAnimatedImage]. +/// +/// This codec is used when the browser's native `ImageDecoder` is unavailable, +/// routing decoding work through WASM Skia (CanvasKit) or C++ FFI (Skwasm). +/// +/// Resizing animated images differs fundamentally between backends: +/// 1. Skwasm supports native C++ resizing (via `SkAndroidCodec` scaling) during +/// decoding, so no frontend scaling is required. +/// 2. CanvasKit's WASM animated image decoder does not support native scaling. +/// Therefore, it decodes frames at their original size, and the frontend must +/// perform an expensive frame-by-frame resize fallback using `scaleImageIfNeeded` +/// (drawing onto a canvas via `ui.PictureRecorder`). +class _SkiaEngineCodec extends EngineCodec { + _SkiaEngineCodec( + BackendAnimatedImage backendAnimatedImage, { + this.targetWidth, + this.targetHeight, + this.allowUpscaling = true, + }) : super._() { + _ref = UniqueRef( + this, + backendAnimatedImage, + 'SkiaCodec', + onDispose: (BackendAnimatedImage image) => image.dispose(), + ); + if ((targetWidth != null || targetHeight != null) && + _ref.nativeObject.frameCount > 1 && + !renderer.supportsResizingAnimatedImages) { + printWarning( + 'targetWidth and targetHeight for multi-frame images are not natively supported by the current renderer. ' + 'Scaling will fall back to expensive frame-by-frame canvas drawing.', + ); + } + } + + late final UniqueRef _ref; + final int? targetWidth; + final int? targetHeight; + final bool allowUpscaling; + bool _disposed = false; + + @override + int get frameCount => _ref.nativeObject.frameCount; + + @override + int get repetitionCount => _ref.nativeObject.repetitionCount; + + @override + void dispose() { + _disposed = true; + _ref.dispose(); + } + + @override + Future getNextFrame() async { + if (_disposed) { + throw StateError('Cannot call getNextFrame() after dispose()'); + } + try { + // Extract the next frame from the native backend. + final BackendFrameInfo backendFrame = await _ref.nativeObject.getNextFrame(); + + // Async Safety Check: If the codec was disposed while the backend was + // decoding the frame, we must dispose of the backend image immediately + // to prevent native memory leaks. + if (_disposed) { + backendFrame.image.dispose(); + throw StateError('Codec disposed during getNextFrame()'); + } + ui.Image image = EngineImage( + backendFrame.image, + backendFrame.image.width, + backendFrame.image.height, + ); + + // Apply the frontend canvas-scaling fallback if the backend does not support + // native resizing of animated images on decode. + if (targetWidth != null || targetHeight != null) { + try { + image = scaleImageIfNeeded( + image, + targetWidth: targetWidth, + targetHeight: targetHeight, + allowUpscaling: allowUpscaling, + ); + } catch (e) { + image.dispose(); + rethrow; + } + } + return AnimatedImageFrameInfo(backendFrame.duration, image); + } catch (e) { + if (_disposed) { + throw StateError('Codec disposed during getNextFrame()'); + } + rethrow; + } + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image.dart index 65dbcc9edb369..910364251cf98 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image.dart @@ -187,6 +187,8 @@ class EngineImage implements ui.Image, StackTraceDebugger { formatStr == 'BGRX') { return await readPixelsFromVideoFrame(videoFrame, format); } + case final CanvasImageSourceWrapper s: + return await readPixelsFromDomImageSource(s.canvasImageSource, format, s.width, s.height); case null: break; } @@ -218,7 +220,7 @@ class EngineImage implements ui.Image, StackTraceDebugger { rawData.lengthInBytes, ); final DomImageData imageData = createDomImageData( - clampedBytes, + clampedBytes.toJS, cloneImage.width, cloneImage.height, ); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image_source.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image_source.dart index 16366256e0af1..35120e2877da7 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image_source.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/primitives/image_source.dart @@ -340,3 +340,24 @@ Future encodeDomImageSourceAsPng( canvas.height = 0; return base64.decode(pngBase64); } + +/// An [ImageSource] implementation wrapping a generic [DomCanvasImageSource] +/// (such as an HTMLCanvasElement, OffscreenCanvas, SVGImageElement, or HTMLVideoElement) +/// that does not require manual resource cleanup. +class CanvasImageSourceWrapper extends ImageSource { + CanvasImageSourceWrapper(this.canvasImageSource, this.width, this.height); + + @override + final DomCanvasImageSource canvasImageSource; + + @override + final int width; + + @override + final int height; + + @override + void _doClose() { + // Generic canvas image sources do not require manual resource disposal. + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart index 7993eb6140987..6fb7698c38762 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart @@ -171,25 +171,114 @@ abstract class Renderer { required ui.ImageFilter inner, }); + bool get isMultiThreaded; + + /// Whether this renderer natively supports resizing/scaling animated images during decoding. + bool get supportsResizingAnimatedImages; + + BackendAnimatedImage createAnimatedImage(Uint8List bytes, {int? targetWidth, int? targetHeight}); + + BackendImage createImageFromImageSource(ImageSource source); + + ui.Image createImageFromImageBitmap(DomImageBitmap imageBitmap) { + final int width = imageBitmap.width; + final int height = imageBitmap.height; + final ImageSource source = ImageBitmapImageSource(imageBitmap); + final BackendImage backendImage = createImageFromImageSource(source); + return EngineImage(backendImage, width, height, imageSource: source); + } + + /// Creates a unified [ui.Image] from a raw browser texture source (such as a + /// [VideoFrame], [DomImageBitmap], [DomHTMLImageElement], or [DomHTMLCanvasElement]). + /// + /// This method is a crucial bridge between the browser's DOM environment and + /// the engine's rendering backends, particularly when managing multi-threaded + /// environments (like Skwasm running in a Web Worker): + /// + /// - **Thread-Safety & Transferability:** In multi-threaded mode, DOM elements + /// (like `HTMLImageElement` or `HTMLCanvasElement`) cannot be transferred + /// across thread boundaries to a Web Worker. Only "transferable" objects + /// (like `ImageBitmap` and `VideoFrame`) are thread-safe. If the renderer is + /// multi-threaded and the source is non-transferable, we must clone it. + /// - **Cloning Heuristic:** We use the browser's native `createImageBitmap` to + /// asynchronously capture a snapshot of the source as a transferable `ImageBitmap`. + /// Cloning is also triggered if [transferOwnership] is false, ensuring the caller's + /// original object remains unaffected by our internal disposal lifecycle. + /// - **Ownership Transfer:** If we had to clone the object but the caller requested + /// ownership transfer (`transferOwnership` is true), we eagerly close the original + /// source (if it is closeable like a `VideoFrame` or `ImageBitmap`) to avoid leaking + /// it, as we are now responsible for the cloned copy instead. + FutureOr createImageFromTextureSource( + JSAny object, { + required int width, + required int height, + required bool transferOwnership, + }) async { + var textureSource = object; + final originalTextureSource = object; + final bool needsClone = !transferOwnership || (isMultiThreaded && !_isTransferable(object)); + if (needsClone) { + textureSource = (await createImageBitmap( + object, + bounds: (x: 0, y: 0, width: width, height: height), + )).toJSAnyShallow; + if (transferOwnership) { + if (originalTextureSource.isA()) { + (originalTextureSource as VideoFrame).close(); + } else if (originalTextureSource.isA()) { + (originalTextureSource as DomImageBitmap).close(); + } + } + } + + final ImageSource imageSource; + if (textureSource.isA()) { + imageSource = ImageBitmapImageSource(textureSource as DomImageBitmap); + } else if (textureSource.isA()) { + imageSource = VideoFrameImageSource(textureSource as VideoFrame); + } else if (textureSource.isA()) { + imageSource = ImageElementImageSource(textureSource as DomHTMLImageElement); + } else { + imageSource = CanvasImageSourceWrapper(textureSource as DomCanvasImageSource, width, height); + } + + final BackendImage backendImage; + try { + backendImage = createImageFromImageSource(imageSource); + } catch (e) { + imageSource.close(); + rethrow; + } + + return EngineImage(backendImage, width, height, imageSource: imageSource); + } + + bool _isTransferable(JSAny object) => + object.isA() || object.isA() || object.isA(); + Future instantiateImageCodec( Uint8List list, { int? targetWidth, int? targetHeight, bool allowUpscaling = true, - }); + }) => engineInstantiateImageCodec( + list, + targetWidth: targetWidth, + targetHeight: targetHeight, + allowUpscaling: allowUpscaling, + ); Future instantiateImageCodecFromUrl( Uri uri, { ui_web.ImageCodecChunkCallback? chunkCallback, - }); + }) => engineInstantiateImageCodecFromUrl(uri, chunkCallback: chunkCallback); - FutureOr createImageFromImageBitmap(DomImageBitmap imageSource); - - FutureOr createImageFromTextureSource( - JSAny object, { + FutureOr decodeBackendImageFromPixels( + Uint8List pixels, { required int width, required int height, - required bool transferOwnership, + required ui.PixelFormat format, + int? rowBytes, }); void decodeImageFromPixels( @@ -202,7 +291,35 @@ abstract class Renderer { int? targetWidth, int? targetHeight, bool allowUpscaling = true, - }); + }) { + Timer.run(() async { + final BackendImage backendImage = await decodeBackendImageFromPixels( + pixels, + width: width, + height: height, + format: format, + rowBytes: rowBytes, + ); + final ui.Image image = EngineImage(backendImage, width, height); + if (targetWidth != null || targetHeight != null) { + final ui.Image scaledImage; + try { + scaledImage = scaleImageIfNeeded( + image, + targetWidth: targetWidth, + targetHeight: targetHeight, + allowUpscaling: allowUpscaling, + ); + } catch (e) { + image.dispose(); + rethrow; + } + callback(scaledImage); + } else { + callback(image); + } + }); + } ui.ImageShader createImageShader( ui.Image image, diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart index 274d4b3554fa9..04618d9b55753 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart @@ -9,72 +9,46 @@ import 'dart:typed_data'; import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; -import 'package:ui/ui.dart' as ui; -class SkwasmBrowserImageDecoder extends BrowserImageDecoder { - SkwasmBrowserImageDecoder({ - required super.contentType, - required super.dataSource, - required super.debugSource, - }); - - @override - ui.Image generateImageFromVideoFrame(VideoFrame frame) { - final int width = frame.displayWidth.toInt(); - final int height = frame.displayHeight.toInt(); - - final surface = renderer.pictureToImageSurface as SkwasmSurface; - - final ImageHandle handle = imageCreateFromTextureSource(frame, width, height, surface.handle); - - return EngineImage( - SkwasmImage(handle), - width, - height, - imageSource: VideoFrameImageSource(frame), - ); - } -} - -class SkwasmDomImageDecoder extends HtmlBlobCodec { - SkwasmDomImageDecoder(super.blob, [this.width, this.height]); - - final int? width; - final int? height; - - @override - FutureOr createImageFromHTMLImageElement( - DomHTMLImageElement image, - int naturalWidth, - int naturalHeight, - ) { - return renderer.createImageFromTextureSource( - image, - width: width ?? naturalWidth, - height: height ?? naturalHeight, - transferOwnership: false, - ); - } -} - -class SkwasmAnimatedImageDecoder implements ui.Codec { +/// The Skwasm-specific implementation of the [BackendAnimatedImage] contract. +/// +/// This class acts as a thin bridge to the C++ Skia animated image codecs compiled +/// into the WebAssembly module, interacting via Dart's FFI (`dart:ffi`). +class SkwasmAnimatedImageDecoder implements BackendAnimatedImage { + /// Allocates native memory and instantiates a native C++ animated image decoder. factory SkwasmAnimatedImageDecoder(Uint8List imageData, [int? width, int? height]) { + // Allocate a native SkData buffer on the WASM heap. final SkDataHandle data = skDataCreate(imageData.length); try { + // Obtain the raw virtual memory address of the allocated buffer. final int dataAddress = skDataGetPointer(data).cast().address; + // Directly copy the Dart bytes into the WASM memory buffer. + // We wrap the WASM module's memory buffer in a JSUint8Array view and use + // the high-speed `.set()` method to copy the Dart Uint8List. This bypasses + // standard serialization/deserialization overhead. final wasmMemory = JSUint8Array(skwasmInstance.wasmMemory.buffer); wasmMemory.set(imageData.toJS, dataAddress); + // Create the native animated image decoder. + // If target width and height are provided, the native C++ decoder (SkAndroidCodec) + // will scale the frames natively on decode, saving CPU/GPU memory. final AnimatedImageHandle handle = animatedImageCreate(data, width ?? 0, height ?? 0); + if (handle == nullptr) { + throw ImageCodecException('Failed to create Skwasm animated image from bytes.'); + } return SkwasmAnimatedImageDecoder._(handle); } finally { + // Clean up the temporary SkData buffer. + // The native animated image decoder has already retained a reference to the + // data, so we must dispose of our local handle to prevent memory leaks. skDataDispose(data); } } SkwasmAnimatedImageDecoder._(this.handle); + /// The raw FFI pointer to the underlying C++ SkAnimatedImage. AnimatedImageHandle handle; @override @@ -96,20 +70,22 @@ class SkwasmAnimatedImageDecoder implements ui.Codec { } @override - Future getNextFrame() async { + Future getNextFrame() { + // Get the duration of the current frame prior to advancing. final duration = Duration( milliseconds: animatedImageGetCurrentFrameDurationMilliseconds(handle), ); + // Extract a native handle to the current frame's SkImage. final ImageHandle frameHandle = animatedImageGetCurrentFrame(handle); + final backendImage = SkwasmImage(frameHandle); - final image = EngineImage( - SkwasmImage(frameHandle), - imageGetWidth(frameHandle), - imageGetHeight(frameHandle), - ); + // Advance the native decoder to the next frame. The next call to + // animatedImageGetCurrentFrame will yield the next frame. + animatedImageDecodeNextFrame(handle); - final ui.FrameInfo frameInfo = AnimatedImageFrameInfo(duration, image); - return frameInfo; + return Future.value( + BackendFrameInfo(duration: duration, image: backendImage), + ); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart index 59a9e06283a73..66a9dae08e0d5 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart @@ -25,8 +25,10 @@ class SkwasmImage implements BackendImage { imageDispose(handle); } + @override int get width => imageGetWidth(handle); + @override int get height => imageGetHeight(handle); @override @@ -42,7 +44,7 @@ class SkwasmImage implements BackendImage { /// The [width] and [height] are the dimensions of the image. /// The [format] specifies the layout of color channels in the buffer. /// The optional [rowBytes] defines the step length between two scan lines. -EngineImage createSkwasmImageFromPixels( +SkwasmImage createSkwasmImageFromPixels( Uint8List pixels, int width, int height, @@ -67,7 +69,7 @@ EngineImage createSkwasmImageFromPixels( format.index, rowBytes ?? 4 * width, ); - return EngineImage(SkwasmImage(imageHandle), width, height); + return SkwasmImage(imageHandle); } finally { skDataDispose(dataHandle); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index 8e022ac3f7e3b..a091527de41e1 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -14,8 +14,12 @@ import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; class SkwasmRenderer extends Renderer { + @override bool get isMultiThreaded => skwasmIsMultiThreaded(); + @override + bool get supportsResizingAnimatedImages => true; + bool get isWimp => skwasmIsWimp(); @override @@ -295,29 +299,16 @@ class SkwasmRenderer extends Renderer { ); @override - void decodeImageFromPixels( - Uint8List pixels, - int width, - int height, - ui.PixelFormat format, - ui.ImageDecoderCallback callback, { + BackendImage decodeBackendImageFromPixels( + Uint8List pixels, { + required int width, + required int height, + required ui.PixelFormat format, int? rowBytes, - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) { - final EngineImage pixelImage = createSkwasmImageFromPixels(pixels, width, height, format); - final ui.Image scaledImage = scaleImageIfNeeded( - pixelImage, - targetWidth: targetWidth, - targetHeight: targetHeight, - allowUpscaling: allowUpscaling, - ); - callback(scaledImage); - } + }) => createSkwasmImageFromPixels(pixels, width, height, format, rowBytes: rowBytes); @override - FutureOr initialize() async { + FutureOr initialize() { rasterizer = OffscreenCanvasRasterizer( (OffscreenCanvasProvider canvasProvider) => SkwasmSurface(canvasProvider), ); @@ -325,74 +316,19 @@ class SkwasmRenderer extends Renderer { } @override - Future instantiateImageCodec( - Uint8List list, { - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) async { - final ImageType? contentType = detectImageType(list); - if (contentType == null) { - throw Exception('Could not determine content type of image from data'); - } - if (browserSupportsImageDecoder) { - final baseDecoder = SkwasmBrowserImageDecoder( - contentType: contentType.mimeType, - dataSource: list.toJS, - debugSource: 'encoded image bytes', - ); - await baseDecoder.initialize(); - if (targetWidth == null && targetHeight == null) { - return baseDecoder; - } - return ResizingCodec( - baseDecoder, - targetWidth: targetWidth, - targetHeight: targetHeight, - allowUpscaling: allowUpscaling, - ); - } else { - if (contentType.isAnimated) { - return SkwasmAnimatedImageDecoder(list, targetWidth, targetHeight); - } else { - final DomBlob blob = createDomBlob([list.buffer]); - return SkwasmDomImageDecoder(blob, targetWidth, targetHeight); - } - } + BackendAnimatedImage createAnimatedImage(Uint8List bytes, {int? targetWidth, int? targetHeight}) { + return SkwasmAnimatedImageDecoder(bytes, targetWidth, targetHeight); } @override - Future instantiateImageCodecFromUrl( - Uri uri, { - ui_web.ImageCodecChunkCallback? chunkCallback, - }) async { - final DomResponse response = await rawHttpGet(uri.toString()); - final String? contentType = response.headers.get('Content-Type'); - if (contentType == null) { - throw Exception('Could not determine content type of image at url $uri'); - } - if (browserSupportsImageDecoder) { - final decoder = SkwasmBrowserImageDecoder( - contentType: contentType, - dataSource: response.body, - debugSource: uri.toString(), - ); - await decoder.initialize(); - return decoder; - } else { - final ByteBuffer buffer = await response.arrayBuffer(); - final Uint8List data = buffer.asUint8List(); - final ImageType? parsedContentType = detectImageType(data); - if (parsedContentType == null) { - throw Exception('Could not determine content type of image from data'); - } - if (parsedContentType.isAnimated) { - return SkwasmAnimatedImageDecoder(data); - } else { - final DomBlob blob = createDomBlob([buffer]); - return SkwasmDomImageDecoder(blob); - } - } + BackendImage createImageFromImageSource(ImageSource source) { + final ImageHandle handle = imageCreateFromTextureSource( + source.canvasImageSource as JSObject, + source.width, + source.height, + (pictureToImageSurface as SkwasmSurface).handle, + ); + return SkwasmImage(handle); } @override @@ -439,57 +375,6 @@ class SkwasmRenderer extends Renderer { lineNumber: lineNumber, ); - @override - ui.Image createImageFromImageBitmap(DomImageBitmap imageSource) { - // Cache the dimensions before passing the image to the texture source creator, - // which may transfer ownership of the bitmap to a web worker and detach it. - final int width = imageSource.width; - final int height = imageSource.height; - - final ImageHandle handle = imageCreateFromTextureSource( - imageSource, - width, - height, - (pictureToImageSurface as SkwasmSurface).handle, - ); - return EngineImage( - SkwasmImage(handle), - width, - height, - imageSource: ImageBitmapImageSource(imageSource), - ); - } - - @override - FutureOr createImageFromTextureSource( - JSAny textureSource, { - required int width, - required int height, - required bool transferOwnership, - }) async { - // If the caller does not wish to transfer ownership, or if the runtime environment - // is multi-threaded and the provided texture type cannot be natively transferred - // between threads, convert the texture to a transferable DomImageBitmap first. - if (!transferOwnership || (isMultiThreaded && !_isTransferable(textureSource))) { - textureSource = (await createImageBitmap(textureSource, ( - x: 0, - y: 0, - width: width, - height: height, - ))).toJSAnyShallow; - } - final ImageHandle handle = imageCreateFromTextureSource( - textureSource as JSObject, - width, - height, - (pictureToImageSurface as SkwasmSurface).handle, - ); - return EngineImage(SkwasmImage(handle), width, height); - } - - bool _isTransferable(JSAny object) => - object.isA() || object.isA() || object.isA(); - @override void dumpDebugInfo() { if (kDebugMode) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart index 17a0e52fc7121..61a67c13bf670 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart @@ -3,17 +3,20 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:js_interop'; import 'dart:math' as math; import 'dart:typed_data'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:ui/ui_web/src/ui_web.dart' as ui_web; class SkwasmRenderer extends Renderer { + @override bool get isMultiThreaded => false; + @override + bool get supportsResizingAnimatedImages => + throw UnimplementedError('Skwasm not implemented on this platform.'); + bool get isWimp => false; @override @@ -231,16 +234,12 @@ class SkwasmRenderer extends Renderer { } @override - void decodeImageFromPixels( - Uint8List pixels, - int width, - int height, - ui.PixelFormat format, - ui.ImageDecoderCallback callback, { + FutureOr decodeBackendImageFromPixels( + Uint8List pixels, { + required int width, + required int height, + required ui.PixelFormat format, int? rowBytes, - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, }) { throw UnimplementedError('Skwasm not implemented on this platform.'); } @@ -250,20 +249,12 @@ class SkwasmRenderer extends Renderer { throw UnimplementedError('Skwasm not implemented on this platform.'); @override - Future instantiateImageCodec( - Uint8List list, { - int? targetWidth, - int? targetHeight, - bool allowUpscaling = true, - }) { + BackendAnimatedImage createAnimatedImage(Uint8List bytes, {int? targetWidth, int? targetHeight}) { throw UnimplementedError('Skwasm not implemented on this platform.'); } @override - Future instantiateImageCodecFromUrl( - Uri uri, { - ui_web.ImageCodecChunkCallback? chunkCallback, - }) { + BackendImage createImageFromImageSource(ImageSource source) { throw UnimplementedError('Skwasm not implemented on this platform.'); } @@ -297,21 +288,6 @@ class SkwasmRenderer extends Renderer { required int lineNumber, }) => throw UnimplementedError('Skwasm not implemented on this platform.'); - @override - ui.Image createImageFromImageBitmap(DomImageBitmap imageSource) { - throw UnimplementedError('Skwasm not implemented on this platform.'); - } - - @override - ui.Image createImageFromTextureSource( - JSAny object, { - required int width, - required int height, - required bool transferOwnership, - }) { - throw UnimplementedError('Skwasm not implemented on this platform.'); - } - @override void dumpDebugInfo() { throw UnimplementedError('Skwasm not implemented on this platform.'); diff --git a/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web/images.dart b/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web/images.dart index bbfe990c88398..056d2df6498b2 100644 --- a/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web/images.dart +++ b/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web/images.dart @@ -17,7 +17,7 @@ typedef ImageCodecChunkCallback = void Function(int cumulativeBytesLoaded, int e /// The [chunkCallback] is called with progress updates as image chunks are /// loaded. Future createImageCodecFromUrl(Uri uri, {ImageCodecChunkCallback? chunkCallback}) { - return renderer.instantiateImageCodecFromUrl(uri, chunkCallback: chunkCallback); + return engineInstantiateImageCodecFromUrl(uri, chunkCallback: chunkCallback); } /// Creates a [ui.Image] from an ImageBitmap object. diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart index 33be50608409c..d4b071d79e765 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart @@ -22,7 +22,7 @@ Future testMain() async { setUpCanvasKitTest(withImplicitView: true); test('ImageDecoder toByteData(PNG)', () async { - final image = CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test'); + final image = EngineCodec.skia(CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test')); final ui.FrameInfo frame = await image.getNextFrame(); final ByteData? png = await frame.image.toByteData(format: ui.ImageByteFormat.png); expect(png, isNotNull); @@ -33,7 +33,7 @@ Future testMain() async { }); test('CkAnimatedImage toByteData(RGBA)', () async { - final image = CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test'); + final image = EngineCodec.skia(CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test')); const expectedColors = >[ [255, 0, 0, 255], [0, 255, 0, 255], diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/image_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/image_test.dart index 68e21038386be..0b779628a765a 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/image_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/image_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -31,38 +30,6 @@ void testMain() { image.dispose(); }); - test('fetchImage fetches image in chunks', () async { - final cumulativeBytesLoadedInvocations = []; - final expectedTotalBytesInvocations = []; - final Uint8List result = await fetchImage('/long_test_payload?length=100000&chunk=1000', ( - int cumulativeBytesLoaded, - int expectedTotalBytes, - ) { - cumulativeBytesLoadedInvocations.add(cumulativeBytesLoaded); - expectedTotalBytesInvocations.add(expectedTotalBytes); - }); - - // Check that image payload was chunked. - expect(cumulativeBytesLoadedInvocations, hasLength(greaterThan(1))); - - // Check that reported total byte count is the same across all invocations. - for (final expectedTotalBytes in expectedTotalBytesInvocations) { - expect(expectedTotalBytes, 100000); - } - - // Check that cumulative byte count grows with each invocation. - cumulativeBytesLoadedInvocations.reduce((int previous, int next) { - expect(next, greaterThan(previous)); - return next; - }); - - // Check that the last cumulative byte count matches the total byte count. - expect(cumulativeBytesLoadedInvocations.last, 100000); - - // Check the contents of the returned data. - expect(result, List.generate(100000, (int i) => i & 0xFF)); - }); - test('EngineImage does not close image source too early', () async { // Create a shared ImageSource wrapping a blank 4x4 image bitmap. final ImageSource imageSource = ImageBitmapImageSource( diff --git a/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart b/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart index f5582c380df65..f22871fd6cceb 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart @@ -22,12 +22,10 @@ void testMain() { }); Future newBitmap(int width, int height) async { - return createImageBitmap(createBlankDomImageData(width, height) as JSAny, ( - x: 0, - y: 0, - width: width, - height: height, - )); + return createImageBitmap( + createBlankDomImageData(width, height) as JSAny, + bounds: (x: 0, y: 0, width: width, height: height), + ); } // Regression test for https://github.com/flutter/flutter/issues/75286 diff --git a/engine/src/flutter/lib/web_ui/test/skwasm/native_memory_test.dart b/engine/src/flutter/lib/web_ui/test/skwasm/native_memory_test.dart index bb3307cb332cd..083532c1cd67b 100644 --- a/engine/src/flutter/lib/web_ui/test/skwasm/native_memory_test.dart +++ b/engine/src/flutter/lib/web_ui/test/skwasm/native_memory_test.dart @@ -18,7 +18,13 @@ void testMain() { group('Skwasm native memory', () { test('SkwasmImage.clone share same handle and box', () { final pixels = Uint8List(4); - final EngineImage image = createSkwasmImageFromPixels(pixels, 1, 1, ui.PixelFormat.rgba8888); + final SkwasmImage backendImage = createSkwasmImageFromPixels( + pixels, + 1, + 1, + ui.PixelFormat.rgba8888, + ); + final image = EngineImage(backendImage, 1, 1); final EngineImage clone = image.clone(); expect( 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 933cc05679630..65d66d13c2462 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 @@ -286,12 +286,15 @@ class BitmapTestCodec extends TestCodec { await imageElement.decode(); - final DomImageBitmap bitmap = await createImageBitmap(imageElement, ( - x: 0, - y: 0, - width: imageElement.naturalWidth.toInt(), - height: imageElement.naturalHeight.toInt(), - )); + final DomImageBitmap bitmap = await createImageBitmap( + imageElement, + bounds: ( + x: 0, + y: 0, + width: imageElement.naturalWidth.toInt(), + height: imageElement.naturalHeight.toInt(), + ), + ); final ui.Image image = await codecFactory(bitmap); return BitmapSingleFrameCodec(bitmap, image); @@ -315,7 +318,7 @@ class BitmapSingleFrameCodec implements ui.Codec { @override Future getNextFrame() async { - return SingleFrameInfo(image); + return AnimatedImageFrameInfo(Duration.zero, image); } @override @@ -394,6 +397,11 @@ Future testMain() async { expect(image.width, isNonZero); expect(image.height, isNonZero); + if (testCodec.description.contains('300 x 300')) { + expect(image.width, 300); + expect(image.height, 300); + } + final ByteData? byteData = await image.toByteData(); expect( byteData, @@ -455,6 +463,20 @@ Future testMain() async { expect(gotError, isTrue, reason: 'Should have got CORS error'); }); + test('does not upscale when allowUpscaling is false', () async { + final HttpFetchResponse response = await httpFetch('/test_images/1x1.png'); + final Uint8List bytes = (await response.payload.asByteBuffer()).asUint8List(); + final ui.Codec codec = await renderer.instantiateImageCodec( + bytes, + targetWidth: 100, + targetHeight: 100, + allowUpscaling: false, + ); + final ui.FrameInfo frame = await codec.getNextFrame(); + expect(frame.image.width, 1); + expect(frame.image.height, 1); + }); + test('isAvif', () { expect(isAvif(Uint8List.fromList([])), isFalse); expect(isAvif(Uint8List.fromList([1, 2, 3])), isFalse); diff --git a/engine/src/flutter/lib/web_ui/test/ui/image/html_image_element_codec_test.dart b/engine/src/flutter/lib/web_ui/test/ui/image/html_image_element_codec_test.dart deleted file mode 100644 index 366e172943f4a..0000000000000 --- a/engine/src/flutter/lib/web_ui/test/ui/image/html_image_element_codec_test.dart +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart: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 'package:ui/ui_web/src/ui_web.dart' as ui_web; - -import '../../common/test_initialization.dart'; -import '../../ui/utils.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -Future testMain() async { - setUpUnitTests(); - setUp(() { - ImageDecodingManager.instance.debugReset(); - }); - group('$HtmlImageElementCodec', () { - test('supports raw images - RGBA8888', () async { - final completer = Completer(); - const width = 200; - const height = 300; - final list = Uint32List(width * height); - for (var index = 0; index < list.length; index += 1) { - list[index] = 0xFF0000FF; - } - ui.decodeImageFromPixels( - list.buffer.asUint8List(), - width, - height, - ui.PixelFormat.rgba8888, - (ui.Image image) => completer.complete(image), - ); - final ui.Image image = await completer.future; - expect(image.width, width); - expect(image.height, height); - }); - test('supports raw images - BGRA8888', () async { - final completer = Completer(); - const width = 200; - const height = 300; - final list = Uint32List(width * height); - for (var index = 0; index < list.length; index += 1) { - list[index] = 0xFF0000FF; - } - ui.decodeImageFromPixels( - list.buffer.asUint8List(), - width, - height, - ui.PixelFormat.bgra8888, - (ui.Image image) => completer.complete(image), - ); - final ui.Image image = await completer.future; - expect(image.width, width); - expect(image.height, height); - }); - test('loads sample image', () async { - final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png'); - final ui.FrameInfo frameInfo = await codec.getNextFrame(); - - expect(codec.imgElement, isNotNull); - expect(codec.imgElement!.src, contains('sample_image1.png')); - expect(codec.imgElement!.crossOrigin, 'anonymous'); - expect(codec.imgElement!.decoding, 'async'); - - expect(frameInfo.image, isNotNull); - expect(frameInfo.image.width, 100); - expect(frameInfo.image.toString(), '[100×100]'); - }); - test('dispose image image', () async { - final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png'); - final ui.FrameInfo frameInfo = await codec.getNextFrame(); - expect(frameInfo.image, isNotNull); - expect(frameInfo.image.debugDisposed, isFalse); - frameInfo.image.dispose(); - expect(frameInfo.image.debugDisposed, isTrue); - }); - test('provides image loading progress', () async { - final buffer = StringBuffer(); - final HtmlImageElementCodec codec = CkImageElementCodec( - 'sample_image1.png', - chunkCallback: (int loaded, int total) { - buffer.write('$loaded/$total,'); - }, - ); - await codec.getNextFrame(); - expect(buffer.toString(), '0/100,100/100,'); - }); - - test('uses ImageDecodingManager', () async { - final ImageDecodingManager manager = ImageDecodingManager.instance; - // Occupy all slots - final requests = []; - for (var i = 0; i < 8; i++) { - requests.add(manager.requestDecodingSlot(100, 100)); - } - - final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png'); - var decoded = false; - final Future decodeFuture = codec.decode().then((_) => decoded = true); - - // Give it some time to load (Phase 1) - await Future.delayed(const Duration(milliseconds: 100)); - expect(decoded, false); // Should be blocked in Phase 2 - - // Release one slot - manager.releaseDecodingSlot(requests[0]); - - // Wait for it to decode (Phase 3) - await decodeFuture; - expect(decoded, true); - - // Clean up remaining slots - for (var i = 1; i < 8; i++) { - manager.releaseDecodingSlot(requests[i]); - } - }); - - test('dispose unblocks ImageDecodingManager queue', () async { - final ImageDecodingManager manager = ImageDecodingManager.instance; - // Occupy all slots - final requests = []; - for (var i = 0; i < 8; i++) { - requests.add(manager.requestDecodingSlot(100, 100)); - } - - final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png'); - var decodeFinished = false; - unawaited(codec.decode().whenComplete(() => decodeFinished = true)); - - // Give it some time to load (Phase 1) - await Future.delayed(const Duration(milliseconds: 100)); - expect(decodeFinished, false); // Should be blocked in Phase 2 - - // Dispose the codec while it's in the queue - codec.dispose(); - - // The decode future should complete (as requested in the plan) - await Future.delayed(Duration.zero); - expect(decodeFinished, true); - - // A new request should be able to get a slot if we release one. - manager.releaseDecodingSlot(requests[0]); - final ImageDecodingRequest request2 = manager.requestDecodingSlot(100, 100); - var granted2 = false; - unawaited(request2.future.then((_) => granted2 = true)); - await Future.delayed(Duration.zero); - expect(granted2, true); - - // Clean up - for (var i = 1; i < 8; i++) { - manager.releaseDecodingSlot(requests[i]); - } - manager.releaseDecodingSlot(request2); - }); - - test('getNextFrame() throws StateError if disposed', () async { - final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png'); - codec.dispose(); - expect(() => codec.getNextFrame(), throwsStateError); - }); - - test('clears src on loading failure', () async { - final HtmlImageElementCodec codec = CkImageElementCodec('non_existent_image.png'); - try { - await codec.getNextFrame(); - fail('Should have thrown an exception'); - } catch (e) { - expect(e, isA()); - } - expect(codec.imgElement?.src, isNot(contains('non_existent_image.png'))); - }); - - test('dispose does not clear src if image handed out', () async { - final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png'); - final ui.FrameInfo frame = await codec.getNextFrame(); - final String? src = codec.imgElement?.src; - expect(src, contains('sample_image1.png')); - - codec.dispose(); - expect(codec.imgElement?.src, src); // Should NOT be cleared - - frame.image.dispose(); - }); - - /// Regression test for Firefox - /// https://github.com/flutter/flutter/issues/66412 - test('Returns nonzero natural width/height', () async { - final HtmlImageElementCodec codec = CkImageElementCodec( - 'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I' - 'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG' - 'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx' - 'MiAyLjM5OSAxMiAxMiAwIDkuNjAxLTIuMzk5IDEyLTEyIDEyLTkuNjAxIDAtMTItMi' - '4zOTktMTItMTJDMCAyLjM5OSAyLjM5OSAwIDEyIDB6bS0xLjk2OSAxOC41NjRjMi41' - 'MjQuMDAzIDQuNjA0LTIuMDcgNC42MDktNC41OTUgMC0yLjUyMS0yLjA3NC00LjU5NS' - '00LjU5NS00LjU5NVM1LjQ1IDExLjQ0OSA1LjQ1IDEzLjk2OWMwIDIuNTE2IDIuMDY1' - 'IDQuNTg4IDQuNTgxIDQuNTk1em04LjM0NC0uMTg5VjUuNjI1SDUuNjI1djIuMjQ3aD' - 'EwLjQ5OHYxMC41MDNoMi4yNTJ6bS04LjM0NC02Ljc0OGEyLjM0MyAyLjM0MyAwIDEx' - 'LS4wMDIgNC42ODYgMi4zNDMgMi4zNDMgMCAwMS4wMDItNC42ODZ6Ii8+PC9zdmc+', - ); - final ui.FrameInfo frameInfo = await codec.getNextFrame(); - expect(frameInfo.image.width, isNot(0)); - }); - }, skip: isSkwasm); - - group('ImageCodecUrl', () { - test('loads sample image from web', () async { - final Uri uri = Uri.base.resolve('sample_image1.png'); - final codec = await ui_web.createImageCodecFromUrl(uri) as HtmlImageElementCodec; - final ui.FrameInfo frameInfo = await codec.getNextFrame(); - - expect(codec.imgElement, isNotNull); - expect(codec.imgElement!.src, contains('sample_image1.png')); - expect(codec.imgElement!.crossOrigin, 'anonymous'); - expect(codec.imgElement!.decoding, 'async'); - - expect(frameInfo.image, isNotNull); - expect(frameInfo.image.width, 100); - }); - test('provides image loading progress from web', () async { - final Uri uri = Uri.base.resolve('sample_image1.png'); - final buffer = StringBuffer(); - final codec = - await ui_web.createImageCodecFromUrl( - uri, - chunkCallback: (int loaded, int total) { - buffer.write('$loaded/$total,'); - }, - ) - as HtmlImageElementCodec; - await codec.getNextFrame(); - expect(buffer.toString(), '0/100,100/100,'); - }); - }, skip: isSkwasm); -} diff --git a/engine/src/flutter/lib/web_ui/test/ui/image_decoder_test.dart b/engine/src/flutter/lib/web_ui/test/ui/image_decoder_test.dart index 17ff29d0c346d..58b8442f2d8b8 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/image_decoder_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/image_decoder_test.dart @@ -2,12 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:js_interop'; +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/test_initialization.dart'; +import 'utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -16,11 +21,273 @@ void main() { Future testMain() async { setUpUnitTests(setUpTestViewDimensions: false); - test('$ResizingCodec gives correct repetition count for GIFs', () async { + test('Codec gives correct repetition count for GIFs', () async { final ui.Codec codec = await renderer.instantiateImageCodecFromUrl( Uri(path: '/test_images/required.gif'), ); - final ui.Codec resizingCodec = ResizingCodec(codec); - expect(resizingCodec.repetitionCount, 0); + expect(codec.repetitionCount, 0); + codec.dispose(); + }); + + test('renderer.createAnimatedImage throws ImageCodecException on invalid bytes', () { + expect( + () => renderer.createAnimatedImage(Uint8List.fromList([1, 2, 3, 4, 5])), + throwsA(isA()), + ); + }); + + test('parseMimeType parses and cleans Content-Type headers', () { + expect(parseMimeType('image/png'), 'image/png'); + expect(parseMimeType('image/jpeg'), 'image/jpeg'); + expect(parseMimeType('IMAGE/PNG'), 'image/png'); + expect(parseMimeType('image/jpeg; charset=utf-8'), 'image/jpeg'); + expect(parseMimeType(' image/gif ; boundary=abc'), 'image/gif'); + expect(parseMimeType('image/webp;foo=bar;baz=qux'), 'image/webp'); + expect(parseMimeType(''), ''); + expect(parseMimeType(null), isNull); + }); + + test('ui.Image.toByteData(format: ui.ImageByteFormat.png) works without crashing', () async { + final HttpFetchResponse response = await httpFetch('/test_images/1x1.png'); + final Uint8List pngBytes = (await response.payload.asByteBuffer()).asUint8List(); + final ui.Codec codec = await renderer.instantiateImageCodec(pngBytes); + final ui.FrameInfo frame = await codec.getNextFrame(); + final ui.Image image = frame.image; + + final ByteData? pngByteData = await image.toByteData(format: ui.ImageByteFormat.png); + expect(pngByteData, isNotNull); + expect(pngByteData!.lengthInBytes, isNonZero); + + final Uint8List resultBytes = pngByteData.buffer.asUint8List(); + expect(resultBytes.length, greaterThan(8)); + expect(resultBytes[0], 0x89); + expect(resultBytes[1], 0x50); + expect(resultBytes[2], 0x4E); + expect(resultBytes[3], 0x47); + + image.dispose(); + codec.dispose(); }); + + test( + 'instantiateImageCodecFromUrl works with generic application/octet-stream MIME type via data URL', + () async { + const dataUrl = + 'data:application/octet-stream;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + + final ui.Codec codec = await renderer.instantiateImageCodecFromUrl(Uri.parse(dataUrl)); + expect(codec.frameCount, 1); + + final ui.FrameInfo frame = await codec.getNextFrame(); + expect(frame.image.width, 1); + expect(frame.image.height, 1); + + codec.dispose(); + }, + ); + + test('BrowserImageDecoder closes native decoder if disposed during initialization', () async { + if (!browserSupportsImageDecoder) { + return; + } + + final HttpFetchResponse response = await httpFetch('/test_images/1x1.png'); + final Uint8List pngBytes = (await response.payload.asByteBuffer()).asUint8List(); + + final decoder = BrowserImageDecoder( + contentType: 'image/png', + dataSource: pngBytes.toJS, + debugSource: 'test', + ); + + final Future initFuture = decoder.initialize(); + decoder.dispose(); + await initFuture; + + expect(decoder.debugCachedWebDecoder, isNull); + }); + + test( + '_BrowserEngineCodec.getNextFrame() throws StateError and cleans up if disposed during decode', + () async { + if (!browserSupportsImageDecoder) { + return; + } + + final HttpFetchResponse response = await httpFetch('/test_images/1x1.png'); + final Uint8List pngBytes = (await response.payload.asByteBuffer()).asUint8List(); + + final ui.Codec codec = await renderer.instantiateImageCodec(pngBytes); + final Future frameFuture = codec.getNextFrame(); + codec.dispose(); + + expect(frameFuture, throwsA(isA())); + }, + ); + + test( + '_SkiaEngineCodec.getNextFrame() throws StateError and cleans up if disposed during decode', + () async { + if (isSkwasm) { + // Skwasm does not compile animated image decoders in the Wasm fallback path + // (it relies entirely on the browser's native ImageDecoder). + return; + } + + final HttpFetchResponse response = await httpFetch('/test_images/flightAnim.gif'); + final Uint8List gifBytes = (await response.payload.asByteBuffer()).asUint8List(); + + final bool originalDecoderSupport = browserSupportsImageDecoder; + browserSupportsImageDecoder = false; + try { + final ui.Codec codec = await renderer.instantiateImageCodec(gifBytes); + final Future frameFuture = codec.getNextFrame(); + codec.dispose(); + + expect(frameFuture, throwsA(isA())); + } finally { + browserSupportsImageDecoder = originalDecoderSupport; + } + }, + ); + + test('handleProgressAndGetStream bypasses stream teeing when chunkCallback is null', () async { + final mockBody = JSObject(); + + final mockResponse = _TestHttpFetchResponse(stream: mockBody as DomReadableStream); + + final DomReadableStream result = await handleProgressAndGetStream(mockResponse, null); + expect(result, mockBody); + }); + + test( + 'handleProgressAndGetStream bypasses stream teeing when Content-Length is missing', + () async { + final mockBody = JSObject(); + + final mockResponse = _TestHttpFetchResponse(stream: mockBody as DomReadableStream); + + final DomReadableStream result = await handleProgressAndGetStream( + mockResponse, + (int loaded, int total) {}, + ); + expect(result, mockBody); + }, + ); + + test( + 'handleProgressAndGetStream tees the stream when chunkCallback and Content-Length are present', + () async { + final HttpFetchResponse response = await httpFetch('/test_images/1x1.png'); + final DomReadableStream originalBody = response.payload.stream; + + var callbackCalled = false; + final DomReadableStream result = await handleProgressAndGetStream(response, ( + int loaded, + int total, + ) { + callbackCalled = true; + }); + + expect(result, isNot(originalBody)); + + // Read the result stream to trigger the progress callback on the teed stream + final DomStreamReader reader = result.getReader(); + while (true) { + final DomStreamChunk chunk = await reader.read(); + if (chunk.done) { + break; + } + } + + expect(callbackCalled, isTrue); + }, + ); + + test('ImageDecoder.dispose is robust against throwing callbacks', () async { + if (!browserSupportsImageDecoder) { + return; + } + + final HttpFetchResponse response = await httpFetch('/test_images/1x1.png'); + final Uint8List pngBytes = (await response.payload.asByteBuffer()).asUint8List(); + + final decoder = BrowserImageDecoder( + contentType: 'image/png', + dataSource: pngBytes.toJS, + debugSource: 'test', + ); + + await decoder.initialize(); + expect(decoder.debugCachedWebDecoder, isNotNull); + + var secondCallbackCalled = false; + decoder.addDisposeCallback(() { + throw Exception('Callback failure'); + }); + decoder.addDisposeCallback(() { + secondCallbackCalled = true; + }); + + decoder.dispose(); + + expect(secondCallbackCalled, isTrue); + expect(decoder.debugCachedWebDecoder, isNull); + }); + + test('BrowserImageDecoder calls dispose callbacks on initialization failure', () async { + if (!browserSupportsImageDecoder) { + return; + } + + final decoder = BrowserImageDecoder( + contentType: 'image/png', + // Providing invalid/empty data will cause initialization/decoding to fail + dataSource: Uint8List(0).toJS, + debugSource: 'test', + ); + + var disposeCalled = false; + decoder.addDisposeCallback(() { + disposeCalled = true; + }); + + try { + await decoder.initialize(); + fail('initialize should have thrown an exception'); + } catch (e) { + decoder.dispose(); + expect(e, isA()); + } + + expect(disposeCalled, isTrue); + }); +} + +class _TestHttpFetchResponse implements HttpFetchResponse { + _TestHttpFetchResponse({required this.stream}); + + final DomReadableStream stream; + + @override + final int? contentLength = null; + + @override + bool get hasPayload => true; + + @override + HttpFetchPayload get payload => _TestHttpFetchPayload(stream); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _TestHttpFetchPayload implements HttpFetchPayload { + _TestHttpFetchPayload(this.stream); + + @override + final DomReadableStream stream; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } diff --git a/engine/src/flutter/lib/web_ui/test/ui/image_golden_test.dart b/engine/src/flutter/lib/web_ui/test/ui/image_golden_test.dart index ba655d1d18eb4..75c55661e6c07 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/image_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/image_golden_test.dart @@ -438,7 +438,7 @@ Future testMain() async { expect(bitmap.width, 150); expect(bitmap.height, 150); - final ui.Image uiImage = await renderer.createImageFromImageBitmap(bitmap); + final ui.Image uiImage = renderer.createImageFromImageBitmap(bitmap); if (isSkwasm && isMultiThreaded) { // Multi-threaded skwasm transfers the bitmap to the web worker, so it should be diff --git a/engine/src/flutter/lib/web_ui/test/ui/image_texture_source_test.dart b/engine/src/flutter/lib/web_ui/test/ui/image_texture_source_test.dart index cdc4d65b245b0..b111caf6a011c 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/image_texture_source_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/image_texture_source_test.dart @@ -62,7 +62,10 @@ Future testMain() async { ); await completer.future; - final DomImageBitmap bitmap = await createImageBitmap(image, (x: 0, y: 0, width: 1, height: 1)); + final DomImageBitmap bitmap = await createImageBitmap( + image, + bounds: (x: 0, y: 0, width: 1, height: 1), + ); final ui.Image uiImage = await ui_web.createImageFromTextureSource( bitmap, diff --git a/engine/src/flutter/skwasm/animated_image.cc b/engine/src/flutter/skwasm/animated_image.cc index 1fc62ecd2184b..e7f309fdf1f69 100644 --- a/engine/src/flutter/skwasm/animated_image.cc +++ b/engine/src/flutter/skwasm/animated_image.cc @@ -47,10 +47,11 @@ SKWASM_EXPORT SkAnimatedImage* animatedImage_create(SkData* data, return SkAnimatedImage::Make(std::move(android_codec)).release(); } - return SkAnimatedImage::Make( - std::move(android_codec), - SkImageInfo::MakeUnknown(target_width, target_height), - SkIRect::MakeWH(target_width, target_height), nullptr) + SkImageInfo info = android_codec->getInfo(); + info = info.makeWH(target_width, target_height); + return SkAnimatedImage::Make(std::move(android_codec), info, + SkIRect::MakeWH(target_width, target_height), + nullptr) .release(); } From 82e2bc242829e0398a270aa1dbaab990e7d022c6 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:42:04 -0700 Subject: [PATCH 038/330] Remove openglessdf from impeller_unittests. (#190469) issue: https://github.com/flutter/flutter/issues/189748 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- engine/src/flutter/testing/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/src/flutter/testing/run_tests.py b/engine/src/flutter/testing/run_tests.py index b4d9c6c634620..df46af9334199 100755 --- a/engine/src/flutter/testing/run_tests.py +++ b/engine/src/flutter/testing/run_tests.py @@ -581,7 +581,7 @@ def make_test( else: workers_flag = [] mac_impeller_unittests_flags = repeat_flags + workers_flag + [ - '--gtest_filter=-*OpenGLES', # These are covered in the golden tests. + '--gtest_filter=-*OpenGLES:*OpenGLESSDF', # These are covered in the golden tests. '--', '--enable_vulkan_validation', ] From 352f8ee486974f6d7e0de78ea1f4365a5fb7770f Mon Sep 17 00:00:00 2001 From: guszxtavo <125822178+guszxtavo@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:26:06 -0300 Subject: [PATCH 039/330] [Impeller] Enable ETC2/ASTC LDR/BC texture compression features at Vulkan device creation (#189303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Enable `textureCompressionETC2`, `textureCompressionASTC_LDR`, and `textureCompressionBC` core Vulkan 1.0 features at device creation in `CapabilitiesVK::GetEnabledDeviceFeatures`. These three features were read back in `SetPhysicalDevice` (~line 670) from `enabled_features`, but never requested in `GetEnabledDeviceFeatures` — so the capability query always returned `false` on every Android Vulkan device, regardless of hardware support. This was a three-line omission in #187077, which wired the read-back on all three backends (Vulkan/GLES/Metal) but only enabled the features on GLES and Metal. ASTC HDR was already correctly enabled via `VK_EXT_texture_compression_astc_hdr`. The fix mirrors the existing `samplerAnisotropy` pattern in the same "Base features" block. ## Related Issue Fixes #189107 ## Tests This change enables features that are already supported by the hardware — it simply requests them at device creation so the existing read-back reflects real capability. No new tests are needed; existing `capabilities_vk_unittests` cover the feature chain plumbing. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added no tests because this is a trivial feature-enable with no new logic. [Contributor Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-Hygiene.md [Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-Hygiene.md [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo --- .../renderer/backend/vulkan/capabilities_vk.cc | 9 +++++++++ .../backend/vulkan/context_vk_unittests.cc | 18 ++++++++++++++++++ .../backend/vulkan/test/mock_vulkan.cc | 9 +++++++++ 3 files changed, 36 insertions(+) diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc index 2820d6e7042e2..b2a72a37bcb3a 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc @@ -456,6 +456,15 @@ CapabilitiesVK::GetEnabledDeviceFeatures( // `max_anisotropy` greater than 1 may only be created when this feature // is enabled. required.samplerAnisotropy = supported.samplerAnisotropy; + + // Enable block-compressed texture format support when available. + // These core Vulkan 1.0 features must be explicitly requested at device + // creation; otherwise the capability read-back in SetPhysicalDevice + // (which reads from enabled_features) will always report false. + // See: https://github.com/flutter/flutter/issues/189107 + required.textureCompressionETC2 = supported.textureCompressionETC2; + required.textureCompressionASTC_LDR = supported.textureCompressionASTC_LDR; + required.textureCompressionBC = supported.textureCompressionBC; } // VK_KHR_sampler_ycbcr_conversion features. if (IsExtensionInList( diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/context_vk_unittests.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/context_vk_unittests.cc index 61c0d2bee16d5..cee7e429dcbe8 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/context_vk_unittests.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/context_vk_unittests.cc @@ -208,6 +208,24 @@ TEST(CapabilitiesVKTest, ContextInitializesWithNoStencilFormat) { PixelFormat::kD32FloatS8UInt); } +// Regression test for https://github.com/flutter/flutter/issues/189107. +// The core Vulkan 1.0 block-compression features (ETC2, ASTC LDR, BC) must be +// explicitly requested in GetEnabledDeviceFeatures so the read-back in +// SetPhysicalDevice reports real support. The mock device advertises all three +// as supported, so a device that enables them correctly must report them here. +TEST(CapabilitiesVKTest, ReportsBlockCompressedTextureSupport) { + const std::shared_ptr context = MockVulkanContextBuilder().Build(); + ASSERT_NE(context, nullptr); + const CapabilitiesVK* capabilities_vk = + reinterpret_cast(context->GetCapabilities().get()); + EXPECT_TRUE(capabilities_vk->SupportsTextureCompression( + CompressedTextureFamily::kETC2)); + EXPECT_TRUE(capabilities_vk->SupportsTextureCompression( + CompressedTextureFamily::kASTC)); + EXPECT_TRUE(capabilities_vk->SupportsTextureCompression( + CompressedTextureFamily::kBC)); +} + // Impeller's 2D renderer relies on hardware support for a combined // depth-stencil format (widely supported). So fail initialization if a suitable // one couldn't be found. That way we have an opportunity to fallback to diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc index faeeb92749242..b47df094d3a15 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc @@ -323,6 +323,15 @@ void vkGetPhysicalDeviceProperties(VkPhysicalDevice physicalDevice, void vkGetPhysicalDeviceFeatures2(VkPhysicalDevice physicalDevice, VkPhysicalDeviceFeatures2* pFeatures) { + // Advertise the core block-compressed texture features so tests can exercise + // the GetEnabledDeviceFeatures -> SetPhysicalDevice plumbing that gates + // CapabilitiesVK::SupportsTextureCompression. Without this the mock reports + // these core Vulkan 1.0 features as VK_FALSE and the capability is always + // false regardless of the enable path. + pFeatures->features.textureCompressionETC2 = VK_TRUE; + pFeatures->features.textureCompressionASTC_LDR = VK_TRUE; + pFeatures->features.textureCompressionBC = VK_TRUE; + // Advertise the features the mock supports by walking the pNext chain. auto* next = reinterpret_cast(pFeatures->pNext); while (next != nullptr) { From 5d3f368babe3f5644bbd2b668e12164075f8c3ab Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:56:15 -0700 Subject: [PATCH 040/330] Revert: iOS: Eliminate use of IOSContextNoop in platform view tests (#190501) Reverts: [iOS: Eliminate use of IOSContextNoop in platform view tests](https://github.com/flutter/flutter/pull/190419) Initiated by: @cbracken Reason for reverting: This was fine as was the patch that removed GetBackend() but parallelising them was not. Original PR Author: @cbracken Reviewed By: @gaaclarke The original PR description is provided below: `FlutterPlatformViewsTest` used the internal engine class `flutter::IOSContextNoop` in a dozen places purely as a no-op context to hand to `submitFrame:withIosContext:`. This replaces those with a local `FakeIOSContext` that replicates the behaviour the tests rely on so that we can delete `IOSContextNoop` in a follow-up. Like `IOSContextNoop` `FakeIOSContext` reports the Impeller backend and returns no external texture, and inherits the null Impeller/Aiks contexts from the base class. The one place that uses the engine's real context (`GetIosContext`) is left untouched. I suspect we can eventually move this code to use the real Metal backend but for now, this is just refactoring with no semantic change. Back in ancient times, before the simulator required Metal, and when iOS still had a Skia software renderer, we ran that on the Simulator due to some issues with our OpenGL implementation. Later, Flutter on iOS migrated from Skia to Impeller. Skia supports a software backend but Impeller did not, and so when running on Impeller, we stubbed out the software backend to no-op context/surfaces, hence IOSContextNoop and IOSSurfaceNoop. The Simulator now *requires* Metal and Flutter has eliminated Skia support altogether so there's no longer a software mode at all, nor a need for a no-op path in case you're using Impeller and ask for a software backend. This is part of a series of changes to remove the dead no-op fallback. No behavioural change; just a test refactoring. Issue: https://github.com/flutter/flutter/issues/190041 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Source/FlutterPlatformViewsTest.mm | 71 ++++++++----------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index aa676568e4ef0..1fdcb42de2585 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -26,32 +26,11 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTestHelper.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView+Test.h" -#include "flutter/shell/platform/darwin/ios/ios_context.h" +#include "flutter/shell/platform/darwin/ios/ios_context_noop.h" #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" FLUTTER_ASSERT_ARC -namespace { -// An IOSContext fake for tests that do not need a real GPU context. -class FakeIOSContext : public flutter::IOSContext { - public: - FakeIOSContext() = default; - ~FakeIOSContext() override = default; - - // |IOSContext| - flutter::IOSRenderingBackend GetBackend() const override { - return flutter::IOSRenderingBackend::kImpeller; - } - - // |IOSContext| - std::unique_ptr CreateExternalTexture( - int64_t texture_id, - NSObject* texture) override { - return nullptr; - } -}; -} // namespace - @class FlutterPlatformViewsTestMockPlatformView; __weak static UIView* gMockPlatformView = nil; const float kFloatCompareEpsilon = 0.001; @@ -4246,8 +4225,9 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return false; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600)); - XCTAssertFalse([flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertFalse([flutterPlatformViewsController + submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); auto embeddedViewParams_2 = std::make_unique(finalMatrix, flutter::DlSize(300, 300), stack); @@ -4262,8 +4242,9 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600)); - XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface_submit_true) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController + submitFrame:std::move(mock_surface_submit_true) + withIosContext:std::make_shared()]); } - (void) @@ -4450,8 +4431,9 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController + submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // platform view is wrapped by touch interceptor, which itself is wrapped by clipping view. UIView* clippingView1 = view1.superview.superview; @@ -4478,8 +4460,9 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController + submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] > [flutterView.subviews indexOfObject:clippingView2], @@ -4552,8 +4535,9 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController + submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // platform view is wrapped by touch interceptor, which itself is wrapped by clipping view. UIView* clippingView1 = view1.superview.superview; @@ -4580,8 +4564,9 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController + submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] < [flutterView.subviews indexOfObject:clippingView2], @@ -5030,8 +5015,9 @@ - (void)testDisposingViewInCompositionOrderDoNotCrash { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController + submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // Disposing won't remove embedded views until the view is removed from the composition_order_ XCTAssertEqual(flutterPlatformViewsController.embeddedViewCount, 2UL); @@ -5056,8 +5042,9 @@ - (void)testDisposingViewInCompositionOrderDoNotCrash { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController + submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // Disposing won't remove embedded views until the view is removed from the composition_order_ XCTAssertEqual(flutterPlatformViewsController.embeddedViewCount, 1UL); @@ -5121,7 +5108,7 @@ - (void)testOnlyPlatformViewsAreRemovedWhenReset { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; UIView* someView = [[UIView alloc] init]; [flutterView addSubview:someView]; @@ -5187,7 +5174,7 @@ - (void)testResetClearsPreviousCompositionOrder { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; // The above code should result in previousCompositionOrder having one viewId in it XCTAssertEqual(flutterPlatformViewsController.previousCompositionOrder.count, 1ul); @@ -5256,7 +5243,7 @@ - (void)testNilPlatformViewDoesntCrash { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; XCTAssertEqual(flutterView.subviews.count, 1u); } @@ -5367,7 +5354,7 @@ - (void)testFlutterPlatformViewControllerSubmitFramePreservingFrameDamage { }); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; XCTAssertTrue(submit_info.has_value()); XCTAssertEqual(*submit_info->frame_damage, flutter::DlIRect::MakeWH(800, 600)); From b766512c65d8289c707ea214507acbc6a9d05c13 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 4 Aug 2026 06:41:39 -0400 Subject: [PATCH 041/330] Roll Dart SDK from 2a799a2404e9 to 9859c0a39adb (4 revisions) (#190521) https://dart.googlesource.com/sdk.git/+log/2a799a2404e9..9859c0a39adb 2026-08-04 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-89.0.dev 2026-08-04 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-88.0.dev 2026-08-04 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-87.0.dev 2026-08-03 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-86.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC codefu@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index c56eb335c3657..ac7ddb7b19be6 100644 --- a/DEPS +++ b/DEPS @@ -55,7 +55,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': '2a799a2404e9c2cf468e0142b2d9a47e4d609a5f', + 'dart_revision': '9859c0a39adb1c418b0831dde11a338f7e1f7ed0', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From 9654781fcc9c5288f7eb1b99617a06f6b8fe9690 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 4 Aug 2026 10:49:19 -0400 Subject: [PATCH 042/330] Roll Skia from a08d918ebd6a to 48b58ee222f1 (11 revisions) (#190527) https://skia.googlesource.com/skia.git/+log/a08d918ebd6a..48b58ee222f1 2026-08-04 alexisdavidc@google.com Revert "[text] Introduce PackedGPUGlyphID to add more metadata to SkPackedGlyphID" 2026-08-04 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-04 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-04 skia-autoroll@skia-public.iam.gserviceaccount.com Roll ANGLE from 272d37f4cc0c to 91d2d125ec00 (9 revisions) 2026-08-04 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Skia Infra from 9c262f334dfc to 45efa0244bed (19 revisions) 2026-08-04 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from 72e9d968d26a to 1923fa1074e1 2026-08-04 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-04 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-04 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from af17ae0ac3a5 to fd9ab9744d0b (9 revisions) 2026-08-03 michaelludwig@google.com [text] Introduce PackedGPUGlyphID to add more metadata to SkPackedGlyphID 2026-08-03 kjlubick@google.com Update viewer help flags If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC alexisdavidc@google.com,codefu@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index ac7ddb7b19be6..9c8da03124268 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'a08d918ebd6a85b03538040ad80da26dc78e387f', + 'skia_revision': '48b58ee222f14b2b14a08c2e8574fae8256b26e0', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 29ce36f91ef85bdd3f98ce071a78ef8cb9b98029 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 4 Aug 2026 11:37:52 -0400 Subject: [PATCH 043/330] Roll Packages from ac87e65333e1 to 3498b9d7b671 (1 revision) (#190532) https://github.com/flutter/packages/compare/ac87e65333e1...3498b9d7b671 2026-08-03 fluttergithubbot@gmail.com Sync release-material_ui-0.0.2 to main (flutter/packages#12268) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- bin/internal/flutter_packages.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 0b3f00dce77db..9404108fe8e31 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -ac87e65333e1159022267053c009f747667f6f50 +3498b9d7b67143a68dc43b90951acb577e92f64e From 86845f698313f9001017b64e9ecae38868c272d1 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:53:04 -0500 Subject: [PATCH 044/330] Prepare device support symbols (#190369) When debugging for iOS devices, Device Support symbols are required to be copied from the iOS device to the host. Previously the only way to trigger this was to open Xcode. We recently discovered there's now a command that automates this. This PR adds a command that calls `xcodebuild -prepareDeviceSupport` before installing/launching the app so that Device Support symbols are verified to be installed. Toward https://github.com/flutter/flutter/issues/189284. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/src/ios/device_support.dart | 114 ++++++++ .../flutter_tools/lib/src/ios/devices.dart | 31 ++- .../flutter_tools/lib/src/macos/xcdevice.dart | 2 + .../ios/device_support_test.dart | 250 ++++++++++++++++++ .../test/general.shard/ios/devices_test.dart | 87 ++++++ .../ios/ios_device_install_test.dart | 3 + .../ios/ios_device_project_test.dart | 3 + .../ios_device_start_nonprebuilt_test.dart | 5 + .../ios/ios_device_start_prebuilt_test.dart | 138 ++++++++++ 9 files changed, 629 insertions(+), 4 deletions(-) create mode 100644 packages/flutter_tools/lib/src/ios/device_support.dart create mode 100644 packages/flutter_tools/test/general.shard/ios/device_support_test.dart diff --git a/packages/flutter_tools/lib/src/ios/device_support.dart b/packages/flutter_tools/lib/src/ios/device_support.dart new file mode 100644 index 0000000000000..03a9ffcbb2800 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/device_support.dart @@ -0,0 +1,114 @@ +// 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 'dart:async'; + +import '../base/io.dart'; +import '../base/logger.dart'; +import '../base/process.dart'; +import '../base/version.dart'; +import '../convert.dart'; +import '../macos/xcode.dart'; + +/// A class to handle preparing the device support for iOS devices. +class IOSDeviceSupport { + IOSDeviceSupport({ + required Logger logger, + required ProcessUtils processUtils, + required Xcode? xcode, + }) : _xcode = xcode, + _logger = logger, + _processUtils = processUtils; + + final Logger _logger; + final ProcessUtils _processUtils; + final Xcode? _xcode; + + /// Calls `xcodebuild -prepareDeviceSupport` for the given [deviceId] and streams the logs when + /// copying is in progress. + /// + /// The command copies symbols from the iOS device to the host machine and stores them in + /// $HOME/Library/Developer/Xcode/iOS DeviceSupport. Without these symbols, debugging is + /// extremely slow. + Future prepareDeviceSupport(String deviceId) async { + final Version? xcodeVersion = _xcode?.currentVersion; + if (xcodeVersion == null || xcodeVersion < Version(16, 3, 0)) { + // The prepareDeviceSupport command is only available on Xcode 16.3+ + return; + } + final command = [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=$deviceId', + ]; + try { + final Process process = await _processUtils.start(command); + + final timer = Timer(const Duration(seconds: 10), () { + _logger.printError( + 'Xcode is taking longer than expected to start preparing Device Support symbols...\n' + 'Connect your device via USB and try running this command manually:\n' + ' "${command.join(' ')}"', + ); + }); + + var printToTrace = true; + final StreamSubscription stdoutSubscription = process.stdout + .transform(utf8.decoder) + .listen((String text) { + if (text.contains('Copying')) { + printToTrace = false; + _logger.printStatus( + 'Copying Device Support symbols. This may take several minutes to complete...\n' + 'Please do not connect or disconnect your device until finished.', + ); + timer.cancel(); + } + if (printToTrace) { + _logger.printTrace(text); + } else { + _logger.printStatus(text, newline: false); + } + }); + + final StreamSubscription stderrSubscription = process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + _logger.printError(line); + }); + + try { + // Wait for stdout and stderr to be fully processed + // because process.exitCode may complete first. + await Future.wait(>[ + stdoutSubscription.asFuture(), + stderrSubscription.asFuture(), + ]); + + unawaited(stdoutSubscription.cancel()); + unawaited(stderrSubscription.cancel()); + + final int exitCode = await process.exitCode; + if (exitCode != 0) { + _logger.printError('xcodebuild -prepareDeviceSupport exited with code $exitCode'); + } + } finally { + timer.cancel(); + } + + // Print an empty line so that next prints aren't inline with logs from here. + if (!printToTrace) { + _logger.printStatus(''); + } + } on ProcessException catch (exception, stackTrace) { + _logger.printError( + 'Process exception running "xcodebuild -prepareDeviceSupport": $exception', + ); + _logger.printTrace('$stackTrace'); + } + } +} diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index c74e1f38a5ce9..62ac4be14837c 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -38,6 +38,7 @@ import '../protocol_discovery.dart'; import '../vmservice.dart'; import 'application_package.dart'; import 'core_devices.dart'; +import 'device_support.dart'; import 'ios_deploy.dart'; import 'ios_workflow.dart'; import 'iproxy.dart'; @@ -308,6 +309,7 @@ class IOSDevice extends Device { IOSDevice( super.id, { required FileSystem fileSystem, + required ProcessUtils processUtils, required this.name, required CpuArch cpuArch, required this.connectionInterface, @@ -325,6 +327,7 @@ class IOSDevice extends Device { required IProxy iProxy, required super.logger, required Analytics analytics, + required Xcode? xcode, }) : _cpuArch = cpuArch, _sdkVersion = sdkVersion, _iosDeploy = iosDeploy, @@ -332,8 +335,10 @@ class IOSDevice extends Device { _coreDeviceControl = coreDeviceControl, _coreDeviceLauncher = coreDeviceLauncher, _xcodeDebug = xcodeDebug, + _xcode = xcode, _iproxy = iProxy, _fileSystem = fileSystem, + _processUtils = processUtils, _logger = logger, _analytics = analytics, _platform = platform, @@ -348,12 +353,14 @@ class IOSDevice extends Device { final IOSDeploy _iosDeploy; final Analytics _analytics; final FileSystem _fileSystem; + final ProcessUtils _processUtils; final Logger _logger; final Platform _platform; final IMobileDevice _iMobileDevice; final IOSCoreDeviceControl _coreDeviceControl; final IOSCoreDeviceLauncher _coreDeviceLauncher; final XcodeDebug _xcodeDebug; + final Xcode? _xcode; final IProxy _iproxy; Version? get sdkVersion { @@ -563,6 +570,15 @@ class IOSDevice extends Device { return LaunchResult.failed(); } + final bool shouldAttachDebugger = shouldAttachLLDBDebugger(debuggingOptions); + if (shouldAttachDebugger) { + await IOSDeviceSupport( + logger: _logger, + processUtils: _processUtils, + xcode: _xcode, + ).prepareDeviceSupport(id); + } + // Step 3: Attempt to install the application on the device. final List launchArguments = debuggingOptions.getIOSLaunchArguments( EnvironmentType.physical, @@ -611,6 +627,7 @@ class IOSDevice extends Device { mainPath: mainPath, discoveryTimeout: discoveryTimeout, shutdownHooks: shutdownHooks ?? globals.shutdownHooks, + shouldAttachDebugger: shouldAttachDebugger, ); installationResult = result ? 0 : 1; deploymentMethod = coreDeviceDeploymentMethod; @@ -1018,6 +1035,15 @@ class IOSDevice extends Device { ); } + /// Whether the LLDB debugger should be attached. + /// + /// The LLDB debugger should only be attached in debug mode or if the user uses the + /// `--ios-profile-debugger` flag in profile mode. + bool shouldAttachLLDBDebugger(DebuggingOptions debuggingOptions) { + return debuggingOptions.buildInfo.isDebug || + (debuggingOptions.buildInfo.isProfile && (debuggingOptions.iosProfileDebugger ?? false)); + } + /// Uses either `devicectl` or Xcode automation to install, launch, and debug /// apps on physical iOS devices. /// @@ -1041,6 +1067,7 @@ class IOSDevice extends Device { required IOSApp package, required List launchArguments, required String? mainPath, + required bool shouldAttachDebugger, required ShutdownHooks shutdownHooks, @visibleForTesting Duration? discoveryTimeout, }) async { @@ -1082,10 +1109,6 @@ class IOSDevice extends Device { await deviceLogReader.listenToCoreDeviceLauncher(_coreDeviceLauncher); } - final bool shouldAttachDebugger = - debuggingOptions.buildInfo.isDebug || - (debuggingOptions.buildInfo.isProfile && (debuggingOptions.iosProfileDebugger ?? false)); - if (shouldAttachDebugger) { final bool launchSuccess = await _coreDeviceLauncher.launchAppWithLLDBDebugger( deviceId: id, diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart index 403e292a87768..89f3d1a70b11c 100644 --- a/packages/flutter_tools/lib/src/macos/xcdevice.dart +++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart @@ -652,10 +652,12 @@ class XCDevice { xcodeProjectInterpreter: globals.xcodeProjectInterpreter!, ), xcodeDebug: _xcodeDebug, + xcode: _xcode, platform: globals.platform, devModeEnabled: devModeEnabled, isPaired: isPaired, isCoreDevice: coreDevice != null, + processUtils: _processUtils, ); } } diff --git a/packages/flutter_tools/test/general.shard/ios/device_support_test.dart b/packages/flutter_tools/test/general.shard/ios/device_support_test.dart new file mode 100644 index 0000000000000..1974ed437df50 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/ios/device_support_test.dart @@ -0,0 +1,250 @@ +// 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:fake_async/fake_async.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/process.dart'; +import 'package:flutter_tools/src/base/version.dart'; +import 'package:flutter_tools/src/ios/device_support.dart'; +import 'package:flutter_tools/src/macos/xcode.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; +import '../../src/fake_process_manager.dart'; + +void main() { + group('IOSDeviceSupport', () { + testWithoutContext('does nothing when Xcode is null', () async { + final processManager = FakeProcessManager.empty(); + final logger = BufferLogger.test(); + final processUtils = ProcessUtils(processManager: processManager, logger: logger); + + final deviceSupport = IOSDeviceSupport( + logger: logger, + processUtils: processUtils, + xcode: null, + ); + + await deviceSupport.prepareDeviceSupport('id-123'); + + expect(processManager, hasNoRemainingExpectations); + expect(logger.statusText, isEmpty); + expect(logger.traceText, isEmpty); + expect(logger.errorText, isEmpty); + }); + + testWithoutContext('does nothing when Xcode version is less than 16.3.0', () async { + final processManager = FakeProcessManager.empty(); + final logger = BufferLogger.test(); + final processUtils = ProcessUtils(processManager: processManager, logger: logger); + + final deviceSupport = IOSDeviceSupport( + logger: logger, + processUtils: processUtils, + xcode: FakeXcode(currentVersion: Version(16, 2, 0)), + ); + + await deviceSupport.prepareDeviceSupport('id-123'); + + expect(processManager, hasNoRemainingExpectations); + expect(logger.statusText, isEmpty); + expect(logger.traceText, isEmpty); + expect(logger.errorText, isEmpty); + }); + + testWithoutContext( + 'runs prepareDeviceSupport and logs stdout to trace when Copying is not present', + () async { + final processManager = FakeProcessManager.empty(); + final logger = BufferLogger.test(); + final processUtils = ProcessUtils(processManager: processManager, logger: logger); + + processManager.addCommand( + const FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=id-123', + ], + stdout: 'Preparing device support...', + ), + ); + + final deviceSupport = IOSDeviceSupport( + logger: logger, + processUtils: processUtils, + xcode: FakeXcode(currentVersion: Version(16, 3, 0)), + ); + + await deviceSupport.prepareDeviceSupport('id-123'); + + expect(processManager, hasNoRemainingExpectations); + expect(logger.traceText, contains('Preparing device support...')); + expect(logger.statusText, isEmpty); + expect(logger.errorText, isEmpty); + }, + ); + + testWithoutContext('runs prepareDeviceSupport and logs Copying messages to status', () async { + final processManager = FakeProcessManager.empty(); + final logger = BufferLogger.test(); + final processUtils = ProcessUtils(processManager: processManager, logger: logger); + + processManager.addCommand( + const FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=id-123', + ], + stdout: 'Copying symbols...', + ), + ); + + final deviceSupport = IOSDeviceSupport( + logger: logger, + processUtils: processUtils, + xcode: FakeXcode(currentVersion: Version(16, 3, 0)), + ); + + await deviceSupport.prepareDeviceSupport('id-123'); + + expect(processManager, hasNoRemainingExpectations); + expect( + logger.statusText, + contains( + 'Copying Device Support symbols. This may take several minutes to complete...\n' + 'Please do not connect or disconnect your device until finished.', + ), + ); + expect(logger.statusText, contains('Copying symbols...')); + expect(logger.statusText, endsWith('\n')); + expect( + logger.traceText, + 'executing: xcrun xcodebuild -prepareDeviceSupport -destination id=id-123\n', + ); + expect(logger.errorText, isEmpty); + }); + + testWithoutContext('runs prepareDeviceSupport and logs stderr to error', () async { + final processManager = FakeProcessManager.empty(); + final logger = BufferLogger.test(); + final processUtils = ProcessUtils(processManager: processManager, logger: logger); + + processManager.addCommand( + const FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=id-123', + ], + stderr: 'Error occurred', + ), + ); + + final deviceSupport = IOSDeviceSupport( + logger: logger, + processUtils: processUtils, + xcode: FakeXcode(currentVersion: Version(17, 0, 0)), + ); + + await deviceSupport.prepareDeviceSupport('id-123'); + + expect(processManager, hasNoRemainingExpectations); + expect(logger.errorText, contains('Error occurred')); + }); + + testWithoutContext( + 'prints error message when prepareDeviceSupport takes longer than 10 seconds', + () { + FakeAsync().run((fakeAsync) { + final processManager = FakeProcessManager.empty(); + final logger = BufferLogger.test(); + final processUtils = ProcessUtils(processManager: processManager, logger: logger); + + processManager.addCommand( + const FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=id-123', + ], + duration: Duration(seconds: 11), + ), + ); + + final deviceSupport = IOSDeviceSupport( + logger: logger, + processUtils: processUtils, + xcode: FakeXcode(currentVersion: Version(16, 3, 0)), + ); + + deviceSupport.prepareDeviceSupport('id-123'); + + fakeAsync.elapse(const Duration(seconds: 10)); + + expect( + logger.errorText, + contains( + 'Xcode is taking longer than expected to start preparing Device Support symbols...\n' + 'Connect your device via USB and try running this command manually:\n' + ' "xcrun xcodebuild -prepareDeviceSupport -destination id=id-123"', + ), + ); + + fakeAsync.elapse(const Duration(seconds: 1)); + }); + }, + ); + + testWithoutContext('does not print error message if Copying is printed within 10 seconds', () { + FakeAsync().run((fakeAsync) { + final processManager = FakeProcessManager.empty(); + final logger = BufferLogger.test(); + final processUtils = ProcessUtils(processManager: processManager, logger: logger); + + processManager.addCommand( + const FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=id-123', + ], + duration: Duration(seconds: 11), + stdout: 'Copying symbols...', + ), + ); + + final deviceSupport = IOSDeviceSupport( + logger: logger, + processUtils: processUtils, + xcode: FakeXcode(currentVersion: Version(16, 3, 0)), + ); + + deviceSupport.prepareDeviceSupport('id-123'); + + fakeAsync.elapse(const Duration(seconds: 11)); + + expect(logger.errorText, isEmpty); + }); + }); + }); +} + +class FakeXcode extends Fake implements Xcode { + FakeXcode({this.currentVersion}); + + @override + final Version? currentVersion; +} diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 45dae90a19898..1498fd0fa285f 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; @@ -50,11 +51,13 @@ void main() { late IOSCoreDeviceControl coreDeviceControl; late IOSCoreDeviceLauncher coreDeviceLauncher; late XcodeDebug xcodeDebug; + late ProcessUtils processUtils; setUp(() { final artifacts = Artifacts.test(); cache = Cache.test(processManager: FakeProcessManager.any()); logger = BufferLogger.test(); + processUtils = ProcessUtils(processManager: FakeProcessManager.any(), logger: logger); fileSystem = MemoryFileSystem.test(); iosDeploy = IOSDeploy( artifacts: artifacts, @@ -95,10 +98,56 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ); expect(await device.isSupported(), isTrue); }); + group('shouldAttachLLDBDebugger', () { + testWithoutContext('returns expected values for different BuildInfo and options', () { + final device = IOSDevice( + 'device-123', + iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), + fileSystem: fileSystem, + logger: logger, + platform: macPlatform, + iosDeploy: iosDeploy, + analytics: FakeAnalytics(), + iMobileDevice: iMobileDevice, + coreDeviceControl: coreDeviceControl, + coreDeviceLauncher: coreDeviceLauncher, + xcodeDebug: xcodeDebug, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArch: .arm64, + connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, + isPaired: true, + devModeEnabled: true, + isCoreDevice: false, + processUtils: processUtils, + xcode: null, + ); + + expect(device.shouldAttachLLDBDebugger(DebuggingOptions.enabled(BuildInfo.debug)), isTrue); + expect( + device.shouldAttachLLDBDebugger( + DebuggingOptions.enabled(BuildInfo.profile, iosProfileDebugger: true), + ), + isTrue, + ); + expect(device.shouldAttachLLDBDebugger(DebuggingOptions.enabled(BuildInfo.profile)), isFalse); + expect( + device.shouldAttachLLDBDebugger( + DebuggingOptions.enabled(BuildInfo.profile, iosProfileDebugger: false), + ), + isFalse, + ); + expect(device.shouldAttachLLDBDebugger(DebuggingOptions.enabled(BuildInfo.release)), isFalse); + }); + }); + testWithoutContext('32-bit devices are unsupported', () async { final device = IOSDevice( 'device-123', @@ -119,6 +168,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ); expect(await device.isSupported(), isFalse); }); @@ -145,6 +196,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).majorSdkVersion, 1, ); @@ -169,6 +222,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).majorSdkVersion, 13, ); @@ -193,6 +248,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).majorSdkVersion, 10, ); @@ -217,6 +274,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).majorSdkVersion, 0, ); @@ -241,6 +300,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).majorSdkVersion, 0, ); @@ -267,6 +328,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).sdkVersion; var expectedVersion = Version(13, 3, 1, text: '13.3.1'); expect(sdkVersion, isNotNull); @@ -293,6 +356,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).sdkVersion; expectedVersion = Version(13, 3, 1, text: '13.3.1 (20ADBC)'); expect(sdkVersion, isNotNull); @@ -319,6 +384,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).sdkVersion; expectedVersion = Version(16, 4, 1, text: '16.4.1(a) (20ADBC)'); expect(sdkVersion, isNotNull); @@ -345,6 +412,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).sdkVersion; expectedVersion = Version(0, 0, 0, text: '0'); expect(sdkVersion, isNotNull); @@ -370,6 +439,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).sdkVersion; expect(sdkVersion, isNull); @@ -393,6 +464,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ).sdkVersion; expect(sdkVersion, isNull); }); @@ -418,6 +491,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ); expect(await device.sdkNameAndVersion, 'iOS 13.3 17C54'); @@ -444,6 +519,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ); expect(device.supportsRuntimeMode(BuildMode.debug), true); @@ -477,6 +554,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ); }, throwsAssertionError); }, @@ -568,6 +647,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: processUtils, + xcode: null, ); logReader1 = createLogReader(device, appPackage1, process1); logReader2 = createLogReader(device, appPackage2, process2); @@ -813,6 +894,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: ProcessUtils(processManager: fakeProcessManager, logger: logger), + xcode: null, ); device2 = IOSDevice( @@ -835,6 +918,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: ProcessUtils(processManager: fakeProcessManager, logger: logger), + xcode: null, ); }); @@ -1165,6 +1250,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: false, + processUtils: ProcessUtils(processManager: fakeProcessManager, logger: logger), + xcode: null, ); }); diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart index a5a2699c6c29c..594c3ecc451e8 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/application_package.dart'; @@ -363,6 +364,8 @@ IOSDevice setUpIOSDevice({ '1234', name: 'iPhone 1', logger: logger, + processUtils: ProcessUtils(processManager: processManager, logger: logger), + xcode: null, fileSystem: fileSystem ?? MemoryFileSystem.test(), sdkVersion: '13.3', cpuArch: .arm64, diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart index cc3ddc77ef8ad..d974898b62918 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/core_devices.dart'; @@ -100,6 +101,8 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) { 'test', fileSystem: fileSystem, logger: logger, + processUtils: ProcessUtils(processManager: processManager, logger: logger), + xcode: null, iosDeploy: IOSDeploy( platform: platform, logger: logger, diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index 925c291684e6b..aec6dd0ef5a0b 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -1481,6 +1481,11 @@ IOSDevice setUpIOSDevice({ sdkVersion: sdkVersion, fileSystem: fileSystem ?? MemoryFileSystem.test(), platform: macPlatform, + processUtils: ProcessUtils( + processManager: processManager ?? FakeProcessManager.any(), + logger: logger, + ), + xcode: null, iProxy: IProxy.test(logger: logger, processManager: processManager ?? FakeProcessManager.any()), logger: logger, iosDeploy: IOSDeploy( diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index e1df4bc55456f..f9652a266747c 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -908,6 +908,72 @@ void main() { }, ); + testUsingContext( + 'calls prepareDeviceSupport when Xcode version is >= 16.3', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + processManager.addCommand( + const FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=123', + ], + ), + ); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + xcode: FakeXcode(currentVersion: Version(26, 0, 0)), + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final DeviceLogReader deviceLogReader = IOSDeviceLogReader.test( + iMobileDevice: FakeIMobileDevice(), + xcode: FakeXcode(currentVersion: Version(26, 0, 0)), + isCoreDevice: true, + ); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer(const Duration(milliseconds: 50), () { + fakeLauncher.coreDeviceLogForwarder.addLog('Foo'); + fakeLauncher.coreDeviceLogForwarder.addLog( + 'The Dart VM service is listening on http://127.0.0.1:456', + ); + }); + + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(processManager, hasNoRemainingExpectations); + }, + overrides: { + Xcode: () => FakeXcode(currentVersion: Version(26, 0, 0)), + Analytics: () => FakeAnalytics(), + }, + ); + testUsingContext('uses Xcode if LLDB fails', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.empty(); @@ -1859,6 +1925,72 @@ void main() { }, ); + testUsingContext( + 'calls prepareDeviceSupport when iosProfileDebugger is true and Xcode version is >= 16.3', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + processManager.addCommand( + const FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-prepareDeviceSupport', + '-destination', + 'id=123', + ], + ), + ); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + xcode: FakeXcode(currentVersion: Version(26, 0, 0)), + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final DeviceLogReader deviceLogReader = IOSDeviceLogReader.test( + iMobileDevice: FakeIMobileDevice(), + xcode: FakeXcode(currentVersion: Version(26, 0, 0)), + isCoreDevice: true, + ); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer(const Duration(milliseconds: 50), () { + fakeLauncher.coreDeviceLogForwarder.addLog('Foo'); + fakeLauncher.coreDeviceLogForwarder.addLog( + 'The Dart VM service is listening on http://127.0.0.1:456', + ); + }); + + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile, iosProfileDebugger: true), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(processManager, hasNoRemainingExpectations); + }, + overrides: { + Xcode: () => FakeXcode(currentVersion: Version(26, 0, 0)), + Analytics: () => FakeAnalytics(), + }, + ); + testUsingContext( 'uses Xcode if less than Xcode 26', () async { @@ -1945,6 +2077,7 @@ IOSDevice setUpIOSDevice({ Analytics? analytics, FakeXcodeDebug? xcodeDebug, FakePlatform? platform, + Xcode? xcode, }) { final artifacts = Artifacts.test(); final FakePlatform macPlatform = @@ -1962,6 +2095,11 @@ IOSDevice setUpIOSDevice({ sdkVersion: sdkVersion, fileSystem: fileSystem ?? MemoryFileSystem.test(), platform: macPlatform, + processUtils: ProcessUtils( + processManager: processManager ?? FakeProcessManager.any(), + logger: logger, + ), + xcode: xcode, iProxy: IProxy.test(logger: logger, processManager: processManager ?? FakeProcessManager.any()), logger: logger, iosDeploy: From 2dc2727acf77cc484712690de3486baf0fb87b81 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:12:24 -0500 Subject: [PATCH 045/330] Fix merge conflict from https://github.com/flutter/flutter/pull/190369 (#190544) Fixes https://github.com/flutter/flutter/actions/runs/30926424776/job/92049821535 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../test/general.shard/ios/devices_test.dart | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 1498fd0fa285f..b04f61d9a79a7 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -137,14 +137,20 @@ void main() { ), isTrue, ); - expect(device.shouldAttachLLDBDebugger(DebuggingOptions.enabled(BuildInfo.profile)), isFalse); + expect( + device.shouldAttachLLDBDebugger(DebuggingOptions.enabled(BuildInfo.profile)), + isFalse, + ); expect( device.shouldAttachLLDBDebugger( DebuggingOptions.enabled(BuildInfo.profile, iosProfileDebugger: false), ), isFalse, ); - expect(device.shouldAttachLLDBDebugger(DebuggingOptions.enabled(BuildInfo.release)), isFalse); + expect( + device.shouldAttachLLDBDebugger(DebuggingOptions.enabled(BuildInfo.release)), + isFalse, + ); }); }); @@ -696,6 +702,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: true, + processUtils: processUtils, + xcode: null, ); expect(device.supportsScreenshot, isFalse); @@ -724,6 +732,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: true, + processUtils: processUtils, + xcode: null, ); final fakeXcode = globals.xcode! as FakeXcode; @@ -757,6 +767,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: true, + processUtils: processUtils, + xcode: null, ); fakeCoreDeviceControl.takeScreenshotSuccess = true; @@ -789,6 +801,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: true, + processUtils: processUtils, + xcode: null, ); fakeCoreDeviceControl.takeScreenshotException = Exception( @@ -826,6 +840,8 @@ void main() { isPaired: true, devModeEnabled: true, isCoreDevice: true, + processUtils: processUtils, + xcode: null, ); expect( From 6e7cc563f655118095097a211000dcfb5bdaa372 Mon Sep 17 00:00:00 2001 From: Ahmet Can Urhan Date: Tue, 4 Aug 2026 19:52:16 +0300 Subject: [PATCH 046/330] Handle unexpected exceptions during Azure metadata detection (#189457) ## Description Azure metadata detection is a best-effort environment check and should never prevent the Flutter CLI from starting. In some network environments (for example, transparent proxies or enterprise firewalls), requests to the Azure metadata endpoint (`http://169.254.169.254/metadata/instance`) may result in unexpected exceptions instead of the currently expected network-related exceptions. These exceptions currently propagate and terminate the Flutter CLI. This change catches unexpected exceptions during Azure metadata detection and treats them as "not running on Azure", which is consistent with the behavior for other metadata lookup failures. A regression test has been added to verify this behavior. ## Issues #189456 ## Pre-launch Checklist - [ X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X ] I signed the [CLA]. - [X ] I listed at least one issue that this PR fixes in the description above. - [ X] I updated/added relevant documentation (doc comments with `///`). - [ X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../lib/src/base/bot_detector.dart | 19 +++++++------------ .../general.shard/base/bot_detector_test.dart | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/bot_detector.dart b/packages/flutter_tools/lib/src/base/bot_detector.dart index 648b6e0ee7a37..08c7b59a67cf1 100644 --- a/packages/flutter_tools/lib/src/base/bot_detector.dart +++ b/packages/flutter_tools/lib/src/base/bot_detector.dart @@ -107,22 +107,17 @@ class AzureDetector { .timeout(requestTimeout); request.headers.add('Metadata', true); await request.close(); - } on SocketException { - // If there is an error on the socket, it probably means that we are not - // running on Azure. - return _isRunningOnAzure = false; } on HttpException { - // If the connection gets set up, but encounters an error condition, it - // still means we're on Azure. + // The connection was established but an HTTP error occurred. + // This still indicates we're running on Azure. return _isRunningOnAzure = true; - } on TimeoutException { - // The HttpClient connected to a host, but it did not respond in a timely - // fashion. Assume we are not on a bot. - return _isRunningOnAzure = false; - } on OSError { - // The HttpClient might be running in a WSL1 environment. + } on Object { + // Metadata detection is best-effort. Any other failure (socket errors, + // timeouts, malformed redirect URIs, WSL1 networking issues, etc.) + // should not prevent Flutter from starting. return _isRunningOnAzure = false; } + // We got a response. We're running on Azure. return _isRunningOnAzure = true; } diff --git a/packages/flutter_tools/test/general.shard/base/bot_detector_test.dart b/packages/flutter_tools/test/general.shard/base/bot_detector_test.dart index f50a9d4a14f06..ed72a013395b2 100644 --- a/packages/flutter_tools/test/general.shard/base/bot_detector_test.dart +++ b/packages/flutter_tools/test/general.shard/base/bot_detector_test.dart @@ -182,5 +182,19 @@ void main() { expect(await azureDetector.isRunningOnAzure, isTrue); }); + testWithoutContext('isRunningOnAzure returns false when an unexpected exception is thrown', () async { + final azureDetector = AzureDetector( + httpClientFactory: () => FakeHttpClient.list([ + FakeRequest( + azureUrl, + responseError: ArgumentError( + 'No host specified in URI http:///e2gerror.php', + ), + ), + ]), + ); + expect(await azureDetector.isRunningOnAzure, isFalse); + }); }); + } From 88b41031482918e316ca513c4647cdd3c3d39208 Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Tue, 4 Aug 2026 18:57:11 +0200 Subject: [PATCH 047/330] [record_use] Migrate IconTreeShaker to `package:record_use` (#190225) Migrates `IconTreeShaker` from `const_finder` to `package:record_use`. Using the Dart compiler feature record use gives us the ability to record all constants reliably: ```dart // supported before and after this PR: const IconData(0xe84e, fontFamily: 'MaterialIcons') // supported after this PR: IconData(0xe84e, fontFamily: 'MaterialIcons') ``` Should fix: * https://github.com/timmaffett/material_symbols_icons/issues/46 Interesting implementation details: * Web has wasm with js fallback. We're taking _one_ of the recorded uses of either compiler. If one compiler can prove code is dead it should not be reachable in the other. * Retains `Artifact.constFinder` for g3 compatibility. (We can clean this up after we migrate g3.) Interesting roll details: * This adds a dependency on package:record_use in Flutter in the g3 build, but the package rolls with the Dart SDK into g3. This means we cannot do any breaking changes to the package anymore. (We marked the package 1.0.) Testing: * Updated the relevant unit test. * Updated one integration test to see the error messages still surface if tree shaking fails. --- .../record_use_test_app/README.md | 7 +- .../record_use_test_app/lib/main.dart | 18 + .../record_use_test_app/pubspec.yaml | 2 +- packages/flutter/lib/foundation.dart | 2 +- .../flutter/lib/src/widgets/icon_data.dart | 1 + packages/flutter_tools/lib/src/artifacts.dart | 2 + .../lib/src/build_system/targets/common.dart | 27 +- .../targets/icon_tree_shaker.dart | 253 +++++--- packages/flutter_tools/pubspec.yaml | 3 +- .../build_system/targets/common_test.dart | 21 + .../targets/icon_tree_shaker_test.dart | 568 ++++++++++++++---- .../record_use_flutter_build_test.dart | 18 + 12 files changed, 689 insertions(+), 233 deletions(-) diff --git a/dev/integration_tests/record_use_test_app/README.md b/dev/integration_tests/record_use_test_app/README.md index d2d98f8dfa5aa..567bf4de4cceb 100644 --- a/dev/integration_tests/record_use_test_app/README.md +++ b/dev/integration_tests/record_use_test_app/README.md @@ -1,6 +1,6 @@ # Record Use Test App -An integration test app for testing the "Record Use" asset tree-shaking feature. +An integration test app for testing the "Record Use" asset tree-shaking feature and icon tree shaking. ## Overview @@ -17,15 +17,16 @@ The feature relies on: 3. This app (`record_use_test_app`) calls `translate('hello')` and `translate('friend')`. 4. During the build, the `link.dart` hook in the package receives a recording of these calls. 5. The hook filters `translations.json` to only include the entries for "hello" and "friend", tree-shaking the unused ones. +6. The app instantiates `IconData(0x1234)`, which `IconTreeShaker` records during build. ## Testing The integration test for this feature is located at: -`packages/flutter_tools/test/integration.shard/isolated/record_use_test.dart` +`packages/flutter_tools/test/integration.shard/isolated/record_use_flutter_build_test.dart` To run the test: ```bash -bin/cache/dart-sdk/bin/dart packages/flutter_tools/test/integration.shard/isolated/record_use_test.dart +bin/flutter test packages/flutter_tools/test/integration.shard/isolated/record_use_flutter_build_test.dart ``` Note: The test requires `--enable-record-use` and `--enable-dart-data-assets` to be enabled in the Flutter config. diff --git a/dev/integration_tests/record_use_test_app/lib/main.dart b/dev/integration_tests/record_use_test_app/lib/main.dart index 81aa256e6b8ca..ebfce0d835261 100644 --- a/dev/integration_tests/record_use_test_app/lib/main.dart +++ b/dev/integration_tests/record_use_test_app/lib/main.dart @@ -20,9 +20,18 @@ void main() async { print('HELLO: $hello'); print('FRIEND: $friend'); print('COUNT: $count'); + // Intentionally missing fontFamily to test that icon tree shaking detects + // constant IconData instances with null fontFamily. + const dummyIcon = IconData(0x1234); + print('ICON: ${dummyIcon.codePoint}'); runApp(MyApp(hello: hello, friend: friend, count: count)); } +// In dart2js, 1 and 1.0 are identical numbers, so `isWasm` evaluates to false. +// In dart2wasm, integer and double representations are distinct, so `isWasm` +// evaluates to true. +const bool isWasm = !identical(1, 1.0); + class MyApp extends StatelessWidget { const MyApp({ super.key, @@ -50,6 +59,15 @@ class MyApp extends StatelessWidget { const SizedBox(height: 20), Text('Loaded translations count: $count', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 20), + // Tests that dual-target web builds (Wasm with JS fallback) retain + // the union of icons used by both compilers in the shared font asset. + // Wasm compiles with Icons.fastfood and JS compiles with Icons.favorite. + const Icon( + isWasm ? Icons.fastfood : Icons.favorite, + color: Colors.blueGrey, + size: 30.0, + ), ], ), ), diff --git a/dev/integration_tests/record_use_test_app/pubspec.yaml b/dev/integration_tests/record_use_test_app/pubspec.yaml index 73384bdf77302..6a0f0f79a8621 100644 --- a/dev/integration_tests/record_use_test_app/pubspec.yaml +++ b/dev/integration_tests/record_use_test_app/pubspec.yaml @@ -1,5 +1,5 @@ name: record_use_test_app -description: A new Flutter application project. +description: An integration test app for testing Record Use in both link hooks and icon tree shaking. version: 0.0.1 publish_to: none diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index b366a781b81d3..f3821e00d81c8 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -11,11 +11,11 @@ library foundation; export 'package:meta/meta.dart' show + RecordUse, awaitNotRequired, factory, immutable, internal, - // ignore: experimental_member_use mustBeConst, mustCallSuper, nonVirtual, diff --git a/packages/flutter/lib/src/widgets/icon_data.dart b/packages/flutter/lib/src/widgets/icon_data.dart index a38934e7883f1..eaa4e96ecb41e 100644 --- a/packages/flutter/lib/src/widgets/icon_data.dart +++ b/packages/flutter/lib/src/widgets/icon_data.dart @@ -19,6 +19,7 @@ import 'package:flutter/foundation.dart'; /// In release builds, the Flutter tool will tree shake out of bundled fonts /// the code points (or instances of [IconData]) which are not referenced from /// Dart app code. See the [staticIconProvider] annotation for more details. +@RecordUse() @immutable final class IconData { /// Creates icon data. diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index 96f0b12a99f73..41ca8f2d8548b 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -82,6 +82,8 @@ enum Artifact { /// Tools related to subsetting or icon font files. fontSubset('font-subset', isExecutable: true), + + /// Still used in g3 so cannot be deleted yet. constFinder('const_finder.dart.snapshot'), /// The location of file generators. diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index b66db673bcc2e..6356a52b58c43 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -13,7 +13,6 @@ import '../../compile.dart'; import '../../dart/package_map.dart'; import '../../darwin/darwin.dart'; import '../../devfs.dart'; -import '../../features.dart'; import '../../globals.dart' as globals show xcode; import '../../isolated/native_assets/dart_hook_result.dart'; import '../../project.dart'; @@ -155,10 +154,9 @@ class KernelSnapshot extends Target { ]; @override - List get outputs => [ - const Source.pattern('{BUILD_DIR}/${KernelSnapshot.dillName}'), - if (featureFlags.isRecordUseEnabled) - const Source.pattern('{BUILD_DIR}/${KernelSnapshot.recordedUsesFileName}'), + List get outputs => const [ + Source.pattern('{BUILD_DIR}/${KernelSnapshot.dillName}'), + Source.pattern('{BUILD_DIR}/${KernelSnapshot.recordedUsesFileName}'), ]; static const depfile = 'kernel_snapshot_program.d'; @@ -216,14 +214,14 @@ class KernelSnapshot extends Target { final File recordedUsesFile = environment.buildDir.childFile( KernelSnapshot.recordedUsesFileName, ); - if (featureFlags.isRecordUseEnabled) { - if (buildMode.isPrecompiled) { - extraFrontEndOptions.add('--recorded-uses=${recordedUsesFile.path}'); - } else { - // Produce an empty file to satisfy the build system in JIT mode. - // Always overwrite to avoid stale data. - recordedUsesFile.writeAsStringSync(KernelSnapshot.recordedUsesEmptyContent); - } + if (buildMode.isPrecompiled) { + // Always pass --recorded-uses in AOT mode because both recorded uses for + // link hooks and icon tree shaking depend on this file. + extraFrontEndOptions.add('--recorded-uses=${recordedUsesFile.path}'); + } else { + // Produce an empty file to satisfy the build system in JIT mode. + // Always overwrite to avoid stale data. + recordedUsesFile.writeAsStringSync(KernelSnapshot.recordedUsesEmptyContent); } final List? fileSystemRoots = environment.defines[kFileSystemRoots]?.split(','); final String? fileSystemScheme = environment.defines[kFileSystemScheme]; @@ -314,6 +312,9 @@ class KernelSnapshot extends Target { if (output == null || output.errorCount != 0) { throw Exception(); } + if (buildMode.isPrecompiled && !recordedUsesFile.existsSync()) { + throw Exception('${KernelSnapshot.recordedUsesFileName} was not generated by the compiler.'); + } } Future _addFlavorToDartDefines( diff --git a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart index aeefa12a0c54e..3f7356a40aee5 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:mime/mime.dart' as mime; import 'package:process/process.dart'; +import 'package:record_use/record_use.dart'; import '../../artifacts.dart'; import '../../base/common.dart'; @@ -67,14 +68,18 @@ class IconTreeShaker { 'application/x-font-ttf', // based on running locally. }; - /// The [Source] inputs that targets using this should depend on. + /// The [Source] inputs that native targets using this should depend on. + /// + /// Web targets (such as `WebReleaseBundle`) do not use this field because + /// their recorded uses inputs are dynamically provided via + /// `Dart2WebTarget.buildPatternStems`. /// /// See [Target.inputs]. static const inputs = [ Source.pattern( '{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart', ), - Source.artifact(Artifact.constFinder), + Source.pattern('{BUILD_DIR}/recorded_uses.json'), Source.artifact(Artifact.fontSubset), ]; @@ -101,16 +106,34 @@ class IconTreeShaker { return; } - final File appDill = environment.buildDir.childFile('app.dill'); - if (!appDill.existsSync()) { + final candidates = [ + 'recorded_uses.json', + 'recorded_uses_js.json', + 'recorded_uses_wasm.json', + ]; + final recordedUsesFiles = []; + for (final candidate in candidates) { + final File file = environment.buildDir.childFile(candidate); + if (file.existsSync() && file.lengthSync() > 0) { + recordedUsesFiles.add(file); + } + } + if (recordedUsesFiles.isEmpty) { + final File defaultFile = environment.buildDir.childFile('recorded_uses.json'); throw IconTreeShakerException._( - 'Expected to find kernel file at ${appDill.path}, but no file found.', + 'Expected to find recorded uses file at ${defaultFile.path}, but no file found.', ); } - final File constFinder = _fs.file(_artifacts.getArtifactPath(Artifact.constFinder)); - final File dart = _fs.file(_artifacts.getArtifactPath(Artifact.engineDartBinary)); - final Map> iconData = await _findConstants(dart, constFinder, appDill); + Recordings? combinedRecordings; + for (final file in recordedUsesFiles) { + final Recordings recordings = await _readRecordings(file); + combinedRecordings = combinedRecordings == null + ? recordings + : combinedRecordings.merge(recordings); + } + + final Map> iconData = _parseRecordings(combinedRecordings!); final Set familyKeys = iconData.keys.toSet(); final Map fonts = await _parseFontJson( @@ -131,11 +154,11 @@ class IconTreeShaker { final result = {}; const kSpacePoint = 32; - for (final MapEntry entry in fonts.entries) { - final List? codePoints = iconData[entry.key]; + for (final MapEntry(:key, :value) in fonts.entries) { + final List? codePoints = iconData[key]; if (codePoints == null) { throw IconTreeShakerException._( - 'Expected to font code points for ${entry.key}, but none were found.', + 'Expected to font code points for $key, but none were found.', ); } @@ -143,9 +166,9 @@ class IconTreeShaker { final optionalCodePoints = _targetPlatform == TargetPlatform.web_javascript ? [kSpacePoint] : []; - result[entry.value] = _IconTreeShakerData( - family: entry.key, - relativePath: entry.value, + result[value] = _IconTreeShakerData( + family: key, + relativePath: value, codePoints: codePoints, optionalCodePoints: optionalCodePoints, ); @@ -169,7 +192,7 @@ class IconTreeShaker { if (!enabled) { return false; } - if (input.lengthSync() < 12) { + if (!input.existsSync() || input.lengthSync() < 12) { return false; } final String? mimeType = mime.lookupMimeType( @@ -287,106 +310,121 @@ class IconTreeShaker { return result; } - Future>> _findConstants(File dart, File constFinder, File appDill) async { - final cmd = [ - dart.path, - constFinder.path, - '--kernel-file', - appDill.path, - '--class-library-uri', - 'package:flutter/src/widgets/icon_data.dart', - '--class-name', - 'IconData', - '--annotation-class-name', - '_StaticIconProvider', - '--annotation-class-library-uri', - 'package:flutter/src/widgets/icon_data.dart', - ]; - _logger.printTrace('Running command: ${cmd.join(' ')}'); - final ProcessResult constFinderProcessResult = await _processManager.run(cmd); - - if (constFinderProcessResult.exitCode != 0) { - throw IconTreeShakerException._('ConstFinder failure: ${constFinderProcessResult.stderr}'); + Future _readRecordings(File recordedUsesFile) async { + final String content = await recordedUsesFile.readAsString(); + final Object? data; + try { + data = json.decode(content); + } on FormatException catch (e) { + throw IconTreeShakerException._('Failed to parse recorded uses file: $e'); } - final Object? constFinderMap = json.decode(constFinderProcessResult.stdout as String); - if (constFinderMap is! Map) { + if (data is! Map) { throw IconTreeShakerException._( - 'Invalid ConstFinder output: expected a top level JSON object, ' - 'got $constFinderMap.', + 'Invalid recorded uses file: expected a top level JSON object.', ); } - final constFinderResult = _ConstFinderResult(constFinderMap); - if (constFinderResult.hasNonConstantLocations) { + + try { + return Recordings.fromJson(data); + } on Exception catch (e) { + throw IconTreeShakerException._('Failed to parse recorded uses file: $e'); + } + } + + Map> _parseRecordings(Recordings recordings) { + final result = >{}; + var hasNonConstant = false; + + for (final MapEntry(:key, :value) in recordings.instances.entries) { + if (_isIconDataDefinition(key)) { + for (final reference in value) { + final _IconDataConstants? constants = _extractIconDataConstants(reference); + if (constants == null) { + hasNonConstant = true; + continue; + } + + if (constants.codePoint is IntConstant) { + final int codePoint = (constants.codePoint! as IntConstant).value; + if (constants.fontFamily is! StringConstant) { + _logger.printTrace( + 'Expected to find fontFamily for constant IconData with codepoint: ' + '$codePoint, but found fontFamily: null. This usually means ' + 'you are relying on the system font. Alternatively, font families in ' + 'an IconData class can be provided in the assets section of your ' + 'pubspec.yaml, or you are missing "uses-material-design: true".', + ); + continue; + } + final String fontFamily = (constants.fontFamily! as StringConstant).value; + final String? fontPackage = constants.fontPackage is StringConstant + ? (constants.fontPackage! as StringConstant).value + : null; + final family = fontPackage == null ? fontFamily : 'packages/$fontPackage/$fontFamily'; + result[family] ??= []; + result[family]!.add(codePoint); + } + } + } + } + if (hasNonConstant) { _logger.printError( 'This application cannot tree shake icons fonts. ' - 'It has non-constant instances of IconData at the ' - 'following locations:', + 'It has non-constant instances of IconData.', emphasis: true, ); - for (final Map location in constFinderResult.nonConstantLocations) { - _logger.printError( - '- ${location['file']}:${location['line']}:${location['column']}', - indent: 2, - hangingIndent: 4, - ); - } throwToolExit( 'Avoid non-constant invocations of IconData or try to ' 'build again with --no-tree-shake-icons.', ); } - return _parseConstFinderResult(constFinderResult); + return result; } - Map> _parseConstFinderResult(_ConstFinderResult constants) { - final result = >{}; - for (final Map iconDataMap in constants.constantInstances) { - final Object? package = iconDataMap['fontPackage']; - final Object? fontFamily = iconDataMap['fontFamily']; - final Object? codePoint = iconDataMap['codePoint']; - if ((package ?? '') is! String || (fontFamily ?? '') is! String || codePoint is! num) { - throw IconTreeShakerException._( - 'Invalid ConstFinder result. Expected "fontPackage" to be a String, ' - '"fontFamily" to be a String, and "codePoint" to be an int, ' - 'got: $iconDataMap.', - ); - } - if (fontFamily == null) { - _logger.printTrace( - 'Expected to find fontFamily for constant IconData with codepoint: ' - '$codePoint, but found fontFamily: $fontFamily. This usually means ' - 'you are relying on the system font. Alternatively, font families in ' - 'an IconData class can be provided in the assets section of your ' - 'pubspec.yaml, or you are missing "uses-material-design: true".', + static const String _iconDataClassName = 'IconData'; + static const String _codePointFieldName = 'codePoint'; + static const String _fontFamilyFieldName = 'fontFamily'; + static const String _fontPackageFieldName = 'fontPackage'; + + bool _isIconDataDefinition(DefinitionWithInstances definition) { + if (definition is Class) { + return definition.name == _iconDataClassName && + definition.library.uri == 'package:flutter/src/widgets/icon_data.dart'; + } + final str = definition.toString(); + return str == 'package:flutter/src/widgets/icon_data.dart::IconData' || + str.startsWith('package:flutter/src/widgets/icon_data.dart::IconData.'); + } + + _IconDataConstants? _extractIconDataConstants(InstanceReference reference) { + if (reference case InstanceConstantReference( + instanceConstant: InstanceConstant(:final fields), + )) { + return ( + codePoint: fields[_codePointFieldName], + fontFamily: fields[_fontFamilyFieldName], + fontPackage: fields[_fontPackageFieldName], + ); + } else if (reference case InstanceCreationReference( + positionalArguments: final positional, + namedArguments: final named, + )) { + final bool hasNonConstantArg = + positional.any((MaybeConstant arg) => arg is! Constant) || + named.values.any((MaybeConstant arg) => arg is! Constant); + if (!hasNonConstantArg) { + return ( + codePoint: positional.isNotEmpty ? positional[0] as Constant : null, + fontFamily: named[_fontFamilyFieldName] as Constant?, + fontPackage: named[_fontPackageFieldName] as Constant?, ); - continue; } - final family = fontFamily as String; - final key = package == null ? family : 'packages/$package/$family'; - result[key] ??= []; - result[key]!.add(codePoint.round()); } - return result; + return null; } } -class _ConstFinderResult { - _ConstFinderResult(this.result); - - final Map result; - - late final List> constantInstances = _getList( - result['constantInstances'], - 'Invalid ConstFinder output: Expected "constInstances" to be a list of objects.', - ); - - late final List> nonConstantLocations = _getList( - result['nonConstantLocations'], - 'Invalid ConstFinder output: Expected "nonConstLocations" to be a list of objects', - ); - - bool get hasNonConstantLocations => nonConstantLocations.isNotEmpty; -} +typedef _IconDataConstants = ({Constant? codePoint, Constant? fontFamily, Constant? fontPackage}); /// The font family name, relative path to font file, and list of code points /// the application is using. @@ -427,3 +465,30 @@ class IconTreeShakerException implements Exception { 'To disable icon tree shaking, pass --no-tree-shake-icons to the requested ' 'flutter build command'; } + +extension on Recordings { + /// Returns a new [Recordings] containing all usages from both `this` and + /// [other]. + /// + /// If a definition is present in both recordings, its usages from both + /// are combined in the returned [Recordings]. + Recordings merge(Recordings other) { + final newCalls = >{}; + for (final MapEntry(:key, :value) in calls.entries) { + newCalls[key] = [...value]; + } + for (final MapEntry(:key, :value) in other.calls.entries) { + newCalls.putIfAbsent(key, () => []).addAll(value); + } + + final newInstances = >{}; + for (final MapEntry(:key, :value) in instances.entries) { + newInstances[key] = [...value]; + } + for (final MapEntry(:key, :value) in other.instances.entries) { + newInstances.putIfAbsent(key, () => []).addAll(value); + } + + return Recordings(calls: newCalls, instances: newInstances); + } +} diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 40a10800ebc9e..e68a49dcbd237 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: hooks: 2.1.0 code_assets: 1.2.1 data_assets: 0.20.0 + record_use: 1.0.0 # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so when upgrading @@ -128,4 +129,4 @@ dartdoc: nodoc: true -# PUBSPEC CHECKSUM: qi12j +# PUBSPEC CHECKSUM: 81k1ci diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index 64f77221e483f..00d27a4573653 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -4,6 +4,7 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/config.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; @@ -113,6 +114,7 @@ void main() { '--depfile', '$build/kernel_snapshot_program.d', '--verbosity=error', + '--recorded-uses=$build/recorded_uses.json', 'file:///lib/main.dart', ], exitCode: 1, @@ -155,9 +157,13 @@ void main() { '--depfile', '$build/kernel_snapshot_program.d', '--verbosity=error', + '--recorded-uses=$build/recorded_uses.json', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + onRun: (_) { + fileSystem.file('$build/recorded_uses.json').createSync(); + }, ), ]); @@ -200,9 +206,13 @@ void main() { '--depfile', '$build/kernel_snapshot_program.d', '--verbosity=error', + '--recorded-uses=$build/recorded_uses.json', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + onRun: (_) { + fileSystem.file('$build/recorded_uses.json').createSync(); + }, ), ]); @@ -245,9 +255,13 @@ void main() { '--depfile', '$build/kernel_snapshot_program.d', '--verbosity=error', + '--recorded-uses=$build/recorded_uses.json', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + onRun: (_) { + fileSystem.file('$build/recorded_uses.json').createSync(); + }, ), ]); @@ -293,9 +307,13 @@ void main() { '--verbosity=error', 'foo', 'bar', + '--recorded-uses=$build/recorded_uses.json', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + onRun: (_) { + fileSystem.file('$build/recorded_uses.json').createSync(); + }, ), ]); @@ -497,6 +515,7 @@ void main() { expect(processManager, hasNoRemainingExpectations); }, overrides: { + Config: () => Config.test(directory: fileSystem.currentDirectory), XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(schemes: ['Runner', 'chocolate']), }, @@ -554,6 +573,7 @@ void main() { expect(processManager, hasNoRemainingExpectations); }, overrides: { + Config: () => Config.test(directory: fileSystem.currentDirectory), XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(schemes: ['Runner', 'chocolate']), }, @@ -631,6 +651,7 @@ void main() { fileSystem: fileSystem, logger: logger, ); + testEnvironment.buildDir.createSync(recursive: true); final String build = testEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart index 72aeb07d84ca6..6e6615596b494 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -10,6 +12,7 @@ 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/icon_tree_shaker.dart'; import 'package:flutter_tools/src/devfs.dart'; +import 'package:record_use/record_use.dart'; import '../../../src/common.dart'; import '../../../src/fake_process_manager.dart'; @@ -30,40 +33,19 @@ void main() { late Artifacts artifacts; late DevFSStringContent fontManifestContent; - late String dartPath; - late String constFinderPath; late String fontSubsetPath; late List fontSubsetArgs; - List getConstFinderArgs(String appDillPath) => [ - dartPath, - constFinderPath, - '--kernel-file', - appDillPath, - '--class-library-uri', - 'package:flutter/src/widgets/icon_data.dart', - '--class-name', - 'IconData', - '--annotation-class-name', - '_StaticIconProvider', - '--annotation-class-library-uri', - 'package:flutter/src/widgets/icon_data.dart', - ]; - - void addConstFinderInvocation( + void writeRecordedUsesFile( String appDillPath, { - int exitCode = 0, - String stdout = '', - String stderr = '', + required String content, + String fileName = 'recorded_uses.json', }) { - processManager.addCommand( - FakeCommand( - command: getConstFinderArgs(appDillPath), - exitCode: exitCode, - stdout: stdout, - stderr: stderr, - ), - ); + final File appDillFile = fileSystem.file(appDillPath); + final Directory buildDir = appDillFile.parent; + buildDir.childFile(fileName) + ..createSync(recursive: true) + ..writeAsStringSync(content); } void resetFontSubsetInvocation({ @@ -90,14 +72,10 @@ void main() { artifacts = Artifacts.test(); fileSystem = MemoryFileSystem.test(); logger = BufferLogger.test(); - dartPath = artifacts.getArtifactPath(Artifact.engineDartBinary); - constFinderPath = artifacts.getArtifactPath(Artifact.constFinder); fontSubsetPath = artifacts.getArtifactPath(Artifact.fontSubset); fontSubsetArgs = [fontSubsetPath, outputPath, inputPath]; - fileSystem.file(constFinderPath).createSync(recursive: true); - fileSystem.file(dartPath).createSync(recursive: true); fileSystem.file(fontSubsetPath).createSync(recursive: true); fileSystem.file(inputPath) ..createSync(recursive: true) @@ -131,23 +109,17 @@ void main() { targetPlatform: TargetPlatform.android, ); + expect(iconTreeShaker.enabled, false); expect( logger.errorText, - 'Font subsetting is not supported in debug mode. The --tree-shake-icons' - ' flag will be ignored.\n', - ); - expect(iconTreeShaker.enabled, false); - - final bool subsets = await iconTreeShaker.subsetFont( - input: fileSystem.file(inputPath), - outputPath: outputPath, - relativePath: relativePath, + contains( + 'Font subsetting is not supported in debug mode. The --tree-shake-icons flag will be ignored.', + ), ); - expect(subsets, false); expect(processManager, hasNoRemainingExpectations); }); - testWithoutContext('Does not get enabled without font manifest', () { + testWithoutContext('Does not get enabled without font manifest', () async { final Environment environment = createEnvironment({ kIconTreeShakerFlag: 'true', kBuildMode: 'release', @@ -163,12 +135,11 @@ void main() { targetPlatform: TargetPlatform.android, ); - expect(logger.errorText, isEmpty); expect(iconTreeShaker.enabled, false); expect(processManager, hasNoRemainingExpectations); }); - testWithoutContext('Gets enabled', () { + testWithoutContext('Gets enabled', () async { final Environment environment = createEnvironment({ kIconTreeShakerFlag: 'true', kBuildMode: 'release', @@ -184,12 +155,11 @@ void main() { targetPlatform: TargetPlatform.android, ); - expect(logger.errorText, isEmpty); expect(iconTreeShaker.enabled, true); expect(processManager, hasNoRemainingExpectations); }); - test('No app.dill throws exception', () async { + testWithoutContext('No recorded uses file throws exception', () async { final Environment environment = createEnvironment({ kIconTreeShakerFlag: 'true', kBuildMode: 'release', @@ -205,12 +175,11 @@ void main() { targetPlatform: TargetPlatform.android, ); + final File input = fileSystem.file(inputPath)..createSync(recursive: true); + input.writeAsBytesSync(_kTtfHeaderBytes); + expect( - () async => iconTreeShaker.subsetFont( - input: fileSystem.file(inputPath), - outputPath: outputPath, - relativePath: relativePath, - ), + iconTreeShaker.subsetFont(input: input, outputPath: outputPath, relativePath: relativePath), throwsA(isA()), ); expect(processManager, hasNoRemainingExpectations); @@ -233,7 +202,7 @@ void main() { targetPlatform: TargetPlatform.android, ); final stdinSink = CompleterIOSink(); - addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); resetFontSubsetInvocation(stdinSink: stdinSink); // Font starts out 2500 bytes long final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List.filled(2500, 0)); @@ -284,7 +253,7 @@ void main() { ); final stdinSink = CompleterIOSink(); - addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); resetFontSubsetInvocation(stdinSink: stdinSink); final File notAFont = fileSystem.file('input/foo/bar.txt') @@ -316,7 +285,7 @@ void main() { ); final stdinSink = CompleterIOSink(); - addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); resetFontSubsetInvocation(stdinSink: stdinSink); final File notAFont = fileSystem.file(inputPath)..writeAsBytesSync([0, 1, 2]); @@ -350,7 +319,7 @@ void main() { targetPlatform: platform, ); - addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid); + writeRecordedUsesFile(appDill.path, content: recordedUsesWithInvalidResult); await expectLater( () => iconTreeShaker.subsetFont( @@ -385,11 +354,7 @@ void main() { targetPlatform: TargetPlatform.android_arm64, ); - addConstFinderInvocation( - appDill.path, - // Does not contain space char - stdout: validConstFinderResult, - ); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); final stdinSink = CompleterIOSink(); resetFontSubsetInvocation(stdinSink: stdinSink); expect(processManager.hasRemainingExpectations, isTrue); @@ -428,11 +393,7 @@ void main() { targetPlatform: TargetPlatform.web_javascript, ); - addConstFinderInvocation( - appDill.path, - // Does not contain space char - stdout: validConstFinderResult, - ); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); final stdinSink = CompleterIOSink(); resetFontSubsetInvocation(stdinSink: stdinSink); expect(processManager.hasRemainingExpectations, isTrue); @@ -473,7 +434,7 @@ void main() { ); final stdinSink = CompleterIOSink(); - addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink); await expectLater( @@ -505,7 +466,7 @@ void main() { ); final stdinSink = CompleterIOSink(throwOnAdd: true); - addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink); await expectLater( @@ -538,7 +499,7 @@ void main() { targetPlatform: TargetPlatform.android, ); - addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); + writeRecordedUsesFile(appDill.path, content: validRecordedUsesResult); await expectLater( () => iconTreeShaker.subsetFont( @@ -571,7 +532,7 @@ void main() { targetPlatform: TargetPlatform.android, ); - addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult); + writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult); // Does not throw await iconTreeShaker.subsetFont( input: fileSystem.file(inputPath), @@ -614,7 +575,7 @@ void main() { targetPlatform: TargetPlatform.android, ); - addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult); + writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult); // Does not throw await iconTreeShaker.subsetFont( input: fileSystem.file(inputPath), @@ -636,14 +597,14 @@ void main() { }, ); - testWithoutContext('ConstFinder non-zero exit', () async { + testWithoutContext('Invalid recorded uses JSON', () async { final Environment environment = createEnvironment({ kIconTreeShakerFlag: 'true', kBuildMode: 'release', }); final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); - fontManifestContent = DevFSStringContent(invalidFontManifestJson); + fontManifestContent = DevFSStringContent(validFontManifestJson); final iconTreeShaker = IconTreeShaker( environment, @@ -655,7 +616,7 @@ void main() { targetPlatform: TargetPlatform.android, ); - addConstFinderInvocation(appDill.path, exitCode: -1); + writeRecordedUsesFile(appDill.path, content: 'invalid json content'); await expectLater( () async => iconTreeShaker.subsetFont( @@ -667,55 +628,422 @@ void main() { ); expect(processManager, hasNoRemainingExpectations); }); -} -const validConstFinderResult = ''' -{ - "constantInstances": [ - { - "codePoint": 59470, - "fontFamily": "MaterialIcons", - "fontPackage": null, - "matchTextDirection": false - } - ], - "nonConstantLocations": [] -} -'''; + testWithoutContext( + 'Can subset a font using InstanceCreationReference with constant arguments', + () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); -const emptyConstFinderResult = ''' -{ - "constantInstances": [ - { - "codePoint": 59470, - "fontFamily": null, - "fontPackage": null, - "matchTextDirection": false - } - ], - "nonConstantLocations": [] -} -'''; + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.android, + ); -const constFinderResultWithInvalid = ''' -{ - "constantInstances": [ - { - "codePoint": 59470, - "fontFamily": "MaterialIcons", - "fontPackage": null, - "matchTextDirection": false - } - ], - "nonConstantLocations": [ - { - "file": "file:///Path/to/hello_world/lib/file.dart", - "line": 19, - "column": 11 - } - ] + writeRecordedUsesFile(appDill.path, content: validRecordedUsesCreationResult); + + final stdinSink = CompleterIOSink(); + resetFontSubsetInvocation(stdinSink: stdinSink); + + final File inputFont = fileSystem.file(inputPath) + ..writeAsBytesSync(List.filled(2500, 0)); + fileSystem.file(outputPath) + ..createSync(recursive: true) + ..writeAsBytesSync(List.filled(1200, 0)); + + final bool subsetted = await iconTreeShaker.subsetFont( + input: inputFont, + outputPath: outputPath, + relativePath: relativePath, + ); + + expect(subsetted, true); + expect(stdinSink.getAndClear(), '59470\n'); + expect(processManager, hasNoRemainingExpectations); + }, + ); + + testWithoutContext( + 'InstanceCreationReference with non-constant arguments fails icon tree shaking', + () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); + + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.android, + ); + + writeRecordedUsesFile(appDill.path, content: recordedUsesNonConstantCreationResult); + + await expectLater( + () async => iconTreeShaker.subsetFont( + input: fileSystem.file(inputPath), + outputPath: outputPath, + relativePath: relativePath, + ), + throwsToolExit(), + ); + expect(processManager, hasNoRemainingExpectations); + }, + ); + + testWithoutContext('ConstructorTearoffReference fails icon tree shaking', () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); + + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.android, + ); + + writeRecordedUsesFile(appDill.path, content: recordedUsesTearoffResult); + + await expectLater( + () async => iconTreeShaker.subsetFont( + input: fileSystem.file(inputPath), + outputPath: outputPath, + relativePath: relativePath, + ), + throwsToolExit(), + ); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('Combines recorded uses from both js and wasm files', () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); + + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.web_javascript, + ); + + writeRecordedUsesFile( + appDill.path, + content: validRecordedUsesResult, + fileName: 'recorded_uses_js.json', + ); + writeRecordedUsesFile( + appDill.path, + content: validRecordedUsesSecondResult, + fileName: 'recorded_uses_wasm.json', + ); + + final stdinSink = CompleterIOSink(); + resetFontSubsetInvocation(stdinSink: stdinSink); + + final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List.filled(2500, 0)); + fileSystem.file(outputPath) + ..createSync(recursive: true) + ..writeAsBytesSync(List.filled(1200, 0)); + + expect( + await iconTreeShaker.subsetFont( + input: inputFont, + outputPath: outputPath, + relativePath: relativePath, + ), + true, + ); + + final String stdin = stdinSink.getAndClear(); + expect(stdin, contains('59470')); + expect(stdin, contains('59471')); + expect(stdin, contains('optional:32')); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext( + 'Non-constant instance of non-Flutter IconData does not fail icon tree shaking', + () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); + + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.android, + ); + + const customLibrary = Library('package:my_package/custom_icon_data.dart'); + const customIconDataClass = Class('IconData', customLibrary); + const customOtherClass = Class('MyIconData', customLibrary); + + final String mixedRecordings = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const InstanceConstantReference( + instanceConstant: InstanceConstant( + definition: iconDataClass, + fields: { + 'codePoint': IntConstant(59470), + 'fontFamily': StringConstant('MaterialIcons'), + }, + ), + loadingUnit: rootLoadingUnit, + ), + ], + customIconDataClass: [ + const InstanceCreationReference( + definition: customIconDataClass, + loadingUnit: rootLoadingUnit, + positionalArguments: [NonConstant()], + namedArguments: {}, + ), + ], + customOtherClass: [ + const InstanceCreationReference( + definition: customOtherClass, + loadingUnit: rootLoadingUnit, + positionalArguments: [NonConstant()], + namedArguments: {}, + ), + ], + }, + ).toJson(), + ); + + writeRecordedUsesFile(appDill.path, content: mixedRecordings); + + final stdinSink = CompleterIOSink(); + resetFontSubsetInvocation(stdinSink: stdinSink); + + final File inputFont = fileSystem.file(inputPath) + ..writeAsBytesSync(List.filled(2500, 0)); + fileSystem.file(outputPath) + ..createSync(recursive: true) + ..writeAsBytesSync(List.filled(1200, 0)); + + final bool subsetted = await iconTreeShaker.subsetFont( + input: inputFont, + outputPath: outputPath, + relativePath: relativePath, + ); + + expect(subsetted, true); + expect(stdinSink.getAndClear(), '59470\n'); + expect(processManager, hasNoRemainingExpectations); + }, + ); + + testWithoutContext('Skips empty recorded uses files', () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); + + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.web_javascript, + ); + + writeRecordedUsesFile(appDill.path, content: '', fileName: 'recorded_uses_js.json'); + writeRecordedUsesFile( + appDill.path, + content: validRecordedUsesResult, + fileName: 'recorded_uses_wasm.json', + ); + + final stdinSink = CompleterIOSink(); + resetFontSubsetInvocation(stdinSink: stdinSink); + + final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List.filled(2500, 0)); + fileSystem.file(outputPath) + ..createSync(recursive: true) + ..writeAsBytesSync(List.filled(1200, 0)); + + expect( + await iconTreeShaker.subsetFont( + input: inputFont, + outputPath: outputPath, + relativePath: relativePath, + ), + true, + ); + + final String stdin = stdinSink.getAndClear(); + expect(stdin, contains('59470')); + expect(processManager, hasNoRemainingExpectations); + }); } -'''; + +const Library iconDataLibrary = Library('package:flutter/src/widgets/icon_data.dart'); +const Class iconDataClass = Class('IconData', iconDataLibrary); +const LoadingUnit rootLoadingUnit = LoadingUnit('root'); + +// Generated from: const IconData(0xe84e, fontFamily: 'MaterialIcons') +final String validRecordedUsesResult = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const InstanceConstantReference( + instanceConstant: InstanceConstant( + definition: iconDataClass, + fields: { + 'codePoint': IntConstant(59470), + 'fontFamily': StringConstant('MaterialIcons'), + }, + ), + loadingUnit: rootLoadingUnit, + ), + ], + }, + ).toJson(), +); + +// Generated from: const IconData(0xe84f, fontFamily: 'MaterialIcons') +final String validRecordedUsesSecondResult = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const InstanceConstantReference( + instanceConstant: InstanceConstant( + definition: iconDataClass, + fields: { + 'codePoint': IntConstant(59471), + 'fontFamily': StringConstant('MaterialIcons'), + }, + ), + loadingUnit: rootLoadingUnit, + ), + ], + }, + ).toJson(), +); + +// Generated from: IconData(0xe84e, fontFamily: 'MaterialIcons') +final String validRecordedUsesCreationResult = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const InstanceCreationReference( + definition: iconDataClass, + loadingUnit: rootLoadingUnit, + positionalArguments: [IntConstant(59470)], + namedArguments: {'fontFamily': StringConstant('MaterialIcons')}, + ), + ], + }, + ).toJson(), +); + +// Generated from: const IconData(0xe84e) +final String emptyRecordedUsesResult = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const InstanceConstantReference( + instanceConstant: InstanceConstant( + definition: iconDataClass, + fields: { + 'codePoint': IntConstant(59470), + 'fontFamily': NullConstant(), + }, + ), + loadingUnit: rootLoadingUnit, + ), + ], + }, + ).toJson(), +); + +// Generated from: IconData(codePoint) (where codePoint is a non-const variable) +final String recordedUsesWithInvalidResult = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const InstanceCreationReference( + definition: iconDataClass, + loadingUnit: rootLoadingUnit, + positionalArguments: [NonConstant()], + namedArguments: {}, + ), + ], + }, + ).toJson(), +); + +// Generated from: IconData(codePoint, fontFamily: 'MaterialIcons') (where codePoint is a non-const variable) +final String recordedUsesNonConstantCreationResult = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const InstanceCreationReference( + definition: iconDataClass, + loadingUnit: rootLoadingUnit, + positionalArguments: [NonConstant()], + namedArguments: {'fontFamily': StringConstant('MaterialIcons')}, + ), + ], + }, + ).toJson(), +); + +// Generated from: const fn = IconData.new; +final String recordedUsesTearoffResult = json.encode( + Recordings( + calls: >{}, + instances: >{ + iconDataClass: [ + const ConstructorTearoffReference(definition: iconDataClass, loadingUnit: rootLoadingUnit), + ], + }, + ).toJson(), +); const validFontManifestJson = ''' [ diff --git a/packages/flutter_tools/test/integration.shard/isolated/record_use_flutter_build_test.dart b/packages/flutter_tools/test/integration.shard/isolated/record_use_flutter_build_test.dart index d1f67105242a7..d084eb50fbc5c 100644 --- a/packages/flutter_tools/test/integration.shard/isolated/record_use_flutter_build_test.dart +++ b/packages/flutter_tools/test/integration.shard/isolated/record_use_flutter_build_test.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +@Timeout(Duration(minutes: 10)) +library; + import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -39,6 +42,21 @@ void main() { 'flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}', ); } + final String stdout = result.stdout.join('\n'); + if (target.first == hostOs) { + // Verify that IconTreeShaker detects dummyIcon with null fontFamily. + // Icon tree shaking for this icon fails with a trace message, but the overall build succeeds. + expect( + stdout, + contains('Expected to find fontFamily for constant IconData with codepoint: 4660'), + ); + } + if (target case ['web', '--wasm']) { + // Verify that both wasm (Icons.fastfood: 57946) and js (Icons.favorite: 57947) + // codepoints are retained when merging recorded uses. + expect(stdout, contains('57946')); + expect(stdout, contains('57947')); + } final Directory buildTargetDir = appRoot .childDirectory('build') .childDirectory(target.first); From 84a72e6c7d60b1e1ee633a2670362459bfae2107 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 4 Aug 2026 12:57:05 -0700 Subject: [PATCH 048/330] [tool][web] Intercept dart2wasm errors & append JS migration footers (#190476) When compiling a web project with `--wasm` (or running a dry-run) that encounters incompatible legacy JS interop dependencies (`dart:html` or `package:js`), standard `--wasm` builds default to an uncaught `ProcessException` dumping an internal stack trace, while dry-runs lack actionable migration remediation. This change: * Invokes `ProcessUtils.run(throwOnError: false)` in `Dart2WasmTarget.build` to forward uncorrupted compiler stdout/stderr directly to the logger without throwing an unformatted `ProcessException`. * Adds `kWasmErrorsMoreInfo` constant targeting https://flutter.dev/to/wasm-errors. * Inspects standard compile failure and Wasm dry-run outputs for instances of `dart:html` or `package:js`, appending an educational migration footer when matched. * Terminates fatal Wasm builds cleanly via `throwToolExit`, preventing internal tool stack trace spillage. Fixes #190466 --- .../lib/src/build_system/targets/web.dart | 29 +++- .../lib/src/web/web_constants.dart | 17 ++- .../targets/web_dry_run_test.dart | 71 +++++++++ .../build_system/targets/web_test.dart | 142 ++++++++++++++++++ 4 files changed, 254 insertions(+), 5 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 57775b9065d9d..11bd122791c9f 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -27,6 +27,7 @@ import '../../web/bootstrap.dart'; import '../../web/compile.dart'; import '../../web/file_generators/flutter_service_worker_js.dart'; import '../../web/file_generators/main_dart.dart' as main_dart; +import '../../web/web_constants.dart'; import '../../web_template.dart'; import '../build_system.dart'; import '../depfile.dart'; @@ -388,12 +389,14 @@ class Dart2WasmTarget extends Dart2WebTarget { processManager: environment.processManager, ); - final RunResult runResult = await processUtils.run( - throwOnError: !compilerConfig.dryRun, - compilationArgs, - ); + final RunResult runResult = await processUtils.run(compilationArgs); if (compilerConfig.dryRun) { await _handleDryRunResult(environment, runResult); + } else if (runResult.exitCode != 0) { + environment.logger.printStatus(runResult.stdout); + environment.logger.printError(runResult.stderr); + _checkForLegacyWebImports(environment, runResult.stdout, runResult.stderr); + throwToolExit('Failed to compile application for the Web.'); } final File recordedUsesFile = environment.buildDir.childFile( LinkHooks.recordedUsesWasmFileName, @@ -576,6 +579,8 @@ class Dart2WasmTarget extends Dart2WebTarget { } result ??= 'unknown'; + _checkForLegacyWebImports(environment, stdout, stderr); + environment.logger.printWarning('Use --no-wasm-dry-run to disable these warnings.'); _analytics.send( @@ -586,6 +591,22 @@ class Dart2WasmTarget extends Dart2WebTarget { ), ); } + + static final RegExp _kLegacyImportErrorPattern = RegExp( + "(?:Dart library|The unavailable library) '(${kLegacyWebLibraries.join('|')})'|" + '(${kLegacyWebLibraries.join('|')}) unsupported', + ); + + void _checkForLegacyWebImports(Environment environment, String stdout, String stderr) { + if (_kLegacyImportErrorPattern.hasMatch(stdout) || + _kLegacyImportErrorPattern.hasMatch(stderr)) { + environment.logger.printStatus( + 'Note: WebAssembly compilation failed due to legacy web imports.\n' + 'Migrate your project from dart:html and package:js to package:web and dart:js_interop.\n' + '$kWasmErrorsMoreInfo', + ); + } + } } /// Unpacks the dart2js or dart2wasm compilation and resources to a given diff --git a/packages/flutter_tools/lib/src/web/web_constants.dart b/packages/flutter_tools/lib/src/web/web_constants.dart index 94ec3ab1b19e6..c4e545ae7fd18 100644 --- a/packages/flutter_tools/lib/src/web/web_constants.dart +++ b/packages/flutter_tools/lib/src/web/web_constants.dart @@ -2,7 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -const kWasmMoreInfo = 'See https://flutter.dev/to/wasm for more information.'; +const String kWasmMoreInfo = 'See https://flutter.dev/to/wasm for more information.'; +const String kWasmErrorsMoreInfo = + 'See https://flutter.dev/to/wasm-errors for diagnostic and migration guidance.'; + +/// Legacy web libraries unsupported in WebAssembly compilation. +const Set kLegacyWebLibraries = { + 'dart:html', + 'dart:indexed_db', + 'dart:js', + 'dart:js_util', + 'dart:svg', + 'dart:web_audio', + 'dart:web_gl', + 'dart:web_sql', + 'package:js', +}; /// Headers required to run Wasm-compiled applications with multi-threading. /// diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart index 10f17402ba58f..cffa0fc0c24a1 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart @@ -6,6 +6,7 @@ import 'dart:math'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; @@ -174,6 +175,18 @@ package:foo/some/path.dart 6:1 - dart:html unsupported (0) final Dart2WasmTarget target = createTarget(); await target.build(environment); + final logger = environment.logger as BufferLogger; + expect( + logger.statusText, + contains('Note: WebAssembly compilation failed due to legacy web imports.'), + ); + expect( + logger.statusText, + contains( + 'Migrate your project from dart:html and package:js to package:web and dart:js_interop.', + ), + ); + expect(fakeAnalytics.sentEvents, hasLength(1)); final Event event = fakeAnalytics.sentEvents[0]; @@ -184,6 +197,64 @@ package:foo/some/path.dart 6:1 - dart:html unsupported (0) }), ); + test( + 'dry run prints JS interop migration footer on dart:svg and dart:js_util unsupported findings', + () => testbed.run(() async { + processManager.addCommand( + FakeCommand( + command: commandArgs, + exitCode: 254, + stdout: ''' +Found incompatibilities with WebAssembly. + +package:foo/some/path.dart 6:1 - dart:svg unsupported (5) +package:bar/some/path.dart 12:4 - dart:js_util unsupported (6) +''', + ), + ); + final Dart2WasmTarget target = createTarget(); + await target.build(environment); + + final logger = environment.logger as BufferLogger; + expect( + logger.statusText, + contains('Note: WebAssembly compilation failed due to legacy web imports.'), + ); + expect( + logger.statusText, + contains( + 'Migrate your project from dart:html and package:js to package:web and dart:js_interop.', + ), + ); + }), + ); + + test( + 'dry run does not print JS interop migration footer on non-web unsupported libraries like dart:ffi or incidental filenames', + () => testbed.run(() async { + processManager.addCommand( + FakeCommand( + command: commandArgs, + exitCode: 254, + stdout: ''' +Found incompatibilities with WebAssembly. + +package:fizz/some/path.dart 80:2 - dart:ffi unsupported (3) +package:foo/some/my_dart_html_wrapper.dart 103:20 - dart:io unsupported (4) +''', + ), + ); + final Dart2WasmTarget target = createTarget(); + await target.build(environment); + + final logger = environment.logger as BufferLogger; + expect( + logger.statusText, + isNot(contains('Note: WebAssembly compilation failed due to legacy web imports.')), + ); + }), + ); + test( 'dry run findings public packages', () => testbed.run(() async { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index a51af6e45bfd6..07966a8c91e0d 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/template.dart'; import 'package:flutter_tools/src/build_info.dart'; @@ -1407,6 +1408,147 @@ _flutter.loader.load(); } } + void addWasmCompilerErrorCommand( + FakeProcessManager processManager, + Environment environment, + String stderr, + ) { + processManager.addCommand( + FakeCommand( + command: [ + ..._kDart2WasmLinuxArgs, + '-Ddart.vm.profile=true', + '-Ddart.vm.product=false', + '--extra-compiler-option=--delete-tostring-package-uri=dart:ui', + '--extra-compiler-option=--delete-tostring-package-uri=package:flutter', + '--extra-compiler-option=--import-shared-memory', + '--extra-compiler-option=--shared-memory-max-pages=32768', + '-DFLUTTER_WEB_USE_SKIA=false', + '-DFLUTTER_WEB_USE_SKWASM=true', + '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', + '--extra-compiler-option=--depfile=${environment.buildDir.childFile('dart2wasm.d').absolute.path}', + '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').absolute.path}', + '--enable-experiment=record-use', + '-O2', + '--no-strip-wasm', + '--no-source-maps', + '--no-minify', + '-o', + environment.buildDir.childFile('main.dart.wasm').absolute.path, + environment.buildDir.childFile('main.dart').absolute.path, + ], + exitCode: 254, + stderr: stderr, + ), + ); + } + + test( + 'Dart2WasmTarget prints JS interop migration footer on dart:html library import failure', + () => testbed.run(() async { + environment.defines[kBuildMode] = 'profile'; + addWasmCompilerErrorCommand( + processManager, + environment, + "Error: Dart library 'dart:html' is not available on this platform.", + ); + + try { + await Dart2WasmTarget( + const WasmCompilerConfig( + optimizationLevel: 2, + stripWasm: false, + sourceMaps: false, + minify: false, + ), + const NoOpAnalytics(), + ).build(environment); + fail('Expected exception'); + } on Exception catch (e) { + expect(e.toString(), contains('Failed to compile application for the Web.')); + } + + final logger = globals.logger as BufferLogger; + expect( + logger.statusText, + contains('Note: WebAssembly compilation failed due to legacy web imports.'), + ); + expect( + logger.statusText, + contains( + 'Migrate your project from dart:html and package:js to package:web and dart:js_interop.', + ), + ); + }, overrides: {ProcessManager: () => processManager}), + ); + + test( + 'Dart2WasmTarget prints JS interop migration footer on dart:svg and dart:js_util failures', + () => testbed.run(() async { + environment.defines[kBuildMode] = 'profile'; + addWasmCompilerErrorCommand( + processManager, + environment, + "Context: The unavailable library 'dart:svg' is imported through these paths:\n" + "Error: Dart library 'dart:js_util' is not available on this platform.", + ); + + try { + await Dart2WasmTarget( + const WasmCompilerConfig( + optimizationLevel: 2, + stripWasm: false, + sourceMaps: false, + minify: false, + ), + const NoOpAnalytics(), + ).build(environment); + fail('Expected exception'); + } on Exception catch (e) { + expect(e.toString(), contains('Failed to compile application for the Web.')); + } + + final logger = globals.logger as BufferLogger; + expect( + logger.statusText, + contains('Note: WebAssembly compilation failed due to legacy web imports.'), + ); + }, overrides: {ProcessManager: () => processManager}), + ); + + test( + 'Dart2WasmTarget does not print JS interop migration footer on incidental mentions of dart:html in unrelated errors', + () => testbed.run(() async { + environment.defines[kBuildMode] = 'profile'; + addWasmCompilerErrorCommand( + processManager, + environment, + "Error: Syntax error in file:///my_dart_html_test.dart at line 4: print('dart:html');", + ); + + try { + await Dart2WasmTarget( + const WasmCompilerConfig( + optimizationLevel: 2, + stripWasm: false, + sourceMaps: false, + minify: false, + ), + const NoOpAnalytics(), + ).build(environment); + fail('Expected exception'); + } on Exception catch (e) { + expect(e.toString(), contains('Failed to compile application for the Web.')); + } + + final logger = globals.logger as BufferLogger; + expect( + logger.statusText, + isNot(contains('Note: WebAssembly compilation failed due to legacy web imports.')), + ); + }, overrides: {ProcessManager: () => processManager}), + ); + test('Dart2WasmTarget.buildFiles respects compilerConfig.sourceMaps and matches modules', () { final File wasmFile = environment.buildDir.childFile('main.dart.wasm')..createSync(); final File mjsFile = environment.buildDir.childFile('main.dart.mjs')..createSync(); From 27a266389d2e24384d501dc4ef0f24ca162c2c89 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 4 Aug 2026 15:58:46 -0400 Subject: [PATCH 049/330] Roll Skia from 48b58ee222f1 to a8583a0a2c11 (2 revisions) (#190537) https://skia.googlesource.com/skia.git/+log/48b58ee222f1..a8583a0a2c11 2026-08-04 skia-autoroll@skia-public.iam.gserviceaccount.com Manual roll Dawn from 36cf1fae0cd8 to a488846c0f17 (18 revisions) 2026-08-04 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from fd9ab9744d0b to b2accf345f13 (2 revisions) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC alexisdavidc@google.com,codefu@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 9c8da03124268..47ef841e86e4b 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '48b58ee222f14b2b14a08c2e8574fae8256b26e0', + 'skia_revision': 'a8583a0a2c114130cd88d7fbca23248d50a91946', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 90fcfdf4e9df2d71f5175ec26d2e1424286b5368 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:05:27 -0700 Subject: [PATCH 050/330] Bumps text gamma on windows to match skia. (#190477) fixes https://github.com/flutter/flutter/issues/190391 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../impeller/entity/contents/text_contents.cc | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/engine/src/flutter/impeller/entity/contents/text_contents.cc b/engine/src/flutter/impeller/entity/contents/text_contents.cc index 1161af16d4c38..5c46309c60aab 100644 --- a/engine/src/flutter/impeller/entity/contents/text_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/text_contents.cc @@ -20,13 +20,11 @@ namespace impeller { namespace { -// TODO(gaaclarke): Investigate if this is still needed for Windows. -// On Linux we use FreeType to rasterize glyphs. FreeType does not perform -// gamma correction itself during rasterization. Because we render in linear -// space, light text on a dark background would look too thin without -// correction. To compensate, we calculate a contrast/gamma correction -// factor based on the text color's luminance, which is used in the shader -// to adjust the glyph's coverage. +// On Linux we use FreeType and on Windows we use DirectWrite/GDI to rasterize +// glyphs. Because we render in linear space, light text on a dark background +// would look too thin without correction. To compensate, we calculate a +// contrast/gamma correction factor based on the text color's luminance, which +// is used in the shader to adjust the glyph's coverage. constexpr bool kPlatformGammaCorrectionDefault = #if FML_OS_LINUX || FML_OS_WIN true; @@ -34,6 +32,19 @@ constexpr bool kPlatformGammaCorrectionDefault = false; #endif +// The contrast/gamma exponent applied in the shader ranges from 1.0 for black +// text to 1.0 + kMaxGammaCorrection for white text. This interpolates the +// exponent based on the text color's luminance. On Linux, 1.2 equates to a +// maximum 2.2 sRGB gamma. On Windows, DirectWrite and GDI employ higher base +// gamma and contrast enhancement, so 1.6 is used to match Skia's perceived +// visual weight and edge sharpness. +constexpr Scalar kMaxGammaCorrection = +#if FML_OS_WIN + 1.6f; +#else + 1.2f; +#endif + Point SizeToPoint(Size size) { return Point(size.width, size.height); } @@ -285,10 +296,6 @@ bool TextContents::Render(const ContentContext& renderer, // Calculate relative luminance using Rec. 709 luma coefficients. Scalar luma = color.red * 0.2126f + color.green * 0.7152f + color.blue * 0.0722f; - // The contrast/gamma exponent applied in the shader ranges from 1.0 for - // black text to 2.2 (standard sRGB gamma) for white text. This interpolates - // the exponent based on the text color's luminance. - constexpr Scalar kMaxGammaCorrection = 1.2f; frag_info.text_contrast = 1.0f + luma * kMaxGammaCorrection; } else { frag_info.text_contrast = 1.0f; From 3c12de8ecbd24ca27a68701b5c308cf4795e3e6e Mon Sep 17 00:00:00 2001 From: Andy Wolff Date: Tue, 4 Aug 2026 13:36:02 -0700 Subject: [PATCH 051/330] android_hardware_smoke_test: Detect blank image failures or EGL initialization warnings and retry (#190110) This PR addresses flakiness for the android_hardware_smoke_test suite. Occasionally, due to race conditions during rendering composition, platform view tests will produce blank images. The screenshot occurs before composition finished, even though we try to sync it deterministically. For these cases, we introduce a retry right there in the process. Also, we refactor the blank image detection and the existing image cropping functions into image_utils.dart, and add a unit test for the blank image method. Occasionally, for reasons I am not totally clear on, CI starts the test suite in a way which contains EGL initialization errors. This also produces blank images, but retrying wouldn't help at all because it's not a timing issue, it's a setup issue. So we introduce a detection mechanism for these which looks for errors in the logcat, then retries by restarting the activity completely, which at least has a theoretical chance to start up again without the EGL initialization problem. These issues are affecting prod and staging for the instrumented shards, though only for the vulkan tests. OpenGLES tests are passing consistently. I don't see any failures of this type in presubmit, though it may be masked by a different problem related to infrastructure failures during dependency downloads. This is intended to improve https://github.com/flutter/flutter/issues/189079 and https://github.com/flutter/flutter/issues/189843#issuecomment-5097114360 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../run_android_hardware_smoke_tests.dart | 129 +++++++++--- .../android_hardware_smoke_test/README.md | 35 ++++ .../FlutterActivityTest.kt | 190 +++++++++++++++--- .../lib/constants.dart | 13 +- .../android_hardware_smoke_test/lib/main.dart | 28 +-- .../test/image_utils_test.dart | 87 ++++++++ .../test_driver/driver_test.dart | 58 +++--- .../test_driver/image_utils.dart | 58 ++++++ 8 files changed, 497 insertions(+), 101 deletions(-) create mode 100644 dev/integration_tests/android_hardware_smoke_test/test/image_utils_test.dart create mode 100644 dev/integration_tests/android_hardware_smoke_test/test_driver/image_utils.dart diff --git a/dev/bots/suite_runners/run_android_hardware_smoke_tests.dart b/dev/bots/suite_runners/run_android_hardware_smoke_tests.dart index b667dc6845e73..80c2a2e6cf70e 100644 --- a/dev/bots/suite_runners/run_android_hardware_smoke_tests.dart +++ b/dev/bots/suite_runners/run_android_hardware_smoke_tests.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io' show Platform; +import 'dart:io' as io; import 'package:file/file.dart'; import 'package:file/local.dart'; @@ -36,13 +36,7 @@ Future runAndroidHardwareSmokeTests({ final String testDir = path.join('dev', 'integration_tests', 'android_hardware_smoke_test'); - // Regenerate standard Android Gradle wrappers - await runCommand('flutter', [ - 'create', - '--platform=android', - '--no-overwrite', - '.', - ], workingDirectory: testDir); + await _regenerateAndroidWrappers(testDir); final String androidDir = path.join(testDir, 'android'); final File androidManifestXml = const LocalFileSystem().file( @@ -55,35 +49,20 @@ Future runAndroidHardwareSmokeTests({ ); try { - // Replace whatever the current backend is with the specified backend. - final impellerBackendMetadata = RegExp(_impellerBackendMetadata(value: '[^"]*')); - if (!impellerBackendMetadata.hasMatch(androidManifestContents)) { - throw StateError( - 'Could not find io.flutter.embedding.android.ImpellerBackend meta-data tag inside AndroidManifest.xml', - ); - } - androidManifestXml.writeAsStringSync( - androidManifestContents.replaceFirst( - impellerBackendMetadata, - _impellerBackendMetadata(value: backend.name), - ), - ); + _setAndroidManifestBackend(androidManifestXml, androidManifestContents, backend); - // 1. Run driver tests to generate reference screenshots - await runCommand('flutter', [ - 'drive', - '--driver=test_driver/driver_test.dart', - '--target=integration_test/integration_test_wrapper.dart', - '--no-dds', - '--no-enable-dart-profiling', - ], workingDirectory: testDir); + // 1. Run driver tests to generate reference screenshots (with retry loop) + final bool success = await _runRetryLoop(testDir); + if (!success) { + return; + } if (runInstrumented) { final String gradle = path.absolute( - path.join(androidDir, Platform.isWindows ? 'gradlew.bat' : 'gradlew'), + path.join(androidDir, io.Platform.isWindows ? 'gradlew.bat' : 'gradlew'), ); - // 3. Build and run the instrumented tests. + // 2. Build and run the instrumented tests. await runCommand(gradle, [ ':app:connectedDebugAndroidTest', '-Pandroid.testInstrumentationRunnerArguments.class=com.example.android_hardware_smoke_test.FlutterActivityTest', @@ -98,3 +77,91 @@ Future runAndroidHardwareSmokeTests({ _cleanGoldensDirectory(destinationDir); } } + +Future _regenerateAndroidWrappers(String testDir) async { + await runCommand('flutter', [ + 'create', + '--platform=android', + '--no-overwrite', + '.', + ], workingDirectory: testDir); +} + +void _setAndroidManifestBackend(File file, String contents, ImpellerBackend backend) { + final impellerBackendMetadata = RegExp(_impellerBackendMetadata(value: '[^"]*')); + if (!impellerBackendMetadata.hasMatch(contents)) { + throw StateError( + 'Could not find io.flutter.embedding.android.ImpellerBackend meta-data tag inside AndroidManifest.xml', + ); + } + file.writeAsStringSync( + contents.replaceFirst(impellerBackendMetadata, _impellerBackendMetadata(value: backend.name)), + ); +} + +Future _runRetryLoop(String testDir) async { + const maxAttempts = 3; + var exitCode = 0; + + for (var attempt = 1; attempt <= maxAttempts; attempt++) { + // Clear logcat buffer before running + await runCommand('adb', ['logcat', '-c']); + + final driveArgs = [ + 'drive', + '--driver=test_driver/driver_test.dart', + '--target=integration_test/integration_test_wrapper.dart', + '--no-dds', + '--no-enable-dart-profiling', + ]; + + if (attempt > 1) { + driveArgs.add('--no-build'); + } + + final Command command = await startCommand('flutter', driveArgs, workingDirectory: testDir); + exitCode = await command.process.exitCode; + if (exitCode == 0) { + return true; + } + + io.stderr.writeln( + 'flutter drive failed with exit code $exitCode on attempt $attempt/$maxAttempts.', + ); + + // Inspect the process logcat on failure to detect if a transient EGL/graphics context + // negotiation error occurred during startup, enabling a safe activity/process level retry. + final bool hasEglWarning = await _checkForTransientEglFailure(); + if (!hasEglWarning) { + // Non-retryable error: exit immediately and log specific failure + foundError([ + 'Android Hardware Smoke Tests driver run failed with exit code $exitCode and no transient EGL warning was found in logcat.', + ]); + return false; + } + + // Retryable EGL warning: log progress and continue if attempts remain + if (attempt < maxAttempts) { + io.stderr.writeln( + 'attempt $attempt of $maxAttempts: detected retryable EGL initialization warning. Retrying...', + ); + } + } + + // Loop finished: exhausted all attempts + foundError([ + 'Android Hardware Smoke Tests driver run failed to initialize EGL after $maxAttempts attempts.', + ]); + return false; +} + +Future _checkForTransientEglFailure() async { + try { + final String logcatOutput = await runAndGetStdout('adb', ['logcat', '-d']).join('\n'); + return logcatOutput.contains('Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED') || + logcatOutput.contains('Failed to initialize 101010-2 format'); + } catch (e) { + io.stderr.writeln('Warning: Failed to check logcat for EGL failure: $e'); + return false; + } +} diff --git a/dev/integration_tests/android_hardware_smoke_test/README.md b/dev/integration_tests/android_hardware_smoke_test/README.md index ecc66472e7e82..dc4b83d732653 100644 --- a/dev/integration_tests/android_hardware_smoke_test/README.md +++ b/dev/integration_tests/android_hardware_smoke_test/README.md @@ -479,3 +479,38 @@ a single step, the test suite executes exactly once, remains low maintenance across AGP upgrades, and cleanly bubbles up any legitimate compiler or runner errors without silent try-catch blocks. + +--- + +## 7. Graphics Initialization & Screenshot Retry Mechanisms + +To guarantee high stability against flakiness on emulators and physical hardware, the test suite implements a two-layered retry mechanism: + +### A. Process & Activity-Level Retries (Graphics Context Initialization) +* **Goal**: Recovers from transient EGL or graphics context negotiation errors (e.g., `Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED` or HWUI startup warnings) that occur globally when the Flutter Engine boots up. +* **Mechanism**: + * **On Host (CI)**: If the `flutter drive` run fails, the host suite runner + ([`run_android_hardware_smoke_tests.dart`](dev/bots/suite_runners/run_android_hardware_smoke_tests.dart)) + checks the device logcat. If transient EGL errors are detected, it restarts + the test run process (up to 3 attempts) to initialize a new EGL context in + a fresh process. + * **On Device (JUnit)**: If an attempt fails, the JUnit runner + ([`FlutterActivityTest.kt`](android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt)) + recreates the Activity scenario and retries **only** on blank screenshot + failures (up to 3 attempts). It does **not** retry on EGL/graphics errors, + as recreating the activity within the same application process cannot + recover from process-level EGL initialization failures; these fail + immediately on the device to bubble up to the host runner. Standard test + failures (e.g., golden pixel mismatches) also fail immediately. + +### B. Screenshot-Level Retries (Blank Platform View Capture) +* **Goal**: Prevents race conditions where a screenshot is taken before native platform view compositing has fully settled, resulting in a blank/transparent frame. +* **Mechanism**: + * **Both Modes**: The screenshot capture loop (both native `UiAutomation` and + host-side `NativeDriver`) checks the cropped image. If the cropped image is + completely transparent or solid black, it retries the capture up to 3 + times, sleeping 200ms between attempts. + * **Short-circuiting**: If an EGL/graphics error is detected in the logcat + during screenshot attempts, the loop is terminated early (bypassing + remaining screenshot retries) to fail the test immediately and bubble the + failure up to the host runner as quickly as possible. diff --git a/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt b/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt index 095c38a655488..fdb3798bf7501 100644 --- a/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt +++ b/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt @@ -25,6 +25,14 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +private class BlankScreenshotException( + message: String +) : IllegalStateException(message) + +private class EglInitializationException( + message: String +) : IllegalStateException(message) + @RunWith(AndroidJUnit4::class) class FlutterActivityTest { companion object { @@ -42,6 +50,64 @@ class FlutterActivityTest { FlutterEngineCache.getInstance().remove(MainActivity.CACHED_ENGINE_KEY) } } + + private fun verifyNoGraphicsPipelineErrors(marker: String) { + val errors = getGraphicsPipelineErrors(marker) + if (errors.isNotEmpty()) { + throw EglInitializationException( + "Graphics pipeline/EGL failure detected in process logcat:\n${errors.joinToString("\n")}" + ) + } + } + + private fun hasGraphicsPipelineErrors(marker: String): Boolean = getGraphicsPipelineErrors(marker).isNotEmpty() + + private fun getGraphicsPipelineErrors(marker: String): List { + val errorLogs = mutableListOf() + try { + val pid = android.os.Process.myPid() + val instrumentation = InstrumentationRegistry.getInstrumentation() + val pfd = instrumentation.uiAutomation.executeShellCommand("logcat -d --pid=$pid *:W") + android.os.ParcelFileDescriptor.AutoCloseInputStream(pfd).bufferedReader().use { reader -> + var line: String? + var seenMarker = false + while (reader.readLine().also { line = it } != null) { + val currentLine = line ?: continue + if (currentLine.contains(marker)) { + seenMarker = true + } + if (seenMarker) { + if (currentLine.contains("libEGL") || + currentLine.contains("HWUI") || + currentLine.contains("EGL_") + ) { + errorLogs.add(currentLine) + } + } + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to self-inspect logcat: ${e.message}") + } + return errorLogs + } + + private fun isBitmapBlank(bitmap: Bitmap): Boolean { + val width = bitmap.width + val height = bitmap.height + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + val firstPixel = pixels[0] + if (firstPixel != android.graphics.Color.TRANSPARENT && firstPixel != android.graphics.Color.BLACK) { + return false + } + for (pixel in pixels) { + if (pixel != firstPixel) { + return false + } + } + return true + } } @get:Rule val rule = ActivityScenarioRule(MainActivity::class.java) @@ -57,13 +123,58 @@ class FlutterActivityTest { * @param testName The descriptive identifier of the test case to render and compare. */ private fun templateTest(testName: String) { - Log.d(TAG, "Starting $testName") - val future = CompletableFuture() + val maxAttempts = 3 + var lastException: Throwable? = null + + for (currentAttempt in 1..maxAttempts) { + val marker = "START_${testName}_attempt${currentAttempt}_${System.currentTimeMillis()}" + Log.i(TAG, marker) + Log.d(TAG, "Starting $testName (attempt $currentAttempt/$maxAttempts)") + + try { + runAttempt(testName, marker) + return + } catch (e: Throwable) { + lastException = e + Log.w(TAG, "Attempt $currentAttempt failed: ${e.message}") + + if (e is org.junit.AssumptionViolatedException) { + throw e + } + if (!isBlankScreenshotException(e)) { + throw RuntimeException( + "Test '$testName' failed on attempt $currentAttempt with a non-retryable error: ${e.message}", + e + ) + } + + if (currentAttempt < maxAttempts) { + Log.i(TAG, "Recreating activity for next attempt...") + rule.scenario.recreate() + } + } + } + + throw RuntimeException( + "Test '$testName' failed to capture a valid screenshot after $maxAttempts attempts.", + lastException + ) + } + + private fun isBlankScreenshotException(e: Throwable?): Boolean { + if (e == null) return false + if (e is BlankScreenshotException) return true + return isBlankScreenshotException(e.cause) + } + private fun runAttempt( + testName: String, + marker: String + ) { + val future = CompletableFuture() rule.scenario.onActivity { activity -> // Confirm screen is not locked by checking activity has lifecycle state RESUMED assertEquals(Lifecycle.State.RESUMED, activity.lifecycle.currentState) - try { val isPlatformView = testName.startsWith(Constants.PLATFORM_VIEW_PREFIX) val message = @@ -73,7 +184,6 @@ class FlutterActivityTest { } Log.d(TAG, "Sending '$message' on message channel") - activity.messageChannel?.send(message) { reply -> try { val replyJson = @@ -90,7 +200,7 @@ class FlutterActivityTest { val width = replyJson.getInt(Constants.KEY_WIDTH) val height = replyJson.getInt(Constants.KEY_HEIGHT) - captureAndSendScreenshot(x, y, width, height, testName, future) + captureAndSendScreenshot(x, y, width, height, testName, marker, future) } else { future.complete(replyMessage) } @@ -149,8 +259,13 @@ class FlutterActivityTest { width: Int, height: Int, testName: String, + marker: String, future: CompletableFuture ) { + if (x < 0 || y < 0 || width <= 0 || height <= 0) { + throw IllegalArgumentException("Invalid crop bounds: x=$x, y=$y, width=$width, height=$height") + } + // Capture the screenshot on a background thread with a short delay. We must NOT sleep or capture // on the Main UI Thread to avoid blocking frame rendering or causing an ANR. val screenshotExecutor = Executors.newSingleThreadScheduledExecutor() @@ -158,26 +273,55 @@ class FlutterActivityTest { try { // Capture the true screen output using UiAutomation from this privileged instrumentation runner process. val instrumentation = InstrumentationRegistry.getInstrumentation() - val screenshot = - instrumentation.uiAutomation.takeScreenshot() - ?: throw IllegalStateException("UiAutomation.takeScreenshot() returned null") - - if (x < 0 || - y < 0 || - width <= 0 || - height <= 0 || - x + width > screenshot.width || - y + height > screenshot.height - ) { - throw IllegalArgumentException( - "Crop bounds out of range: x=$x, y=$y, width=$width, height=$height, screenshot.width=${screenshot.width}, screenshot.height=${screenshot.height}" - ) + var cropped: Bitmap? = null + val maxAttempts = 3 + + for (attempt in 1..maxAttempts) { + val screenshot = instrumentation.uiAutomation.takeScreenshot() + if (screenshot == null) { + Log.w(TAG, "UiAutomation.takeScreenshot() returned null (attempt $attempt/$maxAttempts)") + } else { + if (x + width > screenshot.width || y + height > screenshot.height) { + screenshot.recycle() + throw IllegalArgumentException( + "Crop bounds out of screen range: x=$x, y=$y, width=$width, height=$height, screenshot.width=${screenshot.width}, screenshot.height=${screenshot.height}" + ) + } + + // Crop the full-screen screenshot to the exact widget bounds. + val candidate = Bitmap.createBitmap(screenshot, x, y, width, height) + if (candidate != screenshot) { + screenshot.recycle() + } + + if (!isBitmapBlank(candidate)) { + cropped = candidate + break + } + + Log.w(TAG, "Captured screenshot is blank/empty (attempt $attempt/$maxAttempts)") + candidate.recycle() + } + + // If EGL/graphics pipeline has failed, further retries in this process are futile. + // Break early to trigger activity re-creation. + if (hasGraphicsPipelineErrors(marker)) { + Log.w(TAG, "Graphics pipeline/EGL error detected during screenshot. Short-circuiting retries.") + break + } + + if (attempt < maxAttempts) { + Thread.sleep(200) + } } - // Crop the full-screen screenshot to the exact widget bounds. - val cropped = Bitmap.createBitmap(screenshot, x, y, width, height) - if (cropped != screenshot) { - screenshot.recycle() + // Verify logcat first to prioritize EGL diagnostics over generic blank screenshot errors. + verifyNoGraphicsPipelineErrors(marker) + + if (cropped == null) { + throw BlankScreenshotException( + "Captured screenshot is blank/empty after $maxAttempts attempts." + ) } val stream = ByteArrayOutputStream() diff --git a/dev/integration_tests/android_hardware_smoke_test/lib/constants.dart b/dev/integration_tests/android_hardware_smoke_test/lib/constants.dart index 2713717fb5324..ef09f62a0a168 100644 --- a/dev/integration_tests/android_hardware_smoke_test/lib/constants.dart +++ b/dev/integration_tests/android_hardware_smoke_test/lib/constants.dart @@ -7,7 +7,8 @@ // ============================================================================= /// The MethodChannel name used for query and control of native platform capabilities. -const nativeSupportChannelName = 'com.example.android_hardware_smoke_test/native_support'; +const nativeSupportChannelName = + 'com.example.android_hardware_smoke_test/native_support'; /// The MethodChannel method name used to query the active graphics rendering backend. const methodImpellerBackend = 'impeller_backend'; @@ -121,8 +122,16 @@ const platformViewPrefix = 'platformView'; const kPlatformViewTextureLayerTest = '${platformViewPrefix}TextureLayerTest'; /// Scenario name for embedding a native platform view using Hybrid Composition. -const kPlatformViewHybridCompositionTest = '${platformViewPrefix}HybridCompositionTest'; +const kPlatformViewHybridCompositionTest = + '${platformViewPrefix}HybridCompositionTest'; /// Scenario name for embedding a native platform view using Hybrid Composition++. const kPlatformViewHybridCompositionPlusPlusTest = '${platformViewPrefix}HybridCompositionPlusPlusTest'; + +// ============================================================================= +// 3. Error Substrings +// ============================================================================= + +/// Substring used to identify blank/empty screenshot errors in exceptions. +const String errorBlankScreenshot = 'blank/empty'; diff --git a/dev/integration_tests/android_hardware_smoke_test/lib/main.dart b/dev/integration_tests/android_hardware_smoke_test/lib/main.dart index 79266b1a0fc97..3d81c8fccb411 100644 --- a/dev/integration_tests/android_hardware_smoke_test/lib/main.dart +++ b/dev/integration_tests/android_hardware_smoke_test/lib/main.dart @@ -35,9 +35,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter android hardware smoke test', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - ), + theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)), home: MyWidget(imageLoader: imageLoader), ); } @@ -56,10 +54,7 @@ class MyWidget extends StatefulWidget { class _MyState extends State { static const _nativeChannel = MethodChannel(nativeSupportChannelName); - static const _testChannel = BasicMessageChannel( - testChannelName, - JSONMessageCodec(), - ); + static const _testChannel = BasicMessageChannel(testChannelName, JSONMessageCodec()); String _message = 'Waiting for message...'; late Future _goldenVariantFuture; @@ -90,9 +85,7 @@ class _MyState extends State { goldenVariantValue, ); - return { - keyMessage: failureMessage ?? 'Comparison Success', - }; + return {keyMessage: failureMessage ?? 'Comparison Success'}; } final testName = messageMap?[keyTestName] as String?; @@ -102,8 +95,7 @@ class _MyState extends State { // Widget tests pass captureScreenshot: false. // Image.toByteData runs async on a native thread, which results in an unresolvable deadlock in the widget test's FakeAsync zone. // Comparing pixels is not a responsibility of widget tests anyway, that should be reserved for the integration tests. - final bool captureScreenshot = - messageMap?[keyCaptureScreenshot] as bool? ?? true; + final bool captureScreenshot = messageMap?[keyCaptureScreenshot] as bool? ?? true; if (testName == kPlatformViewHybridCompositionPlusPlusTest) { final bool isHcpp = await HybridAndroidViewController.checkIfSupported(); @@ -135,16 +127,13 @@ class _MyState extends State { _loadedImage = img; }); } catch (e, stackTrace) { - return { - keyMessage: 'Failed to load image asset: $e\n$stackTrace', - }; + return {keyMessage: 'Failed to load image asset: $e\n$stackTrace'}; } } final completer = Completer>(); - final bool isPlatformView = - testName?.startsWith(platformViewPrefix) ?? false; + final bool isPlatformView = testName?.startsWith(platformViewPrefix) ?? false; if (isPlatformView) { _platformViewDrawnCompleter = Completer(); } else { @@ -180,9 +169,7 @@ class _MyState extends State { void initState() { super.initState(); - _goldenVariantFuture = _nativeChannel.invokeMethod( - methodImpellerBackend, - ); + _goldenVariantFuture = _nativeChannel.invokeMethod(methodImpellerBackend); _nativeChannel.setMethodCallHandler((MethodCall call) async { if (call.method == 'onDraw') { if (_platformViewDrawnCompleter?.isCompleted == false) { @@ -214,6 +201,7 @@ class _MyState extends State { kPlatformViewHybridCompositionPlusPlusTest => const AndroidPlatformView( mode: PlatformViewMode.hybridCompositionPlusPlus, ), + kTextTest => const TextDrawingCanvas(), kImageTest => ImageDrawingCanvas(image: _loadedImage), _ => VectorDrawingsCanvas(message: _message), diff --git a/dev/integration_tests/android_hardware_smoke_test/test/image_utils_test.dart b/dev/integration_tests/android_hardware_smoke_test/test/image_utils_test.dart new file mode 100644 index 0000000000000..21c13b036057d --- /dev/null +++ b/dev/integration_tests/android_hardware_smoke_test/test/image_utils_test.dart @@ -0,0 +1,87 @@ +// 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_test/flutter_test.dart'; +import 'package:image/image.dart' as img; +import '../test_driver/image_utils.dart'; + +void main() { + group('isImageBlank', () { + test('returns true for an empty or 0-sized image', () { + final image = img.Image(width: 0, height: 0); + expect(isImageBlank(image), isTrue); + }); + + test('returns true for a fully transparent image (all zero bytes)', () { + final image = img.Image( + width: 10, + height: 10, + ); // default is transparent black (all 0s) + expect(isImageBlank(image), isTrue); + }); + + test('returns true for a solid opaque black image', () { + final image = img.Image(width: 10, height: 10); + for (final pixel in image) { + pixel.r = 0; + pixel.g = 0; + pixel.b = 0; + pixel.a = 255; + } + expect(isImageBlank(image), isTrue); + }); + + test('returns false for a solid opaque red image', () { + final image = img.Image(width: 10, height: 10); + for (final pixel in image) { + pixel.r = 255; + pixel.g = 0; + pixel.b = 0; + pixel.a = 255; + } + expect(isImageBlank(image), isFalse); + }); + + test('returns false for an image with drawings (one non-black pixel)', () { + final image = img.Image(width: 10, height: 10); + // set one pixel to red + final img.Pixel pixel = image.getPixel(5, 5); + pixel.r = 255; + pixel.g = 0; + pixel.b = 0; + pixel.a = 255; + expect(isImageBlank(image), isFalse); + }); + }); + + group('cropImage', () { + test('crops image correctly within bounds', () { + final image = img.Image(width: 10, height: 10); + // Draw a pixel at (2, 2) + final img.Pixel pixel = image.getPixel(2, 2); + pixel.r = 255; + pixel.g = 0; + pixel.b = 0; + pixel.a = 255; + + final img.Image cropped = cropImage(image, 1, 1, 3, 3); + expect(cropped.width, equals(3)); + expect(cropped.height, equals(3)); + + // The drawn pixel at (2, 2) relative to parent is now at (1, 1) relative to cropped image + final img.Pixel croppedPixel = cropped.getPixel(1, 1); + expect(croppedPixel.r, equals(255)); + expect(croppedPixel.a, equals(255)); + }); + + test('throws ArgumentError for invalid crop bounds', () { + final image = img.Image(width: 10, height: 10); + expect(() => cropImage(image, -1, 0, 5, 5), throwsArgumentError); + expect(() => cropImage(image, 0, -1, 5, 5), throwsArgumentError); + expect(() => cropImage(image, 0, 0, 11, 5), throwsArgumentError); + expect(() => cropImage(image, 0, 0, 5, 11), throwsArgumentError); + expect(() => cropImage(image, 8, 8, 5, 5), throwsArgumentError); + }); + }); +} diff --git a/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart b/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart index 69bfae78532e7..120777ee5170c 100644 --- a/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart +++ b/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart @@ -12,14 +12,15 @@ import 'package:android_hardware_smoke_test/constants.dart'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:image/image.dart' as img; import 'package:test/test.dart'; +import 'image_utils.dart'; /// Whether the current environment is LUCI. bool get isLuci => io.Platform.environment['LUCI_CI'] == 'True'; void main() async { - late final FlutterDriver flutterDriver; - late final NativeDriver nativeDriver; - late final String activeGoldenVariant; + late FlutterDriver flutterDriver; + late AndroidNativeDriver nativeDriver; + late String activeGoldenVariant; setUpAll(() async { flutterDriver = await FlutterDriver.connect(); @@ -70,8 +71,6 @@ void main() async { return; } - expect(reply[keyMessage], equals('Rendered $testName')); - final Uint8List imageBytes; final bool isPlatformView = testName.startsWith(platformViewPrefix); if (isPlatformView) { @@ -80,32 +79,40 @@ void main() async { final w = reply[keyWidth]! as int; final h = reply[keyHeight]! as int; - final NativeScreenshot fullScreenshot = await nativeDriver.screenshot(); - final Uint8List fullBytes = await fullScreenshot.readAsBytes(); + img.Image? cropped; + const maxAttempts = 3; - final img.Image? decoded = img.decodePng(fullBytes); - if (decoded == null) { - throw StateError( - 'Failed to decode full screen screenshot for $testName', + for (var attempt = 1; attempt <= maxAttempts; attempt++) { + final NativeScreenshot fullScreenshot = await nativeDriver.screenshot(); + final Uint8List fullBytes = await fullScreenshot.readAsBytes(); + + final img.Image? decoded = img.decodePng(fullBytes); + if (decoded == null) { + throw StateError( + 'Failed to decode full screen screenshot for $testName', + ); + } + + final img.Image candidate = cropImage(decoded, x, y, w, h); + + if (!isImageBlank(candidate)) { + cropped = candidate; + break; + } + + io.stderr.writeln( + 'Captured screenshot is blank/empty (attempt $attempt/$maxAttempts)', ); + if (attempt < maxAttempts) { + await Future.delayed(const Duration(milliseconds: 200)); + } } - if (x < 0 || - y < 0 || - w <= 0 || - h <= 0 || - x + w > decoded.width || - y + h > decoded.height) { + + if (cropped == null) { throw StateError( - 'Crop bounds out of range for $testName: x=$x, y=$y, w=$w, h=$h, image.width=${decoded.width}, image.height=${decoded.height}', + 'Captured screenshot is $errorBlankScreenshot after $maxAttempts attempts.', ); } - final img.Image cropped = img.copyCrop( - decoded, - x: x, - y: y, - width: w, - height: h, - ); imageBytes = Uint8List.fromList(img.encodePng(cropped)); } else { final imageBase64 = reply[keyImageBytes]! as String; @@ -113,6 +120,7 @@ void main() async { } // Compare the bytes to a golden file on the host filesystem using the cached variant + await expectLater( imageBytes, matchesGoldenFile('goldens/$testName$activeGoldenVariant.png'), diff --git a/dev/integration_tests/android_hardware_smoke_test/test_driver/image_utils.dart b/dev/integration_tests/android_hardware_smoke_test/test_driver/image_utils.dart new file mode 100644 index 0000000000000..ef584271156f3 --- /dev/null +++ b/dev/integration_tests/android_hardware_smoke_test/test_driver/image_utils.dart @@ -0,0 +1,58 @@ +// 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 'dart:typed_data'; +import 'package:image/image.dart' as img; + +/// Checks if the image is transparent or solid black. +bool isImageBlank(img.Image image) { + if (image.width == 0 || image.height == 0) { + return true; + } + final img.Pixel pixel0 = image.getPixel(0, 0); + final num r0 = pixel0.r; + final num g0 = pixel0.g; + final num b0 = pixel0.b; + final num a0 = pixel0.a; + + final bool isTransparentOrBlack = a0 == 0 || (r0 == 0 && g0 == 0 && b0 == 0); + if (!isTransparentOrBlack) { + return false; + } + + // Fast path: check raw memory buffer directly to avoid pixel wrapper allocations in the loop + try { + final Uint32List pixels = image.buffer.asUint32List(); + final int val0 = pixels[0]; + for (var i = 1; i < pixels.length; i++) { + if (pixels[i] != val0) { + return false; + } + } + return true; + } catch (_) { + // Fall back to safe iterator if the buffer alignment/type is different + for (final pixel in image) { + if (pixel.r != r0 || pixel.g != g0 || pixel.b != b0 || pixel.a != a0) { + return false; + } + } + return true; + } +} + +/// Validates crop bounds and crops [source] to the specified rect. +img.Image cropImage(img.Image source, int x, int y, int width, int height) { + if (x < 0 || + y < 0 || + width <= 0 || + height <= 0 || + x + width > source.width || + y + height > source.height) { + throw ArgumentError( + 'Crop bounds out of range: x=$x, y=$y, width=$width, height=$height, source.width=${source.width}, source.height=${source.height}', + ); + } + return img.copyCrop(source, x: x, y: y, width: width, height: height); +} From 89b263df72ec03445b7e7e9f76f8f12628530188 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 4 Aug 2026 13:58:58 -0700 Subject: [PATCH 052/330] fix(tool): remove redundant --enable-experiment=record-use flag (#190475) When compiling web targets (dart2js and dart2wasm) or running dry runs with the record-use feature flag enabled, flutter_tools explicitly passed --enable-experiment=record-use to the compiler. Since record-use is enabled by default in recent Dart SDKs, passing this flag caused warning spam during standard compilation and dry runs. * Remove --enable-experiment=record-use from Dart2JSTarget and Dart2WasmTarget in web.dart. * Remove expected flag from test commands in web_test.dart and web_dry_run_test.dart. Fixes #190465 --- .../flutter_tools/lib/src/build_system/targets/web.dart | 9 ++------- .../build_system/targets/web_dry_run_test.dart | 1 - .../general.shard/build_system/targets/web_test.dart | 2 -- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 11bd122791c9f..98a05a5384dc4 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -199,10 +199,7 @@ class Dart2JSTarget extends Dart2WebTarget { else if (buildMode == BuildMode.release) '-Ddart.vm.product=true', for (final String dartDefine in computeDartDefines(environment)) '-D$dartDefine', - if (featureFlags.isRecordUseEnabled) ...[ - '--write-resources', - '--enable-experiment=record-use', - ], + if (featureFlags.isRecordUseEnabled) '--write-resources', ]; // NOTE: most args should be populated in [toSharedCommandOptions]. @@ -374,10 +371,8 @@ class Dart2WasmTarget extends Dart2WebTarget { ...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions), for (final String dartDefine in dartDefines) '-D$dartDefine', '--extra-compiler-option=--depfile=${depFile.path}', - if (featureFlags.isRecordUseEnabled) ...[ + if (featureFlags.isRecordUseEnabled) '--recorded-uses=${environment.buildDir.childFile(LinkHooks.recordedUsesWasmFileName).path}', - '--enable-experiment=record-use', - ], ...compilerConfig.toCommandOptions(buildMode), '-o', outputWasmFile.path, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart index cffa0fc0c24a1..3ea85d5f4f2a2 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart @@ -98,7 +98,6 @@ name: my_app '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--extra-compiler-option=--depfile=${environment.buildDir.childFile('dart2wasm.d').path}', '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').path}', - '--enable-experiment=record-use', '-O0', '--no-strip-wasm', '--no-minify', diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index 07966a8c91e0d..4ee8b23b18a9b 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -41,7 +41,6 @@ const _kStandardFlutterWebDefines = [ '-DFLUTTER_WEB_USE_SKWASM=false', '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--write-resources', - '--enable-experiment=record-use', ]; const _kDart2WasmLinuxArgs = [ @@ -1367,7 +1366,6 @@ _flutter.loader.load(); '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--extra-compiler-option=--depfile=${depFile.absolute.path}', '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').absolute.path}', - '--enable-experiment=record-use', '-O$expectedLevel', if (strip && buildMode == 'release') '--strip-wasm' From 497ab92f767ffc26538be377649563fdd1e542df Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Wed, 5 Aug 2026 06:10:20 +0900 Subject: [PATCH 053/330] iOS: Eliminate use of IOSContextNoop in platform view tests (reland) (#190509) This relands #190419, which I reverted in #190501 because it broke the tree due to a (non-merge) conflict with #190416. #190419 added a local `FakeIOSContext` whose `GetBackend()` override returned `flutter::IOSRenderingBackend::kImpeller`. #190416 landed immediately before it and removed the `IOSContext::GetBackend()` virtual and the `IOSRenderingBackend` enum, so the two changes had no merge conflicts but `FakeIOSContext` no longer compiled once both were in the tree. This reland is identical other than deleting the `GetBackend()` override. `FakeIOSContext` still inherits the null Impeller/Aiks contexts from the base class and returns no external texture, matching the behaviour the tests rely on. Original change: `FlutterPlatformViewsTest` used the internal engine class `flutter::IOSContextNoop` in a dozen places purely as a no-op context to hand to `submitFrame:withIosContext:`. This replaces those with a local `FakeIOSContext` that replicates the behaviour the tests rely on so that we can delete `IOSContextNoop` in a follow-up. Back in ancient times, before the simulator required Metal, and when iOS still had a Skia software renderer, we ran that on the Simulator due to some issues with our OpenGL implementation. Later, Flutter on iOS migrated from Skia to Impeller. Skia supports a software backend but Impeller did not, and so when running on Impeller, we stubbed out the software backend to no-op context/surfaces, hence IOSContextNoop and IOSSurfaceNoop. The Simulator now *requires* Metal and Flutter has eliminated Skia support altogether so there's no longer a software mode at all, nor a need for a no-op path in case you're using Impeller and ask for a software backend. This is part of a series of changes to remove the dead no-op fallback. No behavioural change; just a test refactoring. Issue: https://github.com/flutter/flutter/issues/190041 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Source/FlutterPlatformViewsTest.mm | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 1fdcb42de2585..d40b5a8f9f9c2 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -26,11 +26,27 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTestHelper.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView+Test.h" -#include "flutter/shell/platform/darwin/ios/ios_context_noop.h" +#include "flutter/shell/platform/darwin/ios/ios_context.h" #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" FLUTTER_ASSERT_ARC +namespace { +// An IOSContext fake for tests that do not need a real GPU context. +class FakeIOSContext : public flutter::IOSContext { + public: + FakeIOSContext() = default; + ~FakeIOSContext() override = default; + + // |IOSContext| + std::unique_ptr CreateExternalTexture( + int64_t texture_id, + NSObject* texture) override { + return nullptr; + } +}; +} // namespace + @class FlutterPlatformViewsTestMockPlatformView; __weak static UIView* gMockPlatformView = nil; const float kFloatCompareEpsilon = 0.001; @@ -4225,9 +4241,8 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return false; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600)); - XCTAssertFalse([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertFalse([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); auto embeddedViewParams_2 = std::make_unique(finalMatrix, flutter::DlSize(300, 300), stack); @@ -4242,9 +4257,8 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600)); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface_submit_true) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface_submit_true) + withIosContext:std::make_shared()]); } - (void) @@ -4431,9 +4445,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // platform view is wrapped by touch interceptor, which itself is wrapped by clipping view. UIView* clippingView1 = view1.superview.superview; @@ -4460,9 +4473,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] > [flutterView.subviews indexOfObject:clippingView2], @@ -4535,9 +4547,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // platform view is wrapped by touch interceptor, which itself is wrapped by clipping view. UIView* clippingView1 = view1.superview.superview; @@ -4564,9 +4575,8 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] < [flutterView.subviews indexOfObject:clippingView2], @@ -5015,9 +5025,8 @@ - (void)testDisposingViewInCompositionOrderDoNotCrash { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // Disposing won't remove embedded views until the view is removed from the composition_order_ XCTAssertEqual(flutterPlatformViewsController.embeddedViewCount, 2UL); @@ -5042,9 +5051,8 @@ - (void)testDisposingViewInCompositionOrderDoNotCrash { [](const flutter::SurfaceFrame& surface_frame, flutter::DlCanvas* canvas) { return true; }, [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); - XCTAssertTrue([flutterPlatformViewsController - submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]); + XCTAssertTrue([flutterPlatformViewsController submitFrame:std::move(mock_surface) + withIosContext:std::make_shared()]); // Disposing won't remove embedded views until the view is removed from the composition_order_ XCTAssertEqual(flutterPlatformViewsController.embeddedViewCount, 1UL); @@ -5108,7 +5116,7 @@ - (void)testOnlyPlatformViewsAreRemovedWhenReset { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; UIView* someView = [[UIView alloc] init]; [flutterView addSubview:someView]; @@ -5174,7 +5182,7 @@ - (void)testResetClearsPreviousCompositionOrder { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; // The above code should result in previousCompositionOrder having one viewId in it XCTAssertEqual(flutterPlatformViewsController.previousCompositionOrder.count, 1ul); @@ -5243,7 +5251,7 @@ - (void)testNilPlatformViewDoesntCrash { [](const flutter::SurfaceFrame& surface_frame) { return true; }, /*frame_size=*/flutter::DlISize(800, 600), nullptr, /*display_list_fallback=*/true); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; XCTAssertEqual(flutterView.subviews.count, 1u); } @@ -5354,7 +5362,7 @@ - (void)testFlutterPlatformViewControllerSubmitFramePreservingFrameDamage { }); [flutterPlatformViewsController submitFrame:std::move(mock_surface) - withIosContext:std::make_shared()]; + withIosContext:std::make_shared()]; XCTAssertTrue(submit_info.has_value()); XCTAssertEqual(*submit_info->frame_damage, flutter::DlIRect::MakeWH(800, 600)); From 2d5d4c2acb8bdfdf0ea41d0f836579534345c94f Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Wed, 5 Aug 2026 06:21:36 +0900 Subject: [PATCH 054/330] iOS,macOS: make Logger thread-safe, conform to Sendable (#190488) Previously, `Logger.shared` was a mutable static variable, and updating `Logger.logLevel` or `Logger.outputWriter` replaced the entire singleton instance without any synchronisation. This could cause data races when logging from multiple threads, and prevented `Logger` from conforming to `Sendable` under strict Swift concurrency. We now make `shared` an immutable constant and guard its mutable `logLevel` and `outputWriter` state behind a lock. The caller-supplied message autoclosure is evaluated outside the lock, both to keep the lock scope minimal and to avoid deadlocking should the closure re-enter `Logger` since the lock is not reentrant. Issue: https://github.com/flutter/flutter/issues/44030 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../common/framework/Source/Logger.swift | 65 ++++++++++++++----- .../framework/Source/LoggerTestUtils.swift | 2 +- .../common/framework/Source/LoggerTests.swift | 30 +++++++++ 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift index c396bc97d6d73..4f0c55b176d0e 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift @@ -9,7 +9,7 @@ import Foundation /// /// These levels are used by `Logger` to determine if a message should be output. /// They are ordered by increasing severity. -@objc(FlutterLogLevel) public enum LogLevel: Int { +@objc(FlutterLogLevel) public enum LogLevel: Int, Sendable { /// Informational messages that are helpful for tracing application flow. case info @@ -38,14 +38,38 @@ import Foundation /// Logger.logLevel = .warning // Only show warnings and above /// Logger.logError("Failed to load asset: \(assetKey)") /// ``` -@objc(FlutterLogger) public final class Logger: NSObject { - private static var shared = Logger() - private let outputWriter: OutputWriter - public let logLevel: LogLevel +@objc(FlutterLogger) public final class Logger: NSObject, @unchecked Sendable { + private static let shared = Logger() + private let lock = NSLock() + private var _outputWriter: OutputWriter + private var _logLevel: LogLevel + + public var outputWriter: OutputWriter { + get { + return lock.withLock { _outputWriter } + } + set { + lock.withLock { + _outputWriter = newValue + } + } + } + + public var logLevel: LogLevel { + get { + return lock.withLock { _logLevel } + } + set { + lock.withLock { + _logLevel = newValue + } + } + } public init(outputWriter: OutputWriter, logLevel: LogLevel) { - self.outputWriter = outputWriter - self.logLevel = logLevel + self._outputWriter = outputWriter + self._logLevel = logLevel + super.init() } public override convenience init() { @@ -60,8 +84,19 @@ import Foundation } public func log(level: LogLevel, _ message: @autoclosure () -> String) { - if level.rawValue >= logLevel.rawValue { - outputWriter.writeLine(level: level, message()) + guard level.rawValue >= logLevel.rawValue else { return } + + // Evaluate outside the lock keep lock time minimal and to guard against the possibility of + // someone accidentally calling the Logger from within the autoclosure. + let line = message() + lock.withLock { + _outputWriter.writeLine(level: level, line) + } + } + + public func logDirect(_ message: String) { + lock.withLock { + _outputWriter.writeLine(level: .important, message) } } } @@ -70,13 +105,13 @@ extension Logger { /// Sets the minimum log level. @objc public static var outputWriter: OutputWriter { get { return shared.outputWriter } - set(newValue) { shared = Logger(outputWriter: newValue, logLevel: shared.logLevel) } + set(newValue) { shared.outputWriter = newValue } } /// Sets the minimum log level. @objc public static var logLevel: LogLevel { get { return shared.logLevel } - set(newValue) { shared = Logger(outputWriter: shared.outputWriter, logLevel: newValue) } + set(newValue) { shared.logLevel = newValue } } /// Logs a message at `LogLevel.info`. @@ -138,16 +173,16 @@ extension Logger { /// Logs a message unconditionally. @objc public static func logDirect(_ message: String) { - shared.outputWriter.writeLine(level: .important, message) + shared.logDirect(message) } } @objc(FlutterOutputWriter) -public protocol OutputWriter { +public protocol OutputWriter: Sendable { func writeLine(level: LogLevel, _ message: String) } -final class SyslogOutputWriter: OutputWriter { +final class SyslogOutputWriter: OutputWriter, Sendable { func writeLine(level: LogLevel, _ message: String) { // TODO(cbracken): replace this with os_log-based approach. // https://github.com/flutter/flutter/issues/44030 @@ -155,7 +190,7 @@ final class SyslogOutputWriter: OutputWriter { } } -final class StdoutOutputWriter: OutputWriter { +final class StdoutOutputWriter: OutputWriter, Sendable { func writeLine(level: LogLevel, _ message: String) { fputs(message, stdout) fputs("\n", stdout) diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift index 43121bb348268..7b73808299773 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift @@ -7,7 +7,7 @@ import InternalFlutterSwiftCommon /// An `OutputWriter` that stores the most recently logged output in a string. @objc(FlutterStringOutputWriter) -public final class StringOutputWriter: NSObject, OutputWriter { +public final class StringOutputWriter: NSObject, OutputWriter, @unchecked Sendable { @objc public var didLog = false public var lastLevel: LogLevel! @objc public var lastLine: String! diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift index 20181fae40d1a..9d020789c3ffd 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift @@ -105,4 +105,34 @@ import test_utils_swift #expect(!wasEvaluated) } + // Hammers the shared `Logger` from concurrent tasks so that unsynchronised access to mutable + // state is caught under TSan. + @Test func testConcurrentLoggingAndLevelMutation() async { + let writer = StringOutputWriter() + let oldWriter = Logger.outputWriter + let oldLevel = Logger.logLevel + defer { + Logger.outputWriter = oldWriter + Logger.logLevel = oldLevel + } + Logger.outputWriter = writer + Logger.logLevel = .info + + await withTaskGroup(of: Void.self) { group in + for i in 0..<100 { + group.addTask { + if i % 2 == 0 { + Logger.logLevel = .warning + } else { + Logger.logLevel = .info + } + Logger.logInfo("Message \(i)") + } + } + } + + // After concurrent mutations and logging, Logger should remain in a valid state without data + // races or crashes. + #expect(Logger.logLevel == .info || Logger.logLevel == .warning) + } } From 08d3f0bbdbcd895668987c1509e7a1733225bdd0 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 4 Aug 2026 17:22:54 -0400 Subject: [PATCH 055/330] [flutter_tools] Gracefully handle locked Windows files during clean (#190095) ## Description This PR adds a `--stop-gradle` flag to `flutter clean` and an interactive terminal prompt on Windows to offer stopping active Gradle daemons (`gradlew --stop`) when files in `build/` are locked by background Java/Gradle processes on Windows. Fixes https://github.com/flutter/flutter/issues/184637 ## Tests - Added unit test in `clean_test.dart` verifying `--stop-gradle` execution, status log output, and retried file deletion. --- .../lib/src/base/error_handling_io.dart | 16 +- .../flutter_tools/lib/src/commands/clean.dart | 112 +++++++-- .../commands.shard/hermetic/clean_test.dart | 235 +++++++++++++++--- 3 files changed, 304 insertions(+), 59 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/error_handling_io.dart b/packages/flutter_tools/lib/src/base/error_handling_io.dart index 64a1ab547823e..e1a96d2f58e0b 100644 --- a/packages/flutter_tools/lib/src/base/error_handling_io.dart +++ b/packages/flutter_tools/lib/src/base/error_handling_io.dart @@ -96,13 +96,21 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { /// This can be used to bypass the [ErrorHandlingFileSystem] permission exit /// checks for situations where failure is acceptable, such as the flutter /// persistent settings cache. - static void noExitOnFailure(void Function() operation) { + static T noExitOnFailure(T Function() operation) { final bool previousValue = ErrorHandlingFileSystem._noExitOnFailure; + ErrorHandlingFileSystem._noExitOnFailure = true; try { - ErrorHandlingFileSystem._noExitOnFailure = true; - operation(); - } finally { + final T result = operation(); + if (result is Future) { + return (result.whenComplete(() { + ErrorHandlingFileSystem._noExitOnFailure = previousValue; + })) as T; + } + ErrorHandlingFileSystem._noExitOnFailure = previousValue; + return result; + } catch (_) { ErrorHandlingFileSystem._noExitOnFailure = previousValue; + rethrow; } } diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart index e382891db70ee..59a8558f3b9f1 100644 --- a/packages/flutter_tools/lib/src/commands/clean.dart +++ b/packages/flutter_tools/lib/src/commands/clean.dart @@ -29,6 +29,13 @@ class CleanCommand extends FlutterCommand { 'Also clean the example directory, if one exists. ' 'Useful when developing in a package project.', ); + argParser.addFlag( + 'stop-gradle', + negatable: false, + help: + 'Force stop active Gradle daemons before or during clean. ' + 'Useful on Windows when files in build/ are locked by background processes.', + ); } final bool _verbose; @@ -75,24 +82,24 @@ class CleanCommand extends FlutterCommand { } final Directory buildDir = flutterProject.directory.childDirectory(getBuildDirectory()); - deleteFile(buildDir); + await deleteFile(buildDir, flutterProject); - deleteFile(flutterProject.dartTool); + await deleteFile(flutterProject.dartTool, flutterProject); - deleteFile(flutterProject.android.ephemeralDirectory); + await deleteFile(flutterProject.android.ephemeralDirectory, flutterProject); - deleteFile(flutterProject.ios.ephemeralDirectory); - deleteFile(flutterProject.ios.ephemeralModuleDirectory); - deleteFile(flutterProject.ios.generatedXcodePropertiesFile); - deleteFile(flutterProject.ios.generatedEnvironmentVariableExportScript); - deleteFile(flutterProject.ios.deprecatedCompiledDartFramework); - deleteFile(flutterProject.ios.deprecatedProjectFlutterFramework); - deleteFile(flutterProject.ios.flutterPodspec); + await deleteFile(flutterProject.ios.ephemeralDirectory, flutterProject); + await deleteFile(flutterProject.ios.ephemeralModuleDirectory, flutterProject); + await deleteFile(flutterProject.ios.generatedXcodePropertiesFile, flutterProject); + await deleteFile(flutterProject.ios.generatedEnvironmentVariableExportScript, flutterProject); + await deleteFile(flutterProject.ios.deprecatedCompiledDartFramework, flutterProject); + await deleteFile(flutterProject.ios.deprecatedProjectFlutterFramework, flutterProject); + await deleteFile(flutterProject.ios.flutterPodspec, flutterProject); - deleteFile(flutterProject.linux.ephemeralDirectory); - deleteFile(flutterProject.macos.ephemeralDirectory); - deleteFile(flutterProject.windows.ephemeralDirectory); - deleteFile(flutterProject.flutterPluginsDependenciesFile); + await deleteFile(flutterProject.linux.ephemeralDirectory, flutterProject); + await deleteFile(flutterProject.macos.ephemeralDirectory, flutterProject); + await deleteFile(flutterProject.windows.ephemeralDirectory, flutterProject); + await deleteFile(flutterProject.flutterPluginsDependenciesFile, flutterProject); } Future _cleanXcode(XcodeBasedProject xcodeProject) async { @@ -146,17 +153,15 @@ class CleanCommand extends FlutterCommand { } @visibleForTesting - void deleteFile(FileSystemEntity file) { + Future deleteFile(FileSystemEntity file, [FlutterProject? project]) async { try { - ErrorHandlingFileSystem.noExitOnFailure(() { - _deleteFile(file); - }); + await ErrorHandlingFileSystem.noExitOnFailure(() => _deleteFile(file, project)); } on Exception catch (e) { globals.printError('Failed to remove ${file.path}: $e'); } } - void _deleteFile(FileSystemEntity file) { + Future _deleteFile(FileSystemEntity file, FlutterProject? project) async { // This will throw a FileSystemException if the directory is missing permissions. try { if (!file.existsSync()) { @@ -170,19 +175,80 @@ class CleanCommand extends FlutterCommand { try { file.deleteSync(recursive: true); } on FileSystemException catch (error) { + deletionStatus.stop(); final String path = file.path; if (globals.platform.isWindows) { + if (await _tryStopGradleAndRetryDelete(file, project)) { + return; + } + globals.printError( 'Failed to remove $path. ' - 'A program may still be using a file in the directory or the directory itself. ' - 'To find and stop such a program, see: ' - 'https://superuser.com/questions/1333118/cant-delete-empty-folder-because-it-is-used', + 'A background process (e.g. Gradle daemon or Java) is locking files in the directory.\n' + 'To automatically stop Gradle daemons during clean, run:\n' + ' flutter clean --stop-gradle\n' + 'Or manually stop daemons:\n' + ' cd android && ./gradlew --stop', ); } else { globals.printError('Failed to remove $path: $error'); } + } + } + + /// Attempts to stop active Gradle daemons via `gradlew --stop` when Windows file locks + /// prevent deletion of build files, either via the `--stop-gradle` flag or an interactive prompt. + /// + /// Retries file deletion after stopping Gradle daemons and returns `true` if deletion succeeds. + Future _tryStopGradleAndRetryDelete(FileSystemEntity file, FlutterProject? project) async { + final bool stopGradleFlag = + (argResults?.wasParsed('stop-gradle') ?? false) && boolArg('stop-gradle'); + final bool isInteractive = globals.terminal.stdinHasTerminal && globals.terminal.usesTerminalUi; + var shouldStopGradle = stopGradleFlag; + + if (!stopGradleFlag && isInteractive) { + try { + final String choice = await globals.terminal.promptForCharInput( + ['y', 'n'], + logger: globals.logger, + prompt: + 'Files in build/ are locked by background processes (likely Gradle).\n' + 'Stop active Gradle daemons ("gradlew --stop") and retry clean? [y/N]', + defaultChoiceIndex: 1, + ); + shouldStopGradle = choice.toLowerCase() == 'y'; + } on StateError { + shouldStopGradle = false; + } + } + + if (!shouldStopGradle) { + return false; + } + + final FlutterProject flutterProject = project ?? FlutterProject.current(); + final File gradlewFile = flutterProject.android.hostAppGradleRoot.childFile('gradlew.bat'); + if (!gradlewFile.existsSync()) { + return false; + } + + final Status stopStatus = globals.logger.startProgress('Stopping Gradle daemons...'); + try { + await globals.processUtils.run([ + gradlewFile.path, + '--stop', + ], workingDirectory: gradlewFile.parent.path); + } on Exception catch (e) { + globals.printTrace('Failed to stop Gradle daemons: $e'); } finally { - deletionStatus.stop(); + stopStatus.stop(); + } + + try { + file.deleteSync(recursive: true); + return true; + } on FileSystemException { + return false; } } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart index 1733e9efdbff8..77551757d2037 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_tools/src/base/error_handling_io.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/clean.dart'; @@ -323,11 +324,14 @@ void main() { late FakePlatform windowsPlatform; late MemoryFileSystem fileSystem; late FileExceptionHandler exceptionHandler; + late FakeProcessManager processManager; setUp(() { windowsPlatform = FakePlatform(operatingSystem: 'windows'); exceptionHandler = FileExceptionHandler(); fileSystem = MemoryFileSystem.test(opHandle: exceptionHandler.opHandle); + fileSystem.file('pubspec.yaml').createSync(recursive: true); + processManager = FakeProcessManager.any(); }); testUsingContext( @@ -343,50 +347,217 @@ void main() { ); final command = CleanCommand(); - command.deleteFile(file); - expect(testLogger.errorText, contains('A program may still be using a file')); + await command.deleteFile(file); + expect( + testLogger.errorText, + contains('A background process (e.g. Gradle daemon or Java) is locking files'), + ); }, overrides: { Platform: () => windowsPlatform, Xcode: () => xcode, FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.any(), + ProcessManager: () => processManager, }, ); - testUsingContext('$CleanCommand handles missing delete permissions', () async { - final handler = FileExceptionHandler(); - - // Ensures we handle ErrorHandlingFileSystem appropriately in prod. - // See https://github.com/flutter/flutter/issues/108978. - final FileSystem fileSystem = ErrorHandlingFileSystem( - delegate: MemoryFileSystem.test(opHandle: handler.opHandle), - platform: windowsPlatform, - ); - final File throwingFile = fileSystem.file('bad')..createSync(); - handler.addError( - throwingFile, - FileSystemOp.delete, - const FileSystemException('OS error: Access Denied'), - ); - - xcodeProjectInterpreter.isInstalled = false; - - final command = CleanCommand(); - command.deleteFile(throwingFile); - - expect( - testLogger.errorText, - contains( - 'Failed to remove bad. A program may still be using a file in the directory or the directory itself', - ), - ); - expect(throwingFile, exists); - }, overrides: {Platform: () => windowsPlatform, Xcode: () => xcode}); + testUsingContext( + '$CleanCommand handles missing delete permissions', + () async { + final handler = FileExceptionHandler(); + + // Ensures we handle ErrorHandlingFileSystem appropriately in prod. + // See https://github.com/flutter/flutter/issues/108978. + final FileSystem fileSystem = ErrorHandlingFileSystem( + delegate: MemoryFileSystem.test(opHandle: handler.opHandle), + platform: windowsPlatform, + ); + final File throwingFile = fileSystem.file('bad')..createSync(); + handler.addError( + throwingFile, + FileSystemOp.delete, + const FileSystemException('OS error: Access Denied'), + ); + + xcodeProjectInterpreter.isInstalled = false; + + final command = CleanCommand(); + await command.deleteFile(throwingFile); + + expect( + testLogger.errorText, + contains( + 'Failed to remove bad. A background process (e.g. Gradle daemon or Java) is locking files', + ), + ); + expect(throwingFile, exists); + }, + overrides: {Platform: () => windowsPlatform, Xcode: () => xcode}, + ); + + testUsingContext( + '$CleanCommand invokes gradlew --stop and retries deletion when --stop-gradle flag is passed', + () async { + xcodeProjectInterpreter.isInstalled = false; + + var shouldThrow = true; + fileSystem = MemoryFileSystem.test( + opHandle: (String path, FileSystemOp op) { + if (shouldThrow && op == FileSystemOp.delete && path.endsWith('build')) { + throw const FileSystemException('Locked'); + } + }, + ); + fileSystem.file('pubspec.yaml').createSync(recursive: true); + + final FlutterProject project = setupProjectUnderTest(fileSystem.currentDirectory, false); + final File gradlewFile = project.android.hostAppGradleRoot.childFile('gradlew.bat') + ..createSync(recursive: true); + + final Directory buildDir = project.directory.childDirectory('build') + ..createSync(recursive: true); + buildDir.childFile('locked').createSync(recursive: true); + + processManager = FakeProcessManager.list([ + FakeCommand( + command: [gradlewFile.path, '--stop'], + workingDirectory: gradlewFile.parent.path, + onRun: (_) { + shouldThrow = false; + }, + ), + ]); + + final command = CleanCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run(['clean', '--stop-gradle']); + + expect(testLogger.statusText, contains('Stopping Gradle daemons')); + }, + overrides: { + Platform: () => windowsPlatform, + Xcode: () => xcode, + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }, + ); + + testUsingContext( + '$CleanCommand prompts user and invokes gradlew --stop when locked on Windows interactively', + () async { + xcodeProjectInterpreter.isInstalled = false; + + var shouldThrow = true; + fileSystem = MemoryFileSystem.test( + opHandle: (String path, FileSystemOp op) { + if (shouldThrow && op == FileSystemOp.delete && path.endsWith('build')) { + throw const FileSystemException('Locked'); + } + }, + ); + fileSystem.file('pubspec.yaml').createSync(recursive: true); + + final FlutterProject project = setupProjectUnderTest(fileSystem.currentDirectory, false); + final File gradlewFile = project.android.hostAppGradleRoot.childFile('gradlew.bat') + ..createSync(recursive: true); + + final Directory buildDir = project.directory.childDirectory('build') + ..createSync(recursive: true); + buildDir.childFile('locked').createSync(recursive: true); + + processManager = FakeProcessManager.list([ + FakeCommand( + command: [gradlewFile.path, '--stop'], + workingDirectory: gradlewFile.parent.path, + onRun: (_) { + shouldThrow = false; + }, + ), + ]); + + final command = CleanCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run(['clean']); + + expect(testLogger.statusText, contains('Stopping Gradle daemons')); + }, + overrides: { + Platform: () => windowsPlatform, + Xcode: () => xcode, + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + AnsiTerminal: () => FakeTerminal(), + }, + ); + + testUsingContext( + '$CleanCommand prompts user but skips gradlew --stop when user declines prompt', + () async { + xcodeProjectInterpreter.isInstalled = false; + + fileSystem = MemoryFileSystem.test( + opHandle: (String path, FileSystemOp op) { + if (op == FileSystemOp.delete && path.endsWith('build')) { + throw const FileSystemException('Locked'); + } + }, + ); + fileSystem.file('pubspec.yaml').createSync(recursive: true); + + final FlutterProject project = setupProjectUnderTest(fileSystem.currentDirectory, false); + project.android.hostAppGradleRoot.childFile('gradlew.bat').createSync(recursive: true); + + final Directory buildDir = project.directory.childDirectory('build') + ..createSync(recursive: true); + buildDir.childFile('locked').createSync(recursive: true); + + processManager = FakeProcessManager.empty(); + + final command = CleanCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run(['clean']); + + expect(testLogger.statusText, isNot(contains('Stopping Gradle daemons'))); + expect( + testLogger.errorText, + contains('A background process (e.g. Gradle daemon or Java) is locking files'), + ); + }, + overrides: { + Platform: () => windowsPlatform, + Xcode: () => xcode, + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + AnsiTerminal: () => FakeTerminal(response: 'n'), + }, + ); }); }); } +class FakeTerminal extends Fake implements AnsiTerminal { + FakeTerminal({this.response = 'y'}); + + final String response; + + @override + bool get stdinHasTerminal => true; + + @override + bool get usesTerminalUi => true; + + @override + Future promptForCharInput( + List acceptedCharacters, { + Logger? logger, + String? prompt, + int? defaultChoiceIndex, + bool displayAcceptedCharacters = true, + }) async { + return response; + } +} + FlutterProject setupProjectUnderTest(Directory currentDirectory, bool setupXcodeWorkspace) { // This needs to be run within testWithoutContext and not setUp since FlutterProject uses context. final FlutterProject projectUnderTest = FlutterProject.fromDirectory(currentDirectory); From da1cd5aebea5f753662deb9e6a8a8c8c9867e06d Mon Sep 17 00:00:00 2001 From: Kishan Rathore <34465683+rkishan516@users.noreply.github.com> Date: Wed, 5 Aug 2026 03:20:25 +0530 Subject: [PATCH 056/330] fix: update on_message_ to nullptr after window destroy so that dart gets destroy message (#185807) This PR moves setting on_message_ callback to nullptr after window destroy --- .../shell/platform/windows/window_manager.cc | 8 +- .../windows/window_manager_unittests.cc | 105 ++++++++++++++++++ .../lib/src/widgets/_window_win32.dart | 1 + 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/window_manager.cc b/engine/src/flutter/shell/platform/windows/window_manager.cc index 3ef1416c96c2c..ba1d9f839d68b 100644 --- a/engine/src/flutter/shell/platform/windows/window_manager.cc +++ b/engine/src/flutter/shell/platform/windows/window_manager.cc @@ -87,19 +87,23 @@ FlutterViewId WindowManager::CreatePopupWindow( } void WindowManager::OnEngineShutdown() { - // Don't send any more messages to isolate. - on_message_ = nullptr; std::vector active_handles; active_handles.reserve(active_windows_.size()); for (auto& [hwnd, window] : active_windows_) { active_handles.push_back(hwnd); } + // Destroy the windows before clearing |on_message_| so the WM_DESTROY + // round-trip reaches the isolate. Otherwise per-view Dart controllers + // never observe destruction and may issue follow-up FFI calls (e.g. + // updatePosition) with stale handles after the engine is torn down. for (auto hwnd : active_handles) { // This will destroy the window, which will in turn remove the // HostWindow from map when handling WM_NCDESTROY inside // HandleMessage. InternalFlutterWindows_WindowManager_OnDestroyWindow(hwnd); } + // Don't send any more messages to isolate. + on_message_ = nullptr; } std::optional WindowManager::HandleMessage(HWND hwnd, diff --git a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc index f813e36664d7a..9c33e261b5c1b 100644 --- a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include +#include + #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/testing/egl/mock_context.h" #include "flutter/shell/platform/windows/testing/egl/mock_manager.h" @@ -1142,5 +1145,107 @@ TEST_F(WindowManagerTest, EXPECT_EQ(style & WS_THICKFRAME, 0L); } +// Verifies that |OnEngineShutdown| destroys popup windows BEFORE clearing +// |on_message_|, so the WM_DESTROY round-trip reaches the isolate. Without +// this, |PopupWindowControllerWin32._destroyed| would never be set during +// engine shutdown and queued FFI calls (e.g. updatePosition) would +// dereference stale HostWindow pointers. +TEST_F(WindowManagerTest, OnEngineShutdownDispatchesWmDestroyForPopupWindow) { + IsolateScope isolate_scope(isolate()); + + static std::vector received_messages; + received_messages.clear(); + WindowingInitRequest init_request{.on_message = [](WindowsMessage* message) { + received_messages.push_back(message->message); + }}; + InternalFlutterWindows_WindowManager_Initialize(engine_id(), &init_request); + + const int64_t parent_view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), regular_creation_request()); + const HWND parent_window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle( + engine_id(), parent_view_id); + + auto position_callback = [](const WindowSize& child_size, + const WindowRect& parent_rect, + const WindowRect& output_rect) -> WindowRect* { + WindowRect* rect = static_cast(malloc(sizeof(WindowRect))); + rect->left = parent_rect.left + 10; + rect->top = parent_rect.top + 10; + rect->width = child_size.width; + rect->height = child_size.height; + return rect; + }; + + PopupWindowCreationRequest creation_request{ + .preferred_constraints = {.has_view_constraints = true, + .view_min_width = 100, + .view_min_height = 50, + .view_max_width = 300, + .view_max_height = 200}, + .parent = parent_window_handle, + .get_position_callback = position_callback}; + + InternalFlutterWindows_WindowManager_CreatePopupWindow(engine_id(), + &creation_request); + + received_messages.clear(); + engine()->window_manager()->OnEngineShutdown(); + + EXPECT_NE(std::find(received_messages.begin(), received_messages.end(), + static_cast(WM_DESTROY)), + received_messages.end()); +} + +// Same as above for tooltips. +TEST_F(WindowManagerTest, OnEngineShutdownDispatchesWmDestroyForTooltipWindow) { + IsolateScope isolate_scope(isolate()); + + static std::vector received_messages; + received_messages.clear(); + WindowingInitRequest init_request{.on_message = [](WindowsMessage* message) { + received_messages.push_back(message->message); + }}; + InternalFlutterWindows_WindowManager_Initialize(engine_id(), &init_request); + + const int64_t parent_view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), regular_creation_request()); + const HWND parent_window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle( + engine_id(), parent_view_id); + + auto position_callback = [](const WindowSize& child_size, + const WindowRect& parent_rect, + const WindowRect& output_rect) -> WindowRect* { + WindowRect* rect = static_cast(malloc(sizeof(WindowRect))); + rect->left = parent_rect.left + 10; + rect->top = parent_rect.top + 10; + rect->width = child_size.width; + rect->height = child_size.height; + return rect; + }; + + TooltipWindowCreationRequest creation_request{ + .preferred_constraints = {.has_view_constraints = true, + .view_min_width = 100, + .view_min_height = 50, + .view_max_width = 300, + .view_max_height = 200}, + .parent = parent_window_handle, + .get_position_callback = position_callback}; + + InternalFlutterWindows_WindowManager_CreateTooltipWindow(engine_id(), + &creation_request); + + received_messages.clear(); + engine()->window_manager()->OnEngineShutdown(); + + EXPECT_NE(std::find(received_messages.begin(), received_messages.end(), + static_cast(WM_DESTROY)), + received_messages.end()); +} + } // namespace testing } // namespace flutter diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 0e9d3db33c7a1..2fe25351303b4 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -917,6 +917,7 @@ class TooltipWindowControllerWin32 extends TooltipWindowController @override void updatePosition({Rect? anchorRect, WindowPositioner? positioner}) { + _ensureNotDestroyed(); if (anchorRect != null) { _anchorRect = anchorRect; } From b37d0564a86eb9988b9872fb8b82737135cdfd63 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Wed, 5 Aug 2026 06:53:33 +0900 Subject: [PATCH 057/330] iOS,macOS: add tsan and ubsan support for Swift (#190497) Previously asan/tsan/ubsan builds applied only to C[++]/Obj-C[++] builds but not Swift since the flags were never wired up. This adds support for TSan (`-fsanitize=thread`) and UBSan (`-fsanitize=undefined`) for Swift. ASan is intentionally excluded. Swift is compiled with the Apple toolchain, while C/C++ and the link step use the Fuchsia clang from CIPD, and the two ship different ASan runtimes. Apple's swift-frontend stamps each instrumented module with a version-mismatch guard that the linked Fuchsia runtime doesn't provide, so Swift + ASan fails to link with an undefined symbol: Swift objects reference: ___asan_version_mismatch_check_apple_clang_2100 Fuchsia asan runtime has: ___asan_version_mismatch_check_v8 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- engine/src/build/config/compiler/BUILD.gn | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/engine/src/build/config/compiler/BUILD.gn b/engine/src/build/config/compiler/BUILD.gn index e06cf625abe8f..0e95476b4b2f6 100644 --- a/engine/src/build/config/compiler/BUILD.gn +++ b/engine/src/build/config/compiler/BUILD.gn @@ -143,6 +143,11 @@ config("compiler") { if (is_asan) { cflags += [ "-fsanitize=address" ] ldflags += [ "-fsanitize=address" ] + + # ASan is deliberately not added to swiftflags. + # The Swift frontend emits an ASan version-mismatch guard + # (__asan_version_mismatch_check_apple_clang_*) that the engine's clang + # ASan runtime doesn't provide, so instrumenting Swift fails to link. } if (is_hwasan && is_android && current_cpu == "arm64") { cflags += [ "-fsanitize=hwaddress" ] @@ -155,6 +160,7 @@ config("compiler") { if (is_tsan) { cflags += [ "-fsanitize=thread" ] ldflags += [ "-fsanitize=thread" ] + swiftflags += [ "-sanitize=thread" ] } if (is_msan) { cflags += [ "-fsanitize=memory" ] @@ -163,6 +169,10 @@ config("compiler") { if (is_ubsan) { cflags += [ "-fsanitize=undefined" ] ldflags += [ "-fsanitize=undefined" ] + + # UBSan is deliberately not added to swiftflags. The Swift compiler + # accepts -sanitize=undefined but emits no UBSan instrumentation for Swift + # code. } if (use_custom_libcxx) { @@ -435,9 +445,9 @@ config("compiler") { # to say that it does. Define them here instead. defines += [ "HAVE_SYS_UIO_H" ] - # When Android requires new flags consider also editing the flags in - # the following locations. - # Framework plugin_ffi template: packages/flutter_tools/templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl + # When Android requires new flags consider also editing the flags in the + # following locations. Framework plugin_ffi template: + # packages/flutter_tools/templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl # Example PR: https://github.com/flutter/flutter/pull/155508 # Dart Lang JNI package: pkgs/jni/src/CMakeLists.txt # Example PR: https://github.com/dart-lang/native/pull/1615 @@ -1023,7 +1033,8 @@ config("optimize") { } lto_flags = [] - if (enable_lto && (is_linux || is_ios || is_mac || is_android || is_fuchsia || is_wasm)) { + if (enable_lto && + (is_linux || is_ios || is_mac || is_android || is_fuchsia || is_wasm)) { lto_flags += [ "-flto" ] } From fa4be6cd61f8537f2c2c73e9e360adb27abb06bc Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 4 Aug 2026 17:57:25 -0400 Subject: [PATCH 058/330] Roll Fuchsia Test Scripts from 1frGe_KltAJKkeyPg... to ltbuIH9Z3T_yOuigu... (#190561) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/fuchsia-test-scripts-flutter Please CC chrome-fuchsia-engprod@google.com,codefu@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 47ef841e86e4b..70bacf5f13b4a 100644 --- a/DEPS +++ b/DEPS @@ -199,7 +199,7 @@ vars = { # The version / instance id of the cipd:chromium/fuchsia/test-scripts which # will be used altogether with fuchsia-sdk to setup the build / test # environment. - 'fuchsia_test_scripts_version': '1frGe_KltAJKkeyPgy4cDJqScCYVYSpC9sJfjflcvl4C', + 'fuchsia_test_scripts_version': 'ltbuIH9Z3T_yOuiguCVHKgUR4YOTjj87kcx20LvoNkgC', # The version / instance id of the cipd:chromium/fuchsia/gn-sdk which will be # used altogether with fuchsia-sdk to generate gn based build rules. From ebd8429302bdf326c1a8d87e2187294d6e486e21 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:27:57 -0700 Subject: [PATCH 059/330] Revert: fix(tool): remove redundant --enable-experiment=record-use flag (#190583) Reverts: [fix(tool): remove redundant --enable-experiment=record-use flag](https://github.com/flutter/flutter/pull/190475) Initiated by: @cbracken Reason for reverting: ``` Original PR Author: @kevmoo Reviewed By: @mdebbar The original PR description is provided below: When compiling web targets (dart2js and dart2wasm) or running dry runs with the record-use feature flag enabled, flutter_tools explicitly passed --enable-experiment=record-use to the compiler. Since record-use is enabled by default in recent Dart SDKs, passing this flag caused warning spam during standard compilation and dry runs. * Remove --enable-experiment=record-use from Dart2JSTarget and Dart2WasmTarget in web.dart. * Remove expected flag from test commands in web_test.dart and web_dry_run_test.dart. Fixes #190465 --- .../flutter_tools/lib/src/build_system/targets/web.dart | 9 +++++++-- .../build_system/targets/web_dry_run_test.dart | 1 + .../general.shard/build_system/targets/web_test.dart | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 98a05a5384dc4..11bd122791c9f 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -199,7 +199,10 @@ class Dart2JSTarget extends Dart2WebTarget { else if (buildMode == BuildMode.release) '-Ddart.vm.product=true', for (final String dartDefine in computeDartDefines(environment)) '-D$dartDefine', - if (featureFlags.isRecordUseEnabled) '--write-resources', + if (featureFlags.isRecordUseEnabled) ...[ + '--write-resources', + '--enable-experiment=record-use', + ], ]; // NOTE: most args should be populated in [toSharedCommandOptions]. @@ -371,8 +374,10 @@ class Dart2WasmTarget extends Dart2WebTarget { ...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions), for (final String dartDefine in dartDefines) '-D$dartDefine', '--extra-compiler-option=--depfile=${depFile.path}', - if (featureFlags.isRecordUseEnabled) + if (featureFlags.isRecordUseEnabled) ...[ '--recorded-uses=${environment.buildDir.childFile(LinkHooks.recordedUsesWasmFileName).path}', + '--enable-experiment=record-use', + ], ...compilerConfig.toCommandOptions(buildMode), '-o', outputWasmFile.path, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart index 3ea85d5f4f2a2..cffa0fc0c24a1 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart @@ -98,6 +98,7 @@ name: my_app '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--extra-compiler-option=--depfile=${environment.buildDir.childFile('dart2wasm.d').path}', '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').path}', + '--enable-experiment=record-use', '-O0', '--no-strip-wasm', '--no-minify', diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index 4ee8b23b18a9b..07966a8c91e0d 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -41,6 +41,7 @@ const _kStandardFlutterWebDefines = [ '-DFLUTTER_WEB_USE_SKWASM=false', '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--write-resources', + '--enable-experiment=record-use', ]; const _kDart2WasmLinuxArgs = [ @@ -1366,6 +1367,7 @@ _flutter.loader.load(); '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--extra-compiler-option=--depfile=${depFile.absolute.path}', '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').absolute.path}', + '--enable-experiment=record-use', '-O$expectedLevel', if (strip && buildMode == 'release') '--strip-wasm' From c55b78436fd2ae679d2dfe30c658a7efaa49c1f2 Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:48:52 -0700 Subject: [PATCH 060/330] Update Widgets Localizations from Translation Console (#190503) This PR is to update localizations in widgets layer from translation console. It doesn't fetch material and cupertino localziations because they have been decoupled from core flutter and will be updated in material_ui and cupertino_ui. Fixes https://github.com/flutter/flutter/issues/190408 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../l10n/generated_widgets_localizations.dart | 27 ++++++++++++++++++- .../lib/src/l10n/widgets_es_ES.arb | 18 +++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/flutter_localizations/lib/src/l10n/widgets_es_ES.arb diff --git a/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart b/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart index 020fa71b928d9..a1c0fbdfca444 100644 --- a/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart +++ b/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart @@ -1354,6 +1354,29 @@ class WidgetsLocalizationEsEc extends WidgetsLocalizationEs { String get radioButtonUnselectedLabel => 'Sin seleccionar'; } +/// The translations for Spanish Castilian, as used in Spain (`es_ES`). +class WidgetsLocalizationEsEs extends WidgetsLocalizationEs { + /// Create an instance of the translation bundle for Spanish Castilian, as used in Spain. + /// + /// For details on the meaning of the arguments, see [GlobalWidgetsLocalizations]. + const WidgetsLocalizationEsEs(); + + @override + String get reorderItemToStart => 'Mover al inicio'; + + @override + String get searchResultsFound => 'Se encontraron resultados de la búsqueda'; + + @override + String get noResultsFound => 'No se encontraron resultados'; + + @override + String get lookUpButtonLabel => 'Consultar'; + + @override + String get radioButtonUnselectedLabel => 'Sin seleccionar'; +} + /// The translations for Spanish Castilian, as used in Guatemala (`es_GT`). class WidgetsLocalizationEsGt extends WidgetsLocalizationEs { /// Create an instance of the translation bundle for Spanish Castilian, as used in Guatemala. @@ -5539,7 +5562,7 @@ final Set kWidgetsSupportedLanguages = HashSet.from(const Date: Tue, 4 Aug 2026 17:49:47 -0700 Subject: [PATCH 061/330] render proxy box now defaults baseline calculation to null (#190269) as title the reason been that there is no reason to have baseline calculation **before** mixing in renderproxyboxmixin since it will get overriden by the implementation in renderproxyboxmixin If a render object wants to provide custom logic if no child or child return null, they should provide the implementation **after** mixing in the renderproxyboxmixin. Plus it can cause crash if base class doesn't have an implementation. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/src/rendering/proxy_box.dart | 8 +--- .../test/rendering/proxy_box_test.dart | 45 +++++++++++++++++-- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index e47a18c9a668f..83acb614649bb 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -95,17 +95,13 @@ mixin RenderProxyBoxMixin on RenderBox, RenderObjectWithChi @override double? computeDistanceToActualBaseline(TextBaseline baseline) { - return child?.getDistanceToActualBaseline(baseline) ?? - super.computeDistanceToActualBaseline(baseline); + return child?.getDistanceToActualBaseline(baseline); } @override @protected double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { - final RenderBox? child = this.child; - return child == null - ? super.computeDryBaseline(constraints, baseline) - : child.getDryBaseline(constraints, baseline); + return child?.getDryBaseline(constraints, baseline); } @override diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index 86e7c888082e3..fe3dc0bb4ee4d 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -1093,10 +1093,7 @@ void main() { test('RenderProxyBoxMixin.computeDryBaseline returns null when the child has no baseline', () { // Regression test for https://github.com/flutter/flutter/issues/189711 final child = _RenderNoBaseline(); - final proxy = RenderSemanticsAnnotations( - child: child, - properties: const SemanticsProperties(), - ); + final proxy = RenderSemanticsAnnotations(child: child, properties: const SemanticsProperties()); layout(proxy); expect( proxy.getDryBaseline( @@ -1106,6 +1103,46 @@ void main() { isNull, ); }); + + test( + 'RenderProxyBoxMixin.computeDistanceToActualBaseline returns null when child is null or child has no baseline', + () { + final proxyNoChild = RenderProxyBox(); + final parentNoChild = _TestBaselineParent(proxyNoChild); + layout(parentNoChild, constraints: BoxConstraints.tight(const Size(40.0, 20.0))); + expect(parentNoChild.childBaseline, isNull); + + final child = _RenderNoBaseline(); + final proxyWithChild = RenderProxyBox(child); + final parentWithChild = _TestBaselineParent(proxyWithChild); + layout(parentWithChild, constraints: BoxConstraints.tight(const Size(40.0, 20.0))); + expect(parentWithChild.childBaseline, isNull); + }, + ); + + test('RenderProxyBoxMixin.computeDryBaseline returns null when child is null', () { + final proxyNoChild = RenderProxyBox(); + expect( + proxyNoChild.getDryBaseline( + const BoxConstraints.tightFor(width: 40.0, height: 20.0), + TextBaseline.alphabetic, + ), + isNull, + ); + }); +} + +class _TestBaselineParent extends RenderProxyBox { + _TestBaselineParent(super.child); + + double? childBaseline; + + @override + void performLayout() { + child!.layout(constraints, parentUsesSize: true); + size = child!.size; + childBaseline = child!.getDistanceToBaseline(TextBaseline.alphabetic, onlyReal: true); + } } class _TestRectClipper extends CustomClipper { From 80cabc33032837357c9b233d693266b890860c12 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 5 Aug 2026 00:54:53 +0000 Subject: [PATCH 062/330] Migrate the shell unit tests from legacy Dart native functions to FFI (#190473) This adds a CREATE_FFI_LAMBDA macro that can be used to register a lambda as a Dart FFI function (similar to how CREATE_NATIVE_ENTRY works for the old native function API). The native functions also had to be ported from the Dart_NativeArguments API to FFI-style function signatures. See https://github.com/flutter/flutter/issues/190154 --- .../shell/common/animator_unittests.cc | 10 +- .../shell/common/dart_native_benchmarks.cc | 12 +- .../shell/common/engine_animator_unittests.cc | 20 +- .../shell/common/fixtures/shell_test.dart | 45 +- .../shell/common/input_events_unittests.cc | 32 +- .../shell/common/shell_fuchsia_unittests.cc | 23 +- .../flutter/shell/common/shell_unittests.cc | 534 ++++++++---------- .../testing/test_dart_native_resolver.h | 30 + 8 files changed, 334 insertions(+), 372 deletions(-) diff --git a/engine/src/flutter/shell/common/animator_unittests.cc b/engine/src/flutter/shell/common/animator_unittests.cc index f5f71ae341f72..dc8e202bafeb7 100644 --- a/engine/src/flutter/shell/common/animator_unittests.cc +++ b/engine/src/flutter/shell/common/animator_unittests.cc @@ -57,14 +57,12 @@ TEST_F(ShellTest, VSyncTargetTime) { int64_t target_time; fml::AutoResetWaitableEvent on_target_time_latch; auto nativeOnBeginFrame = [&on_target_time_latch, - &target_time](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - target_time = - tonic::DartConverter::FromArguments(args, 0, exception); + &target_time](int64_t microseconds) { + target_time = microseconds; on_target_time_latch.Signal(); }; - AddNativeCallback("NativeOnBeginFrame", - CREATE_NATIVE_ENTRY(nativeOnBeginFrame)); + AddFfiNativeCallback("NativeOnBeginFrame", + CREATE_FFI_LAMBDA(nativeOnBeginFrame)); // Create all te prerequisites for a shell. ASSERT_FALSE(DartVMRef::IsInstanceRunning()); diff --git a/engine/src/flutter/shell/common/dart_native_benchmarks.cc b/engine/src/flutter/shell/common/dart_native_benchmarks.cc index ff3999eefe0ac..65e6922595c40 100644 --- a/engine/src/flutter/shell/common/dart_native_benchmarks.cc +++ b/engine/src/flutter/shell/common/dart_native_benchmarks.cc @@ -35,10 +35,8 @@ BENCHMARK_F(DartNativeBenchmarks, TimeToFirstNativeMessageFromIsolateInNewVM) fml::AutoResetWaitableEvent latch; st.PauseTiming(); ASSERT_FALSE(DartVMRef::IsInstanceRunning()); - AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { - latch.Signal(); - }))); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA(([&latch]() { latch.Signal(); }))); const auto settings = CreateSettingsForFixture(); DartVMRef vm_ref = DartVMRef::Create(settings); @@ -72,10 +70,8 @@ BENCHMARK_F(DartNativeBenchmarks, MultipleDartToNativeMessages) fml::CountDownLatch latch(1000); st.PauseTiming(); ASSERT_FALSE(DartVMRef::IsInstanceRunning()); - AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments args) { - latch.CountDown(); - }))); + AddFfiNativeCallback( + "NotifyNative", CREATE_FFI_LAMBDA(([&latch]() { latch.CountDown(); }))); const auto settings = CreateSettingsForFixture(); DartVMRef vm_ref = DartVMRef::Create(settings); diff --git a/engine/src/flutter/shell/common/engine_animator_unittests.cc b/engine/src/flutter/shell/common/engine_animator_unittests.cc index bda96f6085009..a9d041670b6ac 100644 --- a/engine/src/flutter/shell/common/engine_animator_unittests.cc +++ b/engine/src/flutter/shell/common/engine_animator_unittests.cc @@ -303,7 +303,8 @@ TEST_F(EngineAnimatorTest, AnimatorAcceptsMultipleRenders) { }); native_latch.Reset(); - AddNativeCallback("NotifyNative", [](auto args) { native_latch.Signal(); }); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([]() { native_latch.Signal(); })); std::unique_ptr animator; PostSync(task_runners_.GetUITaskRunner(), @@ -395,8 +396,7 @@ TEST_F(EngineAnimatorTest, IgnoresDuplicateRenders) { std::unique_ptr engine_context; std::vector> benchmark_layers; - auto capture_root_layer = [&benchmark_layers](Dart_NativeArguments args) { - auto handle = Dart_GetNativeArgument(args, 0); + auto capture_root_layer = [&benchmark_layers](Dart_Handle handle) { intptr_t peer = 0; Dart_Handle result = Dart_GetNativeInstanceField( handle, tonic::DartWrappable::kPeerIndex, &peer); @@ -438,8 +438,8 @@ TEST_F(EngineAnimatorTest, IgnoresDuplicateRenders) { }); }); - AddNativeCallback("CaptureRootLayer", - CREATE_NATIVE_ENTRY(capture_root_layer)); + AddFfiNativeCallback("CaptureRootLayer", + CREATE_FFI_LAMBDA(capture_root_layer)); std::unique_ptr animator; PostSync(task_runners_.GetUITaskRunner(), @@ -506,11 +506,11 @@ TEST_F(EngineAnimatorTest, AnimatorSubmitsImplicitViewBeforeDrawFrameEnds) { native_latch.Reset(); // The native_latch is signaled at the end of handleDrawFrame. - AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY([&rasterization_started](auto args) { - EXPECT_EQ(rasterization_started, true); - native_latch.Signal(); - })); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([&rasterization_started]() { + EXPECT_EQ(rasterization_started, true); + native_latch.Signal(); + })); engine_context = EngineContext::Create(delegate_, settings_, task_runners_, std::move(animator)); diff --git a/engine/src/flutter/shell/common/fixtures/shell_test.dart b/engine/src/flutter/shell/common/fixtures/shell_test.dart index c66cec3b37e2e..6c54545296027 100644 --- a/engine/src/flutter/shell/common/fixtures/shell_test.dart +++ b/engine/src/flutter/shell/common/fixtures/shell_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert' show json, utf8; +import 'dart:ffi'; import 'dart:isolate'; import 'dart:typed_data'; import 'dart:ui'; @@ -23,11 +24,11 @@ void mainNotifyNative() { notifyNative(); } -@pragma('vm:external-name', 'NativeReportTimingsCallback') +@Native(symbol: 'NativeReportTimingsCallback') external void nativeReportTimingsCallback(List timings); -@pragma('vm:external-name', 'NativeOnBeginFrame') +@Native(symbol: 'NativeOnBeginFrame') external void nativeOnBeginFrame(int microseconds); -@pragma('vm:external-name', 'NativeOnPointerDataPacket') +@Native(symbol: 'NativeOnPointerDataPacket') external void nativeOnPointerDataPacket(List sequences); @pragma('vm:entry-point') @@ -50,9 +51,9 @@ void onErrorB() { throw Exception('I should be coming from B'); } -@pragma('vm:external-name', 'NotifyErrorA') +@Native(symbol: 'NotifyErrorA') external void notifyErrorA(String message); -@pragma('vm:external-name', 'NotifyErrorB') +@Native(symbol: 'NotifyErrorB') external void notifyErrorB(String message); @pragma('vm:entry-point') @@ -120,7 +121,7 @@ void reportMetrics() { }; } -@pragma('vm:external-name', 'ReportMetrics') +@Native(symbol: 'ReportMetrics') external void _reportMetrics(double devicePixelRatio, double width, double height); @pragma('vm:entry-point') @@ -133,11 +134,11 @@ void fixturesAreFunctionalMain() { sayHiFromFixturesAreFunctionalMain(); } -@pragma('vm:external-name', 'SayHiFromFixturesAreFunctionalMain') +@Native(symbol: 'SayHiFromFixturesAreFunctionalMain') external void sayHiFromFixturesAreFunctionalMain(); @pragma('vm:entry-point') -@pragma('vm:external-name', 'NotifyNative') +@Native(symbol: 'NotifyNative') external void notifyNative(); @pragma('vm:entry-point') @@ -184,7 +185,7 @@ void testSkiaResourceCacheSendsResponse() { ); } -@pragma('vm:external-name', 'NotifyWidthHeight') +@Native(symbol: 'NotifyWidthHeight') external void notifyWidthHeight(int width, int height); @pragma('vm:entry-point') @@ -215,7 +216,7 @@ void performanceModeImpactsNotifyIdle() { PlatformDispatcher.instance.requestDartPerformanceMode(DartPerformanceMode.balanced); } -@pragma('vm:external-name', 'NotifyMessage') +@Native(symbol: 'NotifyMessage') external void notifyMessage(String string); @pragma('vm:entry-point') @@ -229,10 +230,10 @@ void canRegisterImageDecoders() { ); } -@pragma('vm:external-name', 'NotifyLocalTime') +@Native(symbol: 'NotifyLocalTime') external void notifyLocalTime(String string); -@pragma('vm:external-name', 'WaitFixture') +@Native(symbol: 'WaitFixture') external bool waitFixture(); // Return local date-time as a string, to an hour resolution. So, "2020-07-23 @@ -258,10 +259,10 @@ void timezonesChange() { } while (waitFixture()); } -@pragma('vm:external-name', 'NotifyCanAccessResource') +@Native(symbol: 'NotifyCanAccessResource') external void notifyCanAccessResource(bool success); -@pragma('vm:external-name', 'NotifySetAssetBundlePath') +@Native(symbol: 'NotifySetAssetBundlePath') external void notifySetAssetBundlePath(); @pragma('vm:entry-point') @@ -276,10 +277,10 @@ Future canAccessResourceFromAssetDir() async { ); } -@pragma('vm:external-name', 'NotifyNativeWhenEngineRun') +@Native(symbol: 'NotifyNativeWhenEngineRun') external void notifyNativeWhenEngineRun(bool success); -@pragma('vm:external-name', 'NotifyNativeWhenEngineSpawn') +@Native(symbol: 'NotifyNativeWhenEngineSpawn') external void notifyNativeWhenEngineSpawn(bool success); @pragma('vm:entry-point') @@ -307,7 +308,7 @@ void frameCallback(Object? image, int durationMilliseconds, String decodeError) } } -@pragma('vm:external-name', 'NativeOnBeforeToImageSync') +@Native(symbol: 'NativeOnBeforeToImageSync') external void onBeforeToImageSync(); @pragma('vm:entry-point') @@ -373,7 +374,7 @@ Future runCallback(IsolateParam param) async { } @pragma('vm:entry-point') -@pragma('vm:external-name', 'NotifyNativeBool') +@Native(symbol: 'NotifyNativeBool') external void notifyNativeBool(bool value); @pragma('vm:entry-point') @@ -419,7 +420,7 @@ Future testThatAssetLoadingHappensOnWorkerThread() async { notifyNative(); } -@pragma('vm:external-name', 'NativeReportViewIdsCallback') +@Native(symbol: 'NativeReportViewIdsCallback') external void nativeReportViewIdsCallback(bool hasImplicitView, List viewIds); List getCurrentViewIds() { @@ -468,7 +469,7 @@ List getCurrentViewWidths() { return result; } -@pragma('vm:external-name', 'NativeReportViewWidthsCallback') +@Native(symbol: 'NativeReportViewWidthsCallback') external void nativeReportViewWidthsCallback(List viewWidthPacket); // This entrypoint reports the list of views and their widths using @@ -513,7 +514,7 @@ void renderViewsInFrameAndOutOfFrame() { PlatformDispatcher.instance.scheduleFrame(); } -@pragma('vm:external-name', 'CaptureRootLayer') +@Native(symbol: 'CaptureRootLayer') external void _captureRootLayer(SceneBuilder sceneBuilder); @pragma('vm:entry-point') @@ -634,7 +635,7 @@ void testSendViewFocusEvent() { notifyNative(); } -@pragma('vm:external-name', 'ReportEngineId') +@Native(symbol: 'ReportEngineId') external void _reportEngineId(int? identifier); @pragma('vm:entry-point') diff --git a/engine/src/flutter/shell/common/input_events_unittests.cc b/engine/src/flutter/shell/common/input_events_unittests.cc index a0a22d14ded42..54fdbb25bd89d 100644 --- a/engine/src/flutter/shell/common/input_events_unittests.cc +++ b/engine/src/flutter/shell/common/input_events_unittests.cc @@ -79,7 +79,7 @@ static void TestSimulatedInputEvents( int frame_drawn = 0; auto nativeOnPointerDataPacket = [&events_consumed_at_frame, &will_draw_new_frame, &events_consumed, - &frame_drawn](Dart_NativeArguments args) { + &frame_drawn](Dart_Handle sequences) { events_consumed += 1; if (will_draw_new_frame) { frame_drawn += 1; @@ -89,8 +89,8 @@ static void TestSimulatedInputEvents( events_consumed_at_frame.back() = events_consumed; } }; - fixture->AddNativeCallback("NativeOnPointerDataPacket", - CREATE_NATIVE_ENTRY(nativeOnPointerDataPacket)); + fixture->AddFfiNativeCallback("NativeOnPointerDataPacket", + CREATE_FFI_LAMBDA(nativeOnPointerDataPacket)); ASSERT_TRUE(configuration.IsValid()); fixture->RunEngine(shell.get(), std::move(configuration)); @@ -327,16 +327,15 @@ TEST_F(ShellTest, CanCorrectlyPipePointerPacket) { // Sets up native handler. fml::AutoResetWaitableEvent reportLatch; std::vector result_sequence; - auto nativeOnPointerDataPacket = [&reportLatch, &result_sequence]( - Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - result_sequence = tonic::DartConverter>::FromArguments( - args, 0, exception); + auto nativeOnPointerDataPacket = [&reportLatch, + &result_sequence](Dart_Handle sequences) { + result_sequence = + tonic::DartConverter>::FromDart(sequences); reportLatch.Signal(); }; // Starts engine. - AddNativeCallback("NativeOnPointerDataPacket", - CREATE_NATIVE_ENTRY(nativeOnPointerDataPacket)); + AddFfiNativeCallback("NativeOnPointerDataPacket", + CREATE_FFI_LAMBDA(nativeOnPointerDataPacket)); ASSERT_TRUE(configuration.IsValid()); RunEngine(shell.get(), std::move(configuration)); // Starts test. @@ -392,16 +391,15 @@ TEST_F(ShellTest, CanCorrectlySynthesizePointerPacket) { // Sets up native handler. fml::AutoResetWaitableEvent reportLatch; std::vector result_sequence; - auto nativeOnPointerDataPacket = [&reportLatch, &result_sequence]( - Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - result_sequence = tonic::DartConverter>::FromArguments( - args, 0, exception); + auto nativeOnPointerDataPacket = [&reportLatch, + &result_sequence](Dart_Handle sequences) { + result_sequence = + tonic::DartConverter>::FromDart(sequences); reportLatch.Signal(); }; // Starts engine. - AddNativeCallback("NativeOnPointerDataPacket", - CREATE_NATIVE_ENTRY(nativeOnPointerDataPacket)); + AddFfiNativeCallback("NativeOnPointerDataPacket", + CREATE_FFI_LAMBDA(nativeOnPointerDataPacket)); ASSERT_TRUE(configuration.IsValid()); RunEngine(shell.get(), std::move(configuration)); // Starts test. diff --git a/engine/src/flutter/shell/common/shell_fuchsia_unittests.cc b/engine/src/flutter/shell/common/shell_fuchsia_unittests.cc index da6bd0f641432..07b0e0fc9db47 100644 --- a/engine/src/flutter/shell/common/shell_fuchsia_unittests.cc +++ b/engine/src/flutter/shell/common/shell_fuchsia_unittests.cc @@ -173,22 +173,21 @@ TEST_F(FuchsiaShellTest, LocaltimesVaryOnTimezoneChanges) { // there. fml::AutoResetWaitableEvent latch; std::string dart_isolate_time_str; - AddNativeCallback("NotifyLocalTime", CREATE_NATIVE_ENTRY([&](auto args) { - dart_isolate_time_str = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - latch.Signal(); - })); + AddFfiNativeCallback( + "NotifyLocalTime", CREATE_FFI_LAMBDA([&](Dart_Handle string_handle) { + dart_isolate_time_str = + tonic::DartConverter::FromDart(string_handle); + latch.Signal(); + })); // As long as this is set, the isolate will keep rerunning its only task. bool continue_fixture = true; fml::AutoResetWaitableEvent fixture_latch; - AddNativeCallback("WaitFixture", CREATE_NATIVE_ENTRY([&](auto args) { - // Wait for the test fixture to advance. - fixture_latch.Wait(); - tonic::DartConverter::SetReturnValue( - args, continue_fixture); - })); + AddFfiNativeCallback("WaitFixture", CREATE_FFI_LAMBDA([&]() { + // Wait for the test fixture to advance. + fixture_latch.Wait(); + return continue_fixture; + })); auto settings = CreateSettingsForFixture(); auto configuration = RunConfiguration::InferFromSettings(settings); diff --git a/engine/src/flutter/shell/common/shell_unittests.cc b/engine/src/flutter/shell/common/shell_unittests.cc index 110b4ccbe9640..fbcb085360243 100644 --- a/engine/src/flutter/shell/common/shell_unittests.cc +++ b/engine/src/flutter/shell/common/shell_unittests.cc @@ -592,9 +592,9 @@ TEST_F(ShellTest, FixturesAreFunctional) { configuration.SetEntrypoint("fixturesAreFunctionalMain"); fml::AutoResetWaitableEvent main_latch; - AddNativeCallback( + AddFfiNativeCallback( "SayHiFromFixturesAreFunctionalMain", - CREATE_NATIVE_ENTRY([&main_latch](auto args) { main_latch.Signal(); })); + CREATE_FFI_LAMBDA([&main_latch]() { main_latch.Signal(); })); RunEngine(shell.get(), std::move(configuration)); main_latch.Wait(); @@ -682,9 +682,8 @@ TEST_F(ShellTest, SecondaryIsolateBindingsAreSetupViaShellSettings) { configuration.SetEntrypoint("testCanLaunchSecondaryIsolate"); fml::CountDownLatch latch(2); - AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&latch](auto args) { - latch.CountDown(); - })); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); RunEngine(shell.get(), std::move(configuration)); @@ -708,8 +707,8 @@ TEST_F(ShellTest, LastEntrypoint) { fml::AutoResetWaitableEvent main_latch; std::string last_entry_point; - AddNativeCallback( - "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) { + AddFfiNativeCallback( + "SayHiFromFixturesAreFunctionalMain", CREATE_FFI_LAMBDA([&]() { last_entry_point = shell->GetEngine()->GetLastEntrypoint(); main_latch.Signal(); })); @@ -737,8 +736,8 @@ TEST_F(ShellTest, LastEntrypointArgs) { fml::AutoResetWaitableEvent main_latch; std::vector last_entry_point_args; - AddNativeCallback( - "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) { + AddFfiNativeCallback( + "SayHiFromFixturesAreFunctionalMain", CREATE_FFI_LAMBDA([&]() { last_entry_point_args = shell->GetEngine()->GetLastEntrypointArgs(); main_latch.Signal(); })); @@ -879,16 +878,14 @@ TEST_F(ShellTest, ReportTimingsIsCalled) { configuration.SetEntrypoint("reportTimingsMain"); fml::AutoResetWaitableEvent reportLatch; std::vector timestamps; - auto nativeTimingCallback = [&reportLatch, - ×tamps](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; + auto nativeTimingCallback = [&reportLatch, ×tamps](Dart_Handle timings) { ASSERT_EQ(timestamps.size(), 0ul); - timestamps = tonic::DartConverter>::FromArguments( - args, 0, exception); + timestamps = tonic::DartConverter>::FromDart(timings); reportLatch.Signal(); }; - AddNativeCallback("NativeReportTimingsCallback", - CREATE_NATIVE_ENTRY(nativeTimingCallback)); + AddFfiNativeCallback("NativeReportTimingsCallback", + CREATE_FFI_LAMBDA(nativeTimingCallback)); + RunEngine(shell.get(), std::move(configuration)); // Pump many frames so we can trigger the report quickly instead of waiting @@ -952,13 +949,11 @@ TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) { configuration.SetEntrypoint("onBeginFrameMain"); int64_t frame_target_time; - auto nativeOnBeginFrame = [&frame_target_time](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - frame_target_time = - tonic::DartConverter::FromArguments(args, 0, exception); + auto nativeOnBeginFrame = [&frame_target_time](int64_t microseconds) { + frame_target_time = microseconds; }; - AddNativeCallback("NativeOnBeginFrame", - CREATE_NATIVE_ENTRY(nativeOnBeginFrame)); + AddFfiNativeCallback("NativeOnBeginFrame", + CREATE_FFI_LAMBDA(nativeOnBeginFrame)); RunEngine(shell.get(), std::move(configuration)); PumpOneFrame(shell.get()); @@ -1647,16 +1642,13 @@ TEST_F(ShellTest, ReportTimingsIsCalledImmediatelyAfterTheFirstFrame) { configuration.SetEntrypoint("reportTimingsMain"); fml::AutoResetWaitableEvent reportLatch; std::vector timestamps; - auto nativeTimingCallback = [&reportLatch, - ×tamps](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; + auto nativeTimingCallback = [&reportLatch, ×tamps](Dart_Handle timings) { ASSERT_EQ(timestamps.size(), 0ul); - timestamps = tonic::DartConverter>::FromArguments( - args, 0, exception); + timestamps = tonic::DartConverter>::FromDart(timings); reportLatch.Signal(); }; - AddNativeCallback("NativeReportTimingsCallback", - CREATE_NATIVE_ENTRY(nativeTimingCallback)); + AddFfiNativeCallback("NativeReportTimingsCallback", + CREATE_FFI_LAMBDA(nativeTimingCallback)); ASSERT_TRUE(configuration.IsValid()); RunEngine(shell.get(), std::move(configuration)); @@ -2091,9 +2083,8 @@ TEST_F(ShellTest, SetResourceCacheSizeNotifiesDart) { static_cast(3840000U)); fml::AutoResetWaitableEvent latch; - AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&latch](auto args) { - latch.Signal(); - })); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); RunEngine(shell.get(), std::move(configuration)); PumpOneFrame(shell.get()); @@ -2121,16 +2112,12 @@ TEST_F(ShellTest, CanCreateImagefromDecompressedBytes) { configuration.SetEntrypoint("canCreateImageFromDecompressedData"); fml::AutoResetWaitableEvent latch; - AddNativeCallback("NotifyWidthHeight", - CREATE_NATIVE_ENTRY([&latch](auto args) { - auto width = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - auto height = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 1)); - ASSERT_EQ(width, 10); - ASSERT_EQ(height, 10); - latch.Signal(); - })); + AddFfiNativeCallback("NotifyWidthHeight", + CREATE_FFI_LAMBDA([&latch](int width, int height) { + ASSERT_EQ(width, 10); + ASSERT_EQ(height, 10); + latch.Signal(); + })); RunEngine(shell.get(), std::move(configuration)); @@ -2226,14 +2213,13 @@ TEST_F(ShellTest, IsolateCanAccessPersistentIsolateData) { ); fml::AutoResetWaitableEvent message_latch; - AddNativeCallback("NotifyMessage", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - const auto message_from_dart = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - ASSERT_EQ(message, message_from_dart); - message_latch.Signal(); - })); + AddFfiNativeCallback( + "NotifyMessage", CREATE_FFI_LAMBDA([&](Dart_Handle message_handle) { + const auto message_from_dart = + tonic::DartConverter::FromDart(message_handle); + ASSERT_EQ(message, message_from_dart); + message_latch.Signal(); + })); std::unique_ptr shell = CreateShell(settings, task_runners); @@ -2254,14 +2240,12 @@ TEST_F(ShellTest, CanScheduleFrameFromPlatform) { Settings settings = CreateSettingsForFixture(); TaskRunners task_runners = GetTaskRunnersForFixture(); fml::AutoResetWaitableEvent latch; - AddNativeCallback( - "NotifyNative", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); fml::AutoResetWaitableEvent check_latch; - AddNativeCallback("NativeOnBeginFrame", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - check_latch.Signal(); - })); + AddFfiNativeCallback( + "NativeOnBeginFrame", + CREATE_FFI_LAMBDA([&](int64_t microseconds) { check_latch.Signal(); })); std::unique_ptr shell = CreateShell(settings, task_runners); ASSERT_TRUE(shell->IsSetup()); @@ -2286,20 +2270,19 @@ TEST_F(ShellTest, SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback) { Settings settings = CreateSettingsForFixture(); TaskRunners task_runners = GetTaskRunnersForFixture(); fml::AutoResetWaitableEvent latch; - AddNativeCallback( - "NotifyNative", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); fml::CountDownLatch count_down_latch(2); - AddNativeCallback("NativeOnBeginFrame", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - if (!test_started) { - return; - } - EXPECT_FALSE(is_on_begin_frame_called); - EXPECT_FALSE(is_secondary_callback_called); - is_on_begin_frame_called = true; - count_down_latch.CountDown(); - })); + AddFfiNativeCallback("NativeOnBeginFrame", + CREATE_FFI_LAMBDA([&](int64_t microseconds) { + if (!test_started) { + return; + } + EXPECT_FALSE(is_on_begin_frame_called); + EXPECT_FALSE(is_secondary_callback_called); + is_on_begin_frame_called = true; + count_down_latch.CountDown(); + })); std::unique_ptr shell = CreateShell({ .settings = settings, .task_runners = task_runners, @@ -2417,12 +2400,12 @@ TEST_F(ShellTest, LocaltimesMatch) { // See fixtures/shell_test.dart, the callback NotifyLocalTime is declared // there. - AddNativeCallback("NotifyLocalTime", CREATE_NATIVE_ENTRY([&](auto args) { - dart_isolate_time_str = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - latch.Signal(); - })); + AddFfiNativeCallback( + "NotifyLocalTime", CREATE_FFI_LAMBDA([&](Dart_Handle string_handle) { + dart_isolate_time_str = + tonic::DartConverter::FromDart(string_handle); + latch.Signal(); + })); auto settings = CreateSettingsForFixture(); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -2492,15 +2475,12 @@ class SinglePixelImageGenerator : public ImageGenerator { TEST_F(ShellTest, CanRegisterImageDecoders) { fml::AutoResetWaitableEvent latch; - AddNativeCallback("NotifyWidthHeight", CREATE_NATIVE_ENTRY([&](auto args) { - auto width = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - auto height = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 1)); - ASSERT_EQ(width, 1); - ASSERT_EQ(height, 1); - latch.Signal(); - })); + AddFfiNativeCallback("NotifyWidthHeight", + CREATE_FFI_LAMBDA([&](int width, int height) { + ASSERT_EQ(width, 1); + ASSERT_EQ(height, 1); + latch.Signal(); + })); auto settings = CreateSettingsForFixture(); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -2905,22 +2885,14 @@ TEST_F(ShellTest, IgnoresInvalidMetrics) { double last_device_pixel_ratio; double last_width; double last_height; - auto native_report_device_pixel_ratio = [&](Dart_NativeArguments args) { - auto dpr_handle = Dart_GetNativeArgument(args, 0); - ASSERT_TRUE(Dart_IsDouble(dpr_handle)); - Dart_DoubleValue(dpr_handle, &last_device_pixel_ratio); + auto native_report_device_pixel_ratio = [&](double device_pixel_ratio, + double width, double height) { + last_device_pixel_ratio = device_pixel_ratio; + last_width = width; + last_height = height; ASSERT_FALSE(last_device_pixel_ratio == 0.0); - - auto width_handle = Dart_GetNativeArgument(args, 1); - ASSERT_TRUE(Dart_IsDouble(width_handle)); - Dart_DoubleValue(width_handle, &last_width); ASSERT_FALSE(last_width == 0.0); - - auto height_handle = Dart_GetNativeArgument(args, 2); - ASSERT_TRUE(Dart_IsDouble(height_handle)); - Dart_DoubleValue(height_handle, &last_height); ASSERT_FALSE(last_height == 0.0); - latch.Signal(); }; @@ -2929,8 +2901,8 @@ TEST_F(ShellTest, IgnoresInvalidMetrics) { TaskRunners task_runners("test", task_runner, task_runner, task_runner, task_runner); - AddNativeCallback("ReportMetrics", - CREATE_NATIVE_ENTRY(native_report_device_pixel_ratio)); + AddFfiNativeCallback("ReportMetrics", + CREATE_FFI_LAMBDA(native_report_device_pixel_ratio)); std::unique_ptr shell = CreateShell(settings, task_runners); @@ -2981,11 +2953,10 @@ TEST_F(ShellTest, IgnoresMetricsUpdateToInvalidView) { fml::AutoResetWaitableEvent latch; double last_device_pixel_ratio; // This callback will be called whenever any view's metrics change. - auto native_report_device_pixel_ratio = [&](Dart_NativeArguments args) { + auto native_report_device_pixel_ratio = [&](double device_pixel_ratio, + double width, double height) { // The correct call will have a DPR of 3. - auto dpr_handle = Dart_GetNativeArgument(args, 0); - ASSERT_TRUE(Dart_IsDouble(dpr_handle)); - Dart_DoubleValue(dpr_handle, &last_device_pixel_ratio); + last_device_pixel_ratio = device_pixel_ratio; ASSERT_TRUE(last_device_pixel_ratio > 2.5); latch.Signal(); @@ -2996,8 +2967,8 @@ TEST_F(ShellTest, IgnoresMetricsUpdateToInvalidView) { TaskRunners task_runners("test", task_runner, task_runner, task_runner, task_runner); - AddNativeCallback("ReportMetrics", - CREATE_NATIVE_ENTRY(native_report_device_pixel_ratio)); + AddFfiNativeCallback("ReportMetrics", + CREATE_FFI_LAMBDA(native_report_device_pixel_ratio)); std::unique_ptr shell = CreateShell(settings, task_runners); @@ -3037,32 +3008,29 @@ TEST_F(ShellTest, OnServiceProtocolSetAssetBundlePathWorks) { // Callback used to signal whether the resource was loaded successfully. bool can_access_resource = false; auto native_can_access_resource = [&can_access_resource, - &latch](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - can_access_resource = - tonic::DartConverter::FromArguments(args, 0, exception); + &latch](bool success) { + can_access_resource = success; latch.Signal(); }; - AddNativeCallback("NotifyCanAccessResource", - CREATE_NATIVE_ENTRY(native_can_access_resource)); + AddFfiNativeCallback("NotifyCanAccessResource", + CREATE_FFI_LAMBDA(native_can_access_resource)); // Callback used to delay the asset load until after the service // protocol method has finished. - auto native_notify_set_asset_bundle_path = - [&shell](Dart_NativeArguments args) { - // Update the asset directory to a bonus path. - ServiceProtocol::Handler::ServiceProtocolMap params; - params["assetDirectory"] = "assetDirectory"; - rapidjson::Document document; - OnServiceProtocol(shell.get(), ServiceProtocolEnum::kSetAssetBundlePath, - shell->GetTaskRunners().GetUITaskRunner(), params, - &document); - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - document.Accept(writer); - }; - AddNativeCallback("NotifySetAssetBundlePath", - CREATE_NATIVE_ENTRY(native_notify_set_asset_bundle_path)); + auto native_notify_set_asset_bundle_path = [&shell]() { + // Update the asset directory to a bonus path. + ServiceProtocol::Handler::ServiceProtocolMap params; + params["assetDirectory"] = "assetDirectory"; + rapidjson::Document document; + OnServiceProtocol(shell.get(), ServiceProtocolEnum::kSetAssetBundlePath, + shell->GetTaskRunners().GetUITaskRunner(), params, + &document); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + }; + AddFfiNativeCallback("NotifySetAssetBundlePath", + CREATE_FFI_LAMBDA(native_notify_set_asset_bundle_path)); RunEngine(shell.get(), std::move(configuration)); @@ -3243,19 +3211,18 @@ TEST_F(ShellTest, Spawn) { fml::AutoResetWaitableEvent main_latch; std::string last_entry_point; // Fulfill native function for the first Shell's entrypoint. - AddNativeCallback( - "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) { + AddFfiNativeCallback( + "SayHiFromFixturesAreFunctionalMain", CREATE_FFI_LAMBDA([&]() { last_entry_point = shell->GetEngine()->GetLastEntrypoint(); main_latch.Signal(); })); // Fulfill native function for the second Shell's entrypoint. fml::CountDownLatch second_latch(2); - AddNativeCallback( + AddFfiNativeCallback( // The Dart native function names aren't very consistent but this is // just the native function name of the second vm entrypoint in the // fixture. - "NotifyNative", - CREATE_NATIVE_ENTRY([&](auto args) { second_latch.CountDown(); })); + "NotifyNative", CREATE_FFI_LAMBDA([&]() { second_latch.CountDown(); })); RunEngine(shell.get(), std::move(configuration)); main_latch.Wait(); @@ -3349,25 +3316,21 @@ TEST_F(ShellTest, SpawnWithDartEntrypointArgs) { fml::AutoResetWaitableEvent main_latch; std::string last_entry_point; // Fulfill native function for the first Shell's entrypoint. - AddNativeCallback("NotifyNativeWhenEngineRun", - CREATE_NATIVE_ENTRY(([&](Dart_NativeArguments args) { - ASSERT_TRUE(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0))); - last_entry_point = - shell->GetEngine()->GetLastEntrypoint(); - main_latch.Signal(); - }))); + AddFfiNativeCallback( + "NotifyNativeWhenEngineRun", CREATE_FFI_LAMBDA(([&](bool success) { + ASSERT_TRUE(success); + last_entry_point = shell->GetEngine()->GetLastEntrypoint(); + main_latch.Signal(); + }))); fml::AutoResetWaitableEvent second_latch; // Fulfill native function for the second Shell's entrypoint. - AddNativeCallback("NotifyNativeWhenEngineSpawn", - CREATE_NATIVE_ENTRY(([&](Dart_NativeArguments args) { - ASSERT_TRUE(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0))); - last_entry_point = - shell->GetEngine()->GetLastEntrypoint(); - second_latch.Signal(); - }))); + AddFfiNativeCallback( + "NotifyNativeWhenEngineSpawn", CREATE_FFI_LAMBDA(([&](bool success) { + ASSERT_TRUE(success); + last_entry_point = shell->GetEngine()->GetLastEntrypoint(); + second_latch.Signal(); + }))); RunEngine(shell.get(), std::move(configuration)); main_latch.Wait(); @@ -3919,10 +3882,10 @@ TEST_F(ShellTest, UIWorkAfterOnPlatformViewDestroyed) { fml::AutoResetWaitableEvent latch; fml::AutoResetWaitableEvent notify_native_latch; - AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) { - notify_native_latch.Signal(); - latch.Wait(); - })); + AddFfiNativeCallback("NotifyNative", CREATE_FFI_LAMBDA([&]() { + notify_native_latch.Signal(); + latch.Wait(); + })); RunEngine(shell.get(), std::move(configuration)); // Wait to make sure we get called back from Dart and thus have latched @@ -4011,21 +3974,19 @@ TEST_F(ShellTest, SpawnWorksWithOnError) { fml::CountDownLatch latch(2); - AddNativeCallback( - "NotifyErrorA", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto string_handle = Dart_GetNativeArgument(args, 0); - const char* c_str; - Dart_StringToCString(string_handle, &c_str); - EXPECT_STREQ(c_str, "Exception: I should be coming from A"); + AddFfiNativeCallback( + "NotifyErrorA", CREATE_FFI_LAMBDA([&](Dart_Handle string_handle) { + const auto message = + tonic::DartConverter::FromDart(string_handle); + EXPECT_EQ(message, "Exception: I should be coming from A"); latch.CountDown(); })); - AddNativeCallback( - "NotifyErrorB", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto string_handle = Dart_GetNativeArgument(args, 0); - const char* c_str; - Dart_StringToCString(string_handle, &c_str); - EXPECT_STREQ(c_str, "Exception: I should be coming from B"); + AddFfiNativeCallback( + "NotifyErrorB", CREATE_FFI_LAMBDA([&](Dart_Handle string_handle) { + const auto message = + tonic::DartConverter::FromDart(string_handle); + EXPECT_EQ(message, "Exception: I should be coming from B"); latch.CountDown(); })); @@ -4071,8 +4032,8 @@ TEST_F(ShellTest, ImmutableBufferLoadsAssetOnBackgroundThread) { std::unique_ptr shell = CreateShell(settings, task_runners); fml::CountDownLatch latch(1); - AddNativeCallback("NotifyNative", - CREATE_NATIVE_ENTRY([&](auto args) { latch.CountDown(); })); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([&]() { latch.CountDown(); })); // Create the surface needed by rasterizer PlatformViewNotifyCreated(shell.get()); @@ -4109,18 +4070,17 @@ TEST_F(ShellTest, PictureToImageSync) { }), }); - AddNativeCallback("NativeOnBeforeToImageSync", - CREATE_NATIVE_ENTRY([&](auto args) { - // nop - })); + AddFfiNativeCallback("NativeOnBeforeToImageSync", CREATE_FFI_LAMBDA([&]() { + // nop + })); fml::CountDownLatch latch(2); - AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) { - // Teardown and set up rasterizer again. - PlatformViewNotifyDestroyed(shell.get()); - PlatformViewNotifyCreated(shell.get()); - latch.CountDown(); - })); + AddFfiNativeCallback("NotifyNative", CREATE_FFI_LAMBDA([&]() { + // Teardown and set up rasterizer again. + PlatformViewNotifyDestroyed(shell.get()); + PlatformViewNotifyCreated(shell.get()); + latch.CountDown(); + })); ASSERT_NE(shell, nullptr); ASSERT_TRUE(shell->IsSetup()); @@ -4152,18 +4112,17 @@ TEST_F(ShellTest, PictureToImageSyncImpellerNoSurface) { }), }); - AddNativeCallback("NativeOnBeforeToImageSync", - CREATE_NATIVE_ENTRY([&](auto args) { - // nop - })); + AddFfiNativeCallback("NativeOnBeforeToImageSync", CREATE_FFI_LAMBDA([&]() { + // nop + })); fml::CountDownLatch latch(2); - AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) { - // Teardown and set up rasterizer again. - PlatformViewNotifyDestroyed(shell.get()); - PlatformViewNotifyCreated(shell.get()); - latch.CountDown(); - })); + AddFfiNativeCallback("NotifyNative", CREATE_FFI_LAMBDA([&]() { + // Teardown and set up rasterizer again. + PlatformViewNotifyDestroyed(shell.get()); + PlatformViewNotifyCreated(shell.get()); + latch.CountDown(); + })); ASSERT_NE(shell, nullptr); ASSERT_TRUE(shell->IsSetup()); @@ -4204,21 +4163,21 @@ TEST_F(ShellTest, PictureToImageSyncWithTrampledContext) { }), }); - AddNativeCallback( - "NativeOnBeforeToImageSync", CREATE_NATIVE_ENTRY([&](auto args) { - // Trample the GL context. If the rasterizer fails - // to make the right one current again, test will - // fail. - ::eglMakeCurrent(::eglGetCurrentDisplay(), NULL, NULL, NULL); - })); + AddFfiNativeCallback("NativeOnBeforeToImageSync", CREATE_FFI_LAMBDA([&]() { + // Trample the GL context. If the rasterizer fails + // to make the right one current again, test will + // fail. + ::eglMakeCurrent(::eglGetCurrentDisplay(), NULL, NULL, + NULL); + })); fml::CountDownLatch latch(2); - AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) { - // Teardown and set up rasterizer again. - PlatformViewNotifyDestroyed(shell.get()); - PlatformViewNotifyCreated(shell.get()); - latch.CountDown(); - })); + AddFfiNativeCallback("NotifyNative", CREATE_FFI_LAMBDA([&]() { + // Teardown and set up rasterizer again. + PlatformViewNotifyDestroyed(shell.get()); + PlatformViewNotifyCreated(shell.get()); + latch.CountDown(); + })); ASSERT_NE(shell, nullptr); ASSERT_TRUE(shell->IsSetup()); @@ -4241,12 +4200,10 @@ TEST_F(ShellTest, PluginUtilitiesCallbackHandleErrorHandling) { fml::AutoResetWaitableEvent latch; bool test_passed; - AddNativeCallback("NotifyNativeBool", CREATE_NATIVE_ENTRY([&](auto args) { - Dart_Handle exception = nullptr; - test_passed = tonic::DartConverter::FromArguments( - args, 0, exception); - latch.Signal(); - })); + AddFfiNativeCallback("NotifyNativeBool", CREATE_FFI_LAMBDA([&](bool value) { + test_passed = value; + latch.Signal(); + })); ASSERT_NE(shell, nullptr); ASSERT_TRUE(shell->IsSetup()); @@ -4328,11 +4285,8 @@ TEST_F(ShellTest, NotifyIdleNotCalledInLatencyMode) { // succeed. After the first `NotifyNativeBool` we expect to be in latency // mode, where we expect idle notifications to fail. fml::CountDownLatch latch(2); - AddNativeCallback( - "NotifyNativeBool", CREATE_NATIVE_ENTRY([&](auto args) { - Dart_Handle exception = nullptr; - bool is_in_latency_mode = - tonic::DartConverter::FromArguments(args, 0, exception); + AddFfiNativeCallback( + "NotifyNativeBool", CREATE_FFI_LAMBDA([&](bool is_in_latency_mode) { auto runtime_controller = const_cast( shell->GetEngine()->GetRuntimeController()); bool success = @@ -4443,12 +4397,11 @@ TEST_F(ShellTest, SemanticsActionsFlushMessageLoop) { RunEngine(shell.get(), std::move(configuration)); fml::CountDownLatch latch(1); - AddNativeCallback( + AddFfiNativeCallback( // The Dart native function names aren't very consistent but this is // just the native function name of the second vm entrypoint in the // fixture. - "NotifyNative", - CREATE_NATIVE_ENTRY([&](auto args) { latch.CountDown(); })); + "NotifyNative", CREATE_FFI_LAMBDA([&]() { latch.CountDown(); })); task_runners.GetPlatformTaskRunner()->PostTask([&] { SendSemanticsAction(shell.get(), 456, 0, SemanticsAction::kTap, @@ -4477,12 +4430,11 @@ TEST_F(ShellTest, PointerPacketFlushMessageLoop) { RunEngine(shell.get(), std::move(configuration)); fml::CountDownLatch latch(1); - AddNativeCallback( + AddFfiNativeCallback( // The Dart native function names aren't very consistent but this is // just the native function name of the second vm entrypoint in the // fixture. - "NotifyNative", - CREATE_NATIVE_ENTRY([&](auto args) { latch.CountDown(); })); + "NotifyNative", CREATE_FFI_LAMBDA([&]() { latch.CountDown(); })); DispatchFakePointerData(shell.get(), 23); latch.Wait(); @@ -4509,11 +4461,11 @@ TEST_F(ShellTest, DISABLED_PointerPacketsAreDispatchedWithTask) { RunEngine(shell.get(), std::move(configuration)); fml::CountDownLatch latch(1); bool did_invoke_callback = false; - AddNativeCallback( + AddFfiNativeCallback( // The Dart native function names aren't very consistent but this is // just the native function name of the second vm entrypoint in the // fixture. - "NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) { + "NotifyNative", CREATE_FFI_LAMBDA([&]() { did_invoke_callback = true; latch.CountDown(); })); @@ -4546,21 +4498,6 @@ TEST_F(ShellTest, DiesIfSoftwareRenderingAndImpellerAreEnabledDeathTest) { #endif // OS_FUCHSIA } -// Parse the arguments of NativeReportViewIdsCallback and -// store them in hasImplicitView and viewIds. -static void ParseViewIdsCallback(const Dart_NativeArguments& args, - bool* hasImplicitView, - std::vector* viewIds) { - Dart_Handle exception = nullptr; - viewIds->clear(); - *hasImplicitView = - tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_EQ(exception, nullptr); - *viewIds = tonic::DartConverter>::FromArguments( - args, 1, exception); - ASSERT_EQ(exception, nullptr); -} - TEST_F(ShellTest, ShellStartsWithImplicitView) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); Settings settings = CreateSettingsForFixture(); @@ -4573,13 +4510,15 @@ TEST_F(ShellTest, ShellStartsWithImplicitView) { bool hasImplicitView; std::vector viewIds; fml::AutoResetWaitableEvent reportLatch; - auto nativeViewIdsCallback = [&reportLatch, &hasImplicitView, - &viewIds](Dart_NativeArguments args) { - ParseViewIdsCallback(args, &hasImplicitView, &viewIds); + auto nativeViewIdsCallback = [&reportLatch, &hasImplicitView, &viewIds]( + bool has_implicit_view, + Dart_Handle view_ids) { + hasImplicitView = has_implicit_view; + viewIds = tonic::DartConverter>::FromDart(view_ids); reportLatch.Signal(); }; - AddNativeCallback("NativeReportViewIdsCallback", - CREATE_NATIVE_ENTRY(nativeViewIdsCallback)); + AddFfiNativeCallback("NativeReportViewIdsCallback", + CREATE_FFI_LAMBDA(nativeViewIdsCallback)); PlatformViewNotifyCreated(shell.get()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -4613,13 +4552,15 @@ TEST_F(ShellTest, ShellCanAddViewOrRemoveView) { bool hasImplicitView; std::vector viewIds; fml::AutoResetWaitableEvent reportLatch; - auto nativeViewIdsCallback = [&reportLatch, &hasImplicitView, - &viewIds](Dart_NativeArguments args) { - ParseViewIdsCallback(args, &hasImplicitView, &viewIds); + auto nativeViewIdsCallback = [&reportLatch, &hasImplicitView, &viewIds]( + bool has_implicit_view, + Dart_Handle view_ids) { + hasImplicitView = has_implicit_view; + viewIds = tonic::DartConverter>::FromDart(view_ids); reportLatch.Signal(); }; - AddNativeCallback("NativeReportViewIdsCallback", - CREATE_NATIVE_ENTRY(nativeViewIdsCallback)); + AddFfiNativeCallback("NativeReportViewIdsCallback", + CREATE_FFI_LAMBDA(nativeViewIdsCallback)); PlatformViewNotifyCreated(shell.get()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -4680,11 +4621,13 @@ TEST_F(ShellTest, ShellCannotAddDuplicateViewId) { bool has_implicit_view; std::vector view_ids; fml::AutoResetWaitableEvent report_latch; - AddNativeCallback("NativeReportViewIdsCallback", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - ParseViewIdsCallback(args, &has_implicit_view, &view_ids); - report_latch.Signal(); - })); + AddFfiNativeCallback( + "NativeReportViewIdsCallback", + CREATE_FFI_LAMBDA([&](bool has_implicit, Dart_Handle ids) { + has_implicit_view = has_implicit; + view_ids = tonic::DartConverter>::FromDart(ids); + report_latch.Signal(); + })); PlatformViewNotifyCreated(shell.get()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -4747,11 +4690,13 @@ TEST_F(ShellTest, ShellCannotRemoveNonexistentId) { bool has_implicit_view; std::vector view_ids; fml::AutoResetWaitableEvent report_latch; - AddNativeCallback("NativeReportViewIdsCallback", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - ParseViewIdsCallback(args, &has_implicit_view, &view_ids); - report_latch.Signal(); - })); + AddFfiNativeCallback( + "NativeReportViewIdsCallback", + CREATE_FFI_LAMBDA([&](bool has_implicit, Dart_Handle ids) { + has_implicit_view = has_implicit; + view_ids = tonic::DartConverter>::FromDart(ids); + report_latch.Signal(); + })); PlatformViewNotifyCreated(shell.get()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -4781,14 +4726,11 @@ TEST_F(ShellTest, ShellCannotRemoveNonexistentId) { // Parse the arguments of NativeReportViewWidthsCallback and // store them in viewWidths. -static void ParseViewWidthsCallback(const Dart_NativeArguments& args, +static void ParseViewWidthsCallback(Dart_Handle view_width_packet, std::map* viewWidths) { - Dart_Handle exception = nullptr; viewWidths->clear(); std::vector viewWidthPacket = - tonic::DartConverter>::FromArguments(args, 0, - exception); - ASSERT_EQ(exception, nullptr); + tonic::DartConverter>::FromDart(view_width_packet); ASSERT_EQ(viewWidthPacket.size() % 2, 0ul); for (size_t packetIndex = 0; packetIndex < viewWidthPacket.size(); packetIndex += 2) { @@ -4827,15 +4769,15 @@ TEST_F(ShellTest, ShellFlushesPlatformStatesByMain) { bool first_report = true; std::map viewWidths; fml::AutoResetWaitableEvent reportLatch; - auto nativeViewWidthsCallback = [&reportLatch, &viewWidths, - &first_report](Dart_NativeArguments args) { + auto nativeViewWidthsCallback = [&reportLatch, &viewWidths, &first_report]( + Dart_Handle view_width_packet) { EXPECT_TRUE(first_report); first_report = false; - ParseViewWidthsCallback(args, &viewWidths); + ParseViewWidthsCallback(view_width_packet, &viewWidths); reportLatch.Signal(); }; - AddNativeCallback("NativeReportViewWidthsCallback", - CREATE_NATIVE_ENTRY(nativeViewWidthsCallback)); + AddFfiNativeCallback("NativeReportViewWidthsCallback", + CREATE_FFI_LAMBDA(nativeViewWidthsCallback)); PlatformViewNotifyCreated(shell.get()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -4881,13 +4823,14 @@ TEST_F(ShellTest, CanRemoveViewBeforeLaunchingIsolate) { bool first_report = true; std::map view_widths; fml::AutoResetWaitableEvent report_latch; - AddNativeCallback("NativeReportViewWidthsCallback", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - EXPECT_TRUE(first_report); - first_report = false; - ParseViewWidthsCallback(args, &view_widths); - report_latch.Signal(); - })); + AddFfiNativeCallback("NativeReportViewWidthsCallback", + CREATE_FFI_LAMBDA([&](Dart_Handle view_width_packet) { + EXPECT_TRUE(first_report); + first_report = false; + ParseViewWidthsCallback(view_width_packet, + &view_widths); + report_latch.Signal(); + })); PlatformViewNotifyCreated(shell.get()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -4935,13 +4878,14 @@ TEST_F(ShellTest, IgnoresBadAddViewsBeforeLaunchingIsolate) { bool first_report = true; std::map view_widths; fml::AutoResetWaitableEvent report_latch; - AddNativeCallback("NativeReportViewWidthsCallback", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - EXPECT_TRUE(first_report); - first_report = false; - ParseViewWidthsCallback(args, &view_widths); - report_latch.Signal(); - })); + AddFfiNativeCallback("NativeReportViewWidthsCallback", + CREATE_FFI_LAMBDA([&](Dart_Handle view_width_packet) { + EXPECT_TRUE(first_report); + first_report = false; + ParseViewWidthsCallback(view_width_packet, + &view_widths); + report_latch.Signal(); + })); PlatformViewNotifyCreated(shell.get()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -4986,18 +4930,16 @@ TEST_F(ShellTest, SendViewFocusEvent) { fml::AutoResetWaitableEvent latch; std::string last_event; - AddNativeCallback( - "NotifyNative", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); - - AddNativeCallback("NotifyMessage", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - const auto message_from_dart = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - last_event = message_from_dart; - latch.Signal(); - })); + AddFfiNativeCallback("NotifyNative", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); + + AddFfiNativeCallback( + "NotifyMessage", CREATE_FFI_LAMBDA([&](Dart_Handle message) { + const auto message_from_dart = + tonic::DartConverter::FromDart(message); + last_event = message_from_dart; + latch.Signal(); + })); fml::AutoResetWaitableEvent check_latch; std::unique_ptr shell = CreateShell(settings, task_runners); @@ -5036,9 +4978,8 @@ TEST_F(ShellTest, ProvidesEngineId) { std::optional reported_handle = std::nullopt; - AddNativeCallback( - "ReportEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - Dart_Handle arg = Dart_GetNativeArgument(args, 0); + AddFfiNativeCallback( + "ReportEngineId", CREATE_FFI_LAMBDA([&](Dart_Handle arg) { if (Dart_IsNull(arg)) { reported_handle = std::nullopt; } else { @@ -5077,9 +5018,8 @@ TEST_F(ShellTest, ProvidesNullEngineId) { std::optional reported_handle = std::nullopt; - AddNativeCallback( - "ReportEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - Dart_Handle arg = Dart_GetNativeArgument(args, 0); + AddFfiNativeCallback( + "ReportEngineId", CREATE_FFI_LAMBDA([&](Dart_Handle arg) { if (Dart_IsNull(arg)) { reported_handle = std::nullopt; } else { @@ -5122,8 +5062,8 @@ TEST_F(ShellTest, MergeUIAndPlatformThreadsAfterLaunch) { task_runners.GetPlatformTaskRunner()->GetTaskQueueId())); fml::AutoResetWaitableEvent latch; - AddNativeCallback( - "NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) { + AddFfiNativeCallback( + "NotifyNative", CREATE_FFI_LAMBDA([&]() { ASSERT_TRUE( task_runners.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); latch.Signal(); diff --git a/engine/src/flutter/testing/test_dart_native_resolver.h b/engine/src/flutter/testing/test_dart_native_resolver.h index 87174379f2309..c847cad1d6191 100644 --- a/engine/src/flutter/testing/test_dart_native_resolver.h +++ b/engine/src/flutter/testing/test_dart_native_resolver.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "flutter/fml/macros.h" #include "third_party/dart/runtime/include/dart_api.h" @@ -23,6 +24,19 @@ return entrypoint; \ })() +/// A macro that converts a lambda into a function pointer that can be called +/// through Dart FFI. +/// +/// Note: The lambda is stored in a global static variable. To avoid memory +/// leaks and teardown crashes, the lambda should only capture variables by +/// reference. +#define CREATE_FFI_LAMBDA(lambda) \ + ([&]() { \ + using FfiWrapper = ::flutter::testing::FfiLambda; \ + FfiWrapper::function = (lambda); \ + return reinterpret_cast(FfiWrapper::FfiFunction); \ + })() + namespace flutter::testing { using NativeEntry = std::function; @@ -55,6 +69,22 @@ class TestDartNativeResolver FML_DISALLOW_COPY_AND_ASSIGN(TestDartNativeResolver); }; +template +struct FfiLambdaFunction {}; + +/// Wraps a lambda in a function pointer that can be called through Dart FFI. +template +struct FfiLambdaFunction { + inline static std::function function; + + static ReturnType FfiFunction(Args... args) { + return function(std::forward(args)...); + } +}; + +template +using FfiLambda = FfiLambdaFunction; + } // namespace flutter::testing #endif // FLUTTER_TESTING_TEST_DART_NATIVE_RESOLVER_H_ From 01dc54a1dc761ed9dbf2e1080cebd7992ee0dee5 Mon Sep 17 00:00:00 2001 From: Chikamatsu Kazuya <43089218+chika3742@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:54:54 +0900 Subject: [PATCH 063/330] doc: fix typo in see also section for PrimaryScrollController.maybeOf (#190386) I found an incorrect reference in dartdoc and corrected it. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter/lib/src/widgets/primary_scroll_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/primary_scroll_controller.dart b/packages/flutter/lib/src/widgets/primary_scroll_controller.dart index dc8d211429d91..c017e9167ddde 100644 --- a/packages/flutter/lib/src/widgets/primary_scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/primary_scroll_controller.dart @@ -147,7 +147,7 @@ class PrimaryScrollController extends InheritedWidget { /// /// See also: /// - /// * [PrimaryScrollController.maybeOf], which is similar to this method, but + /// * [PrimaryScrollController.of], which is similar to this method, but /// asserts if no [PrimaryScrollController] ancestor is found. static ScrollController? maybeOf(BuildContext context) { final PrimaryScrollController? result = context From d3ac5cdfb70971a1b28ca67240860675a6b3598c Mon Sep 17 00:00:00 2001 From: Cole Springer <93888664+ColeSpringer@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:55:55 -0500 Subject: [PATCH 064/330] [web] Use thread local strike caches in skwasm (#190048) ## Description Multithreaded skwasm is built with `-sWASM_WORKERS` but without `-pthread`, which links the single threaded emscripten system libraries where mutexes are no-ops. Text layout on the main thread and the raster worker share the global `SkStrikeCache`, so under heavy text churn the two threads corrupt the heap and the tab ends up pinned at 100% CPU. The fix is Skia's experimental thread local strike cache flag, enabled from a constructor in `surface.cc`. Each thread gets its own strike cache, so there is no shared state left to race on. Two adjacent fixes: - `context_lost_callback_id_` was allocated on the worker by incrementing `current_callback_id_`, which the main thread also increments. It is now allocated on the main thread in `SetCanvas`. - `surface_setResourceCacheLimitBytes` touched the worker owned `GrDirectContext` from the main thread. It now dispatches to the worker, and the value is stored and reapplied when the render context is recreated. This also fixes a null deref when Dart sets the limit before surface init. ## Tests New regression test `lib/web_ui/test/skwasm/concurrent_text_layout_raster_test.dart`: text layout with never repeating font parameters on the main thread while the worker rasterizes the previous frame, disposing everything per frame and toggling the resource cache limit. Without the flag it fails 2 of 2 runs (one wasm timeout, one hard page freeze); with it, 6 of 6 pass. The context loss suite passes 4 of 4, covering the callback id and cache limit changes. No golden churn is expected. Fixes https://github.com/flutter/flutter/issues/190039 Possibly also fixes #184858, which looks like the same corruption from the dispose path. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../concurrent_text_layout_raster_test.dart | 120 ++++++++++++++++++ .../flutter/skwasm/library_skwasm_support.js | 17 ++- engine/src/flutter/skwasm/skwasm_support.h | 6 +- engine/src/flutter/skwasm/surface.cc | 42 +++++- engine/src/flutter/skwasm/surface.h | 8 ++ 5 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/test/skwasm/concurrent_text_layout_raster_test.dart diff --git a/engine/src/flutter/lib/web_ui/test/skwasm/concurrent_text_layout_raster_test.dart b/engine/src/flutter/lib/web_ui/test/skwasm/concurrent_text_layout_raster_test.dart new file mode 100644 index 0000000000000..97a84c5873f48 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/skwasm/concurrent_text_layout_raster_test.dart @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart' show renderer; +import 'package:ui/ui.dart' as ui; + +import '../common/rendering.dart'; +import '../common/test_initialization.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +const int _maxFrames = 200; +const Duration _maxTestTime = Duration(seconds: 10); +const int _paragraphsPerFrame = 24; +const int _spansPerParagraph = 3; + +const List _fontFamilies = ['Roboto', 'RobotoVariable', 'Ahem']; +const List _fontWeights = [ + ui.FontWeight.w100, + ui.FontWeight.w300, + ui.FontWeight.w400, + ui.FontWeight.w500, + ui.FontWeight.w700, + ui.FontWeight.w900, +]; + +Future testMain() async { + setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false); + + // Advanced by an irrational stride so fractional font sizes never repeat, + // forcing a fresh SkStrike for every span. + double fontSizeSeed = 0; + + List buildAndLayoutParagraphs(int frame) { + final paragraphs = []; + for (var i = 0; i < _paragraphsPerFrame; i++) { + final builder = ui.ParagraphBuilder(ui.ParagraphStyle()); + for (var span = 0; span < _spansPerParagraph; span++) { + fontSizeSeed += 0.6180339887; + builder.pushStyle( + ui.TextStyle( + color: const ui.Color(0xFF000000), + fontFamily: _fontFamilies[(i + span) % _fontFamilies.length], + fontSize: 8.0 + (fontSizeSeed % 32.0), + fontWeight: _fontWeights[(frame + i + span) % _fontWeights.length], + fontStyle: (frame + i + span).isEven ? ui.FontStyle.normal : ui.FontStyle.italic, + ), + ); + builder.addText('Quick zephyrs blow 0123456789 frame $frame paragraph $i span $span. '); + builder.pop(); + } + final ui.Paragraph paragraph = builder.build(); + paragraph.layout(const ui.ParagraphConstraints(width: 400)); + paragraphs.add(paragraph); + } + return paragraphs; + } + + ui.Picture recordPicture(List paragraphs) { + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder, const ui.Rect.fromLTWH(0, 0, 500, 500)); + canvas.drawColor(const ui.Color(0xFFFFFFFF), ui.BlendMode.src); + var offset = 0.0; + for (final paragraph in paragraphs) { + canvas.drawParagraph(paragraph, ui.Offset(0, offset)); + offset += 20.0; + } + return recorder.endRecording(); + } + + test('concurrent text layout and rasterization does not corrupt the heap', () async { + List paragraphs = buildAndLayoutParagraphs(0); + final stopwatch = Stopwatch()..start(); + var frame = 0; + while (frame < _maxFrames && stopwatch.elapsed < _maxTestTime) { + frame++; + + final ui.Picture picture = recordPicture(paragraphs); + final sceneBuilder = ui.SceneBuilder(); + sceneBuilder.addPicture(ui.Offset.zero, picture); + + // Kick off rasterization unawaited, then yield once so the render + // message reaches the raster thread. + final Future renderFuture = renderScene(sceneBuilder.build()); + await Future.delayed(Duration.zero); + + // Lay out the next frame's paragraphs while the raster thread is + // rasterizing this one — the concurrent strike cache access under test. + final List nextParagraphs = buildAndLayoutParagraphs(frame); + + await renderFuture; + + // Disposing exercises the concurrent teardown path of + // https://github.com/flutter/flutter/issues/184858. + for (final paragraph in paragraphs) { + paragraph.dispose(); + } + picture.dispose(); + paragraphs = nextParagraphs; + + // Shrink and re-grow the GPU resource cache so purging runs against + // in-flight rasterization. + if (frame % 16 == 0) { + renderer.resourceCacheMaxBytes = frame % 32 == 0 ? 1024 * 1024 : 64 * 1024 * 1024; + } + } + + for (final paragraph in paragraphs) { + paragraph.dispose(); + } + + // Completing without a RuntimeError or hang is the pass condition. + expect(frame, greaterThan(0)); + }, timeout: const Timeout(Duration(minutes: 2))); +} diff --git a/engine/src/flutter/skwasm/library_skwasm_support.js b/engine/src/flutter/skwasm/library_skwasm_support.js index 5d23456261782..c6d9488b8f1ae 100644 --- a/engine/src/flutter/skwasm/library_skwasm_support.js +++ b/engine/src/flutter/skwasm/library_skwasm_support.js @@ -161,6 +161,9 @@ mergeInto(LibraryManager.library, { data.callbackId, ); return; + case 'setResourceCacheLimit': + _surface_setResourceCacheLimitOnWorker(data.surface, data.bytes); + return; default: console.warn(`unrecognized skwasm message: ${skwasmMessage}`); } @@ -210,11 +213,10 @@ mergeInto(LibraryManager.library, { callbackId, }, [canvas], threadId); }; - _skwasm_reportInitialized = function (surfaceHandle, contextLostCallbackId, callbackId) { + _skwasm_reportInitialized = function (surfaceHandle, callbackId) { skwasm_postMessage({ skwasmMessage: 'onInitialized', surface: surfaceHandle, - contextLostCallbackId, callbackId, }, []); }; @@ -290,6 +292,15 @@ mergeInto(LibraryManager.library, { }); } + // Resource Cache + _skwasm_dispatchSetResourceCacheLimit = function(threadId, surface, bytes) { + skwasm_postMessage({ + skwasmMessage: 'setResourceCacheLimit', + surface, + bytes, + }, [], threadId); + }; + // Context Loss _skwasm_dispatchTriggerContextLoss = function (threadId, surfaceHandle, callbackId) { skwasm_postMessage({ @@ -421,6 +432,8 @@ mergeInto(LibraryManager.library, { skwasm_createGlTextureFromTextureSource__deps: ['$skwasm_support_setup'], skwasm_dispatchDisposeSurface: function() {}, skwasm_dispatchDisposeSurface__deps: ['$skwasm_support_setup'], + skwasm_dispatchSetResourceCacheLimit: function() {}, + skwasm_dispatchSetResourceCacheLimit__deps: ['$skwasm_support_setup'], skwasm_dispatchRasterizeImage: function() {}, skwasm_dispatchRasterizeImage__deps: ['$skwasm_support_setup'], skwasm_postRasterizeResult: function() {}, diff --git a/engine/src/flutter/skwasm/skwasm_support.h b/engine/src/flutter/skwasm/skwasm_support.h index ab13ca9df8271..d7f3150ff6c6d 100644 --- a/engine/src/flutter/skwasm/skwasm_support.h +++ b/engine/src/flutter/skwasm/skwasm_support.h @@ -39,8 +39,7 @@ extern uint32_t skwasm_getGlContextForCanvas(SkwasmObject canvas, bool antialias, Skwasm::Surface* surface); extern void skwasm_reportInitialized(Skwasm::Surface* surface, - uint32_t callback_id, - uint32_t context_lost_callback_id); + uint32_t callback_id); extern void skwasm_reportResizeComplete(Skwasm::Surface* surface, uint32_t callback_id); extern void skwasm_dispatchResizeSurface(unsigned long thread_id, @@ -76,6 +75,9 @@ extern void skwasm_dispatchTransferCanvas(unsigned long thread_id, uint32_t callback_id); extern void skwasm_dispatchDisposeSurface(unsigned long thread_id, Skwasm::Surface* surface); +extern void skwasm_dispatchSetResourceCacheLimit(unsigned long thread_id, + Skwasm::Surface* surface, + int bytes); extern void skwasm_dispatchRasterizeImage(unsigned long thread_id, Skwasm::Surface* surface, flutter::DlImage* image, diff --git a/engine/src/flutter/skwasm/surface.cc b/engine/src/flutter/skwasm/surface.cc index 6bf56ad4b3615..65f6d9de09e23 100644 --- a/engine/src/flutter/skwasm/surface.cc +++ b/engine/src/flutter/skwasm/surface.cc @@ -41,6 +41,16 @@ // the Dart code, which will complete the future that was returned by the // original Dart method call. +// See https://github.com/flutter/flutter/pull/190048 +extern bool + gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental; + +namespace { +__attribute__((constructor)) void UseThreadLocalStrikeCaches() { + gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental = true; +} +} // namespace + unsigned long Skwasm::GetRasterThread() { static unsigned long thread = []() { if (skwasm_isSingleThreaded()) { @@ -86,6 +96,11 @@ void Skwasm::Surface::Dispose() { uint32_t Skwasm::Surface::SetCanvas(SkwasmObject canvas) { assert(emscripten_is_main_browser_thread()); uint32_t callback_id = ++current_callback_id_; + + // Allocated here instead of on the worker so that current_callback_id_ is + // only ever modified on the main thread. + context_lost_callback_id_ = ++current_callback_id_; + skwasm_dispatchTransferCanvas(GetRasterThread(), this, canvas, callback_id); return callback_id; } @@ -129,9 +144,11 @@ void Skwasm::Surface::ReceiveCanvasOnWorker(SkwasmObject canvas, render_context_ = Skwasm::RenderContext::Make(sample_count, stencil); render_context_->Resize(canvas_width_, canvas_height_); - context_lost_callback_id_ = ++current_callback_id_; + if (resource_cache_limit_) { + render_context_->SetResourceCacheLimit(*resource_cache_limit_); + } - skwasm_reportInitialized(this, context_lost_callback_id_, callback_id); + skwasm_reportInitialized(this, callback_id); } // Resizing @@ -288,8 +305,20 @@ void Skwasm::Surface::OnContextLost() { // Other +// Main thread only void Skwasm::Surface::SetResourceCacheLimit(int bytes) { - render_context_->SetResourceCacheLimit(bytes); + assert(emscripten_is_main_browser_thread()); + skwasm_dispatchSetResourceCacheLimit(GetRasterThread(), this, bytes); +} + +// Worker thread only +void Skwasm::Surface::SetResourceCacheLimitOnWorker(int bytes) { + // Always stored so ReceiveCanvasOnWorker can reapply it whenever the + // render context is (re)created. + resource_cache_limit_ = bytes; + if (render_context_) { + render_context_->SetResourceCacheLimit(bytes); + } } std::unique_ptr @@ -410,9 +439,16 @@ SKWASM_EXPORT void surface_dispose(Skwasm::Surface* surface) { SKWASM_EXPORT void surface_setResourceCacheLimitBytes(Skwasm::Surface* surface, int bytes) { + // Dispatch to the worker, which owns the render context. surface->SetResourceCacheLimit(bytes); } +SKWASM_EXPORT void surface_setResourceCacheLimitOnWorker( + Skwasm::Surface* surface, + int bytes) { + surface->SetResourceCacheLimitOnWorker(bytes); +} + SKWASM_EXPORT uint32_t surface_renderPictures(Skwasm::Surface* surface, flutter::DisplayList** pictures, int count) { diff --git a/engine/src/flutter/skwasm/surface.h b/engine/src/flutter/skwasm/surface.h index 9306b288532c6..56cb23def0e3b 100644 --- a/engine/src/flutter/skwasm/surface.h +++ b/engine/src/flutter/skwasm/surface.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "export.h" #include "render_context.h" #include "wrappers.h" @@ -80,6 +81,7 @@ class Surface { // Other void SetResourceCacheLimit(int bytes); + void SetResourceCacheLimitOnWorker(int bytes); std::unique_ptr CreateTextureSourceWrapper( SkwasmObject textureSource); @@ -89,6 +91,8 @@ class Surface { void RecreateSurface(); CallbackHandler* callback_handler_ = nullptr; + + // Main thread only uint32_t current_callback_id_ = 0; int canvas_width_ = 0; @@ -98,6 +102,10 @@ class Surface { std::unique_ptr render_context_; uint32_t context_lost_callback_id_ = 0; + // Worker thread only: the desired resource cache limit for the surface, + // reapplied whenever the render context is (re)created. + std::optional resource_cache_limit_; + bool is_initialized_ = false; }; } // namespace Skwasm From 3fdac69e10bb63dfccad65ffce3ffa81bbd63e19 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 4 Aug 2026 17:58:56 -0700 Subject: [PATCH 065/330] Move examples of `flutter/widgets` widgets out from `flutter/material` (#189532) There are several example files that are testing widgets defined in `flutter/widgets`, but the example files are located in `flutter/material`. This PR moves these example files to `flutter/widgets`, so that they'll continue working after we remove `flutter/material`. In order to simplify reviewing, this PR does not change any of these example files. Resolving the cross-import is for another PR (likely by somebody else). A corresponding PR, https://github.com/flutter/packages/pull/12179, has been landed to remove these files from `flutter/packages/material_ui`. After this PR, `flutter/widgets` still refer to two examples in `flutter/material`: - `examples/api/lib/material/selection_area/selection_area.1.dart` - `examples/api/lib/material/selection_area/selection_area.2.dart` However, `SelectionArea` is a Material widget. Referring them in `flutter/widgets` is likely wrong, and I assume resolving them might needs more non-trivial changes. **Note to reviewers:** This PR is of very low priority. It only blocks removing `material` from this repo. So take your time to review. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Navaron Bracke Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../context_menu/context_menu_controller.0.dart | 0 .../editable_text_toolbar_builder.0.dart | 14 ++++++++------ .../editable_text_toolbar_builder.1.dart | 6 ++++-- .../expansible/expansible.0.dart | 0 .../platform_menu_bar/platform_menu_bar.0.dart | 0 .../selectable_region/selectable_region.0.dart | 0 .../selection_container/selection_container.0.dart | 0 .../selection_container_disabled.0.dart | 0 .../context_menu_controller.0_test.dart | 2 +- .../editable_text_toolbar_builder.0_test.dart | 2 +- .../editable_text_toolbar_builder.1_test.dart | 2 +- .../expansible/expansible.0_test.dart | 2 +- .../platform_menu_bar.0_test.dart | 2 +- .../selectable_region.0_test.dart | 2 +- .../selection_container.0_test.dart | 2 +- .../selection_container_disabled.0_test.dart | 2 +- .../lib/src/widgets/context_menu_controller.dart | 2 +- .../flutter/lib/src/widgets/editable_text.dart | 4 ++-- packages/flutter/lib/src/widgets/expansible.dart | 2 +- .../flutter/lib/src/widgets/platform_menu_bar.dart | 2 +- .../flutter/lib/src/widgets/selectable_region.dart | 6 +++--- .../lib/src/widgets/selection_container.dart | 4 ++-- packages/flutter/lib/src/widgets/text.dart | 2 +- 23 files changed, 31 insertions(+), 27 deletions(-) rename examples/api/lib/{material => widgets}/context_menu/context_menu_controller.0.dart (100%) rename examples/api/lib/{material => widgets}/context_menu/editable_text_toolbar_builder.0.dart (89%) rename examples/api/lib/{material => widgets}/context_menu/editable_text_toolbar_builder.1.dart (94%) rename examples/api/lib/{material => widgets}/expansible/expansible.0.dart (100%) rename examples/api/lib/{material => widgets}/platform_menu_bar/platform_menu_bar.0.dart (100%) rename examples/api/lib/{material => widgets}/selectable_region/selectable_region.0.dart (100%) rename examples/api/lib/{material => widgets}/selection_container/selection_container.0.dart (100%) rename examples/api/lib/{material => widgets}/selection_container/selection_container_disabled.0.dart (100%) rename examples/api/test/{material => widgets}/context_menu/context_menu_controller.0_test.dart (94%) rename examples/api/test/{material => widgets}/context_menu/editable_text_toolbar_builder.0_test.dart (93%) rename examples/api/test/{material => widgets}/context_menu/editable_text_toolbar_builder.1_test.dart (96%) rename examples/api/test/{material => widgets}/expansible/expansible.0_test.dart (91%) rename examples/api/test/{material => widgets}/platform_menu_bar/platform_menu_bar.0_test.dart (97%) rename examples/api/test/{material => widgets}/selectable_region/selectable_region.0_test.dart (95%) rename examples/api/test/{material => widgets}/selection_container/selection_container.0_test.dart (95%) rename examples/api/test/{material => widgets}/selection_container/selection_container_disabled.0_test.dart (96%) diff --git a/examples/api/lib/material/context_menu/context_menu_controller.0.dart b/examples/api/lib/widgets/context_menu/context_menu_controller.0.dart similarity index 100% rename from examples/api/lib/material/context_menu/context_menu_controller.0.dart rename to examples/api/lib/widgets/context_menu/context_menu_controller.0.dart diff --git a/examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart b/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart similarity index 89% rename from examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart rename to examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart index 8e7615b395f37..fbe6e9f6ad9aa 100644 --- a/examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart +++ b/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart @@ -22,10 +22,7 @@ class EditableTextToolbarBuilderExampleApp extends StatefulWidget { class _EditableTextToolbarBuilderExampleAppState extends State { - final TextEditingController _controller = TextEditingController( - text: - 'Right click (desktop) or long press (mobile) to see the menu with custom buttons.', - ); + late final TextEditingController _controller; @override void initState() { @@ -35,10 +32,15 @@ class _EditableTextToolbarBuilderExampleAppState if (kIsWeb) { BrowserContextMenu.disableContextMenu(); } + _controller = TextEditingController( + text: + 'Right click (desktop) or long press (mobile) to see the menu with custom buttons.', + ); } @override void dispose() { + _controller.dispose(); if (kIsWeb) { BrowserContextMenu.enableContextMenu(); } @@ -70,8 +72,8 @@ class _EditableTextToolbarBuilderExampleAppState ContextMenuButtonItem buttonItem, ) { return CupertinoButton( - color: const Color(0xffaaaa00), - disabledColor: const Color(0xffaaaaff), + color: const Color(0xFFAAAA00), + disabledColor: const Color(0xFFAAAAFF), onPressed: buttonItem.onPressed, padding: const .all(10.0), pressedOpacity: 0.7, diff --git a/examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart b/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart similarity index 94% rename from examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart rename to examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart index 5e227460111ce..a21ebeef426f0 100644 --- a/examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart +++ b/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart @@ -24,7 +24,7 @@ class EditableTextToolbarBuilderExampleApp extends StatefulWidget { class _EditableTextToolbarBuilderExampleAppState extends State { - final TextEditingController _controller = TextEditingController(text: text); + late final TextEditingController _controller; void _showDialog(BuildContext context) { Navigator.of(context).push( @@ -44,10 +44,12 @@ class _EditableTextToolbarBuilderExampleAppState if (kIsWeb) { BrowserContextMenu.disableContextMenu(); } + _controller = TextEditingController(text: text); } @override void dispose() { + _controller.dispose(); if (kIsWeb) { BrowserContextMenu.enableContextMenu(); } @@ -75,7 +77,7 @@ class _EditableTextToolbarBuilderExampleAppState // Here we add an "Email" button to the default TextField // context menu for the current platform, but only if an email // address is currently selected. - final TextEditingValue value = _controller.value; + final TextEditingValue value = editableTextState.textEditingValue; if (_isValidEmail( value.selection.textInside(value.text), )) { diff --git a/examples/api/lib/material/expansible/expansible.0.dart b/examples/api/lib/widgets/expansible/expansible.0.dart similarity index 100% rename from examples/api/lib/material/expansible/expansible.0.dart rename to examples/api/lib/widgets/expansible/expansible.0.dart diff --git a/examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart b/examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart similarity index 100% rename from examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart rename to examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart diff --git a/examples/api/lib/material/selectable_region/selectable_region.0.dart b/examples/api/lib/widgets/selectable_region/selectable_region.0.dart similarity index 100% rename from examples/api/lib/material/selectable_region/selectable_region.0.dart rename to examples/api/lib/widgets/selectable_region/selectable_region.0.dart diff --git a/examples/api/lib/material/selection_container/selection_container.0.dart b/examples/api/lib/widgets/selection_container/selection_container.0.dart similarity index 100% rename from examples/api/lib/material/selection_container/selection_container.0.dart rename to examples/api/lib/widgets/selection_container/selection_container.0.dart diff --git a/examples/api/lib/material/selection_container/selection_container_disabled.0.dart b/examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart similarity index 100% rename from examples/api/lib/material/selection_container/selection_container_disabled.0.dart rename to examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart diff --git a/examples/api/test/material/context_menu/context_menu_controller.0_test.dart b/examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart similarity index 94% rename from examples/api/test/material/context_menu/context_menu_controller.0_test.dart rename to examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart index 74abc9b8fd2b7..b3bb824204462 100644 --- a/examples/api/test/material/context_menu/context_menu_controller.0_test.dart +++ b/examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_api_samples/material/context_menu/context_menu_controller.0.dart' +import 'package:flutter_api_samples/widgets/context_menu/context_menu_controller.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/api/test/material/context_menu/editable_text_toolbar_builder.0_test.dart b/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart similarity index 93% rename from examples/api/test/material/context_menu/editable_text_toolbar_builder.0_test.dart rename to examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart index 3ea1623f8202f..c5b18d58412d3 100644 --- a/examples/api/test/material/context_menu/editable_text_toolbar_builder.0_test.dart +++ b/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart @@ -6,7 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_api_samples/material/context_menu/editable_text_toolbar_builder.0.dart' +import 'package:flutter_api_samples/widgets/context_menu/editable_text_toolbar_builder.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/api/test/material/context_menu/editable_text_toolbar_builder.1_test.dart b/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart similarity index 96% rename from examples/api/test/material/context_menu/editable_text_toolbar_builder.1_test.dart rename to examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart index 9fcf3956a73c6..5b16a23baa2dd 100644 --- a/examples/api/test/material/context_menu/editable_text_toolbar_builder.1_test.dart +++ b/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_api_samples/material/context_menu/editable_text_toolbar_builder.1.dart' +import 'package:flutter_api_samples/widgets/context_menu/editable_text_toolbar_builder.1.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/api/test/material/expansible/expansible.0_test.dart b/examples/api/test/widgets/expansible/expansible.0_test.dart similarity index 91% rename from examples/api/test/material/expansible/expansible.0_test.dart rename to examples/api/test/widgets/expansible/expansible.0_test.dart index bd667a5c1cc4e..b3844558bd16f 100644 --- a/examples/api/test/material/expansible/expansible.0_test.dart +++ b/examples/api/test/widgets/expansible/expansible.0_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_api_samples/material/expansible/expansible.0.dart' +import 'package:flutter_api_samples/widgets/expansible/expansible.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/api/test/material/platform_menu_bar/platform_menu_bar.0_test.dart b/examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart similarity index 97% rename from examples/api/test/material/platform_menu_bar/platform_menu_bar.0_test.dart rename to examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart index 93e7a2743e90e..192544ad175c1 100644 --- a/examples/api/test/material/platform_menu_bar/platform_menu_bar.0_test.dart +++ b/examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_api_samples/material/platform_menu_bar/platform_menu_bar.0.dart' +import 'package:flutter_api_samples/widgets/platform_menu_bar/platform_menu_bar.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/api/test/material/selectable_region/selectable_region.0_test.dart b/examples/api/test/widgets/selectable_region/selectable_region.0_test.dart similarity index 95% rename from examples/api/test/material/selectable_region/selectable_region.0_test.dart rename to examples/api/test/widgets/selectable_region/selectable_region.0_test.dart index 2bc72a892cf05..3fc827f219e66 100644 --- a/examples/api/test/material/selectable_region/selectable_region.0_test.dart +++ b/examples/api/test/widgets/selectable_region/selectable_region.0_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_api_samples/material/selectable_region/selectable_region.0.dart' +import 'package:flutter_api_samples/widgets/selectable_region/selectable_region.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/api/test/material/selection_container/selection_container.0_test.dart b/examples/api/test/widgets/selection_container/selection_container.0_test.dart similarity index 95% rename from examples/api/test/material/selection_container/selection_container.0_test.dart rename to examples/api/test/widgets/selection_container/selection_container.0_test.dart index 23a5531f614eb..79655771b89ce 100644 --- a/examples/api/test/material/selection_container/selection_container.0_test.dart +++ b/examples/api/test/widgets/selection_container/selection_container.0_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_api_samples/material/selection_container/selection_container.0.dart' +import 'package:flutter_api_samples/widgets/selection_container/selection_container.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/api/test/material/selection_container/selection_container_disabled.0_test.dart b/examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart similarity index 96% rename from examples/api/test/material/selection_container/selection_container_disabled.0_test.dart rename to examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart index 673b32cb94269..a9a9b8809f21d 100644 --- a/examples/api/test/material/selection_container/selection_container_disabled.0_test.dart +++ b/examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_api_samples/material/selection_container/selection_container_disabled.0.dart' +import 'package:flutter_api_samples/widgets/selection_container/selection_container_disabled.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/flutter/lib/src/widgets/context_menu_controller.dart b/packages/flutter/lib/src/widgets/context_menu_controller.dart index 2e1742f101143..ff697fe84968d 100644 --- a/packages/flutter/lib/src/widgets/context_menu_controller.dart +++ b/packages/flutter/lib/src/widgets/context_menu_controller.dart @@ -20,7 +20,7 @@ import 'overlay.dart'; /// This example shows how to use a GestureDetector to show a context menu /// anywhere in a widget subtree that receives a right click or long press. /// -/// ** See code in examples/api/lib/material/context_menu/context_menu_controller.0.dart ** +/// ** See code in examples/api/lib/widgets/context_menu/context_menu_controller.0.dart ** /// {@end-tool} /// /// See also: diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index d43f4c6465e20..ca6a88fbba1ab 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -2023,14 +2023,14 @@ class EditableText extends StatefulWidget { /// This example shows how to customize the menu, in this case by keeping the /// default buttons for the platform but modifying their appearance. /// - /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart ** + /// ** See code in examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This example shows how to show a custom button only when an email address /// is currently selected. /// - /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart ** + /// ** See code in examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart ** /// {@end-tool} /// /// See also: diff --git a/packages/flutter/lib/src/widgets/expansible.dart b/packages/flutter/lib/src/widgets/expansible.dart index 41f7c7a07cea5..3698be3e97f26 100644 --- a/packages/flutter/lib/src/widgets/expansible.dart +++ b/packages/flutter/lib/src/widgets/expansible.dart @@ -230,7 +230,7 @@ class ExpansibleController extends ChangeNotifier { /// This example demonstrates how to use the [Expansible] widget and how an /// [ExpansibleController] can be used to programmatically expand or collapse it. /// -/// ** See code in examples/api/lib/material/expansible/expansible.0.dart ** +/// ** See code in examples/api/lib/widgets/expansible/expansible.0.dart ** /// {@end-tool} /// /// See also: diff --git a/packages/flutter/lib/src/widgets/platform_menu_bar.dart b/packages/flutter/lib/src/widgets/platform_menu_bar.dart index 8edda62b7018b..8370e56889661 100644 --- a/packages/flutter/lib/src/widgets/platform_menu_bar.dart +++ b/packages/flutter/lib/src/widgets/platform_menu_bar.dart @@ -438,7 +438,7 @@ class DefaultPlatformMenuDelegate extends PlatformMenuDelegate { /// /// **This example will only work on macOS.** /// -/// ** See code in examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart ** +/// ** See code in examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart ** /// {@end-tool} /// /// The menus could just as effectively be managed without using the widget tree diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart index bb34e8a722e30..ca7f777d8a507 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart @@ -160,7 +160,7 @@ const double _kSelectableVerticalComparingThreshold = 3.0; /// This sample demonstrates how to create an adapter widget that makes any /// child widget selectable. /// -/// ** See code in examples/api/lib/material/selectable_region/selectable_region.0.dart ** +/// ** See code in examples/api/lib/widgets/selectable_region/selectable_region.0.dart ** /// {@end-tool} /// /// ## Complex layout @@ -173,7 +173,7 @@ const double _kSelectableVerticalComparingThreshold = 3.0; /// This sample demonstrates how to create a [SelectionContainer] that only /// allows selecting everything or nothing with no partial selection. /// -/// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart ** +/// ** See code in examples/api/lib/widgets/selection_container/selection_container.0.dart ** /// {@end-tool} /// /// In the case where a group of widgets should be excluded from selection under @@ -183,7 +183,7 @@ const double _kSelectableVerticalComparingThreshold = 3.0; /// {@tool dartpad} /// This sample demonstrates how to disable selection for a Text in a Column. /// -/// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** +/// ** See code in examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart ** /// {@end-tool} /// /// To create a separate selection system from its parent selection area, diff --git a/packages/flutter/lib/src/widgets/selection_container.dart b/packages/flutter/lib/src/widgets/selection_container.dart index 5d5a2386a7a21..eb6720ffa9f0d 100644 --- a/packages/flutter/lib/src/widgets/selection_container.dart +++ b/packages/flutter/lib/src/widgets/selection_container.dart @@ -34,7 +34,7 @@ import 'framework.dart'; /// This sample demonstrates how to create a [SelectionContainer] that only /// allows selecting everything or nothing with no partial selection. /// -/// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart ** +/// ** See code in examples/api/lib/widgets/selection_container/selection_container.0.dart ** /// {@end-tool} /// /// See also: @@ -60,7 +60,7 @@ class SelectionContainer extends StatefulWidget { /// This sample demonstrates how to disable selection for a Text under a /// SelectionArea. /// - /// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** + /// ** See code in examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart ** /// {@end-tool} const SelectionContainer.disabled({super.key, required this.child}) : registrar = null, diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index a44b97eeb0bb2..ba8e8d8ede929 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -486,7 +486,7 @@ class DefaultTextHeightBehavior extends InheritedTheme { /// This sample demonstrates how to disable selection for a Text under a /// SelectionArea. /// -/// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** +/// ** See code in examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart ** /// {@end-tool} /// /// See also: From 8ef2fe42554dbd8fb60098208c4ac5925f117a3a Mon Sep 17 00:00:00 2001 From: Shah Fahad <49402500+fahaddoc@users.noreply.github.com> Date: Wed, 5 Aug 2026 06:02:12 +0500 Subject: [PATCH 066/330] Document super call order for State.didChangeDependencies (#185945) The doc comment on `State.didChangeDependencies` is silent about where overrides should place the `super.didChangeDependencies()` call, even though `@mustCallSuper` is set on it. The sibling lifecycle hook `State.initState` already documents this convention, so the two methods read inconsistently and contributors hitting `didChangeDependencies` for the first time have to guess. This PR adds a short note to the dartdoc that mirrors the wording on `initState`, telling implementations to start with a call to the inherited method (`super.didChangeDependencies()`). It is a documentation-only change to `packages/flutter/lib/src/widgets/framework.dart`. There is no behavior change, no API change, and no new public surface. Fixes #28925. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. This PR is test-exempt because it only edits documentation text in a dartdoc comment, with no behavioral or API changes. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter/lib/src/widgets/framework.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 2bc2e3f3012b7..fb9805fabc2e1 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -1477,6 +1477,9 @@ abstract class State with Diagnosticable { /// this method because they need to do some expensive work (e.g., network /// fetches) when their dependencies change, and that work would be too /// expensive to do for every build. + /// + /// Implementations of this method should start with a call to the inherited + /// method, as in `super.didChangeDependencies`. @protected @mustCallSuper void didChangeDependencies() {} From 222295b161524d69a72cf362b09055c45530c559 Mon Sep 17 00:00:00 2001 From: Vincent Ong <256906086+mvincentong@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:08:56 +0800 Subject: [PATCH 067/330] Document frozen embedder API structs (#186842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Issue Part of #38470. The issue asks for embedder API structs that must not be extended in place to be documented. Review feedback asked what basis was used to decide which structs were frozen. ## Fix Narrowed the PR to document only the structs that are already protected by the `EmbedderFrozen` golden test in `engine/src/flutter/shell/platform/embedder/tests/embedder_frozen_unittests.cc`. This removes the earlier unsupported `ABI-sealed` labels from renderer, backing-store, task, Dart object, and AOT structs that are not covered by that test-backed source of truth. ## Tests - `git diff --check` - Not run: `clang-format --dry-run --Werror engine/src/flutter/shell/platform/embedder/embedder.h`, because `clang-format` is not installed in this checkout. ## Risk Docs-only change to `engine/src/flutter/shell/platform/embedder/embedder.h`. No generated output. Other possible sealed embedder structs may still need a separate verified pass before being documented. --------- Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- .../flutter/shell/platform/embedder/embedder.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/engine/src/flutter/shell/platform/embedder/embedder.h b/engine/src/flutter/shell/platform/embedder/embedder.h index e9f3769963b2f..5f6cf7675141d 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.h +++ b/engine/src/flutter/shell/platform/embedder/embedder.h @@ -31,6 +31,8 @@ // - Instead of array of structures, prefer array of pointers to structures. // This ensures that array indexing does not break if members are added // to the structure. +// - Structures documented as frozen must not have members added. Introduce a +// new versioned structure instead. // // These changes are allowed: // - Adding new struct members at the end of a structure as long as the struct @@ -392,6 +394,7 @@ typedef struct _FlutterEngine* FLUTTER_API_SYMBOL(FlutterEngine); /// opaque to the engine; the engine does not interpret view IDs in any way. typedef int64_t FlutterViewId; +// Frozen because adding members would break the ABI of `FlutterSemanticsNode`. typedef struct { /// horizontal scale factor double scaleX; @@ -645,6 +648,9 @@ typedef struct { } FlutterUIntSize; /// A structure to represent a rectangle. +/// +// Frozen because adding members would break the ABI of `FlutterSemanticsNode` +// and `FlutterDamage`. typedef struct { double left; double top; @@ -653,6 +659,8 @@ typedef struct { } FlutterRect; /// A structure to represent a 2D point. +/// +// Frozen because adding members would break the ABI of `FlutterLayer`. typedef struct { double x; double y; @@ -668,6 +676,8 @@ typedef struct { } FlutterRoundedRect; /// A structure to represent a damage region. +/// +// Frozen because adding members would break the ABI of `FlutterPresentInfo`. typedef struct { /// The size of this struct. Must be sizeof(FlutterDamage). size_t struct_size; @@ -1582,6 +1592,9 @@ typedef struct { /// ABI compatibility for existing users, no new fields will be /// added to this struct. New fields will continue to be added /// to `FlutterSemanticsNode2`. +/// +// Frozen because adding members would break the ABI of +// `FlutterSemanticsUpdate`. typedef struct { /// The size of this struct. Must be sizeof(FlutterSemanticsNode). size_t struct_size; @@ -1789,6 +1802,9 @@ extern const int32_t kFlutterSemanticsCustomActionIdBatchEnd; /// preserve ABI compatility for existing users, no new fields /// will be added to this struct. New fields will continue to /// be added to `FlutterSemanticsCustomAction2`. +/// +// Frozen because adding members would break the ABI of +// `FlutterSemanticsUpdate`. typedef struct { /// The size of the struct. Must be sizeof(FlutterSemanticsCustomAction). size_t struct_size; From 72345132be6f1fc4def7a6f572aa910c8b248892 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 4 Aug 2026 21:26:54 -0400 Subject: [PATCH 068/330] Roll Fuchsia Test Scripts from ltbuIH9Z3T_yOuigu... to vcANVO8VIDQHasH1X... (#190589) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/fuchsia-test-scripts-flutter Please CC chrome-fuchsia-engprod@google.com,codefu@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 70bacf5f13b4a..b43b941044231 100644 --- a/DEPS +++ b/DEPS @@ -199,7 +199,7 @@ vars = { # The version / instance id of the cipd:chromium/fuchsia/test-scripts which # will be used altogether with fuchsia-sdk to setup the build / test # environment. - 'fuchsia_test_scripts_version': 'ltbuIH9Z3T_yOuiguCVHKgUR4YOTjj87kcx20LvoNkgC', + 'fuchsia_test_scripts_version': 'vcANVO8VIDQHasH1X_XRoSYLvx7fNwvTbDM1NT9TwA4C', # The version / instance id of the cipd:chromium/fuchsia/gn-sdk which will be # used altogether with fuchsia-sdk to generate gn based build rules. From 8eef451a28d4731bbf6168df33958c0028d52e71 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Wed, 5 Aug 2026 10:53:23 +0900 Subject: [PATCH 069/330] Windows: Propagate enabled accessibility state (#190507) Relands #184501, which was reverted in #186492 because mapping read-only text fields to Role::kTextField caused the macOS embedder to instantiate a FlutterTextPlatformNode for them, producing detached accessibility elements and test crashes. The underlying macOS issue is now fixed by #190330 and #190353. This relands the remaining bits of #184501. In the common bits of the bridge, read-only text fields now map to `Role::kTextField`. In the Windows implementation `ENABLED_CHANGED` and `READONLY_CHANGED` are dispatched as MSAA `kStateChanged` events and `AXPlatformNodeWin` now raises the corresponding `UIA_IsEnabledPropertyId` change. Fixes: #184559 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../platform/common/accessibility_bridge.cc | 2 +- .../common/accessibility_bridge_unittests.cc | 35 +++++++++++++++++++ .../windows/accessibility_bridge_windows.cc | 7 ++-- .../accessibility_bridge_windows_unittests.cc | 10 ++++++ .../ax/platform/ax_platform_node_win.cc | 5 ++- 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/engine/src/flutter/shell/platform/common/accessibility_bridge.cc b/engine/src/flutter/shell/platform/common/accessibility_bridge.cc index 41c954c6c2a7a..e4cda5b14add1 100644 --- a/engine/src/flutter/shell/platform/common/accessibility_bridge.cc +++ b/engine/src/flutter/shell/platform/common/accessibility_bridge.cc @@ -318,7 +318,7 @@ void AccessibilityBridge::SetRoleFromFlutterUpdate(ui::AXNodeData& node_data, node_data.role = ax::mojom::Role::kButton; return; } - if (flags->is_text_field && !flags->is_read_only) { + if (flags->is_text_field) { node_data.role = ax::mojom::Role::kTextField; return; } diff --git a/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc b/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc index 4f013b1cc3318..b47ddd3623d6e 100644 --- a/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc +++ b/engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc @@ -356,6 +356,41 @@ TEST(AccessibilityBridgeTest, ReadOnlyTextFieldHasReadOnlyRestriction) { ax::mojom::Restriction::kReadOnly); } +TEST(AccessibilityBridgeTest, ReadOnlyTextFieldHasTextFieldRole) { + std::shared_ptr bridge = + std::make_shared(); + FlutterSemanticsNode2 root = CreateSemanticsNode(0, "root"); + auto flags = FlutterSemanticsFlags{ + .is_enabled = FlutterTristate::kFlutterTristateTrue, + .is_text_field = true, + .is_read_only = true, + }; + root.flags2 = &flags; + bridge->AddFlutterSemanticsNodeUpdate(root); + bridge->CommitUpdates(); + + auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); + EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kTextField); +} + +TEST(AccessibilityBridgeTest, EditableTextFieldHasTextFieldRole) { + std::shared_ptr bridge = + std::make_shared(); + FlutterSemanticsNode2 root = CreateSemanticsNode(0, "root"); + auto flags = FlutterSemanticsFlags{ + .is_enabled = FlutterTristate::kFlutterTristateTrue, + .is_text_field = true, + .is_read_only = false, + }; + root.flags2 = &flags; + bridge->AddFlutterSemanticsNodeUpdate(root); + bridge->CommitUpdates(); + + auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); + EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kTextField); + EXPECT_TRUE(root_node->GetData().HasState(ax::mojom::State::kEditable)); +} + // Ensure that checkboxes have their checked status set apropriately // Previously, only Radios could have this flag updated // Resulted in the issue seen at diff --git a/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows.cc b/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows.cc index 085397060edca..412dbb2ae46cd 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows.cc @@ -103,6 +103,11 @@ void AccessibilityBridgeWindows::OnAccessibilityEvent( DispatchWinAccessibilityEvent(win_delegate, ax::mojom::Event::kStateChanged); break; + case ui::AXEventGenerator::Event::ENABLED_CHANGED: + case ui::AXEventGenerator::Event::READONLY_CHANGED: + DispatchWinAccessibilityEvent(win_delegate, + ax::mojom::Event::kStateChanged); + break; case ui::AXEventGenerator::Event::ACCESS_KEY_CHANGED: case ui::AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED: case ui::AXEventGenerator::Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED: @@ -116,7 +121,6 @@ void AccessibilityBridgeWindows::OnAccessibilityEvent( case ui::AXEventGenerator::Event::DESCRIPTION_CHANGED: case ui::AXEventGenerator::Event::DOCUMENT_TITLE_CHANGED: case ui::AXEventGenerator::Event::DROPEFFECT_CHANGED: - case ui::AXEventGenerator::Event::ENABLED_CHANGED: case ui::AXEventGenerator::Event::EXPANDED: case ui::AXEventGenerator::Event::FLOW_FROM_CHANGED: case ui::AXEventGenerator::Event::FLOW_TO_CHANGED: @@ -142,7 +146,6 @@ void AccessibilityBridgeWindows::OnAccessibilityEvent( case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED: case ui::AXEventGenerator::Event::PORTAL_ACTIVATED: case ui::AXEventGenerator::Event::POSITION_IN_SET_CHANGED: - case ui::AXEventGenerator::Event::READONLY_CHANGED: case ui::AXEventGenerator::Event::RELATED_NODE_CHANGED: case ui::AXEventGenerator::Event::REQUIRED_STATE_CHANGED: case ui::AXEventGenerator::Event::ROLE_CHANGED: diff --git a/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows_unittests.cc b/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows_unittests.cc index 3f7ff3d21a3ba..ee6eb6cf0e36f 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_bridge_windows_unittests.cc @@ -403,5 +403,15 @@ TEST(AccessibilityBridgeWindows, OnDocumentSelectionChanged) { ax::mojom::Event::kDocumentSelectionChanged, 2); } +TEST(AccessibilityBridgeWindows, OnAccessibilityEnabledChanged) { + ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::ENABLED_CHANGED, + ax::mojom::Event::kStateChanged); +} + +TEST(AccessibilityBridgeWindows, OnAccessibilityReadOnlyChanged) { + ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::READONLY_CHANGED, + ax::mojom::Event::kStateChanged); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/third_party/accessibility/ax/platform/ax_platform_node_win.cc b/engine/src/flutter/third_party/accessibility/ax/platform/ax_platform_node_win.cc index 8a2ff0401863e..4c8918337065f 100644 --- a/engine/src/flutter/third_party/accessibility/ax/platform/ax_platform_node_win.cc +++ b/engine/src/flutter/third_party/accessibility/ax/platform/ax_platform_node_win.cc @@ -2599,7 +2599,8 @@ IFACEMETHODIMP AXPlatformNodeWin::QueryService(REFGUID guidService, void** object) { COM_OBJECT_VALIDATE_1_ARG(object); - if (guidService == IID_IAccessible || (guidService == IID_IAccessibleEx && delegate_->IsIAccessibleExEnabled())) { + if (guidService == IID_IAccessible || (guidService == IID_IAccessibleEx && + delegate_->IsIAccessibleExEnabled())) { return QueryInterface(riid, object); } @@ -5313,6 +5314,8 @@ std::optional AXPlatformNodeWin::MojoEventToUIAProperty( return UIA_ToggleToggleStatePropertyId; } return std::nullopt; + case ax::mojom::Event::kStateChanged: + return UIA_IsEnabledPropertyId; default: return std::nullopt; } From 27b098811f3b08b5f92f8d21fea0dcf4e22b6f28 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 4 Aug 2026 19:27:21 -0700 Subject: [PATCH 070/330] reland(tool): remove redundant --enable-experiment=record-use flag (#190591) Relands #190475 (reverted in #190583 due to a semantic collision with #190476). When compiling web targets (dart2js and dart2wasm) or running dry runs with the record-use feature flag enabled, flutter_tools explicitly passed --enable-experiment=record-use to the compiler. Since record-use is enabled by default in recent Dart SDKs, passing this flag caused warning spam during standard compilation and dry runs. * Remove --enable-experiment=record-use from Dart2JSTarget and Dart2WasmTarget in web.dart. * Remove expected flag from test commands in web_test.dart and web_dry_run_test.dart, including the recently added occurrence in addWasmCompilerErrorCommand from #190476 that caused the post-commit test failure. Fixes #190465 --- .../flutter_tools/lib/src/build_system/targets/web.dart | 9 ++------- .../build_system/targets/web_dry_run_test.dart | 1 - .../general.shard/build_system/targets/web_test.dart | 3 --- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 11bd122791c9f..98a05a5384dc4 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -199,10 +199,7 @@ class Dart2JSTarget extends Dart2WebTarget { else if (buildMode == BuildMode.release) '-Ddart.vm.product=true', for (final String dartDefine in computeDartDefines(environment)) '-D$dartDefine', - if (featureFlags.isRecordUseEnabled) ...[ - '--write-resources', - '--enable-experiment=record-use', - ], + if (featureFlags.isRecordUseEnabled) '--write-resources', ]; // NOTE: most args should be populated in [toSharedCommandOptions]. @@ -374,10 +371,8 @@ class Dart2WasmTarget extends Dart2WebTarget { ...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions), for (final String dartDefine in dartDefines) '-D$dartDefine', '--extra-compiler-option=--depfile=${depFile.path}', - if (featureFlags.isRecordUseEnabled) ...[ + if (featureFlags.isRecordUseEnabled) '--recorded-uses=${environment.buildDir.childFile(LinkHooks.recordedUsesWasmFileName).path}', - '--enable-experiment=record-use', - ], ...compilerConfig.toCommandOptions(buildMode), '-o', outputWasmFile.path, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart index cffa0fc0c24a1..3ea85d5f4f2a2 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart @@ -98,7 +98,6 @@ name: my_app '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--extra-compiler-option=--depfile=${environment.buildDir.childFile('dart2wasm.d').path}', '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').path}', - '--enable-experiment=record-use', '-O0', '--no-strip-wasm', '--no-minify', diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index 07966a8c91e0d..027ff48b7a79a 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -41,7 +41,6 @@ const _kStandardFlutterWebDefines = [ '-DFLUTTER_WEB_USE_SKWASM=false', '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--write-resources', - '--enable-experiment=record-use', ]; const _kDart2WasmLinuxArgs = [ @@ -1367,7 +1366,6 @@ _flutter.loader.load(); '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--extra-compiler-option=--depfile=${depFile.absolute.path}', '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').absolute.path}', - '--enable-experiment=record-use', '-O$expectedLevel', if (strip && buildMode == 'release') '--strip-wasm' @@ -1428,7 +1426,6 @@ _flutter.loader.load(); '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', '--extra-compiler-option=--depfile=${environment.buildDir.childFile('dart2wasm.d').absolute.path}', '--recorded-uses=${environment.buildDir.childFile('recorded_uses_wasm.json').absolute.path}', - '--enable-experiment=record-use', '-O2', '--no-strip-wasm', '--no-source-maps', From f520d04a3ef43f329a69bf9d61f41c651c7969fe Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 5 Aug 2026 00:45:24 -0400 Subject: [PATCH 071/330] Roll Skia from a8583a0a2c11 to ad7abeecbb6d (5 revisions) (#190593) https://skia.googlesource.com/skia.git/+log/a8583a0a2c11..ad7abeecbb6d 2026-08-04 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-04 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-04 michaelludwig@google.com [graphite] Tune search limit parameters for DrawListLayers 2026-08-04 skia-autoroll@skia-public.iam.gserviceaccount.com Manual roll ANGLE from 91d2d125ec00 to d5c8131b66e3 (10 revisions) 2026-08-04 michaelludwig@google.com Reland "[text] Introduce PackedGPUGlyphID to add more metadata to SkPackedGlyphID" If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC alexisdavidc@google.com,codefu@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index b43b941044231..b485e3cd0862d 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'a8583a0a2c114130cd88d7fbca23248d50a91946', + 'skia_revision': 'ad7abeecbb6ddc1ceaaec7cb987fc893ca7e9a62', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 7820e78090390805756acb8906fd472cc4a790da Mon Sep 17 00:00:00 2001 From: Gray Mackall <34871572+gmackall@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:53:16 -0700 Subject: [PATCH 072/330] Add `--[no-]enable-hcpp` build flag and pipe through via Gradle manifest transformation (#189955) - Adds `--[no-]enable-hcpp` to `flutter build apk` and `flutter build appbundle`. Not added to `build aar`: a value injected into a module's library manifest can fail an add-to-app host's build in the manifest merger when the host selects the opposite value, so the host application's manifest stays the source of truth. - Passes `-Penable-hcpp=` (the tool's default) and `-Pexplicit-enable-hcpp=` (set only when the developer passed the flag) to Gradle. - Registers `EnableHcppManifestTask` in the Flutter Gradle Plugin, a transform of the application project's merged manifest. Precedence is `--[no-]enable-hcpp` > `AndroidManifest.xml` > the tool's default: an explicit flag is written into the merged manifest over whatever is there and reported at lifecycle level; without the flag, the default is only injected when the manifest does not set `EnableHcpp` at all. - Known tradeoff: when the transform modifies the manifest it re-serializes it, which drops XML comments including the manifest merger's provenance annotations. This is invisible to aapt2 and is covered by a test. - Adds Kotlin Gradle task and Dart tool unit/integration test coverage. --- .../gradle/src/main/kotlin/FlutterPlugin.kt | 4 + .../src/main/kotlin/FlutterPluginUtils.kt | 54 ++ .../kotlin/tasks/EnableHcppManifestTask.kt | 58 ++ .../tasks/EnableHcppManifestTaskHelper.kt | 101 +++ .../src/test/kotlin/FlutterPluginUtilsTest.kt | 93 +++ .../tasks/EnableHcppManifestTaskTest.kt | 573 ++++++++++++++++++ .../flutter_tools/lib/src/build_info.dart | 26 + .../lib/src/commands/build_aar.dart | 5 + .../lib/src/commands/build_apk.dart | 5 +- .../lib/src/commands/build_appbundle.dart | 5 +- .../flutter_tools/lib/src/commands/run.dart | 1 - packages/flutter_tools/lib/src/project.dart | 24 +- .../lib/src/runner/flutter_command.dart | 34 +- .../hermetic/build_aar_test.dart | 2 +- .../permeable/build_aar_test.dart | 33 + .../permeable/build_apk_test.dart | 195 ++++++ .../permeable/build_appbundle_test.dart | 93 +++ .../test/general.shard/build_info_test.dart | 52 ++ .../runner/flutter_command_test.dart | 45 ++ 19 files changed, 1394 insertions(+), 9 deletions(-) create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTask.kt create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTaskHelper.kt create mode 100644 packages/flutter_tools/gradle/src/test/kotlin/tasks/EnableHcppManifestTaskTest.kt diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt index 806fc5097a022..8d898e0d72b59 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt @@ -304,6 +304,10 @@ class FlutterPlugin : Plugin { FlutterPluginUtils.addTaskForPrintNdkVersion(projectToAddTasksTo) FlutterPluginUtils.addTasksForOutputsAppLinkSettings(projectToAddTasksTo) } + // Only applies to app projects. For module (aar) projects the host app's manifest is + // the source of truth for HCPP; see addTasksForEnableHcppManifest for why injecting + // into the library manifest would break host builds that explicitly opt out. + FlutterPluginUtils.addTasksForEnableHcppManifest(projectToAddTasksTo) val targetPlatforms: List = FlutterPluginUtils.getTargetPlatforms(projectToAddTasksTo) diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 50779e6d0b8d0..eb618a2b9008f 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -13,6 +13,7 @@ import com.android.build.gradle.BaseExtension import com.android.builder.model.BuildType import com.flutter.gradle.plugins.PluginHandler import com.flutter.gradle.tasks.DeepLinkJsonFromManifestTask +import com.flutter.gradle.tasks.EnableHcppManifestTask import com.flutter.gradle.tasks.PrintTask import com.flutter.gradle.tasks.ValidateCompileSdkVersionTask import groovy.lang.Closure @@ -38,6 +39,8 @@ object FlutterPluginUtils { // recommended to use these const values in tests. internal const val PROP_SHOULD_SHRINK_RESOURCES = "shrink" internal const val PROP_SPLIT_PER_ABI = "split-per-abi" + internal const val PROP_ENABLE_HCPP = "enable-hcpp" + internal const val PROP_EXPLICIT_ENABLE_HCPP = "explicit-enable-hcpp" internal const val PROP_LOCAL_ENGINE_REPO = "local-engine-repo" internal const val PROP_IS_VERBOSE = "verbose" internal const val PROP_TARGET = "target" @@ -1156,4 +1159,55 @@ object FlutterPluginUtils { ).toTransform(SingleArtifact.MERGED_MANIFEST) // (3) Indicate the artifact and operation type. } } + + /** + * Adds tasks that inject the `io.flutter.embedding.android.EnableHcpp` meta-data into the + * merged manifest of each variant, when the flutter tool passed `-Penable-hcpp=true` (i.e. + * when the `--enable-hcpp` flag was passed). + * + * The meta-data is only added when not already present in the merged manifest, so an + * explicit value in the developer's manifest always takes priority over the tool's default. + * An explicit `--enable-hcpp`/`--no-enable-hcpp` on `flutter run`/`flutter test` is passed + * to the engine at launch instead, which takes priority over the manifest at runtime. + * + * Only applies to application projects. Injecting into a module (aar) library manifest + * would propagate into the host app's merged manifest, and if the host app explicitly sets + * the meta-data to a different value the manifest merger fails the host build with an + * attribute conflict ("Attribute meta-data#...EnableHcpp@value value=(false) ... is also + * present at [library] ... value=(true)") instead of letting the host win. Add-to-app hosts + * therefore control HCPP exclusively through their own manifest. + */ + @JvmStatic + @JvmName("addTasksForEnableHcppManifest") + internal fun addTasksForEnableHcppManifest(project: Project) { + if (!isFlutterAppProject(project)) { + return + } + val enableHcpp: Boolean = + project.findProperty(PROP_ENABLE_HCPP)?.toString()?.toBoolean() ?: false + val explicitEnableHcpp: Boolean? = + project.findProperty(PROP_EXPLICIT_ENABLE_HCPP)?.toString()?.toBoolean() + if (!enableHcpp && explicitEnableHcpp == null) { + return + } + val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) + androidComponents.onVariants { variant -> + val hcppManifestUpdater = + project.tasks.register( + "enableHcppInManifest${capitalize(variant.name)}", + EnableHcppManifestTask::class.java + ) { + this.requestedEnableHcpp.set(enableHcpp) + if (explicitEnableHcpp != null) { + this.explicitEnableHcpp.set(explicitEnableHcpp) + } + } + variant.artifacts + .use(hcppManifestUpdater) + .wiredWithFiles( + EnableHcppManifestTask::manifestFile, + EnableHcppManifestTask::updatedManifest + ).toTransform(SingleArtifact.MERGED_MANIFEST) + } + } } diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTask.kt new file mode 100644 index 0000000000000..6eeb66b73db98 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTask.kt @@ -0,0 +1,58 @@ +// 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. + +package com.flutter.gradle.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Manages the `io.flutter.embedding.android.EnableHcpp` meta-data in the merged + * AndroidManifest. + * + * If [explicitEnableHcpp] is provided it is written to the manifest, replacing any value already + * there. Otherwise [requestedEnableHcpp] is injected only when the manifest does not set the + * metadata at all. See [EnableHcppManifestTaskHelper.processHcppManifest] for the precedence. + * + * The message reporting that the flag overrode the manifest is emitted from the task action, so it + * is only printed when the task actually runs. A subsequent up-to-date or cached build produces the + * same (correct) manifest without repeating the message. + */ +@CacheableTask +abstract class EnableHcppManifestTask : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val manifestFile: RegularFileProperty + + @get:OutputFile + abstract val updatedManifest: RegularFileProperty + + @get:Input + @get:Optional + abstract val requestedEnableHcpp: Property + + @get:Input + @get:Optional + abstract val explicitEnableHcpp: Property + + @TaskAction + fun processManifest() { + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile.get().asFile, + updatedManifest = updatedManifest.get().asFile, + requestedEnableHcpp = requestedEnableHcpp.getOrElse(false), + explicitEnableHcpp = explicitEnableHcpp.orNull, + logger = logger + ) + } +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTaskHelper.kt new file mode 100644 index 0000000000000..a701cbd269b4d --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/EnableHcppManifestTaskHelper.kt @@ -0,0 +1,101 @@ +// 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. + +package com.flutter.gradle.tasks + +import groovy.util.Node +import groovy.xml.XmlNodePrinter +import groovy.xml.XmlParser +import org.gradle.api.logging.Logger +import java.io.File +import java.io.PrintWriter + +/** + * Stateless object to contain the logic used in [EnableHcppManifestTask]. + */ +object EnableHcppManifestTaskHelper { + private const val MANIFEST_NAME_KEY = "android:name" + private const val MANIFEST_VALUE_KEY = "android:value" + internal const val HCPP_METADATA_NAME = "io.flutter.embedding.android.EnableHcpp" + + // The flutter tool flags that set the properties this task consumes. Only used to name the + // flag the developer passed when reporting that it overrode the manifest. + internal const val ENABLE_HCPP_FLAG = "--enable-hcpp" + internal const val NO_ENABLE_HCPP_FLAG = "--no-enable-hcpp" + + /** + * Processes [manifestFile] and writes to [updatedManifest]. + * + * [explicitEnableHcpp] is the value of an explicit `--[no-]enable-hcpp`, or null when the + * developer did not pass the flag. When it is non-null it is written to the merged manifest, + * replacing any value already there: a flag passed at invocation time takes priority over + * checked in configuration, matching how the same flag behaves at launch for + * `flutter run`/`test`/`drive`, and how Gradle orders `-P` properties ahead of + * `gradle.properties`. + * + * With no explicit flag, [requestedEnableHcpp] is only a default: it is injected when the + * merged manifest does not set `EnableHcpp` at all, so an entry in the manifest wins. + * + * The resulting precedence is `--[no-]enable-hcpp` > AndroidManifest.xml > the tool's default. + * + * Note that the manifest is re-serialized from the parsed tree whenever it is modified, + * which drops XML comments (including the provenance comments the manifest merger emits). + * This is invisible to aapt2, but does affect the merged manifest as read by a human. + */ + fun processHcppManifest( + manifestFile: File, + updatedManifest: File, + requestedEnableHcpp: Boolean, + explicitEnableHcpp: Boolean? = null, + logger: Logger? = null + ) { + val manifest: Node = + XmlParser(false, false) + .parse(manifestFile) + val applicationNode: Node = + manifest.children().filterIsInstance().find { node -> + node.name() == "application" + } ?: Node(manifest, "application") + val metaDataNode: Node? = + applicationNode.children().filterIsInstance().find { node -> + node.name() == "meta-data" && node.attribute(MANIFEST_NAME_KEY) == HCPP_METADATA_NAME + } + + val valueToWrite: String? = + when { + // An explicit flag always wins, whether or not the manifest already says something. + explicitEnableHcpp != null -> explicitEnableHcpp.toString() + // Otherwise only supply a default, and only when the manifest is silent. + metaDataNode == null && requestedEnableHcpp -> true.toString() + else -> null + } + if (valueToWrite == null) { + manifestFile.copyTo(updatedManifest, overwrite = true) + return + } + + if (metaDataNode != null) { + val existingValue = metaDataNode.attribute(MANIFEST_VALUE_KEY)?.toString() + if (existingValue == valueToWrite) { + manifestFile.copyTo(updatedManifest, overwrite = true) + return + } + val flagName = if (explicitEnableHcpp == true) ENABLE_HCPP_FLAG else NO_ENABLE_HCPP_FLAG + logger?.lifecycle( + "$flagName overrides the merged Android manifest, which sets $HCPP_METADATA_NAME " + + "to \"$existingValue\". This artifact is built with $HCPP_METADATA_NAME=$valueToWrite." + ) + metaDataNode.attributes()[MANIFEST_VALUE_KEY] = valueToWrite + } else { + applicationNode.appendNode( + "meta-data", + mapOf(MANIFEST_NAME_KEY to HCPP_METADATA_NAME, MANIFEST_VALUE_KEY to valueToWrite) + ) + } + updatedManifest.printWriter().use { writer: PrintWriter -> + writer.println("""""") + XmlNodePrinter(writer).print(manifest) + } + } +} diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index 21fc5d8786f9d..2e50f4175bb9f 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -19,6 +19,7 @@ import com.flutter.gradle.FlutterPluginUtils.BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS import com.flutter.gradle.FlutterPluginUtils.BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS import com.flutter.gradle.FlutterPluginUtils.detectApplyingKotlinGradlePlugin import com.flutter.gradle.plugins.PluginHandler +import com.flutter.gradle.tasks.EnableHcppManifestTask import com.flutter.gradle.tasks.PrintTask import io.mockk.called import io.mockk.every @@ -2752,4 +2753,96 @@ class FlutterPluginUtilsTest { mockPrintTask.description = "Prints out all build variants for this Android project" } } + + // addTasksForEnableHcppManifest + @Test + fun `addTasksForEnableHcppManifest skips module (library) projects`() { + // Injecting into a library manifest would propagate into the host app's merged + // manifest, where a conflicting explicit host value fails the build in the manifest + // merger instead of taking priority. See addTasksForEnableHcppManifest. + val project = mockk() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null + + FlutterPluginUtils.addTasksForEnableHcppManifest(project) + + // The function must return before reading properties or registering any tasks. + verify(exactly = 0) { project.findProperty(any()) } + verify(exactly = 0) { project.tasks } + } + + @Test + fun `addTasksForEnableHcppManifest skips app projects when enable-hcpp is not true and no explicit flag passed`() { + val project = mockk() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk() + every { project.findProperty(FlutterPluginUtils.PROP_ENABLE_HCPP) } returns "false" + every { project.findProperty(FlutterPluginUtils.PROP_EXPLICIT_ENABLE_HCPP) } returns null + + FlutterPluginUtils.addTasksForEnableHcppManifest(project) + + verify(exactly = 0) { project.tasks } + } + + /** + * Sets up [project] as an application project with the given hcpp properties, and drives + * [FlutterPluginUtils.addTasksForEnableHcppManifest] through a single "debug" variant. + * + * Returns the [EnableHcppManifestTask] the registration action was applied to, so callers can + * verify how it was configured. + */ + private fun registerHcppTaskForDebugVariant( + enableHcppProperty: String, + explicitEnableHcppProperty: String? + ): EnableHcppManifestTask { + val project = mockk(relaxed = true) + val androidComponents = mockk>() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk() + every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns androidComponents + every { project.findProperty(FlutterPluginUtils.PROP_ENABLE_HCPP) } returns enableHcppProperty + every { + project.findProperty(FlutterPluginUtils.PROP_EXPLICIT_ENABLE_HCPP) + } returns explicitEnableHcppProperty + every { project.tasks.register(any(), eq(EnableHcppManifestTask::class.java), any()) } returns mockk(relaxed = true) + + // AGP's onVariants takes a defaulted selector, so selector().all() has to be stubbed for + // the call to get through to the captured callback. + every { androidComponents.selector() } returns mockk { every { all() } returns mockk() } + val onVariantsSlot = slot<(Variant) -> Unit>() + every { androidComponents.onVariants(any(), capture(onVariantsSlot)) } answers { + onVariantsSlot.captured.invoke(mockk(relaxed = true) { every { name } returns "debug" }) + } + + FlutterPluginUtils.addTasksForEnableHcppManifest(project) + + val configureSlot = slot>() + verify(exactly = 1) { + project.tasks.register( + "enableHcppInManifestDebug", + eq(EnableHcppManifestTask::class.java), + capture(configureSlot) + ) + } + val task = mockk(relaxed = true) + configureSlot.captured.execute(task) + return task + } + + @Test + fun `addTasksForEnableHcppManifest registers manifest transform on app projects when enable-hcpp is true`() { + val task = registerHcppTaskForDebugVariant(enableHcppProperty = "true", explicitEnableHcppProperty = null) + + verify(exactly = 1) { task.requestedEnableHcpp.set(true) } + // Without an explicit flag there is nothing to warn about, so the task must not be told + // to compare against one. + verify(exactly = 0) { task.explicitEnableHcpp.set(any()) } + } + + @Test + fun `addTasksForEnableHcppManifest registers manifest transform when explicit-enable-hcpp is present even if enable-hcpp is false`() { + val task = registerHcppTaskForDebugVariant(enableHcppProperty = "false", explicitEnableHcppProperty = "false") + + // Nothing is injected, but the task still runs so that it can warn when the manifest + // explicitly disagrees with --no-enable-hcpp. + verify(exactly = 1) { task.requestedEnableHcpp.set(false) } + verify(exactly = 1) { task.explicitEnableHcpp.set(false) } + } } diff --git a/packages/flutter_tools/gradle/src/test/kotlin/tasks/EnableHcppManifestTaskTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/tasks/EnableHcppManifestTaskTest.kt new file mode 100644 index 0000000000000..c08a2fe0f96d4 --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/tasks/EnableHcppManifestTaskTest.kt @@ -0,0 +1,573 @@ +// 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. + +package com.flutter.gradle.tasks + +import groovy.util.Node +import io.mockk.mockk +import io.mockk.verify +import org.gradle.api.logging.Logger +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +/** + * Tests for [EnableHcppManifestTaskHelper]. + */ +class EnableHcppManifestTaskTest { + private val defaultNamespace = "dev.flutter.example" + + private fun createTempManifestFile(content: String): File { + val manifestFile = File.createTempFile("AndroidManifestTest", ".xml") + manifestFile.deleteOnExit() + manifestFile.writeText(content.trimIndent()) + return manifestFile + } + + private fun createTempOutputFile(): File { + val outputFile = File.createTempFile("AndroidManifestUpdated", ".xml") + outputFile.deleteOnExit() + return outputFile + } + + private fun findHcppMetadataValue(manifestFile: File): String? { + val manifest: Node = + groovy.xml + .XmlParser(false, false) + .parse(manifestFile) + val applicationNode: Node = + manifest.children().filterIsInstance().find { node -> + node.name() == "application" + } ?: return null + val metadataNode: Node? = + applicationNode.children().filterIsInstance().find { node -> + node.name() == "meta-data" && + node.attribute("android:name") == EnableHcppManifestTaskHelper.HCPP_METADATA_NAME + } + return metadataNode?.attribute("android:value")?.toString() + } + + @Test + fun addsMetadataWhenAbsent() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + } + + @Test + fun manifestFalseWinsOverDefaultWhenNoExplicitFlag() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true + ) + + assertEquals("false", findHcppMetadataValue(updatedManifest)) + assertEquals( + manifestFile.readText(), + updatedManifest.readText(), + "A manifest that sets the value should be copied unmodified when no flag was passed" + ) + } + + @Test + fun manifestTrueWinsOverDefaultWhenNoExplicitFlag() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + assertEquals( + manifestFile.readText(), + updatedManifest.readText(), + "A manifest that sets the value should be copied unmodified when no flag was passed" + ) + } + + @Test + fun explicitEnableHcppOverridesManifestFalse() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + val logger = mockk(relaxed = true) + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true, + explicitEnableHcpp = true, + logger = logger + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + verify(exactly = 1) { + logger.lifecycle( + match { message -> + message.contains("${EnableHcppManifestTaskHelper.ENABLE_HCPP_FLAG} overrides") && + message.contains("to \"false\"") + } + ) + } + } + + @Test + fun explicitNoEnableHcppOverridesManifestTrue() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + val logger = mockk(relaxed = true) + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = false, + explicitEnableHcpp = false, + logger = logger + ) + + assertEquals("false", findHcppMetadataValue(updatedManifest)) + verify(exactly = 1) { + logger.lifecycle( + match { message -> + message.contains("${EnableHcppManifestTaskHelper.NO_ENABLE_HCPP_FLAG} overrides") && + message.contains("to \"true\"") + } + ) + } + } + + @Test + fun saysNothingWhenExplicitFlagMatchesManifest() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + val logger = mockk(relaxed = true) + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true, + explicitEnableHcpp = true, + logger = logger + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + assertEquals( + manifestFile.readText(), + updatedManifest.readText(), + "A manifest that already agrees with the flag should be copied unmodified" + ) + verify(exactly = 0) { logger.lifecycle(any()) } + verify(exactly = 0) { logger.warn(any()) } + } + + @Test + fun explicitFlagOverridesResourceRef() { + // A resource reference cannot be resolved here, so the flag replaces it outright rather + // than trying to guess what it evaluates to. + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + val logger = mockk(relaxed = true) + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true, + explicitEnableHcpp = true, + logger = logger + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + verify(exactly = 1) { + logger.lifecycle( + match { message -> + message.contains("${EnableHcppManifestTaskHelper.ENABLE_HCPP_FLAG} overrides") && + message.contains("to \"@bool/enable_hcpp\"") + } + ) + } + } + + @Test + fun addsApplicationElementWhenAbsent() { + // A library (add-to-app module) manifest may not contain an application element. + val manifestFile = + createTempManifestFile( + """ + + + + """ + ) + val updatedManifest = createTempOutputFile() + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + } + + @Test + fun keepsOtherMetadataIntact() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + val manifest: Node = + groovy.xml + .XmlParser(false, false) + .parse(updatedManifest) + val applicationNode: Node? = + manifest.children().filterIsInstance().find { node -> + node.name() == "application" + } + assertNotNull(applicationNode) + val impellerNode: Node? = + applicationNode.children().filterIsInstance().find { node -> + node.name() == "meta-data" && + node.attribute("android:name") == "io.flutter.embedding.android.EnableImpeller" + } + assertNotNull(impellerNode, "Existing meta-data should be preserved") + assertEquals("Test App", applicationNode.attribute("android:label")) + } + + @Test + fun preservesRealMergedManifestContent() { + // Fixture captured from an actual AGP 8.11.1 processDebugMainManifest output + // (MERGED_MANIFEST artifact), which is what this task transforms in practice. + val manifestFile = + createTempManifestFile( + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + + // The rewritten manifest must preserve all elements and attributes. + val manifest: Node = + groovy.xml + .XmlParser(false, false) + .parse(updatedManifest) + assertEquals("com.example.host", manifest.attribute("package")) + assertEquals( + "http://schemas.android.com/apk/res/android", + manifest.attribute("xmlns:android"), + "The android namespace declaration must be preserved" + ) + val topLevel: List = manifest.children().filterIsInstance() + assertEquals( + "24", + topLevel + .first { it.name() == "uses-sdk" } + .attribute("android:minSdkVersion") + ) + assertEquals( + "android.permission.INTERNET", + topLevel + .first { it.name() == "uses-permission" } + .attribute("android:name") + ) + val queriesIntent: Node = + topLevel + .first { it.name() == "queries" } + .children() + .filterIsInstance() + .first { it.name() == "intent" } + assertEquals( + "text/plain", + queriesIntent + .children() + .filterIsInstance() + .first { it.name() == "data" } + .attribute("android:mimeType") + ) + val applicationNode: Node = topLevel.first { it.name() == "application" } + assertEquals("Host", applicationNode.attribute("android:label")) + assertEquals("true", applicationNode.attribute("android:debuggable")) + val activityNode: Node = + applicationNode + .children() + .filterIsInstance() + .first { it.name() == "activity" } + assertEquals("com.example.host.MainActivity", activityNode.attribute("android:name")) + assertEquals("singleTop", activityNode.attribute("android:launchMode")) + val intentFilter: Node = + activityNode + .children() + .filterIsInstance() + .first { it.name() == "intent-filter" } + assertEquals( + "android.intent.action.MAIN", + intentFilter + .children() + .filterIsInstance() + .first { it.name() == "action" } + .attribute("android:name") + ) + val metadataValuesByName: Map = + applicationNode + .children() + .filterIsInstance() + .filter { it.name() == "meta-data" } + .associate { it.attribute("android:name") to it.attribute("android:value") } + assertEquals("2", metadataValuesByName["flutterEmbedding"]) + assertEquals("true", metadataValuesByName["io.flutter.embedding.android.EnableImpeller"]) + assertEquals("519", metadataValuesByName["io.flutter.embedding.android.OldGenHeapSize"]) + assertEquals("true", metadataValuesByName[EnableHcppManifestTaskHelper.HCPP_METADATA_NAME]) + } + + @Test + fun copiesManifestUnmodifiedWhenNotRequestedAndMetadataAbsent() { + val manifestFile = + createTempManifestFile( + """ + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + val logger = mockk(relaxed = true) + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = false, + logger = logger + ) + + assertNull( + findHcppMetadataValue(updatedManifest), + "No metadata should be injected when it was not requested" + ) + assertEquals( + manifestFile.readText(), + updatedManifest.readText(), + "The manifest should be copied byte for byte when nothing is injected" + ) + verify(exactly = 0) { logger.warn(any()) } + } + + @Test + fun dropsXmlCommentsWhenInjecting() { + // Documents a known side effect of re-serializing the parsed manifest: groovy's XmlParser + // discards comments, so the provenance comments the manifest merger emits do not survive + // injection. aapt2 does not care, but the merged manifest is less readable. + val manifestFile = + createTempManifestFile( + """ + + + + + + + + """ + ) + val updatedManifest = createTempOutputFile() + + EnableHcppManifestTaskHelper.processHcppManifest( + manifestFile = manifestFile, + updatedManifest = updatedManifest, + requestedEnableHcpp = true + ) + + assertEquals("true", findHcppMetadataValue(updatedManifest)) + assertFalse( + updatedManifest.readText().contains("Added by com.example.somelibrary"), + "Comments are expected to be dropped; update this test if that changes" + ) + } + + @Test + fun noMetadataInManifestWithoutInjection() { + // Sanity check that the test helper does not find metadata that is not there. + val manifestFile = + createTempManifestFile( + """ + + + + + """ + ) + assertNull(findHcppMetadataValue(manifestFile)) + } +} diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 551da4d7bc108..6f2b277cb12c6 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -48,6 +48,8 @@ class BuildInfo { this.codeSizeDirectory, this.androidGradleDaemon = true, this.androidSkipBuildDependencyValidation = false, + this.androidEnableHcpp, + this.explicitAndroidEnableHcpp, this.packageConfig = PackageConfig.empty, this.initializeFromDill, this.assumeInitializeFromDillUpToDate = false, @@ -93,6 +95,8 @@ class BuildInfo { codeSizeDirectory: codeSizeDirectory, androidGradleDaemon: androidGradleDaemon, androidSkipBuildDependencyValidation: androidSkipBuildDependencyValidation, + androidEnableHcpp: androidEnableHcpp, + explicitAndroidEnableHcpp: explicitAndroidEnableHcpp, packageConfig: packageConfig ?? this.packageConfig, initializeFromDill: initializeFromDill ?? this.initializeFromDill, assumeInitializeFromDillUpToDate: assumeInitializeFromDillUpToDate, @@ -201,6 +205,26 @@ class BuildInfo { /// dependencies. final bool androidSkipBuildDependencyValidation; + /// The default `enable-hcpp` value (currently false unless the CLI flag was + /// passed), given to Gradle so the Flutter Gradle Plugin can inject the + /// corresponding manifest metadata if absent. + /// + /// The injection only happens for application projects, and only when the + /// merged manifest does not already contain the + /// `io.flutter.embedding.android.EnableHcpp` metadata, so a value in the + /// app's manifest takes priority over this one. Module (aar) manifests are + /// never injected; the add-to-app host's manifest is the source of truth. + /// When null, no property is passed and no injection happens. + final bool? androidEnableHcpp; + + /// The explicit `--[no-]enable-hcpp` value passed by the user on the CLI, or + /// null if the user did not pass the flag explicitly. + /// + /// Passed to Gradle, which writes it into the merged manifest over any value + /// already there, so it takes priority over both [androidEnableHcpp] and the + /// app's manifest. When null, the manifest decides. + final bool? explicitAndroidEnableHcpp; + /// Additional key value pairs that are passed directly to the gradle project via the `-P` /// flag. final List androidProjectArgs; @@ -423,6 +447,8 @@ class BuildInfo { if (performanceMeasurementFile != null) '-Pperformance-measurement-file=$performanceMeasurementFile', if (codeSizeDirectory != null) '-Pcode-size-directory=$codeSizeDirectory', + if (androidEnableHcpp != null) '-Penable-hcpp=$androidEnableHcpp', + if (explicitAndroidEnableHcpp != null) '-Pexplicit-enable-hcpp=$explicitAndroidEnableHcpp', for (final String projectArg in androidProjectArgs) '-P$projectArg', if (androidGradleProjectCacheDir != null) '--project-cache-dir=$androidGradleProjectCacheDir', ]; diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart index ef719e6e7271b..f90f6252adc7f 100644 --- a/packages/flutter_tools/lib/src/commands/build_aar.dart +++ b/packages/flutter_tools/lib/src/commands/build_aar.dart @@ -50,6 +50,11 @@ class BuildAarCommand extends BuildSubCommand { usesTrackWidgetCreation(verboseHelp: false); addEnableExperimentation(hide: !verboseHelp); addAndroidSpecificBuildOptions(hide: !verboseHelp); + // No --[no-]enable-hcpp flag here: the Flutter Gradle Plugin intentionally + // does not inject the EnableHcpp metadata into module (aar) manifests, + // because a library-provided value that conflicts with an explicit value in + // the host app's manifest fails the host build in the manifest merger. The + // host app's manifest is the source of truth for HCPP in add-to-app. argParser.addMultiOption( 'target-platform', defaultsTo: ['android-arm', 'android-arm64', 'android-x64'], diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart index 824a1008a01a6..7da02d519bb46 100644 --- a/packages/flutter_tools/lib/src/commands/build_apk.dart +++ b/packages/flutter_tools/lib/src/commands/build_apk.dart @@ -34,6 +34,7 @@ class BuildApkCommand extends BuildSubCommand { usesAnalyzeSizeFlag(); addAndroidSpecificBuildOptions(hide: !verboseHelp); addIgnoreDeprecationOption(); + addEnableHcppFlag(verboseHelp: verboseHelp); argParser ..addFlag( 'split-per-abi', @@ -110,7 +111,9 @@ class BuildApkCommand extends BuildSubCommand { buildApkTargetPlatform: _targetArchs.join(','), buildApkBuildMode: _buildMode.cliName, buildApkSplitPerAbi: boolArg('split-per-abi'), - buildApkEnableHcpp: FlutterProject.current().android.computeHcppEnabled(), + buildApkEnableHcpp: + explicitEnableHcpp ?? + FlutterProject.current().android.computeHcppEnabled(ifAbsent: enableHcpp), ); } diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart index f96180990545f..9a8c320fba9b0 100644 --- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart +++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart @@ -38,6 +38,7 @@ class BuildAppBundleCommand extends BuildSubCommand { usesAnalyzeSizeFlag(); addAndroidSpecificBuildOptions(hide: !verboseHelp); addIgnoreDeprecationOption(); + addEnableHcppFlag(verboseHelp: verboseHelp); argParser.addMultiOption( 'target-platform', defaultsTo: ['android-arm', 'android-arm64', 'android-x64'], @@ -109,7 +110,9 @@ class BuildAppBundleCommand extends BuildSubCommand { commandHasTerminal: hasTerminal, buildAppBundleTargetPlatform: stringsArg('target-platform').join(','), buildAppBundleBuildMode: buildMode, - buildBundleEnableHcpp: FlutterProject.current().android.computeHcppEnabled(), + buildBundleEnableHcpp: + explicitEnableHcpp ?? + FlutterProject.current().android.computeHcppEnabled(ifAbsent: enableHcpp), ); } diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index ff276124bbcf6..2063b544440cb 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -276,7 +276,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment bool get enableVulkanValidation => boolArg('enable-vulkan-validation'); bool get uninstallFirst => boolArg('uninstall-first'); bool get enableEmbedderApi => boolArg('enable-embedder-api'); - bool get enableHcpp => boolArg('enable-hcpp'); bool get testFlag => boolArg('test-flag'); @override diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 0cbfe1435c031..c7fe80e3ea0db 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -1099,13 +1099,29 @@ See the link below for more information: ); } - bool computeHcppEnabled() { - return _computeManifestMetadataBoolValue('io.flutter.embedding.android.EnableHcpp', false); + /// Returns the `io.flutter.embedding.android.EnableHcpp` manifest value. + /// + /// If there is no manifest file, or the key is not present, returns + /// [ifAbsent]. Callers should pass the value the build injects into the + /// manifest when the key is not explicitly set, so that the result reflects + /// what is actually packaged. + /// + /// This reads the app's primary source manifest, so it is an estimate of the + /// packaged value: it does not account for contributions from build + /// type/flavor overlay manifests or library manifests merged in at build + /// time. Intended for analytics, not for correctness-sensitive decisions. + bool computeHcppEnabled({bool ifAbsent = false}) { + return _computeManifestMetadataBoolValueOrNull('io.flutter.embedding.android.EnableHcpp') ?? + ifAbsent; } bool _computeManifestMetadataBoolValue(String metadataKey, bool defaultValue) { + return _computeManifestMetadataBoolValueOrNull(metadataKey) ?? defaultValue; + } + + bool? _computeManifestMetadataBoolValueOrNull(String metadataKey) { if (!appManifestFile.existsSync()) { - return defaultValue; + return null; } final XmlDocument document; try { @@ -1136,7 +1152,7 @@ See the link below for more information: } } } - return defaultValue; + return null; } } diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index bbc03a2e9d864..057ffbb36db8c 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -1319,10 +1319,40 @@ abstract class FlutterCommand extends Command { argParser.addFlag( 'enable-hcpp', hide: !verboseHelp, - help: 'Whether to enable the HCPP platform view mode on the Impeller rendering backend.', + help: + 'Enable the use of the HCPP platform view rendering mode on the Impeller rendering ' + 'backend. An explicit value takes priority over the EnableHcpp metadata in ' + 'AndroidManifest.xml: build commands write it into the manifest of the artifact they ' + 'produce, and "run", "test", and "drive" additionally apply it at launch. Without the ' + 'flag, the manifest decides.', ); } + /// The explicit `--[no-]enable-hcpp` value, or null when the flag was not + /// passed (or the command does not define it). + /// + /// This takes priority over the `io.flutter.embedding.android.EnableHcpp` + /// manifest entry: it is passed to Gradle, which writes it into the merged + /// manifest over any value already there. Commands that launch the app + /// (run/test/drive) additionally forward it to the device. + bool? get explicitEnableHcpp { + final ArgResults? results = argResults; + if (results == null || + !results.options.contains('enable-hcpp') || + !results.wasParsed('enable-hcpp')) { + return null; + } + return boolArg('enable-hcpp'); + } + + /// The HCPP value for an Android artifact when the developer did not pass + /// `--[no-]enable-hcpp`: currently always false. + /// + /// This is only a default. Gradle injects it when the merged manifest does + /// not set `io.flutter.embedding.android.EnableHcpp` at all, so an entry in + /// the manifest wins over it. [explicitEnableHcpp] in turn wins over both. + bool get enableHcpp => explicitEnableHcpp ?? false; + void addTestFlag({required bool verboseHelp}) { argParser.addFlag( 'test-flag', @@ -1524,6 +1554,8 @@ abstract class FlutterCommand extends Command { codeSizeDirectory: codeSizeDirectory, androidGradleDaemon: androidGradleDaemon, androidSkipBuildDependencyValidation: androidSkipBuildDependencyValidation, + androidEnableHcpp: enableHcpp, + explicitAndroidEnableHcpp: explicitEnableHcpp, packageConfig: packageConfig, androidProjectArgs: androidProjectArgs, androidGradleProjectCacheDir: androidGradleProjectCacheDir, diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart index 3afcd16e00e99..e95ef5efc0bf0 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart @@ -114,7 +114,7 @@ flutter: '-I=/flutter/packages/flutter_tools/gradle/aar_init_script.gradle', ...List.filled(4, RegExp(r'-P[a-zA-Z-]+=.*')), '-q', - ...List.filled(6, RegExp(r'-P[a-zA-Z-]+=.*')), + ...List.filled(7, RegExp(r'-P[a-zA-Z-]+=.*')), 'assembleAar$buildMode', ], onRun: (_) => fs.directory('/build/host/outputs/repo').createSync(recursive: true), diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart index 420325ae8e98b..1b784c8037aca 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart @@ -282,6 +282,38 @@ void main() { expect(buildInfo.dartObfuscation, isTrue); expect(buildInfo.dartDefines.contains('foo=bar'), isTrue); }, overrides: {AndroidBuilder: () => fakeAndroidBuilder}); + + testUsingContext('defaults androidEnableHcpp to false without explicit flag', () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=module'], + ); + await runBuildAar(projectPath, arguments: ['--no-pub']); + + final Invocation buildAarCall = fakeAndroidBuilder.capturedBuildAarCalls.single; + for (final androidBuildInfo + in buildAarCall.namedArguments[#androidBuildInfo] as Set) { + // The property is piped to the aar gradle build for consistency (defaulting to + // false in this PR), but the Flutter Gradle Plugin only consumes it for application + // projects: injecting into a module (aar) manifest would conflict + // with an explicit value in the add-to-app host's manifest and fail + // the host build in the manifest merger. + expect(androidBuildInfo.buildInfo.androidEnableHcpp, isFalse); + } + }, overrides: {AndroidBuilder: () => fakeAndroidBuilder}); + + testUsingContext('does not define --enable-hcpp', () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=module'], + ); + // HCPP for add-to-app is controlled by the host app's manifest; an aar + // level flag would be a silent no-op, so the command must reject it. + await expectLater( + runBuildAar(projectPath, arguments: ['--no-pub', '--no-enable-hcpp']), + throwsA(isA()), + ); + }, overrides: {AndroidBuilder: () => fakeAndroidBuilder}); }); group('Gradle', () { @@ -413,6 +445,7 @@ void main() { '-Pextra-front-end-options=foo,bar', '-Ptrack-widget-creation=true', '-Ptree-shake-icons=true', + '-Penable-hcpp=false', ...ndkProvisioningProperties, '-Ptarget-platform=android-arm,android-arm64,android-x64', 'assembleAarRelease', diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart index db338b6a4f304..49d4ef1c35f2d 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart @@ -138,6 +138,142 @@ void main() { }, ); + testUsingContext( + 'reports hcpp analytics default false when not in the manifest and no explicit flag is passed', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + await runBuildApkCommand(projectPath); + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'apk', + commandHasTerminal: false, + buildApkTargetPlatform: 'android-arm,android-arm64,android-x64', + buildApkBuildMode: 'release', + buildApkSplitPerAbi: false, + buildApkEnableHcpp: false, + ), + ), + ); + }, + overrides: { + AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + }, + ); + + testUsingContext( + 'reports hcpp analytics from an explicit --no-enable-hcpp flag', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + await runBuildApkCommand(projectPath, arguments: ['--no-enable-hcpp']); + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'apk', + commandHasTerminal: false, + buildApkTargetPlatform: 'android-arm,android-arm64,android-x64', + buildApkBuildMode: 'release', + buildApkSplitPerAbi: false, + buildApkEnableHcpp: false, + ), + ), + ); + }, + overrides: { + AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + }, + ); + + testUsingContext( + 'reports hcpp analytics from an explicit --enable-hcpp flag when not in the manifest', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + // The manifest does not set EnableHcpp, so the build injects the flag value and the + // packaged app has HCPP on. Analytics has to report what was packaged, not what the + // source manifest happened to say. + await runBuildApkCommand(projectPath, arguments: ['--enable-hcpp']); + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'apk', + commandHasTerminal: false, + buildApkTargetPlatform: 'android-arm,android-arm64,android-x64', + buildApkBuildMode: 'release', + buildApkSplitPerAbi: false, + buildApkEnableHcpp: true, + ), + ), + ); + }, + overrides: { + AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + }, + ); + + testUsingContext( + 'reports hcpp analytics from an explicit --enable-hcpp over a manifest value', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + final File manifestFile = globals.fs.file( + globals.fs.path.join(projectPath, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'), + ); + manifestFile.writeAsStringSync( + manifestFile.readAsStringSync().replaceFirst( + '', + ' \n' + ' ', + ), + ); + + // The flag is passed at invocation time, so it wins over the checked in manifest value + // and gradle writes true into the merged manifest. Analytics reports what is packaged. + await runBuildApkCommand(projectPath, arguments: ['--enable-hcpp']); + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'apk', + commandHasTerminal: false, + buildApkTargetPlatform: 'android-arm,android-arm64,android-x64', + buildApkBuildMode: 'release', + buildApkSplitPerAbi: false, + buildApkEnableHcpp: true, + ), + ), + ); + }, + overrides: { + AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + }, + ); + testUsingContext( 'Each build mode respects --target-platform', () async { @@ -534,6 +670,7 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=true', '-Ptree-shake-icons=true', + '-Penable-hcpp=false', 'assembleRelease', ], exitCode: 1, @@ -584,6 +721,7 @@ void main() { '-Psplit-debug-info=${tempDir.path}', '-Ptrack-widget-creation=true', '-Ptree-shake-icons=true', + '-Penable-hcpp=false', 'assembleRelease', ], exitCode: 1, @@ -637,6 +775,7 @@ void main() { '-Pextra-front-end-options=foo,bar', '-Ptrack-widget-creation=true', '-Ptree-shake-icons=true', + '-Penable-hcpp=false', 'assembleRelease', ], exitCode: 1, @@ -689,6 +828,7 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=true', '-Ptree-shake-icons=true', + '-Penable-hcpp=false', 'assembleRelease', ], exitCode: 1, @@ -744,6 +884,7 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=true', '-Ptree-shake-icons=true', + '-Penable-hcpp=false', 'assembleRelease', ], ), @@ -804,6 +945,7 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=true', '-Ptree-shake-icons=true', + '-Penable-hcpp=false', 'assembleRelease', ], ), @@ -846,6 +988,59 @@ void main() { AndroidStudio: () => FakeAndroidStudio(), }, ); + + testUsingContext( + 'passes enable-hcpp and explicit-enable-hcpp to gradle for --enable-hcpp', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app', '--platform=android'], + ); + processManager.addCommand( + FakeCommand( + command: [ + gradlew, + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}', + '-Pbase-application-name=android.app.Application', + '-Pdart-defines=${encodeDartDefinesMap({ + 'FLUTTER_BUILD_NAME': '1.0.0', + 'FLUTTER_BUILD_NUMBER': '1', + 'FLUTTER_VERSION': '0.0.0', // + 'FLUTTER_CHANNEL': 'master', + 'FLUTTER_GIT_URL': 'https://github.com/flutter/flutter.git', + 'FLUTTER_FRAMEWORK_REVISION': '11111', + 'FLUTTER_ENGINE_REVISION': 'abcde', + 'FLUTTER_DART_VERSION': '12', + })}', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=true', + '-Ptree-shake-icons=true', + '-Penable-hcpp=true', + '-Pexplicit-enable-hcpp=true', + 'assembleRelease', + ], + ), + ); + + // The command throws a [ToolExit] because it expects an APK in the file system. + await expectLater( + () => runBuildApkCommand(projectPath, arguments: ['--enable-hcpp']), + throwsToolExit(), + ); + + expect(processManager, hasNoRemainingExpectations); + }, + overrides: { + AndroidSdk: () => mockAndroidSdk, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + Java: () => null, + ProcessManager: () => processManager, + Analytics: () => analytics, + AndroidStudio: () => FakeAndroidStudio(), + }, + ); }); } diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart index 6e06eff2f5e7b..81cb6de6e611c 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart @@ -74,6 +74,99 @@ void main() { }, ); + testUsingContext( + 'reports hcpp analytics default false when not in the manifest and no explicit flag is passed', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + await runBuildAppBundleCommand(projectPath); + + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'appbundle', + commandHasTerminal: false, + buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64', + buildAppBundleBuildMode: 'release', + buildBundleEnableHcpp: false, + ), + ), + ); + }, + overrides: { + AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + }, + ); + + testUsingContext( + 'reports hcpp analytics from an explicit --enable-hcpp flag when not in the manifest', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + // The manifest does not set EnableHcpp, so the build injects the flag value and the + // packaged app has HCPP on. Analytics has to report what was packaged, not what the + // source manifest happened to say. + await runBuildAppBundleCommand(projectPath, arguments: ['--enable-hcpp']); + + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'appbundle', + commandHasTerminal: false, + buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64', + buildAppBundleBuildMode: 'release', + buildBundleEnableHcpp: true, + ), + ), + ); + }, + overrides: { + AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + }, + ); + + testUsingContext( + 'reports hcpp analytics from an explicit --no-enable-hcpp flag', + () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + await runBuildAppBundleCommand(projectPath, arguments: ['--no-enable-hcpp']); + + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'appbundle', + commandHasTerminal: false, + buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64', + buildAppBundleBuildMode: 'release', + buildBundleEnableHcpp: false, + ), + ), + ); + }, + overrides: { + AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + }, + ); + testUsingContext('alias aab', () async { final command = BuildAppBundleCommand(logger: BufferLogger.test()); expect(command.aliases, contains('aab')); diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart index 57fcd7f4dad76..311dec93df998 100644 --- a/packages/flutter_tools/test/general.shard/build_info_test.dart +++ b/packages/flutter_tools/test/general.shard/build_info_test.dart @@ -324,6 +324,58 @@ void main() { ]); }); + testWithoutContext('toGradleConfig encoding of androidEnableHcpp', () { + const buildInfo = BuildInfo( + BuildMode.debug, + '', + treeShakeIcons: true, + packageConfigPath: 'foo/.dart_tool/package_config.json', + androidEnableHcpp: true, + explicitAndroidEnableHcpp: true, + ); + + expect(buildInfo.toGradleConfig(), contains('-Penable-hcpp=true')); + expect(buildInfo.toGradleConfig(), contains('-Pexplicit-enable-hcpp=true')); + expect( + buildInfo.copyWith().androidEnableHcpp, + isTrue, + reason: 'copyWith should preserve androidEnableHcpp', + ); + expect( + buildInfo.copyWith().explicitAndroidEnableHcpp, + isTrue, + reason: 'copyWith should preserve explicitAndroidEnableHcpp', + ); + + const disabledBuildInfo = BuildInfo( + BuildMode.debug, + '', + treeShakeIcons: true, + packageConfigPath: 'foo/.dart_tool/package_config.json', + androidEnableHcpp: false, + explicitAndroidEnableHcpp: false, + ); + expect(disabledBuildInfo.toGradleConfig(), contains('-Penable-hcpp=false')); + expect(disabledBuildInfo.toGradleConfig(), contains('-Pexplicit-enable-hcpp=false')); + + const unsetBuildInfo = BuildInfo( + BuildMode.debug, + '', + treeShakeIcons: true, + packageConfigPath: 'foo/.dart_tool/package_config.json', + ); + expect( + unsetBuildInfo.toGradleConfig(), + isNot(anyElement(contains('-Penable-hcpp'))), + reason: 'no property should be passed when unset', + ); + expect( + unsetBuildInfo.toGradleConfig(), + isNot(anyElement(contains('-Pexplicit-enable-hcpp'))), + reason: 'no property should be passed when unset', + ); + }); + testWithoutContext('encodeDartDefines encodes define values with base64 encoded components', () { expect(encodeDartDefines(['"hello"']), 'ImhlbGxvIg=='); expect( diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart index 39face9966bf0..88cdae2683ece 100644 --- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart @@ -652,6 +652,45 @@ void main() { }, ); + testUsingContext( + 'reports an explicit --[no-]enable-hcpp to gradle so it overrides the manifest', + () async { + final enabledCommand = DummyHcppFlutterCommand(); + await createTestCommandRunner(enabledCommand).run(['dummy', '--enable-hcpp']); + final BuildInfo enabledBuildInfo = await enabledCommand.getBuildInfo( + forcedBuildMode: BuildMode.debug, + ); + expect(enabledBuildInfo.explicitAndroidEnableHcpp, isTrue); + expect(enabledBuildInfo.toGradleConfig(), contains('-Pexplicit-enable-hcpp=true')); + + // The negation has to be reported too, otherwise gradle cannot tell it apart from the + // flag being absent and would leave a manifest value of true in place. + final disabledCommand = DummyHcppFlutterCommand(); + await createTestCommandRunner(disabledCommand).run(['dummy', '--no-enable-hcpp']); + final BuildInfo disabledBuildInfo = await disabledCommand.getBuildInfo( + forcedBuildMode: BuildMode.debug, + ); + expect(disabledBuildInfo.explicitAndroidEnableHcpp, isFalse); + expect(disabledBuildInfo.toGradleConfig(), contains('-Pexplicit-enable-hcpp=false')); + + // Without the flag nothing is reported, so the manifest decides. + final defaultCommand = DummyHcppFlutterCommand(); + await createTestCommandRunner(defaultCommand).run(['dummy']); + final BuildInfo defaultBuildInfo = await defaultCommand.getBuildInfo( + forcedBuildMode: BuildMode.debug, + ); + expect(defaultBuildInfo.explicitAndroidEnableHcpp, isNull); + expect( + defaultBuildInfo.toGradleConfig(), + isNot(anyElement(contains('-Pexplicit-enable-hcpp'))), + ); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }, + ); + testUsingContext( 'use fileSystemScheme to generate BuildInfo', () async { @@ -2108,3 +2147,9 @@ class DummyMachineFlutterCommand extends DummyFlutterCommand { addMachineOutputFlag(verboseHelp: false); } } + +class DummyHcppFlutterCommand extends DummyFlutterCommand { + DummyHcppFlutterCommand() : super(name: 'dummy') { + addEnableHcppFlag(verboseHelp: false); + } +} From 67cfcfb6d4e93c9abd75711ea18cc4d677633d8a Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Wed, 5 Aug 2026 17:06:18 +0900 Subject: [PATCH 073/330] iOS: Remove software renderer/headless fallback (#190590) iOS is Impeller-only and requires iOS 15, so there is no software renderer. The `IOSRenderingAPI::kSoftware` path doesn't actually render in software: on a simulator whose host exposed no Metal device it selects a no-op context and surface that draw nothing. This removes that fallback and instead fails immediately when Metal is unavailable. This matches the existing runtime behaviour (which already `FML_CHECK`ed) and the macOS embedder, which refuses to start without a Metal device. The no-op path was added in flutter/engine#54856 to stop Impeller-enabled simulators from exiting when no Metal context was available. That goes back way before we bumped the minimum supported iOS version to 15. Every device capable of running iOS 15 has a Metal-capable GPU, and a simulator requires a host with Metal support, so the fallback isn't necessary and a source of additional complexity and possibly bugs from misconfigured environments. `GPUSurfaceNoop` is now unused and `IOSRenderingAPI` is down to just one enum value. I'll remove both in followups. This also fixes a lint in `IOSSurface::Create()` that was revealed by removing `IOSSurfaceNoop`. `IOSSurfaceMetalImpeller` constructor takes a `const std::shared_ptr&`, so in this code, the move was doing nothing. ```cc std::unique_ptr IOSSurface::Create(std::shared_ptr context, return std::make_unique( static_cast(layer), // Metal layer std::move(context) // context ); ``` The lint wasn't triggering before because in the same method, we were constructing a fallback `IOSSurfaceNoop`, whose ctor takes a `std::shared_ptr` by value, not by const ref. That move WAS doing real work, but effectively covered prevented the lint from firing. Issue: https://github.com/flutter/flutter/issues/190041 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../shell/platform/darwin/ios/BUILD.gn | 6 --- .../shell/platform/darwin/ios/ios_context.mm | 8 ---- .../platform/darwin/ios/ios_context_noop.h | 32 -------------- .../platform/darwin/ios/ios_context_noop.mm | 31 ------------- .../darwin/ios/ios_context_noop_unittests.mm | 24 ----------- .../shell/platform/darwin/ios/ios_surface.h | 3 +- .../shell/platform/darwin/ios/ios_surface.mm | 12 ++++-- .../platform/darwin/ios/ios_surface_noop.h | 43 ------------------- .../platform/darwin/ios/ios_surface_noop.mm | 35 --------------- .../darwin/ios/ios_surface_noop_unittests.mm | 33 -------------- .../darwin/ios/rendering_api_selection.h | 1 - .../darwin/ios/rendering_api_selection.mm | 24 ++++------- 12 files changed, 19 insertions(+), 233 deletions(-) delete mode 100644 engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h delete mode 100644 engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm delete mode 100644 engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm delete mode 100644 engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.h delete mode 100644 engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.mm delete mode 100644 engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop_unittests.mm diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index 52f3054d9d8af..90a1f9ac5936c 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -169,8 +169,6 @@ source_set("flutter_framework_source") { "ios_context.mm", "ios_context_metal_impeller.h", "ios_context_metal_impeller.mm", - "ios_context_noop.h", - "ios_context_noop.mm", "ios_external_texture_metal.h", "ios_external_texture_metal.mm", "ios_external_view_embedder.h", @@ -179,8 +177,6 @@ source_set("flutter_framework_source") { "ios_surface.mm", "ios_surface_metal_impeller.h", "ios_surface_metal_impeller.mm", - "ios_surface_noop.h", - "ios_surface_noop.mm", "platform_message_handler_ios.h", "platform_message_handler_ios.mm", "platform_view_ios.h", @@ -328,8 +324,6 @@ if (enable_ios_unittests) { "framework/Source/VsyncWaiterIOSTest.mm", "framework/Source/accessibility_bridge_test.mm", "framework/Source/availability_version_check_test.mm", - "ios_context_noop_unittests.mm", - "ios_surface_noop_unittests.mm", "platform_message_handler_ios_test.mm", "platform_view_ios_test.mm", ] diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm index 25db4909ed6fa..4b7304b4a444a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm @@ -9,7 +9,6 @@ #include "flutter/fml/logging.h" #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #include "flutter/shell/platform/darwin/ios/ios_context_metal_impeller.h" -#include "flutter/shell/platform/darwin/ios/ios_context_noop.h" #include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" FLUTTER_ASSERT_ARC @@ -25,13 +24,6 @@ const std::shared_ptr& is_gpu_disabled_sync_switch, const Settings& settings) { switch (api) { - case IOSRenderingAPI::kSoftware: - [FlutterLogger logImportant:@"Software rendering is incompatible with Impeller.\n" - "Software rendering may have been automatically selected when " - "running on a simulator in an environment that does not support " - "Metal. Enabling GPU passthrough in your environment may fix " - "this."]; - return std::make_unique(); case IOSRenderingAPI::kMetal: return std::make_unique(settings, is_gpu_disabled_sync_switch); } diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h deleted file mode 100644 index 912b69ba1e48b..0000000000000 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_CONTEXT_NOOP_H_ -#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_CONTEXT_NOOP_H_ - -#import "flutter/shell/platform/darwin/ios/ios_context.h" - -namespace flutter { - -/// @brief A noop rendering context for usage on simulators without metal support. -class IOSContextNoop final : public IOSContext { - public: - IOSContextNoop(); - - // |IOSContext| - ~IOSContextNoop(); - - // |IOSContext| - std::unique_ptr CreateExternalTexture(int64_t texture_id, - NSObject* texture) override; - - private: - IOSContextNoop(const IOSContextNoop&) = delete; - - IOSContextNoop& operator=(const IOSContextNoop&) = delete; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_CONTEXT_NOOP_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm deleted file mode 100644 index f5d2b64d07a8f..0000000000000 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop.mm +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "flutter/shell/platform/darwin/ios/ios_context_noop.h" -#include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" -#include "ios_context.h" - -FLUTTER_ASSERT_ARC - -namespace flutter { - -IOSContextNoop::IOSContextNoop() = default; - -// |IOSContext| -IOSContextNoop::~IOSContextNoop() = default; - -// |IOSContext| -std::unique_ptr IOSContextNoop::CreateExternalTexture(int64_t texture_id, - NSObject* texture) { - // Don't use FML for logging as it will contain engine specific details. This is a user facing - // message. - NSLog(@"Flutter: Attempted to composite external texture sources using the noop backend. " - @"This backend is only used on simulators. This feature is only available on actual " - @"devices where Metal is used for rendering."); - - // Not supported in this backend. - return nullptr; -} - -} // namespace flutter diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm deleted file mode 100644 index 2878bea423b17..0000000000000 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context_noop_unittests.mm +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include -#import - -#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" -#include "shell/platform/darwin/ios/ios_context_noop.h" -#include "shell/platform/darwin/ios/rendering_api_selection.h" - -FLUTTER_ASSERT_ARC - -@interface IOSContextNoopTest : XCTestCase -@end - -@implementation IOSContextNoopTest -- (void)testCreateNoop { - flutter::IOSContextNoop noop; - - XCTAssertTrue(noop.GetImpellerContext() == nullptr); -} - -@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_surface.h b/engine/src/flutter/shell/platform/darwin/ios/ios_surface.h index a9fd3f5f5d250..5185fef8fb5c9 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_surface.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_surface.h @@ -19,7 +19,8 @@ namespace flutter { class IOSSurface { public: - static std::unique_ptr Create(std::shared_ptr context, CALayer* layer); + static std::unique_ptr Create(const std::shared_ptr& context, + CALayer* layer); std::shared_ptr GetContext() const; diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm index 647fb77b9f9a3..37e9cf6b41f40 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_surface.mm @@ -6,16 +6,16 @@ #include +#include "flutter/fml/logging.h" #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #import "flutter/shell/platform/darwin/ios/ios_surface_metal_impeller.h" -#import "flutter/shell/platform/darwin/ios/ios_surface_noop.h" #include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" FLUTTER_ASSERT_ARC namespace flutter { -std::unique_ptr IOSSurface::Create(std::shared_ptr context, +std::unique_ptr IOSSurface::Create(const std::shared_ptr& context, CALayer* layer) { FML_DCHECK(layer); FML_DCHECK(context); @@ -24,11 +24,15 @@ if ([layer isKindOfClass:[CAMetalLayer class]]) { return std::make_unique( static_cast(layer), // Metal layer - std::move(context) // context + context // context ); } } - return std::make_unique(std::move(context)); + // The layer MUST be a CAMetalLayer or FlutterMetalLayer, which overrides + // isKindOfClass to return true for the above check. Anything else means the + // rendering surface was misconfigured. + FML_CHECK(false) << "Expected a Metal-backed layer for iOS rendering."; + FML_UNREACHABLE(); } IOSSurface::IOSSurface(std::shared_ptr ios_context) diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.h b/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.h deleted file mode 100644 index c97c80c356d17..0000000000000 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.h +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_SURFACE_NOOP_H_ -#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_SURFACE_NOOP_H_ - -#import "flutter/shell/platform/darwin/ios/ios_context.h" -#import "flutter/shell/platform/darwin/ios/ios_surface.h" - -@class CALayer; - -namespace flutter { - -/// @brief A rendering surface that accepts rendering intent but does not render -/// anything. -/// -/// This is useful for running on platforms that need an engine instance and -/// don't have the required drivers. -class IOSSurfaceNoop final : public IOSSurface { - public: - explicit IOSSurfaceNoop(std::shared_ptr context); - - ~IOSSurfaceNoop() override; - - // |IOSSurface| - bool IsValid() const override; - - // |IOSSurface| - void UpdateStorageSizeIfNecessary() override; - - // |IOSSurface| - std::unique_ptr CreateGPUSurface() override; - - private: - IOSSurfaceNoop(const IOSSurfaceNoop&) = delete; - - IOSSurfaceNoop& operator=(const IOSSurfaceNoop&) = delete; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_SURFACE_NOOP_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.mm deleted file mode 100644 index a41c5e53cd5d1..0000000000000 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop.mm +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "flutter/shell/platform/darwin/ios/ios_surface_noop.h" -#include "shell/gpu/gpu_surface_noop.h" - -#include - -#include - -#include "flutter/fml/logging.h" -#include "flutter/fml/platform/darwin/cf_utils.h" -#include "flutter/fml/trace_event.h" - -FLUTTER_ASSERT_ARC - -namespace flutter { - -IOSSurfaceNoop::IOSSurfaceNoop(std::shared_ptr context) - : IOSSurface(std::move(context)) {} - -IOSSurfaceNoop::~IOSSurfaceNoop() = default; - -bool IOSSurfaceNoop::IsValid() const { - return true; -} - -void IOSSurfaceNoop::UpdateStorageSizeIfNecessary() {} - -std::unique_ptr IOSSurfaceNoop::CreateGPUSurface() { - return std::make_unique(); -} - -} // namespace flutter diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop_unittests.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop_unittests.mm deleted file mode 100644 index b85dbfedcc010..0000000000000 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_surface_noop_unittests.mm +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include -#import - -#import "flutter/shell/platform/darwin/ios/ios_surface_noop.h" - -#import "flutter/common/task_runners.h" -#import "flutter/fml/message_loop.h" -#import "flutter/fml/thread.h" -#import "flutter/lib/ui/window/platform_message.h" -#import "flutter/lib/ui/window/platform_message_response.h" -#import "flutter/shell/common/thread_host.h" -#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" -#import "flutter/shell/platform/darwin/ios/ios_context_noop.h" - -FLUTTER_ASSERT_ARC - -@interface IOSSurfaceNoopTest : XCTestCase -@end - -@implementation IOSSurfaceNoopTest -- (void)testCreateSurface { - auto context = std::make_shared(); - flutter::IOSSurfaceNoop noop(context); - - XCTAssertTrue(noop.IsValid()); - XCTAssertTrue(!!noop.CreateGPUSurface()); -} - -@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h index 8af497b41212d..67af7a65bcc05 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h +++ b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h @@ -12,7 +12,6 @@ namespace flutter { enum class IOSRenderingAPI { - kSoftware, kMetal, }; diff --git a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm index 60e623475e411..68fdd21278cb0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm @@ -18,8 +18,9 @@ FLUTTER_ASSERT_ARC namespace flutter { +namespace { -bool ShouldUseMetalRenderer() { +bool IsMetalAvailable() { bool ios_version_supports_metal = false; if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { id device = MTLCreateSystemDefaultDevice(); @@ -28,27 +29,20 @@ bool ShouldUseMetalRenderer() { return ios_version_supports_metal; } +} // namespace + IOSRenderingAPI GetRenderingAPIForProcess() { - static bool should_use_metal = ShouldUseMetalRenderer(); - if (should_use_metal) { + static bool metal_available = IsMetalAvailable(); + if (metal_available) { return IOSRenderingAPI::kMetal; } - - // When Metal isn't available we use Skia software rendering since it performs - // a little better than emulated OpenGL. Also, omitting an OpenGL backend - // reduces binary footprint. -#if TARGET_OS_SIMULATOR - return IOSRenderingAPI::kSoftware; -#else - FML_CHECK(false) << "Metal may only be unavailable on simulators"; - return IOSRenderingAPI::kSoftware; -#endif // TARGET_OS_SIMULATOR + FML_CHECK(false) << "Metal is unavailable. On a simulator this means the host environment " + "does not expose a Metal device; enabling GPU passthrough may fix this."; + FML_UNREACHABLE(); } Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api) { switch (rendering_api) { - case IOSRenderingAPI::kSoftware: - return [CALayer class]; case IOSRenderingAPI::kMetal: if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { if ([FlutterMetalLayer enabled]) { From 57bb1d492c04b37b3ec8cdab9fd2e84797e5a86e Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 5 Aug 2026 08:58:20 +0000 Subject: [PATCH 074/330] Fix a flake in ShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback (#190490) That test calls the engine's ScheduleSecondaryVsyncCallback and ScheduleFrame APIs. If a vsync occurs between the two calls, then the secondary callback will be executed at that vsync, but the frame will be scheduled at the next vsync. That will cause a false negative in the test. This PR changes the test to execute the API calls shortly after a vsync happens. That should ensure that both calls happen during the same vsync interval. This also fixes an issue where VsyncWaiter::FireCallback was not properly resetting the VsyncWaiter's callback. The failure to reset the callback meant that a second call to AsyncWaitForVsync with a different callback would not actually take effect. Fixes https://github.com/flutter/flutter/issues/190430 --- .../flutter/shell/common/shell_unittests.cc | 33 ++++++++++++------- .../src/flutter/shell/common/vsync_waiter.cc | 2 +- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/engine/src/flutter/shell/common/shell_unittests.cc b/engine/src/flutter/shell/common/shell_unittests.cc index fbcb085360243..95d38893c15a4 100644 --- a/engine/src/flutter/shell/common/shell_unittests.cc +++ b/engine/src/flutter/shell/common/shell_unittests.cc @@ -2296,23 +2296,34 @@ TEST_F(ShellTest, SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback) { // Wait for the application to attach the listener. latch.Wait(); + auto vsync_task = [&]() { + shell->GetEngine()->ScheduleSecondaryVsyncCallback(0, [&]() { + if (!test_started) { + return; + } + EXPECT_TRUE(is_on_begin_frame_called); + EXPECT_FALSE(is_secondary_callback_called); + is_secondary_callback_called = true; + count_down_latch.CountDown(); + }); + shell->GetEngine()->ScheduleFrame(); + test_started = true; + }; + + // Run the test task after a vsync occurs so that the + // ScheduleSecondaryVsyncCallback and ScheduleFrame calls will happen within + // the same vsync interval. fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetUITaskRunner(), [&]() { - shell->GetEngine()->ScheduleSecondaryVsyncCallback(0, [&]() { - if (!test_started) { - return; - } - EXPECT_TRUE(is_on_begin_frame_called); - EXPECT_FALSE(is_secondary_callback_called); - is_secondary_callback_called = true; - count_down_latch.CountDown(); - }); - shell->GetEngine()->ScheduleFrame(); - test_started = true; + auto vsync_waiter = shell->GetEngine()->GetVsyncWaiter().lock(); + vsync_waiter->AsyncWaitForVsync( + [&](auto frame_timings_recorder) { vsync_task(); }); }); + count_down_latch.Wait(); EXPECT_TRUE(is_on_begin_frame_called); EXPECT_TRUE(is_secondary_callback_called); + DestroyShell(std::move(shell), task_runners); } diff --git a/engine/src/flutter/shell/common/vsync_waiter.cc b/engine/src/flutter/shell/common/vsync_waiter.cc index 5aa60e92fce35..f9abf3dc3a564 100644 --- a/engine/src/flutter/shell/common/vsync_waiter.cc +++ b/engine/src/flutter/shell/common/vsync_waiter.cc @@ -94,7 +94,7 @@ void VsyncWaiter::FireCallback(fml::TimePoint frame_start_time, { std::scoped_lock lock(callback_mutex_); - callback = std::move(callback_); + callback_.swap(callback); for (auto& pair : secondary_callbacks_) { secondary_callbacks.push_back(std::move(pair.second)); } From f0bbd8333c91b52d879e7c034645887b5768b43a Mon Sep 17 00:00:00 2001 From: flutter-pub-roller-bot <137456488+flutter-pub-roller-bot@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:45:41 -0700 Subject: [PATCH 075/330] Roll pub packages (#190605) This PR was generated by `flutter update-packages --force-upgrade`. --- packages/flutter_tools/pubspec.yaml | 4 ++-- pubspec.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index e68a49dcbd237..0f27e556cfd49 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -80,7 +80,7 @@ dependencies: boolean_selector: 2.1.2 browser_launcher: 1.1.3 built_collection: 5.1.1 - built_value: 8.12.6 + built_value: 8.12.7 cli_config: 0.2.0 clock: 1.1.2 csslib: 1.0.2 @@ -129,4 +129,4 @@ dartdoc: nodoc: true -# PUBSPEC CHECKSUM: 81k1ci +# PUBSPEC CHECKSUM: 9v55jf diff --git a/pubspec.lock b/pubspec.lock index 1345c3610b65d..c87e37b573fc4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -102,10 +102,10 @@ packages: dependency: transitive description: name: built_value - sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" url: "https://pub.dev" source: hosted - version: "8.12.6" + version: "8.12.7" characters: dependency: "direct main" description: From 4adca2fa3468f656c3469f471e11e17caa89a5c4 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 5 Aug 2026 14:27:17 +0200 Subject: [PATCH 076/330] Add dot shorthands support for focus order (#190062) This PR adds dot shorthand support for lexical / numeric focus traversal order Fixes https://github.com/flutter/flutter/issues/189991 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../new_gallery/lib/studies/rally/home.dart | 2 +- .../lib/studies/rally/tabs/overview.dart | 2 +- .../focus_traversal_group.0.dart | 33 +- .../ordered_traversal_policy.0.dart | 15 +- .../lib/src/widgets/focus_traversal.dart | 9 +- .../test/widgets/focus_traversal_test.dart | 369 +++++++++--------- .../flutter/test/widgets/framework_test.dart | 2 +- 7 files changed, 204 insertions(+), 228 deletions(-) diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/home.dart b/dev/integration_tests/new_gallery/lib/studies/rally/home.dart index ba4a3306ddbb3..f4e40c55ba18b 100644 --- a/dev/integration_tests/new_gallery/lib/studies/rally/home.dart +++ b/dev/integration_tests/new_gallery/lib/studies/rally/home.dart @@ -221,7 +221,7 @@ class _RallyTabBar extends StatelessWidget { @override Widget build(BuildContext context) { return FocusTraversalOrder( - order: const NumericFocusOrder(0), + order: const .numeric(0), child: TabBar( // Setting isScrollable to true prevents the tabs from being // wrapped in [Expanded] widgets, which allows for more diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/overview.dart b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/overview.dart index c811e9a09c1bb..a892df6b52dee 100644 --- a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/overview.dart +++ b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/overview.dart @@ -237,7 +237,7 @@ class _FinancialView extends StatelessWidget { Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return FocusTraversalOrder( - order: NumericFocusOrder(order!), + order: .numeric(order!), child: ColoredBox( color: RallyColors.cardBackground, child: Column( diff --git a/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart b/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart index 2b8d871d85503..0571239db3c18 100644 --- a/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart +++ b/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart @@ -43,10 +43,7 @@ class _OrderedButtonState extends State> { @override void initState() { super.initState(); - focusNode = FocusNode( - debugLabel: widget.name, - canRequestFocus: widget.canRequestFocus, - ); + focusNode = FocusNode(debugLabel: widget.name, canRequestFocus: widget.canRequestFocus); } @override @@ -70,8 +67,8 @@ class _OrderedButtonState extends State> { @override Widget build(BuildContext context) { final FocusOrder order = switch (widget.order) { - final num number => NumericFocusOrder(number.toDouble()), - final Object? object => LexicalFocusOrder(object.toString()), + final num number => .numeric(number.toDouble()), + final Object? object => .lexical(object.toString()), }; return FocusTraversalOrder( @@ -85,19 +82,13 @@ class _OrderedButtonState extends State> { overlayColor: WidgetStateProperty.fromMap( // If neither of these states is active, the property will // resolve to null, deferring to the default overlay color. - { - WidgetState.focused: Colors.red, - WidgetState.hovered: Colors.blue, - }, + {.focused: Colors.red, .hovered: Colors.blue}, ), foregroundColor: WidgetStateProperty.fromMap( // "WidgetState.focused | WidgetState.hovered" could be used // instead of separate map keys, but this setup allows setting // the button style to a constant value for improved efficiency. - { - WidgetState.focused: Colors.white, - WidgetState.hovered: Colors.white, - }, + {.focused: Colors.white, .hovered: Colors.white}, ), ), onPressed: () => _handleOnPressed(), @@ -141,13 +132,8 @@ class FocusTraversalGroupExample extends StatelessWidget { mainAxisAlignment: .center, children: List.generate(3, (int index) { // Order as "C" "B", "A". - final String order = String.fromCharCode( - 'A'.codeUnitAt(0) + (2 - index), - ); - return OrderedButton( - name: 'String: $order', - order: order, - ); + final String order = String.fromCharCode('A'.codeUnitAt(0) + (2 - index)); + return OrderedButton(name: 'String: $order', order: order); }), ), ), @@ -162,10 +148,7 @@ class FocusTraversalGroupExample extends StatelessWidget { child: Row( mainAxisAlignment: .center, children: List.generate(3, (int index) { - return OrderedButton( - name: 'ignored num: ${3 - index}', - order: 3 - index, - ); + return OrderedButton(name: 'ignored num: ${3 - index}', order: 3 - index); }), ), ), diff --git a/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart b/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart index bc07cf2a382e6..cec083faa4478 100644 --- a/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart +++ b/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart @@ -23,12 +23,7 @@ class OrderedTraversalPolicyExampleApp extends StatelessWidget { } class DemoButton extends StatelessWidget { - const DemoButton({ - super.key, - required this.name, - this.autofocus = false, - required this.order, - }); + const DemoButton({super.key, required this.name, this.autofocus = false, required this.order}); final String name; final bool autofocus; @@ -42,12 +37,8 @@ class DemoButton extends StatelessWidget { @override Widget build(BuildContext context) { return FocusTraversalOrder( - order: NumericFocusOrder(order), - child: TextButton( - autofocus: autofocus, - onPressed: () => _handleOnPressed(), - child: Text(name), - ), + order: .numeric(order), + child: TextButton(autofocus: autofocus, onPressed: () => _handleOnPressed(), child: Text(name)), ); } } diff --git a/packages/flutter/lib/src/widgets/focus_traversal.dart b/packages/flutter/lib/src/widgets/focus_traversal.dart index e54eb5fbcd3b8..5e4458f9124ee 100644 --- a/packages/flutter/lib/src/widgets/focus_traversal.dart +++ b/packages/flutter/lib/src/widgets/focus_traversal.dart @@ -1371,8 +1371,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { final FocusScopeNode nearestScope = currentNode.nearestScope!; final FocusNode? focusedChild = nearestScope.focusedChild; if (focusedChild == null) { - final FocusNode firstFocus = - findFirstFocusInDirection(currentNode, direction) ?? currentNode; + final FocusNode firstFocus = findFirstFocusInDirection(currentNode, direction) ?? currentNode; switch (direction) { case TraversalDirection.up: case TraversalDirection.left: @@ -1795,6 +1794,12 @@ abstract class FocusOrder with Diagnosticable implements Comparable /// const constructors so that they can be used in const expressions. const FocusOrder(); + /// Creates a [LexicalFocusOrder] that describes its order in lexical order. + const factory FocusOrder.lexical(String order) = LexicalFocusOrder; + + /// Creates a [NumericFocusOrder] that describes its order numerically. + const factory FocusOrder.numeric(double order) = NumericFocusOrder; + /// Compares this object to another [Comparable]. /// /// When overriding [FocusOrder], implement [doCompare] instead of this diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart index 0d5697b8e571c..dc84864a57dc3 100644 --- a/packages/flutter/test/widgets/focus_traversal_test.dart +++ b/packages/flutter/test/widgets/focus_traversal_test.dart @@ -1196,11 +1196,11 @@ void main() { child: Column( children: [ FocusTraversalOrder( - order: const NumericFocusOrder(2), + order: const .numeric(2), child: Focus(child: SizedBox(key: key1, width: 100, height: 100)), ), FocusTraversalOrder( - order: const NumericFocusOrder(1), + order: const .numeric(1), child: Focus(child: SizedBox(key: key2, width: 100, height: 100)), ), ], @@ -1299,7 +1299,7 @@ void main() { children: List.generate( nodeCount, (int index) => FocusTraversalOrder( - order: NumericFocusOrder(nodeCount - index.toDouble()), + order: .numeric(nodeCount - index.toDouble()), child: Focus( focusNode: nodes[index], child: const SizedBox(width: 10, height: 10), @@ -1357,7 +1357,7 @@ void main() { children: List.generate( nodeCount, (int index) => FocusTraversalOrder( - order: LexicalFocusOrder(keys[index]), + order: .lexical(keys[index]), child: Focus( focusNode: nodes[index], child: const SizedBox(width: 10, height: 10), @@ -1413,27 +1413,27 @@ void main() { child: Row( children: [ FocusTraversalOrder( - order: const NumericFocusOrder(0), + order: const .numeric(0), child: FocusTraversalGroup( policy: WidgetOrderTraversalPolicy(), child: Row( children: [ FocusTraversalOrder( - order: const NumericFocusOrder(9), + order: const .numeric(9), child: Focus( focusNode: nodes[9], child: const SizedBox(width: 10, height: 10), ), ), FocusTraversalOrder( - order: const NumericFocusOrder(8), + order: const .numeric(8), child: Focus( focusNode: nodes[8], child: const SizedBox(width: 10, height: 10), ), ), FocusTraversalOrder( - order: const NumericFocusOrder(7), + order: const .numeric(7), child: Focus( focusNode: nodes[7], child: const SizedBox(width: 10, height: 10), @@ -1444,27 +1444,27 @@ void main() { ), ), FocusTraversalOrder( - order: const NumericFocusOrder(1), + order: const .numeric(1), child: FocusTraversalGroup( policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()), child: Row( children: [ FocusTraversalOrder( - order: const NumericFocusOrder(4), + order: const .numeric(4), child: Focus( focusNode: nodes[4], child: const SizedBox(width: 10, height: 10), ), ), FocusTraversalOrder( - order: const NumericFocusOrder(5), + order: const .numeric(5), child: Focus( focusNode: nodes[5], child: const SizedBox(width: 10, height: 10), ), ), FocusTraversalOrder( - order: const NumericFocusOrder(6), + order: const .numeric(6), child: Focus( focusNode: nodes[6], child: const SizedBox(width: 10, height: 10), @@ -1475,34 +1475,34 @@ void main() { ), ), FocusTraversalOrder( - order: const NumericFocusOrder(2), + order: const .numeric(2), child: FocusTraversalGroup( policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()), child: Row( children: [ FocusTraversalOrder( - order: const LexicalFocusOrder('D'), + order: const .lexical('D'), child: Focus( focusNode: nodes[3], child: const SizedBox(width: 10, height: 10), ), ), FocusTraversalOrder( - order: const LexicalFocusOrder('C'), + order: const .lexical('C'), child: Focus( focusNode: nodes[2], child: const SizedBox(width: 10, height: 10), ), ), FocusTraversalOrder( - order: const LexicalFocusOrder('B'), + order: const .lexical('B'), child: Focus( focusNode: nodes[1], child: const SizedBox(width: 10, height: 10), ), ), FocusTraversalOrder( - order: const LexicalFocusOrder('A'), + order: const .lexical('A'), child: Focus( focusNode: nodes[0], child: const SizedBox(width: 10, height: 10), @@ -1548,7 +1548,7 @@ void main() { child: Builder( builder: (BuildContext context) { return FocusTraversalOrder( - order: const NumericFocusOrder(0), + order: const .numeric(0), child: TestButton( key: key1, focusNode: testNode1, @@ -1564,7 +1564,7 @@ void main() { ) { return Center( child: FocusTraversalOrder( - order: const NumericFocusOrder(0), + order: const .numeric(0), child: TestButton( key: key2, focusNode: testNode2, @@ -3108,169 +3108,165 @@ void main() { variant: KeySimulatorTransitModeVariant.all(), ); - testWidgets( - 'Arrow focus traversal actions can be re-enabled for text fields.', - (WidgetTester tester) async { - final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); - final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); - final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); - final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey'); + testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', ( + WidgetTester tester, + ) async { + final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); + final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); + final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); + final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey'); - final controller1 = TextEditingController(); - addTearDown(controller1.dispose); - final controller2 = TextEditingController(); - addTearDown(controller2.dispose); - final controller3 = TextEditingController(); - addTearDown(controller3.dispose); - final controller4 = TextEditingController(); - addTearDown(controller4.dispose); - - final focusNodeUpperLeft = FocusNode(debugLabel: 'upperLeft'); - addTearDown(focusNodeUpperLeft.dispose); - final focusNodeUpperRight = FocusNode(debugLabel: 'upperRight'); - addTearDown(focusNodeUpperRight.dispose); - final focusNodeLowerLeft = FocusNode(debugLabel: 'lowerLeft'); - addTearDown(focusNodeLowerLeft.dispose); - final focusNodeLowerRight = FocusNode(debugLabel: 'lowerRight'); - addTearDown(focusNodeLowerRight.dispose); - - Widget generateTestWidgets(bool ignoreTextFields) { - final shortcuts = { - const SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent( - TraversalDirection.left, - ignoreTextFields: ignoreTextFields, - ), - const SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent( - TraversalDirection.right, - ignoreTextFields: ignoreTextFields, - ), - const SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent( - TraversalDirection.down, - ignoreTextFields: ignoreTextFields, - ), - const SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent( - TraversalDirection.up, - ignoreTextFields: ignoreTextFields, - ), - }; + final controller1 = TextEditingController(); + addTearDown(controller1.dispose); + final controller2 = TextEditingController(); + addTearDown(controller2.dispose); + final controller3 = TextEditingController(); + addTearDown(controller3.dispose); + final controller4 = TextEditingController(); + addTearDown(controller4.dispose); + + final focusNodeUpperLeft = FocusNode(debugLabel: 'upperLeft'); + addTearDown(focusNodeUpperLeft.dispose); + final focusNodeUpperRight = FocusNode(debugLabel: 'upperRight'); + addTearDown(focusNodeUpperRight.dispose); + final focusNodeLowerLeft = FocusNode(debugLabel: 'lowerLeft'); + addTearDown(focusNodeLowerLeft.dispose); + final focusNodeLowerRight = FocusNode(debugLabel: 'lowerRight'); + addTearDown(focusNodeLowerRight.dispose); + + Widget generateTestWidgets(bool ignoreTextFields) { + final shortcuts = { + const SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent( + TraversalDirection.left, + ignoreTextFields: ignoreTextFields, + ), + const SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent( + TraversalDirection.right, + ignoreTextFields: ignoreTextFields, + ), + const SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent( + TraversalDirection.down, + ignoreTextFields: ignoreTextFields, + ), + const SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent( + TraversalDirection.up, + ignoreTextFields: ignoreTextFields, + ), + }; - return TestWidgetsApp( - home: Shortcuts( - shortcuts: shortcuts, - child: FocusScope( - debugLabel: 'scope', - child: Column( - children: [ - Row( - children: [ - SizedBox.square( - dimension: 100.0, - child: EditableText( - autofocus: true, - key: upperLeftKey, - controller: controller1, - focusNode: focusNodeUpperLeft, - cursorColor: const Color(0xffffffff), - backgroundCursorColor: const Color(0xff808080), - style: const TextStyle(), - ), + return TestWidgetsApp( + home: Shortcuts( + shortcuts: shortcuts, + child: FocusScope( + debugLabel: 'scope', + child: Column( + children: [ + Row( + children: [ + SizedBox.square( + dimension: 100.0, + child: EditableText( + autofocus: true, + key: upperLeftKey, + controller: controller1, + focusNode: focusNodeUpperLeft, + cursorColor: const Color(0xffffffff), + backgroundCursorColor: const Color(0xff808080), + style: const TextStyle(), ), - SizedBox.square( - dimension: 100.0, - child: EditableText( - key: upperRightKey, - controller: controller2, - focusNode: focusNodeUpperRight, - cursorColor: const Color(0xffffffff), - backgroundCursorColor: const Color(0xff808080), - style: const TextStyle(), - ), + ), + SizedBox.square( + dimension: 100.0, + child: EditableText( + key: upperRightKey, + controller: controller2, + focusNode: focusNodeUpperRight, + cursorColor: const Color(0xffffffff), + backgroundCursorColor: const Color(0xff808080), + style: const TextStyle(), ), - ], - ), - Row( - children: [ - SizedBox.square( - dimension: 100.0, - child: EditableText( - key: lowerLeftKey, - controller: controller3, - focusNode: focusNodeLowerLeft, - cursorColor: const Color(0xffffffff), - backgroundCursorColor: const Color(0xff808080), - style: const TextStyle(), - ), + ), + ], + ), + Row( + children: [ + SizedBox.square( + dimension: 100.0, + child: EditableText( + key: lowerLeftKey, + controller: controller3, + focusNode: focusNodeLowerLeft, + cursorColor: const Color(0xffffffff), + backgroundCursorColor: const Color(0xff808080), + style: const TextStyle(), ), - SizedBox.square( - dimension: 100.0, - child: EditableText( - key: lowerRightKey, - controller: controller4, - focusNode: focusNodeLowerRight, - cursorColor: const Color(0xffffffff), - backgroundCursorColor: const Color(0xff808080), - style: const TextStyle(), - ), + ), + SizedBox.square( + dimension: 100.0, + child: EditableText( + key: lowerRightKey, + controller: controller4, + focusNode: focusNodeLowerRight, + cursorColor: const Color(0xffffffff), + backgroundCursorColor: const Color(0xff808080), + style: const TextStyle(), ), - ], - ), - ], - ), + ), + ], + ), + ], ), ), - ); - } - - await tester.pumpWidget(generateTestWidgets(false)); - - expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - expect(focusNodeUpperRight.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - expect(focusNodeLowerRight.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - expect(focusNodeLowerLeft.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - - await tester.pumpWidget(generateTestWidgets(true)); - - expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - expect(focusNodeUpperRight.hasPrimaryFocus, isFalse); - expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - expect(focusNodeLowerRight.hasPrimaryFocus, isFalse); - expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - expect(focusNodeLowerLeft.hasPrimaryFocus, isFalse); - expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - }, - variant: KeySimulatorTransitModeVariant.all(), - ); + ), + ); + } - testWidgets( - 'Focus traversal does not break when no focusable is available on a WidgetsApp', - (WidgetTester tester) async { - final events = []; + await tester.pumpWidget(generateTestWidgets(false)); + + expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + expect(focusNodeUpperRight.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + expect(focusNodeLowerRight.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + expect(focusNodeLowerLeft.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); + + await tester.pumpWidget(generateTestWidgets(true)); + + expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + expect(focusNodeUpperRight.hasPrimaryFocus, isFalse); + expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + expect(focusNodeLowerRight.hasPrimaryFocus, isFalse); + expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + expect(focusNodeLowerLeft.hasPrimaryFocus, isFalse); + expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); + }, variant: KeySimulatorTransitModeVariant.all()); + + testWidgets('Focus traversal does not break when no focusable is available on a WidgetsApp', ( + WidgetTester tester, + ) async { + final events = []; - await tester.pumpWidget(TestWidgetsApp(home: Container())); + await tester.pumpWidget(TestWidgetsApp(home: Container())); - HardwareKeyboard.instance.addHandler((KeyEvent event) { - events.add(event); - return true; - }); + HardwareKeyboard.instance.addHandler((KeyEvent event) { + events.add(event); + return true; + }); - await tester.idle(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.idle(); + await tester.idle(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.idle(); - expect(events.length, 2); - }, - variant: KeySimulatorTransitModeVariant.all(), - ); + expect(events.length, 2); + }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('Focus traversal does not throw when no focusable is available in a group', ( WidgetTester tester, @@ -3286,26 +3282,24 @@ void main() { expect(primaryFocus, equals(initialFocus)); }); - testWidgets( - 'Focus traversal does not break when no focusable is available on a WidgetsApp', - (WidgetTester tester) async { - final events = []; + testWidgets('Focus traversal does not break when no focusable is available on a WidgetsApp', ( + WidgetTester tester, + ) async { + final events = []; - await tester.pumpWidget(const TestWidgetsApp(home: Placeholder())); + await tester.pumpWidget(const TestWidgetsApp(home: Placeholder())); - HardwareKeyboard.instance.addHandler((KeyEvent event) { - events.add(event); - return true; - }); + HardwareKeyboard.instance.addHandler((KeyEvent event) { + events.add(event); + return true; + }); - await tester.idle(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.idle(); + await tester.idle(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.idle(); - expect(events.length, 2); - }, - variant: KeySimulatorTransitModeVariant.all(), - ); + expect(events.length, 2); + }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('Custom requestFocusCallback gets called on focusInDirection up/down/left/right.', ( WidgetTester tester, @@ -3490,7 +3484,10 @@ void main() { // FocusScope above. parentNode reparents it to the root scope. child: FocusTraversalGroup( parentNode: FocusManager.instance.rootScope, - child: Focus(focusNode: childNode, child: SizedBox(key: key)), + child: Focus( + focusNode: childNode, + child: SizedBox(key: key), + ), ), ), ), diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index 000097ab1f519..7a2b17226a064 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -1714,7 +1714,7 @@ void main() { final element = TestRenderObjectElement(); final focusTraversalOrder = _TestInheritedElement( - const FocusTraversalOrder(order: LexicalFocusOrder(''), child: Placeholder()), + const FocusTraversalOrder(order: .lexical(''), child: Placeholder()), ); final directionality = _TestInheritedElement( const Directionality(textDirection: TextDirection.ltr, child: Placeholder()), From 020674670f3f015e1416f19734eb42fe71754361 Mon Sep 17 00:00:00 2001 From: udit Date: Wed, 5 Aug 2026 10:28:48 -0400 Subject: [PATCH 077/330] Skip platform tooling regeneration for non-Flutter pub workspace packages (#189705) `flutter pub get` post-processing iterates every workspace root package listed in `.dart_tool/package_graph.json` and unconditionally regenerates platform-specific tooling for each of them. Since `regeneratePlatformSpecificTooling` gates only on whether platform directories exist on disk, a pub workspace root that is a plain Dart package (no dependency on Flutter) with a stray `ios/` or `android/` directory got that directory populated with Flutter project files (`GeneratedPluginRegistrant.*`, `Generated.xcconfig`, `flutter_export_environment.sh`, ...). This PR skips platform tooling regeneration for workspace packages that do not depend on the `flutter` package, directly or transitively, according to the resolved package graph. Checking the transitive closure (rather than just the direct dependencies of the pubspec) keeps workspace members that only depend on Flutter through another package (e.g. an app that only declares a dependency on a plugin) working exactly as before, which is covered by the existing test `get creates plugin registrants for each app in workspace`. Adds a regression test that sets up a workspace whose root is a plain Dart package with empty `ios/` and `android/` directories, and verifies that `flutter pub get` leaves them empty while still processing the Flutter app member of the workspace. The test fails before this change (`ios/` gains `Runner/` and `Flutter/` directories) and passes after it. Fixes https://github.com/flutter/flutter/issues/189550 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Ben Konyi --- .../lib/src/commands/packages.dart | 43 ++++++++++++++--- .../permeable/packages_test.dart | 46 +++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart index d901aa96c67bb..a34d124f19a33 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:collection'; + import 'package:args/args.dart'; import 'package:package_config/package_config.dart'; import 'package:pool/pool.dart'; @@ -397,12 +399,20 @@ class PackagesGetCommand extends FlutterCommand { const ignoreReleaseModeSinceItsNotABuildAndHopeItWorks = false; // We need to regenerate the platform specific tooling for both the // project itself and example (if present). - await project.regeneratePlatformSpecificTooling( - releaseMode: ignoreReleaseModeSinceItsNotABuildAndHopeItWorks, - pubspecCache: pubspecCache, - packageGraph: graph, - packageConfig: packageConfig, - ); + // + // Workspace packages that do not depend on Flutter (such as a pub + // workspace root that is a plain Dart package) are skipped, so that a + // stray ios/ or android/ directory in one of them is not populated + // with Flutter project files. + // See https://github.com/flutter/flutter/issues/189550. + if (_dependsOnFlutter(graph, workspaceRootName)) { + await project.regeneratePlatformSpecificTooling( + releaseMode: ignoreReleaseModeSinceItsNotABuildAndHopeItWorks, + pubspecCache: pubspecCache, + packageGraph: graph, + packageConfig: packageConfig, + ); + } if (example && project.hasExampleApp && project.example.pubspecFile.existsSync()) { final FlutterProject exampleProject = project.example; // Skip if the example is already a workspace root — it will be @@ -423,6 +433,27 @@ class PackagesGetCommand extends FlutterCommand { return FlutterCommandResult.success(); } + /// Whether [packageName] depends on the `flutter` package, directly or + /// transitively, according to the resolved package [graph]. + static bool _dependsOnFlutter(PackageGraph graph, String packageName) { + final visited = {}; + final toVisit = Queue.of([packageName]); + while (toVisit.isNotEmpty) { + final String current = toVisit.removeFirst(); + if (!visited.add(current)) { + continue; + } + if (current == 'flutter') { + return true; + } + final List? dependencies = graph.dependencies[current]; + if (dependencies != null) { + toVisit.addAll(dependencies); + } + } + return false; + } + late final Future> _pluginsFound = (() async { final FlutterProject? rootProject = _rootProject; if (rootProject == null) { diff --git a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart index 830c8d55c81c6..f886156392d66 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart @@ -458,6 +458,52 @@ workspace: }, ); + testUsingContext( + 'get does not generate platform tooling for a non-Flutter workspace root', + // Regression test for https://github.com/flutter/flutter/issues/189550. + () async { + tempDir.childFile('pubspec.yaml').writeAsStringSync(''' +name: workspace +environment: + sdk: ^3.7.0-0 +workspace: + - flutter_project +'''); + // A stray platform directory in the plain Dart workspace root must not + // be populated with Flutter project files. + tempDir.childDirectory('ios').createSync(); + tempDir.childDirectory('android').createSync(); + final String projectPath = await createProject(tempDir, arguments: ['--no-pub']); + final File pubspecFile = fileSystem.file(fileSystem.path.join(projectPath, 'pubspec.yaml')); + final pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final pubspec = { + ...pubspecYaml.value.cast(), + 'resolution': 'workspace', + 'environment': {'sdk': '^3.5.0-0'}, + }; + pubspecFile.writeAsStringSync(jsonEncode(pubspec)); + await runCommandIn(tempDir.path, 'get'); + + expectDependenciesResolved(tempDir.path); + expect(tempDir.childDirectory('ios').listSync(), isEmpty); + expect(tempDir.childDirectory('android').listSync(), isEmpty); + // The Flutter app member of the workspace is still processed. + expectExists(projectPath, 'ios/Flutter/Generated.xcconfig'); + }, + overrides: { + Stdio: () => mockStdio, + Pub: () => Pub.test( + fileSystem: globals.fs, + logger: globals.logger, + processManager: globals.processManager, + botDetector: globals.botDetector, + platform: globals.platform, + stdio: mockStdio, + ), + Analytics: () => fakeAnalytics, + }, + ); + testUsingContext( 'get generates files into lib/l10n', () async { From 62df70166dbc5605d6dec676e50d86b59bd1a764 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Wed, 5 Aug 2026 07:35:54 -0700 Subject: [PATCH 078/330] [master] Update Flutter DEPS to Dart bb17f25f176f5209179b23d51aab9725ffb5146d (#190600) This PR updates the transitive dependencies in the engine `DEPS` file based on Dart SDK hash `bb17f25f176f5209179b23d51aab9725ffb5146d`. --- DEPS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPS b/DEPS index b485e3cd0862d..142cb94b30cb8 100644 --- a/DEPS +++ b/DEPS @@ -55,12 +55,12 @@ 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': '9859c0a39adb1c418b0831dde11a338f7e1f7ed0', + 'dart_revision': 'bb17f25f176f5209179b23d51aab9725ffb5146d', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py 'dart_binaryen_rev': '9926156a583cec3d22d521232b31c70fa9a87dc1', - 'dart_boringssl_rev': 'f1f2556a5dfa59e147d9d47279cc3f7f8a18b433', + 'dart_boringssl_rev': '7515be5ccd601ae0433d3682ba7737592b402014', 'dart_core_rev': 'fe516ee1b38cc60e7a8c6e082c337037a043d782', 'dart_devtools_rev': '21f1838f3a9b138ac377efb953ca5a53c8832e75', 'dart_ecosystem_rev': 'edfdb3b4063b9034b708144633a700204f865f43', From f31202e29a4c54cc3884d8839faf0905e4fe8bad Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 5 Aug 2026 10:35:54 -0400 Subject: [PATCH 079/330] [flutter_tools] Handle WDAC block error code 4551 for impellerc (#190108) [flutter_tools] Handle WDAC block error code 4551 for impellerc On Windows, when impellerc is blocked by Windows Defender Application Control (WDAC), it throws a ProcessException with error code 4551. Define the constant for this error code next to the existing code 1260 and check for it in ShaderCompiler._isBlockedBySecurityPolicy to exit gracefully. Fixes #190232 --- .../build_system/tools/shader_compiler.dart | 4 +- .../targets/shader_compiler_test.dart | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart b/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart index a87a36960bc76..04db5b6d03f8a 100644 --- a/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart +++ b/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart @@ -336,7 +336,9 @@ class ShaderCompiler { return false; } const winErrorAccessDisabledByPolicy = 1260; - return exception.errorCode == winErrorAccessDisabledByPolicy; + const winErrorSystemIntegrityPolicyViolation = 4551; + return exception.errorCode == winErrorAccessDisabledByPolicy || + exception.errorCode == winErrorSystemIntegrityPolicyViolation; } void _logSecurityBlockError(String impellercPath) { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart index 94dda581a3f96..94c17c233a0f1 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart @@ -830,6 +830,54 @@ void main() { }, ); + testWithoutContext( + 'compileShader throws ToolExit and logs friendly message when impellerc is blocked by WDAC (4551) ' + '(regression test for https://github.com/flutter/flutter/issues/190232)', + () async { + final blockedException = ProcessException( + impellerc, + [], + 'An Application Control policy has blocked this file', + 4551, + ); + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + impellerc, + '--runtime-stage-metal', + '--iplr', + '--sl=$outputPath', + '--spirv=$outputPath.spirv', + '--input=$fragPath', + '--input-type=frag', + '--include=$fragDir', + '--include=$shaderLibDir', + ], + exception: blockedException, + ), + ]); + final shaderCompiler = ShaderCompiler( + processManager: processManager, + logger: logger, + fileSystem: fileSystem, + artifacts: artifacts, + platform: FakePlatform(operatingSystem: 'windows'), + ); + + await expectLater( + shaderCompiler.compileShader( + input: fileSystem.file(fragPath), + outputPath: outputPath, + targetPlatform: TargetPlatform.ios, + ), + throwsToolExit(message: 'Impeller shader compiler was blocked by security policy.'), + ); + + expect(logger.errorText, contains('blocked by system')); + expect(logger.errorText, contains(impellerc)); + }, + ); + testWithoutContext( 'compileShader throws ToolExit and logs friendly message when impellerc is blocked by group policy', () async { From 7e85c6b68a66ab880add22a38689d4cc03d28b16 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 5 Aug 2026 11:28:13 -0400 Subject: [PATCH 080/330] [flutter_tools] Fix PathNotFoundException during native assets cleanup (#190102) Avoid crashing with a PathNotFoundException if a file is deleted by the OS or another process after being listed but before being deleted by the tool. Use ErrorHandlingFileSystem.deleteIfExists instead of entity.delete. Fixes #190234 --- .../isolated/native_assets/native_assets.dart | 5 ++- .../isolated/native_assets_test.dart | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart index 3e0ec38639659..f021e6380281b 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart @@ -12,6 +12,7 @@ import 'package:logging/logging.dart' as logging; import 'package:package_config/package_config_types.dart'; import '../../base/common.dart'; +import '../../base/error_handling_io.dart'; import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/platform.dart'; @@ -830,8 +831,8 @@ Future> _copyNativeCodeAssetsForOS( if (!targetDir.existsSync()) { targetDir.createSync(recursive: true); } - await for (final FileSystemEntity entity in targetDir.list()) { - await entity.delete(recursive: true); + for (final FileSystemEntity entity in await targetDir.list().toList()) { + ErrorHandlingFileSystem.deleteIfExists(entity, recursive: true); } if (assetTargetLocations.isEmpty) { diff --git a/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart index 6926e7ece5988..de19fba961649 100644 --- a/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart @@ -393,6 +393,50 @@ CMAKE_LINKER:FILEPATH=/usr/bin/ld.ldd expect(target.didSetCCompilerConfig, isTrue); }, ); + + testUsingContext( + 'installCodeAssets cleans up existing stale files in target directory without crashing ' + '(regression test for https://github.com/flutter/flutter/issues/190234)', + overrides: {ProcessManager: () => FakeProcessManager.empty()}, + () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + final Uri nonFlutterTesterAssetUri = environment.buildDir + .childFile(InstallCodeAssets.nativeAssetsFilename) + .uri; + await packageConfig.parent.create(); + await packageConfig.create(); + + final environmentDefines = {kBuildMode: BuildMode.debug.cliName}; + final DartHooksResult dartHookResult = await runFlutterSpecificHooks( + environmentDefines: environmentDefines, + targetPlatform: TargetPlatform.windows_x64, + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeFlutterNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: ['bar'], + ), + buildCodeAssets: const BuildCodeAssetsOptions(appBuildDirectory: null), + buildDataAssets: true, + recordedUsesFile: null, + ); + final Directory targetDirectory = environment.buildDir.childDirectory('native_assets'); + await targetDirectory.create(recursive: true); + final File staleFile = targetDirectory.childFile('stale.txt'); + staleFile.writeAsStringSync('stale'); + + await installCodeAssets( + dartHookResult: dartHookResult, + environmentDefines: environmentDefines, + targetPlatform: TargetPlatform.windows_x64, + projectUri: projectUri, + fileSystem: fileSystem, + nativeAssetsFileUri: nonFlutterTesterAssetUri, + targetUri: targetDirectory.uri, + ); + expect(targetDirectory, exists); + expect(staleFile, isNot(exists)); + }, + ); } class _SetCCompilerConfigTarget extends FakeFlutterNativeAssetsBuildRunner { From cfc8706bbdd5e25d5e0d7c28fc2e0e33f2220f1a Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 6 Aug 2026 00:52:46 +0900 Subject: [PATCH 081/330] gpu: Remove unused GPUSurfaceNoop (#190610) `GPUSurfaceNoop` was only ever used by the iOS `IOSSurfaceNoop`, which was deleted when the iOS software-renderer fallback was removed in #190590. Since there are no remaining callers, this removes the class. No behavioural change; the code was already unreachable. The compiler is the test! Issue: https://github.com/flutter/flutter/issues/190041 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- engine/src/flutter/shell/gpu/BUILD.gn | 7 +- .../src/flutter/shell/gpu/gpu_surface_noop.h | 68 ------------- .../src/flutter/shell/gpu/gpu_surface_noop.mm | 97 ------------------- .../shell/gpu/gpu_surface_noop_unittests.mm | 25 ----- 4 files changed, 1 insertion(+), 196 deletions(-) delete mode 100644 engine/src/flutter/shell/gpu/gpu_surface_noop.h delete mode 100644 engine/src/flutter/shell/gpu/gpu_surface_noop.mm delete mode 100644 engine/src/flutter/shell/gpu/gpu_surface_noop_unittests.mm diff --git a/engine/src/flutter/shell/gpu/BUILD.gn b/engine/src/flutter/shell/gpu/BUILD.gn index 3e0ed6f660ef5..9206beb12e1da 100644 --- a/engine/src/flutter/shell/gpu/BUILD.gn +++ b/engine/src/flutter/shell/gpu/BUILD.gn @@ -103,8 +103,6 @@ if (shell_enable_metal) { "gpu_surface_metal_delegate.h", "gpu_surface_metal_skia.h", "gpu_surface_metal_skia.mm", - "gpu_surface_noop.h", - "gpu_surface_noop.mm", ] public_deps = gpu_common_deps @@ -126,10 +124,7 @@ if (is_mac) { cflags_objc = flutter_cflags_objc cflags_objcc = flutter_cflags_objcc - sources = [ - "gpu_surface_metal_impeller_unittests.mm", - "gpu_surface_noop_unittests.mm", - ] + sources = [ "gpu_surface_metal_impeller_unittests.mm" ] frameworks = [ "AppKit.framework", diff --git a/engine/src/flutter/shell/gpu/gpu_surface_noop.h b/engine/src/flutter/shell/gpu/gpu_surface_noop.h deleted file mode 100644 index faaca53c38914..0000000000000 --- a/engine/src/flutter/shell/gpu/gpu_surface_noop.h +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef FLUTTER_SHELL_GPU_GPU_SURFACE_NOOP_H_ -#define FLUTTER_SHELL_GPU_GPU_SURFACE_NOOP_H_ - -#include - -#include "flutter/flow/surface.h" - -namespace flutter { - -/// @brief A rendering surface that accepts rendering intent but does not render -/// anything. -/// -/// This is useful for running on platforms that need an engine instance and -/// don't have the required drivers. -class GPUSurfaceNoop : public Surface { - public: - explicit GPUSurfaceNoop(); - - // |Surface| - ~GPUSurfaceNoop(); - - // |Surface| - bool IsValid() override; - - // |Surface| - Surface::SurfaceData GetSurfaceData() const override; - - private: - // |Surface| - std::unique_ptr AcquireFrame( - const DlISize& frame_size) override; - - std::unique_ptr AcquireFrameFromCAMetalLayer( - const DlISize& frame_size); - - std::unique_ptr AcquireFrameFromMTLTexture( - const DlISize& frame_size); - - // |Surface| - DlMatrix GetRootTransformation() const override; - - // |Surface| - GrDirectContext* GetContext() override; - - // |Surface| - std::unique_ptr MakeRenderContextCurrent() override; - - // |Surface| - bool AllowsDrawingWhenGpuDisabled() const override; - - // |Surface| - bool EnableRasterCache() const override; - - // |Surface| - std::shared_ptr GetAiksContext() const override; - - GPUSurfaceNoop(const GPUSurfaceNoop&) = delete; - - GPUSurfaceNoop& operator=(const GPUSurfaceNoop&) = delete; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_GPU_GPU_SURFACE_NOOP_H_ diff --git a/engine/src/flutter/shell/gpu/gpu_surface_noop.mm b/engine/src/flutter/shell/gpu/gpu_surface_noop.mm deleted file mode 100644 index 618874c650f31..0000000000000 --- a/engine/src/flutter/shell/gpu/gpu_surface_noop.mm +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "flutter/shell/gpu/gpu_surface_noop.h" - -#import -#import - -#include "flow/surface.h" -#include "flow/surface_frame.h" -#include "flutter/common/settings.h" -#include "flutter/fml/mapping.h" -#include "flutter/fml/trace_event.h" - -static_assert(__has_feature(objc_arc), "ARC must be enabled."); - -namespace flutter { - -GPUSurfaceNoop::GPUSurfaceNoop() = default; - -GPUSurfaceNoop::~GPUSurfaceNoop() = default; - -// |Surface| -bool GPUSurfaceNoop::IsValid() { - return true; -} - -Surface::SurfaceData GPUSurfaceNoop::GetSurfaceData() const { - return Surface::SurfaceData{}; -} - -// |Surface| -std::unique_ptr GPUSurfaceNoop::AcquireFrame(const DlISize& frame_size) { - auto callback = [](const SurfaceFrame&, DlCanvas*) { return true; }; - auto submit_callback = [](const SurfaceFrame&) { return true; }; - SurfaceFrame::FramebufferInfo framebuffer_info; - - return std::make_unique( - /*surface=*/nullptr, - /*framebuffer_info=*/framebuffer_info, - /*encode_callback=*/callback, - /*submit_callback=*/submit_callback, - /*frame_size=*/frame_size, - /*context_result=*/nullptr, - /*display_list_fallback=*/true); -} - -std::unique_ptr GPUSurfaceNoop::AcquireFrameFromMTLTexture( - const DlISize& frame_size) { - auto callback = [](const SurfaceFrame&, DlCanvas*) { return true; }; - auto submit_callback = [](const SurfaceFrame&) { return true; }; - SurfaceFrame::FramebufferInfo framebuffer_info; - - return std::make_unique( - /*surface=*/nullptr, - /*framebuffer_info=*/framebuffer_info, - /*encode_callback=*/callback, - /*submit_callback=*/submit_callback, - /*frame_size=*/frame_size, - /*context_result=*/nullptr, - /*display_list_fallback=*/true); -} - -// |Surface| -DlMatrix GPUSurfaceNoop::GetRootTransformation() const { - // This backend does not currently support root surface transformations. Just - // return identity. - return {}; -} - -// |Surface| -GrDirectContext* GPUSurfaceNoop::GetContext() { - return nullptr; -} - -// |Surface| -std::unique_ptr GPUSurfaceNoop::MakeRenderContextCurrent() { - // This backend has no such concept. - return std::make_unique(true); -} - -bool GPUSurfaceNoop::AllowsDrawingWhenGpuDisabled() const { - return true; -} - -// |Surface| -bool GPUSurfaceNoop::EnableRasterCache() const { - return false; -} - -// |Surface| -std::shared_ptr GPUSurfaceNoop::GetAiksContext() const { - return nullptr; -} - -} // namespace flutter diff --git a/engine/src/flutter/shell/gpu/gpu_surface_noop_unittests.mm b/engine/src/flutter/shell/gpu/gpu_surface_noop_unittests.mm deleted file mode 100644 index ec462a400cb36..0000000000000 --- a/engine/src/flutter/shell/gpu/gpu_surface_noop_unittests.mm +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include -#include - -#include "flutter/shell/gpu/gpu_surface_noop.h" -#include "gtest/gtest.h" -#include "impeller/entity/mtl/entity_shaders.h" -#include "impeller/entity/mtl/framebuffer_blend_shaders.h" -#include "impeller/entity/mtl/modern_shaders.h" -#include "impeller/renderer/backend/metal/context_mtl.h" - -namespace flutter { -namespace testing { - -TEST(GPUSurfaceNoop, InvalidImpellerContextCreatesCausesSurfaceToBeInvalid) { - auto surface = std::make_shared(); - - EXPECT_TRUE(surface->IsValid()); -} - -} // namespace testing -} // namespace flutter From 12939d71062f98152b769320b0246e165ca39276 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Thu, 6 Aug 2026 00:50:07 +0800 Subject: [PATCH 082/330] [Android] Add numeric-password variation to TextInputType (embedding) (#190207) Working towards https://github.com/flutter/flutter/issues/190204. Adds `isPassword` to the Android embedding's `TextInputChannel.InputType` and OR's `TYPE_NUMBER_VARIATION_PASSWORD` into the NUMBER branch of `TextInputPlugin.inputTypeFromTextInputType` when set. `Configuration.fromJson` / `InputType.fromJson` parse the additive `"password"` JSON key. The framework Dart field is a companion change (not in this PR). ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> --- .../systemchannels/TextInputChannel.java | 16 +- .../plugin/editing/TextInputPlugin.java | 3 + .../systemchannels/TextInputChannelTest.java | 30 ++++ .../plugin/editing/TextInputPluginTest.java | 146 ++++++++++++++++++ 4 files changed, 193 insertions(+), 2 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index c032fcfc25864..b0df9ed3cf929 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -718,7 +718,7 @@ public Configuration( * A text input type. * *

If the {@link #type} is {@link TextInputType#NUMBER}, this {@code InputType} also reports - * whether that number {@link #isSigned} and {@link #isDecimal}. + * whether that number {@link #isSigned}, {@link #isDecimal}, and {@link #isPassword}. */ public static class InputType { @NonNull @@ -727,17 +727,29 @@ public static InputType fromJson(@NonNull JSONObject json) return new InputType( TextInputType.fromValue(json.getString("name")), json.optBoolean("signed", false), - json.optBoolean("decimal", false)); + json.optBoolean("decimal", false), + json.optBoolean("password", false)); } @NonNull public final TextInputType type; public final boolean isSigned; public final boolean isDecimal; + public final boolean isPassword; + /** + * Convenience overload equivalent to {@code new InputType(type, isSigned, isDecimal, false)}. + */ public InputType(@NonNull TextInputType type, boolean isSigned, boolean isDecimal) { + this(type, isSigned, isDecimal, false); + } + + /** Constructs an {@code InputType} for {@code type} with the given NUMBER-variation flags. */ + public InputType( + @NonNull TextInputType type, boolean isSigned, boolean isDecimal, boolean isPassword) { this.type = type; this.isSigned = isSigned; this.isDecimal = isDecimal; + this.isPassword = isPassword; } } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 67cda46e3b27b..8dc7a92aacb01 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -261,6 +261,9 @@ private static int inputTypeFromTextInputType( if (type.isDecimal) { textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL; } + if (type.isPassword) { + textType |= InputType.TYPE_NUMBER_VARIATION_PASSWORD; + } return textType; } else if (type.type == TextInputChannel.TextInputType.PHONE) { return InputType.TYPE_CLASS_PHONE; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java index 19bdec0a67139..f824f0b12b22f 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java @@ -7,6 +7,8 @@ import static io.flutter.Build.API_LEVELS; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -114,4 +116,32 @@ private JSONObject createConfigurationJsonWithAutofillHint(String hint) throws J return arguments; } + + @Test + public void inputTypeFromJsonParsesPassword() throws JSONException, NoSuchFieldException { + final JSONObject json = new JSONObject(); + json.put("name", "TextInputType.number"); + json.put("password", true); + + final TextInputChannel.InputType inputType = TextInputChannel.InputType.fromJson(json); + + assertEquals(TextInputChannel.TextInputType.NUMBER, inputType.type); + assertTrue(inputType.isPassword); + assertFalse(inputType.isSigned); + assertFalse(inputType.isDecimal); + } + + @Test + public void inputTypeFromJsonDefaultsPasswordToFalseWhenMissing() + throws JSONException, NoSuchFieldException { + // Backward-compatibility guard: an older framework payload without the "password" key must + // deserialize to isPassword == false so plain numeric fields keep their existing IME behavior. + final JSONObject json = new JSONObject(); + json.put("name", "TextInputType.number"); + + final TextInputChannel.InputType inputType = TextInputChannel.InputType.fromJson(json); + + assertEquals(TextInputChannel.TextInputType.NUMBER, inputType.type); + assertFalse(inputType.isPassword); + } } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 57339634d0b23..936088c97402d 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -3040,6 +3040,152 @@ public void inputConnection_hintLocalesIsSetInEditorInfo() { assertEquals(editorInfo.hintLocales, new LocaleList(hintLocales)); } + /** + * Helper that constructs a minimal {@link TextInputChannel.Configuration} with the given number + * variation flags and captures the resulting {@link EditorInfo} from {@link + * TextInputPlugin#createInputConnection}. + */ + private EditorInfo editorInfoForNumberConfig( + boolean isSigned, + boolean isDecimal, + boolean isPassword, + boolean obscureText, + boolean enableIMEPersonalizedLearning, + TextInputChannel.Configuration.Autofill autofill) { + View testView = new View(ctx); + DartExecutor dartExecutor = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + obscureText, + false, + true, + enableIMEPersonalizedLearning, + false, + TextInputChannel.TextCapitalization.NONE, + new TextInputChannel.InputType( + TextInputChannel.TextInputType.NUMBER, isSigned, isDecimal, isPassword), + null, + null, + autofill, + null, + null, + null)); + + EditorInfo editorInfo = new EditorInfo(); + textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo); + return editorInfo; + } + + @Test + public void inputType_number() { + EditorInfo editorInfo = editorInfoForNumberConfig(false, false, false, false, true, null); + assertEquals(InputType.TYPE_CLASS_NUMBER, editorInfo.inputType); + } + + @Test + public void inputType_numberWithPassword() { + EditorInfo editorInfo = editorInfoForNumberConfig(false, false, true, false, true, null); + assertEquals( + InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD, + editorInfo.inputType); + } + + @Test + public void inputType_numberWithSigned() { + EditorInfo editorInfo = editorInfoForNumberConfig(true, false, false, false, true, null); + assertEquals( + InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED, editorInfo.inputType); + } + + @Test + public void inputType_numberWithDecimal() { + EditorInfo editorInfo = editorInfoForNumberConfig(false, true, false, false, true, null); + assertEquals( + InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL, editorInfo.inputType); + } + + @Test + public void inputType_numberWithSignedAndPassword() { + EditorInfo editorInfo = editorInfoForNumberConfig(true, false, true, false, true, null); + assertEquals( + InputType.TYPE_CLASS_NUMBER + | InputType.TYPE_NUMBER_FLAG_SIGNED + | InputType.TYPE_NUMBER_VARIATION_PASSWORD, + editorInfo.inputType); + } + + @Test + public void inputType_numberWithDecimalAndPassword() { + EditorInfo editorInfo = editorInfoForNumberConfig(false, true, true, false, true, null); + assertEquals( + InputType.TYPE_CLASS_NUMBER + | InputType.TYPE_NUMBER_FLAG_DECIMAL + | InputType.TYPE_NUMBER_VARIATION_PASSWORD, + editorInfo.inputType); + } + + @Test + public void inputType_numberObscureTextDoesNotSetPasswordVariation() { + // obscureText is orthogonal to the numeric password variation; it must not implicitly set + // TYPE_NUMBER_VARIATION_PASSWORD on a NUMBER field. This test is load-bearing because the + // obscureText branch lives in the same inputTypeFromTextInputType() function as the NUMBER + // branch — the two are the most plausible place for accidental coupling. + EditorInfo editorInfo = editorInfoForNumberConfig(false, false, false, true, true, null); + assertEquals(InputType.TYPE_CLASS_NUMBER, editorInfo.inputType); + assertEquals(0, editorInfo.inputType & InputType.TYPE_NUMBER_VARIATION_PASSWORD); + } + + @Test + public void inputType_numberWithPasswordJsonRoundTrip() + throws JSONException, NoSuchFieldException { + // End-to-end coverage from JSON through Configuration.fromJson -> InputType.fromJson -> + // setTextInputClient -> createInputConnection. Catches typos in the "password" JSON key, + // gaps in Configuration.fromJson's recursive inputType handling, and any break in the + // JSON -> field -> plugin chain that the matrix tests bypass by calling new InputType(...) + // directly. + final JSONObject inputType = new JSONObject(); + inputType.put("name", "TextInputType.number"); + inputType.put("password", true); + + final JSONObject arguments = new JSONObject(); + arguments.put("inputAction", "TextInputAction.done"); + arguments.put("textCapitalization", "TextCapitalization.none"); + arguments.put("inputType", inputType); + + final TextInputChannel.Configuration configuration = + TextInputChannel.Configuration.fromJson(arguments); + + View testView = new View(ctx); + DartExecutor dartExecutor = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); + textInputPlugin.setTextInputClient(0, configuration); + + EditorInfo editorInfo = new EditorInfo(); + textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo); + + assertEquals( + InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD, + editorInfo.inputType); + } + interface EventHandler { void sendAppPrivateCommand(View view, String action, Bundle data); } From 8ebfb2e49104ba1f63ac9b52d6ce7a866aabd2d0 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Wed, 5 Aug 2026 17:52:00 +0100 Subject: [PATCH 083/330] [flutter_tools] [DAP] Extract and forward DevTools Deep Link URLs to DAP clients (#190455) This is functionality from the legacy DAPs that was missed when migrating to the SDK. Some kinds of errors (such as Overflow errors) includes nodes to deep-link into DevTools. By forwarding these to the IDE, instead of just a text link the IDE can show a toast notification with a button to open the embedded version of DevTools. This is part of the work to fix https://github.com/Dart-Code/Dart-Code/issues/6134. There are some additional fixes required elsewhere, but once those are resolved, this will result in a notification like this: image Clicked "Inspect Widget" will open the embedded Inspector on the correct widget. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../src/debug_adapters/error_formatter.dart | 40 ++++++-- .../src/debug_adapters/flutter_adapter.dart | 13 ++- .../dap/flutter_adapter_test.dart | 95 ++++++++++++++++++- 3 files changed, 139 insertions(+), 9 deletions(-) diff --git a/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart index 4141b9efcee60..af9ed21ffb2d2 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart @@ -12,19 +12,27 @@ typedef _OutputSender = int? variablesReference, }); -/// A formatter for improving the display of Flutter structured errors over DAP. +/// Deserializes and formats a Flutter structured error. /// -/// The formatter deserializes a `Flutter.Error` event and produces output -/// similar to the `renderedErrorText` field, but may include ansi color codes -/// to provide improved formatting (such as making stack frames from non-user -/// code faint) if the client indicated support. +/// Produces output similar to the `renderedErrorText` field, but may include +/// ansi color codes to provide improved formatting (such as making stack frames +/// from non-user code faint) if the client indicated support. /// /// Lines that look like stack frames will be marked so they can be parsed by /// the base adapter and attached as `Source`s to allow them to be clickable /// in the client. +/// +/// If the error contains a `DevToolsDeepLinkProperty` node, its URL will be +/// extracted into [devToolsDeepLinkUrl]. class FlutterErrorFormatter { final batchedOutput = <_BatchedOutput>[]; + /// The text of the ErrorSummary node, if exists. + String? errorSummary; + + /// The url of any DevTools deep link node. + String? devToolsDeepLinkUrl; + /// Formats a Flutter error. /// /// If this is not the first error since the reload, only a summary will be @@ -89,6 +97,14 @@ class FlutterErrorFormatter { /// Writes [node] to the output using [indent], recursing unless [recursive] /// is `false`. void _writeNode(_ErrorNode node, {int indent = 0, bool recursive = true}) { + if (node.type == _DiagnosticsNodeType.ErrorSummary) { + // Probably there is only one error summary, but keep the first + // (outer-most) if not. + errorSummary ??= node.description; + } else if (node.type == _DiagnosticsNodeType.DevToolsDeepLinkProperty) { + _parseDevToolsDeepLink(node); + } + // Errors, summaries and lines starting "Exception:" are marked as errors so // they go to stderr instead of stdout (this may cause the client to colour // them like errors). @@ -138,6 +154,17 @@ class FlutterErrorFormatter { ); } } + + /// Parse the DevTools deep link URL out of a + /// [_DiagnosticsNodeType.DevToolsDeepLinkProperty] node. + void _parseDevToolsDeepLink(_ErrorNode node) { + assert(node.type == _DiagnosticsNodeType.DevToolsDeepLinkProperty); + if (node.value case final url?) { + // Probably there is only one deep link, but keep the first + // (outer-most) if not. + devToolsDeepLinkUrl ??= url; + } + } } /// A container for output to be sent to the client. @@ -160,7 +187,7 @@ enum _DiagnosticsNodeLevel { error, summary } enum _DiagnosticsNodeStyle { flat } -enum _DiagnosticsNodeType { DiagnosticsBlock } +enum _DiagnosticsNodeType { ErrorSummary, DevToolsDeepLinkProperty, DiagnosticsBlock } class _ErrorData extends _ErrorNode { _ErrorData(super.data); @@ -182,6 +209,7 @@ class _ErrorNode { bool get showName => data['showName'] != false; _DiagnosticsNodeStyle? get style => asEnum('style', _DiagnosticsNodeStyle.values); _DiagnosticsNodeType? get type => asEnum('type', _DiagnosticsNodeType.values); + String? get value => asString('value'); String? asString(String field) { final Object? value = data[field]; diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart index a6b434a0edf05..9d315f2f9941e 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart @@ -228,9 +228,20 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter with VmServiceInfoFile return; } - FlutterErrorFormatter() + final formatter = FlutterErrorFormatter() ..formatError(errorData) ..sendOutput(sendOutput); + + // Forward any DevTools deep-links in a 'dart.flutter.devToolsDeepLink' + // event. + if (formatter case FlutterErrorFormatter(:final errorSummary?, :final devToolsDeepLinkUrl?)) { + // This event is interpreted by IDEs extensions like like Dart-Code and + // should not be changed in breaking ways without coordination. + sendEvent( + RawEventBody({'summary': errorSummary, 'deepLinkUrl': devToolsDeepLinkUrl}), + eventType: 'dart.flutter.devToolsDeepLink', + ); + } } /// Called by [launchRequest] to request that we actually start the app to be run/debugged. diff --git a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart index df9af9f2247f5..b036258118a30 100644 --- a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart @@ -16,7 +16,7 @@ import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart'; import 'package:flutter_tools/src/globals.dart' as globals show fs, platform; import 'package:test/fake.dart'; import 'package:test/test.dart'; -import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service.dart' as vm; import 'mocks.dart'; @@ -601,6 +601,56 @@ void main() { 'params': {'warning': 'This is a test warning'}, }); }); + + test('forward inspector deep links as dart.flutter.devToolsDeepLink events', () async { + final adapter = FakeFlutterDebugAdapter( + fileSystem: MemoryFileSystem.test(style: fsStyle), + platform: platform, + ); + + // Simulate startup. + final args = FlutterLaunchRequestArguments(cwd: '.', program: 'foo.dart'); + final responseCompleter = Completer(); + await adapter.configurationDoneRequest(FakeRequest(), null, () {}); + await adapter.launchRequest(FakeRequest(), args, responseCompleter.complete); + + // Start listening for the forwarded event (don't await it yet, it won't + // be triggered until the call below). + final Future> forwardedEvent = adapter.dapToClientMessages.firstWhere( + (Map data) => data['event'] == 'dart.flutter.devToolsDeepLink', + ); + + // Simulate Flutter asking for a URL to be launched. + await adapter.handleExtensionEvent( + vm.Event( + kind: vm.EventKind.kExtension, + extensionKind: 'Flutter.Error', + extensionData: vm.ExtensionData.parse({ + 'properties': >[ + { + 'type': 'ErrorSummary', + 'description': 'An overflow occurred', + 'properties': >[ + { + 'type': 'DevToolsDeepLinkProperty', + 'description': 'Click to open the inspector', + 'value': 'http://127.0.0.1:9100/inspector?uri=x&inspectorRef=y', + }, + ], + }, + ], + }), + ), + ); + + // Wait for the forwarded event. + final Map message = await forwardedEvent; + // Ensure the body of the event matches the original event sent by Flutter. + expect(message['body'], { + 'summary': 'An overflow occurred', + 'deepLinkUrl': 'http://127.0.0.1:9100/inspector?uri=x&inspectorRef=y', + }); + }); }); group('handles reverse requests', () { @@ -907,11 +957,52 @@ stdout "The relevant error-causing widget was:\n MyWidget:file:///path/to/wid stderr "════════════════════════════════════════════════════════════════════════════════\n" '''); }); + + test('extracts the error summary', () { + final formatter = FlutterErrorFormatter() + ..formatError({ + 'type': 'NotErrorSummary', + 'description': 'xxx', + 'properties': >[ + {'description': 'yyy'}, + { + 'type': 'ErrorSummary', + 'description': 'my error summary', + 'children': >[ + {'type': 'NotErrorSummary2', 'description': 'zzz'}, + ], + }, + ], + }); + + expect(formatter.errorSummary, 'my error summary'); + }); + + test('extracts a DevTools Deep Link', () { + final formatter = FlutterErrorFormatter() + ..formatError({ + 'type': 'NotErrorSummary', + 'description': 'xxx', + 'properties': >[ + {'description': 'yyy'}, + { + 'type': 'DevToolsDeepLinkProperty', + 'description': 'Click to open the inspector', + 'value': 'http://127.0.0.1:9100/inspector?uri=x&inspectorRef=y', + }, + ], + }); + + expect( + formatter.devToolsDeepLinkUrl, + 'http://127.0.0.1:9100/inspector?uri=x&inspectorRef=y', + ); + }); }); }); } -class _FakeVm extends Fake implements VM { +class _FakeVm extends Fake implements vm.VM { _FakeVm({this.pid = 1}); @override From e4c022467f03b458086cdb867d845f435e9a0d57 Mon Sep 17 00:00:00 2001 From: Krystic Chung Date: Thu, 6 Aug 2026 01:21:41 +0800 Subject: [PATCH 084/330] Separate ARM64 Linux Desktop and Embedded engine builds to fix CJK and non-ASCII font rendering (#180235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Separate ARM64 Linux Desktop and Embedded engine builds to fix CJK and non-ASCII font rendering ## Problem On ARM64 Linux desktop environments (GNOME, KDE, etc.), **Chinese characters and other non-ASCII text render as squares** in Flutter applications. This affects production applications like Ubuntu App Center which uses Flutter on ARM64. ### Root Cause The ARM64 Linux engine builds (`linux_arm_host_engine`) were missing the `--enable-fontconfig` flag, unlike x64 Linux builds which have it. Without fontconfig support, the Flutter GTK engine cannot discover and load system fonts properly on desktop Linux distributions. ### Why This Wasn't Caught Earlier The same build configuration was used for both: - **Desktop environments** (GNOME/KDE with GTK) - requires fontconfig - **Embedded/IoT systems** (headless or custom UI) - doesn't need fontconfig This conflation meant adding fontconfig would unnecessarily bloat embedded deployments. --- ## Solution This PR **separates ARM64 Linux builds into two distinct configurations**, following the same architecture pattern as x64 Linux: ### 1. Desktop Builds (NEW: `linux_arm_host_desktop_engine`) - **Target**: GNOME/KDE desktop environments - **Includes**: `--enable-fontconfig` for proper font rendering - **Builds**: `flutter_gtk` library for profile/debug/release modes - **Use case**: Desktop applications (App Center, desktop Flutter apps) ### 2. Embedded/IoT Builds (MODIFIED: `linux_arm_host_engine`) - **Target**: Headless or custom UI embedded systems - **Excludes**: fontconfig dependency (not needed) - **Builds**: embedder library, artifacts, Dart SDK, Impeller SDK - **Use case**: Raspberry Pi, embedded Linux, IoT devices --- ## Changes Made 1. **Created**: `engine/src/flutter/ci/builders/linux_arm_host_desktop_engine.json` - New builder configuration for ARM64 desktop - Adds `--enable-fontconfig` to all build modes - Produces `flutter_gtk` libraries 2. **Modified**: `engine/src/flutter/ci/builders/linux_arm_host_engine.json` - Now focuses on embedded/IoT builds only - Removed `flutter_gtk` targets (moved to desktop config) - Removed `--enable-fontconfig` (not needed for embedded) - Removed profile/release configs (only build embedder in debug) 3. **Modified**: `engine/src/flutter/.ci.yaml` - Registered new `linux_arm_host_desktop_engine` builder - Same CI configuration as existing ARM64 builder --- ## Testing & Verification ### ✅ Manual Testing - **Platform**: Ubuntu 25.10 on ARM64 (Parallels VM on Apple M4) - **Application**: Ubuntu App Center (Snap package) - **Flutter Version**: 3.38.1 **Before** (Official ARM64 build without fontconfig): - Chinese characters display as squares (□□□) - All CJK and non-Latin text affected **After** (Custom build with fontconfig): - Chinese characters render correctly using system fonts - All text displays properly ### Test Commands Used ```bash # Compile custom engine with fontconfig ./flutter/tools/gn --runtime-mode release --enable-fontconfig \ --target-os linux --linux-cpu arm64 --no-goma ninja -C out/linux_release_arm64 # Verify fontconfig linking readelf -d out/linux_release_arm64/libflutter_linux_gtk.so | grep fontconfig # Output: libfontconfig.so.1 # Replace engine in App Center snap (via mount) sudo mount --bind custom_libflutter_linux_gtk.so \ /snap/snap-store/current/bin/lib/libflutter_linux_gtk.so # Launch App Center - Chinese text now renders correctly snap-store ``` --- ## Architecture Benefits ✅ **Decoupling**: Desktop and embedded builds are now independent ✅ **No Breaking Changes**: Embedded builds remain unchanged (same targets, no fontconfig) ✅ **Transparent**: Flutter Tool and VS Code don't need modifications ✅ **Consistency**: Follows the same pattern as x64 Linux architecture ✅ **Minimal Impact**: Only adds new builds, doesn't modify existing ones --- ## Downstream Impact ### Who Benefits - ARM64 Linux desktop users (Ubuntu, Fedora, Debian on ARM) - Applications using Flutter GTK on ARM64 (App Center, custom desktop apps) - Developers targeting ARM64 desktop environments ### Who Is NOT Affected - Embedded/IoT users (they continue using `linux_arm_host_engine` as before) - x64 Linux users (already have fontconfig) - Mobile platforms (Android, iOS) - Windows/macOS platforms --- ## Related Issues Fixes #139293 #90951 --- ## Checklist - [x] I read the [Contributor Guide](https://github.com/flutter/flutter/blob/master/CONTRIBUTING.md) and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene](https://github.com/flutter/flutter/wiki/Tree-hygiene) wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo). - [x] I signed the CLA. - [x] I listed at least one issue that this PR fixes in the description above. (Fixes #139293) - [x] I updated/added relevant documentation (comments in JSON files). - [x] I added new tests to check the change I am making, or this PR is test-exempt. (CI infrastructure change) - [x] All existing and new tests are passing. (JSON syntax verified) --- ## Core Reviewers @flutter/engine-ci @flutter/linux-desktop --- ## Screenshots ### Before (Without Fontconfig) Chinese text displays as squares in Ubuntu App Center on ARM64: ``` 标题: □□□□□ 描述: □□□□□□□□ ``` ### After (With Fontconfig) Chinese text renders correctly: ``` 标题: 应用中心 描述: 发现和安装应用程序 ``` *(Actual screenshots can be added to the PR after creation)* --- ## Additional Context This issue was discovered while investigating font rendering problems in Ubuntu App Center on ARM64 systems. The fix has been verified to work with Flutter 3.38.1 on Ubuntu 25.10 ARM64. The architecture follows the principle of **separation of concerns**: desktop applications need system font integration via fontconfig, while embedded systems typically use bundled fonts and don't need this dependency. --------- Co-authored-by: John McDole Co-authored-by: Jason Simmons --- engine/src/flutter/.ci.yaml | 13 ++ .../linux_arm_host_desktop_engine.json | 144 ++++++++++++++++++ .../ci/builders/linux_arm_host_engine.json | 86 ----------- 3 files changed, 157 insertions(+), 86 deletions(-) create mode 100644 engine/src/flutter/ci/builders/linux_arm_host_desktop_engine.json diff --git a/engine/src/flutter/.ci.yaml b/engine/src/flutter/.ci.yaml index f33e21a91f065..1d8ba4f220fa0 100644 --- a/engine/src/flutter/.ci.yaml +++ b/engine/src/flutter/.ci.yaml @@ -219,6 +219,19 @@ targets: # at https://github.com/flutter/flutter/issues/152186. cores: "8" + - name: Linux linux_arm_host_desktop_engine + recipe: engine_v2/engine_v2 + timeout: 120 + bringup: true + properties: + add_recipes_cq: "true" + release_build: "true" + config_name: linux_arm_host_desktop_engine + drone_dimensions: + - os=Linux + dimensions: + cores: "8" + - name: Linux linux_host_engine recipe: engine_v2/engine_v2 timeout: 120 diff --git a/engine/src/flutter/ci/builders/linux_arm_host_desktop_engine.json b/engine/src/flutter/ci/builders/linux_arm_host_desktop_engine.json new file mode 100644 index 0000000000000..0bd5dd58eb474 --- /dev/null +++ b/engine/src/flutter/ci/builders/linux_arm_host_desktop_engine.json @@ -0,0 +1,144 @@ +{ + "_comment": [ + "The builds defined in this file should not contain tests, ", + "and the file should not contain builds that are essentially tests. ", + "The only builds in this file should be the builds necessary to produce ", + "release artifacts. ", + "Tests to run on linux hosts should go in one of the other linux_ build ", + "definition files." + ], + "luci_flags": { + "upload_content_hash": true + }, + "builds": [ + { + "archives": [ + { + "name": "ci/linux_debug_arm64_desktop", + "base_path": "out/ci/linux_debug_arm64_desktop/zip_archives/", + "type": "gcs", + "include_paths": [ + "out/ci/linux_debug_arm64_desktop/zip_archives/linux-arm64-debug/linux-arm64-flutter-gtk.zip" + ], + "realm": "production" + } + ], + "drone_dimensions": [ + "device_type=none", + "os=Linux" + ], + "gclient_variables": { + "download_android_deps": false, + "download_jdk": false, + "use_rbe": true + }, + "gn": [ + "--runtime-mode", + "debug", + "--enable-fontconfig", + "--prebuilt-dart-sdk", + "--no-lto", + "--rbe", + "--no-goma", + "--target-dir", + "ci/linux_debug_arm64_desktop", + "--target-os=linux", + "--linux-cpu=arm64" + ], + "name": "ci/linux_debug_arm64_desktop", + "description": "Produces debug mode artifacts to target arm64 Linux from a Linux host with fontconfig enabled.", + "ninja": { + "config": "ci/linux_debug_arm64_desktop", + "targets": [ + "flutter/shell/platform/linux:flutter_gtk" + ] + } + }, + { + "archives": [ + { + "name": "ci/linux_profile_arm64_desktop", + "base_path": "out/ci/linux_profile_arm64_desktop/zip_archives/", + "type": "gcs", + "include_paths": [ + "out/ci/linux_profile_arm64_desktop/zip_archives/linux-arm64-profile/linux-arm64-flutter-gtk.zip" + ], + "realm": "production" + } + ], + "drone_dimensions": [ + "device_type=none", + "os=Linux" + ], + "gclient_variables": { + "download_android_deps": false, + "download_jdk": false, + "use_rbe": true + }, + "gn": [ + "--runtime-mode", + "profile", + "--enable-fontconfig", + "--prebuilt-dart-sdk", + "--no-lto", + "--rbe", + "--no-goma", + "--target-dir", + "ci/linux_profile_arm64_desktop", + "--target-os=linux", + "--linux-cpu=arm64" + ], + "name": "ci/linux_profile_arm64_desktop", + "description": "Produces profile mode artifacts to target arm64 Linux from a Linux host with fontconfig enabled.", + "ninja": { + "config": "ci/linux_profile_arm64_desktop", + "targets": [ + "flutter/shell/platform/linux:flutter_gtk" + ] + } + }, + { + "archives": [ + { + "name": "ci/linux_release_arm64_desktop", + "base_path": "out/ci/linux_release_arm64_desktop/zip_archives/", + "type": "gcs", + "include_paths": [ + "out/ci/linux_release_arm64_desktop/zip_archives/linux-arm64-release/linux-arm64-flutter-gtk.zip" + ], + "realm": "production" + } + ], + "drone_dimensions": [ + "device_type=none", + "os=Linux" + ], + "gclient_variables": { + "download_android_deps": false, + "download_jdk": false, + "use_rbe": true + }, + "gn": [ + "--runtime-mode", + "release", + "--enable-fontconfig", + "--prebuilt-dart-sdk", + "--no-lto", + "--rbe", + "--no-goma", + "--target-dir", + "ci/linux_release_arm64_desktop", + "--target-os=linux", + "--linux-cpu=arm64" + ], + "name": "ci/linux_release_arm64_desktop", + "description": "Produces release mode artifacts to target arm64 Linux from a Linux host with fontconfig enabled.", + "ninja": { + "config": "ci/linux_release_arm64_desktop", + "targets": [ + "flutter/shell/platform/linux:flutter_gtk" + ] + } + } + ] +} diff --git a/engine/src/flutter/ci/builders/linux_arm_host_engine.json b/engine/src/flutter/ci/builders/linux_arm_host_engine.json index 7574304bec748..30c551b2858d3 100644 --- a/engine/src/flutter/ci/builders/linux_arm_host_engine.json +++ b/engine/src/flutter/ci/builders/linux_arm_host_engine.json @@ -11,48 +11,6 @@ "upload_content_hash": true }, "builds": [ - { - "archives": [ - { - "name": "ci/linux_profile_arm64", - "type": "gcs", - "base_path": "out/ci/linux_profile_arm64/zip_archives/", - "include_paths": [ - "out/ci/linux_profile_arm64/zip_archives/linux-arm64-profile/linux-arm64-flutter-gtk.zip" - ], - "realm": "production" - } - ], - "drone_dimensions": [ - "device_type=none", - "os=Linux" - ], - "gclient_variables": { - "download_android_deps": false, - "download_jdk": false, - "use_rbe": true - }, - "gn": [ - "--target-dir", - "ci/linux_profile_arm64", - "--runtime-mode", - "profile", - "--target-os=linux", - "--linux-cpu=arm64", - "--prebuilt-dart-sdk", - "--no-lto", - "--rbe", - "--no-goma" - ], - "name": "ci/linux_profile_arm64", - "description": "Produces profile mode artifacts to target arm64 Linux from a Linux host.", - "ninja": { - "config": "ci/linux_profile_arm64", - "targets": [ - "flutter/shell/platform/linux:flutter_gtk" - ] - } - }, { "archives": [ { @@ -64,7 +22,6 @@ "out/ci/linux_debug_arm64/zip_archives/linux-arm64/impeller_sdk.zip", "out/ci/linux_debug_arm64/zip_archives/linux-arm64/linux-arm64-embedder.zip", "out/ci/linux_debug_arm64/zip_archives/linux-arm64/font-subset.zip", - "out/ci/linux_debug_arm64/zip_archives/linux-arm64-debug/linux-arm64-flutter-gtk.zip", "out/ci/linux_debug_arm64/zip_archives/dart-sdk-linux-arm64.zip" ], "realm": "production" @@ -99,53 +56,10 @@ "flutter/build/archives:artifacts", "flutter/build/archives:dart_sdk_archive", "flutter/tools/font_subset", - "flutter/shell/platform/linux:flutter_gtk", "flutter/impeller/toolkit/interop:sdk", "flutter/build/archives:embedder" ] } - }, - { - "archives": [ - { - "name": "ci/linux_release_arm64", - "type": "gcs", - "base_path": "out/ci/linux_release_arm64/zip_archives/", - "include_paths": [ - "out/ci/linux_release_arm64/zip_archives/linux-arm64-release/linux-arm64-flutter-gtk.zip" - ], - "realm": "production" - } - ], - "drone_dimensions": [ - "device_type=none", - "os=Linux" - ], - "gclient_variables": { - "download_android_deps": false, - "download_jdk": false, - "use_rbe": true - }, - "gn": [ - "--target-dir", - "ci/linux_release_arm64", - "--runtime-mode", - "release", - "--target-os=linux", - "--linux-cpu=arm64", - "--prebuilt-dart-sdk", - "--no-lto", - "--rbe", - "--no-goma" - ], - "name": "ci/linux_release_arm64", - "description": "Produces release mode artifacts to target arm64 Linux from a Linux host.", - "ninja": { - "config": "ci/linux_release_arm64", - "targets": [ - "flutter/shell/platform/linux:flutter_gtk" - ] - } } ] } From 359410b60fe459dd29c3246e4de70e395e8506e4 Mon Sep 17 00:00:00 2001 From: LinXunFeng Date: Thu, 6 Aug 2026 01:22:53 +0800 Subject: [PATCH 085/330] Allow valid UTF-8 replacement characters in flutter_tools decoding (#188901) Fixes https://github.com/flutter/flutter/issues/177509 This updates `flutter_tools` UTF-8 error detection so that a valid `U+FFFD` replacement character is not mistaken for malformed UTF-8. Previously, `Utf8Decoder` decoded bytes with `allowMalformed: true` and then treated any decoded string containing `U+FFFD` as evidence that the original bytes were malformed. That incorrectly rejects valid UTF-8 input such as `EF BF BD`, which is the legitimate encoding of `U+FFFD`. This PR changes the decoder to use a strict UTF-8 decoder when `reportErrors` is enabled. If strict decoding succeeds, the decoded string is returned as-is. If strict decoding throws `FormatException`, the input is decoded again with `allowMalformed: true` only to preserve the existing `ToolExit` diagnostic message and source bytes. The permissive `reportErrors: false` path is unchanged and continues to decode malformed bytes with replacement characters. - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Ben Konyi --- packages/flutter_tools/lib/src/convert.dart | 22 +++++++++++-------- .../test/general.shard/convert_test.dart | 11 ++++++++++ .../test/general.shard/vmservice_test.dart | 10 +++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/flutter_tools/lib/src/convert.dart b/packages/flutter_tools/lib/src/convert.dart index 5e8f24a6c5b78..6383d02d37ee1 100644 --- a/packages/flutter_tools/lib/src/convert.dart +++ b/packages/flutter_tools/lib/src/convert.dart @@ -67,16 +67,20 @@ const Encoding utf8 = Utf8Codec(); class Utf8Decoder extends Converter, String> { const Utf8Decoder({this.reportErrors = true}); - static const _systemDecoder = cnv.Utf8Decoder(allowMalformed: true); + static const _allowMalformedDecoder = cnv.Utf8Decoder(allowMalformed: true); + static const _strictDecoder = cnv.Utf8Decoder(); final bool reportErrors; @override String convert(List input, [int start = 0, int? end]) { - final String result = _systemDecoder.convert(input, start, end); - // Finding a Unicode replacement character indicates that the input - // was malformed. - if (reportErrors && result.contains('\u{FFFD}')) { + if (!reportErrors) { + return _allowMalformedDecoder.convert(input, start, end); + } + try { + return _strictDecoder.convert(input, start, end); + } on FormatException { + final String result = _allowMalformedDecoder.convert(input, start, end); throwToolExit( 'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found while decoding string: $result. ' 'The Flutter team would greatly appreciate if you could file a bug explaining ' @@ -85,16 +89,16 @@ class Utf8Decoder extends Converter, String> { 'The source bytes were:\n$input\n\n', ); } - return result; } @override ByteConversionSink startChunkedConversion(Sink sink) => - _systemDecoder.startChunkedConversion(sink); + _allowMalformedDecoder.startChunkedConversion(sink); @override - Stream bind(Stream> stream) => _systemDecoder.bind(stream); + Stream bind(Stream> stream) => _allowMalformedDecoder.bind(stream); @override - Converter, T> fuse(Converter other) => _systemDecoder.fuse(other); + Converter, T> fuse(Converter other) => + _allowMalformedDecoder.fuse(other); } diff --git a/packages/flutter_tools/test/general.shard/convert_test.dart b/packages/flutter_tools/test/general.shard/convert_test.dart index b014b7718e133..815867540e357 100644 --- a/packages/flutter_tools/test/general.shard/convert_test.dart +++ b/packages/flutter_tools/test/general.shard/convert_test.dart @@ -23,6 +23,17 @@ void main() { expect(decoder.convert(passedString.codeUnits), passedString); }); + testWithoutContext('Decode a string containing a valid replacement character', () async { + expect( + decoder.convert(utf8ForTesting.encode('normal string => \u{FFFD}')), + 'normal string => \u{FFFD}', + ); + }); + + testWithoutContext('Throw on malformed UTF-8 bytes', () async { + expect(() => decoder.convert([0xc3, 0x28]), throwsToolExit()); + }); + testWithoutContext('Decode a malformed string without throwing', () async { expect(utf8AllowMalformed.decode(nonpassString.codeUnits), nonpassString); }); diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart index 53a7e0228394d..c56742ea5114e 100644 --- a/packages/flutter_tools/test/general.shard/vmservice_test.dart +++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart @@ -638,6 +638,16 @@ void main() { expect(processVmServiceMessage(event), 'Hello There'); }); + testWithoutContext('Can process log events containing a valid replacement character', () { + final event = vm_service.Event( + bytes: base64.encode(utf8ForTesting.encode('flutter: \u{FFFD}\n')), + timestamp: 0, + kind: vm_service.EventKind.kLogging, + ); + + expect(processVmServiceMessage(event), 'flutter: \u{FFFD}'); + }); + testUsingContext('WebSocket URL construction uses correct URI join primitives', () async { final completer = Completer(); openChannelForTesting = From 93866488913ea389400f04545f1ad7a7f1dd3440 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 5 Aug 2026 17:23:45 +0000 Subject: [PATCH 086/330] Revert "Reduce the number of gtest-parallel workers when running Impeller tests on Mac Minis used by CI (#189813)" (#190560) This reverts commit 285bf671f395d4e4a3e1592e50826cd30fafdd3e. This was an experiment that tried to reduce the frequency of flakes when running Impeller OpenGLESSDF rendering tests. It is obsolete now that these tests were disabled by https://github.com/flutter/flutter/pull/190469 --- engine/src/flutter/testing/run_tests.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/engine/src/flutter/testing/run_tests.py b/engine/src/flutter/testing/run_tests.py index df46af9334199..3b5c4b0ad6c39 100755 --- a/engine/src/flutter/testing/run_tests.py +++ b/engine/src/flutter/testing/run_tests.py @@ -164,12 +164,6 @@ def is_aarm64() -> bool: return aarm64 -def mac_hardware_model() -> str: - assert is_mac() - output = subprocess.check_output(['sysctl', '-n', 'hw.model']) - return output.decode('utf-8').strip() - - def is_linux() -> bool: return sys_platform.startswith('linux') @@ -574,13 +568,7 @@ def make_test( ) extra_env = metal_validation_env() extra_env.update(vulkan_validation_env(build_dir)) - if mac_hardware_model() == 'Macmini9,1': - # For the Mac Minis used on CI, limit the number of Impeller test cases run in parallel - # in order to reduce the risk of resource exhaustion errors. - workers_flag = ['--workers=%d' % (os.cpu_count() - 2)] - else: - workers_flag = [] - mac_impeller_unittests_flags = repeat_flags + workers_flag + [ + mac_impeller_unittests_flags = repeat_flags + [ '--gtest_filter=-*OpenGLES:*OpenGLESSDF', # These are covered in the golden tests. '--', '--enable_vulkan_validation', From c96497535920d94ee5fd9c8df366c442eca28f88 Mon Sep 17 00:00:00 2001 From: Matt Boetger Date: Wed, 5 Aug 2026 10:37:03 -0700 Subject: [PATCH 087/330] Enable Gradle cache for a single test target (#190474) Enables Gradle CI Cache for single test target. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. --- .ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci.yaml b/.ci.yaml index 2a7328234f192..3271859e84468 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -7147,7 +7147,8 @@ targets: [ {"dependency": "android_sdk", "version": "version:36v4"}, {"dependency": "open_jdk", "version": "version:21"}, - {"dependency": "vs_build", "version": "version:vs2019"} + {"dependency": "vs_build", "version": "version:vs2019"}, + {"dependency": "gradle_dists", "version": "8.4-bin, 8.13-rc-1-bin, 8.14-bin, 9.3.1-bin, 9.3.1-all"} ] shard: tool_tests_commands subshard: 2_2 From 1470e0a0c733f586f04de517d0a71f325eea0e68 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 5 Aug 2026 13:46:53 -0400 Subject: [PATCH 088/330] Roll Packages from 3498b9d7b671 to b1424e248121 (5 revisions) (#190622) https://github.com/flutter/packages/compare/3498b9d7b671...b1424e248121 2026-08-05 saurabhmirajkar000@gmail.com [go_router] Document regex constraints for path parameters (flutter/packages#12281) 2026-08-04 fluttergithubbot@gmail.com Sync release-cupertino_ui-0.0.3 to main (flutter/packages#12367) 2026-08-04 43054281+camsim99@users.noreply.github.com [camera_android_camerax] Fix video recording after backgrounding app (flutter/packages#12145) 2026-08-04 jmccandless@google.com [material_ui and cupertino_ui] Remove workspaces and fix CI (flutter/packages#12351) 2026-08-04 fluttergithubbot@gmail.com Sync release-material_ui-0.0.3 to main (flutter/packages#12352) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- bin/internal/flutter_packages.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 9404108fe8e31..33aba2ab4a198 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -3498b9d7b67143a68dc43b90951acb577e92f64e +b1424e248121f15bb8f78a8fa6155e71f12bca79 From e930aa72476cfd96a46e8eb95a822d1c40267f06 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 5 Aug 2026 13:48:21 -0400 Subject: [PATCH 089/330] [flutter_tools] Support boolean selectors and multiple tag options in test command (#189193) (#190541) Fixes https://github.com/flutter/flutter/issues/189193 In `TestCommand`, `--tags` (`-t`) and `--exclude-tags` (`-x`) were registered using `addOption(...)` instead of `addMultiOption(...)`. This scalar option definition retained only the last CLI occurrence when passed multiple times (`-t a -t b` became `'b'`). Additionally, default comma splitting in `package:args` mangled composite boolean selector expressions (`||`, `&&`, `!`) before forwarding arguments to `package:test`. This PR migrates `--tags` and `--exclude-tags` to `addMultiOption(..., splitCommas: false)` in `TestCommand` and updates `FlutterTestRunner` to forward all `--tags` and `--exclude-tags` values sequentially to `dart test`. ## Testing - Added hermetic regression test `passes boolean selectors and multiple tags/exclude-tags through to package:test` in `packages/flutter_tools/test/commands.shard/hermetic/test_test.dart`. --- .../flutter_tools/lib/src/commands/test.dart | 10 +++-- .../flutter_tools/lib/src/test/runner.dart | 16 +++---- .../commands.shard/hermetic/test_test.dart | 44 +++++++++++++++++-- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 78f4cd5a4c3c2..1523a4d85ede1 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -101,17 +101,19 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { valueHelp: 'substring', splitCommas: false, ) - ..addOption( + ..addMultiOption( 'tags', abbr: 't', help: 'Run only tests associated with the specified tags. See: https://pub.dev/packages/test#tagging-tests', + splitCommas: false, ) - ..addOption( + ..addMultiOption( 'exclude-tags', abbr: 'x', help: 'Run only tests that do not have the specified tags. See: https://pub.dev/packages/test#tagging-tests', + splitCommas: false, ) ..addFlag( 'start-paused', @@ -428,8 +430,8 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { final bool buildTestAssets = boolArg('test-assets'); final List names = stringsArg('name'); final List plainNames = stringsArg('plain-name'); - final String? tags = stringArg('tags'); - final String? excludeTags = stringArg('exclude-tags'); + final List tags = stringsArg('tags'); + final List excludeTags = stringsArg('exclude-tags'); final BuildInfo buildInfo = await getBuildInfo( forcedBuildMode: BuildMode.debug, forcedUseLocalCanvasKit: true, diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart index af01c969b26f5..7578dcb277bca 100644 --- a/packages/flutter_tools/lib/src/test/runner.dart +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -38,8 +38,8 @@ interface class FlutterTestRunner { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], - String? tags, - String? excludeTags, + List tags = const [], + List excludeTags = const [], bool enableVmService = false, bool machine = false, String? precompiledDillPath, @@ -82,8 +82,8 @@ interface class FlutterTestRunner { for (final String name in names) ...['--name', name], for (final String plainName in plainNames) ...['--plain-name', plainName], if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed', - if (tags != null) ...['--tags', tags], - if (excludeTags != null) ...['--exclude-tags', excludeTags], + for (final String tag in tags) ...['--tags', tag], + for (final String excludeTag in excludeTags) ...['--exclude-tags', excludeTag], if (failFast) '--fail-fast', if (runSkipped) '--run-skipped', if (totalShards != null) '--total-shards=$totalShards', @@ -589,8 +589,8 @@ class SpawnPlugin extends PlatformPlugin { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], - String? tags, - String? excludeTags, + List tags = const [], + List excludeTags = const [], bool machine = false, bool updateGoldens = false, required int? concurrency, @@ -656,8 +656,8 @@ class SpawnPlugin extends PlatformPlugin { for (final String name in names) ...['--name', name], for (final String plainName in plainNames) ...['--plain-name', plainName], if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed', - if (tags != null) ...['--tags', tags], - if (excludeTags != null) ...['--exclude-tags', excludeTags], + for (final String tag in tags) ...['--tags', tag], + for (final String excludeTag in excludeTags) ...['--exclude-tags', excludeTag], if (failFast) '--fail-fast', if (runSkipped) '--run-skipped', if (totalShards != null) '--total-shards=$totalShards', diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index 6145d100308b7..fc904a53419c4 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -216,6 +216,42 @@ dev_dependencies: Cache: () => Cache.test(processManager: FakeProcessManager.any()), }, ); + + testUsingContext( + 'passes boolean selectors and multiple tags/exclude-tags through to package:test', + () async { + final fakePackageTest = FakePackageTest(); + final testCommand = TestCommand(testWrapper: fakePackageTest); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + await commandRunner.run([ + 'test', + '--no-pub', + '--tags=a || b', + '--tags=c', + '--exclude-tags=d && e', + '--exclude-tags=f', + ]); + expect( + fakePackageTest.lastArgs, + containsAllInOrder([ + '--tags', + 'a || b', + '--tags', + 'c', + '--exclude-tags', + 'd && e', + '--exclude-tags', + 'f', + ]), + ); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + }, + ); }); group('--reporter/-r', () { @@ -1692,8 +1728,8 @@ class FakeFlutterTestRunner implements FlutterTestRunner { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], - String? tags, - String? excludeTags, + List tags = const [], + List excludeTags = const [], bool enableVmService = false, bool ipv6 = false, bool machine = false, @@ -1750,8 +1786,8 @@ class FakeFlutterTestRunner implements FlutterTestRunner { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], - String? tags, - String? excludeTags, + List tags = const [], + List excludeTags = const [], bool machine = false, bool updateGoldens = false, required int? concurrency, From be3dae817f0139300cfd8610d1e4a6b66eeef5aa Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 5 Aug 2026 10:58:02 -0700 Subject: [PATCH 090/330] Fix `snippets` test and add it to CI (#190327) The `snippets` test fails with one error on HEAD. Apparently it's not run on CI. This PR fixes the failure by removing the test case that verifies `--no-format-output`. The CLI option was removed in https://github.com/flutter/flutter/pull/161347/changes . A deadcode parameter is also removed. This PR also adds this test to CI. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .ci.yaml | 15 +++++++++++++++ TESTOWNERS | 3 +++ dev/snippets/test/snippets_test.dart | 18 +----------------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 3271859e84468..31e8c2da76eeb 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -680,6 +680,21 @@ targets: - DEPS - dartdoc_options.yaml + - name: Linux snippets + recipe: flutter/flutter_drone + bringup: true + timeout: 30 + properties: + tags: > + ["framework", "hostonly", "shard", "linux"] + shard: snippets + runIf: + - dev/snippets/** + - dev/bots/** + - .ci.yaml + - engine/** + - DEPS + - name: Linux engine_dependency_proxy_test recipe: devicelab/devicelab_drone timeout: 60 diff --git a/TESTOWNERS b/TESTOWNERS index 339d363e24288..5af7f17425d06 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -340,6 +340,8 @@ # Linux docs_generate_release # Linux docs_publish /dev/bots/docs.sh @Piinks @flutter/framework +# Linux snippets +/dev/snippets @dkwingsmt @flutter/framework # Linux packages_autoroller /dev/conductor/core @christopherfujino @flutter/tool # Linux web_e2e_test @@ -374,6 +376,7 @@ # coverage @christopherfujino @flutter/infra # customer_testing @Piinks @flutter/framework # docs @Piinks @flutter/framework +# snippets @Piinks @flutter/framework # flutter_packaging @christopherfujino @flutter/infra # flutter_plugins @stuartmorgan-g @flutter/plugin # framework_tests @Piinks @flutter/framework diff --git a/dev/snippets/test/snippets_test.dart b/dev/snippets/test/snippets_test.dart index bdfb4f92a7f34..4d0a214e72b88 100644 --- a/dev/snippets/test/snippets_test.dart +++ b/dev/snippets/test/snippets_test.dart @@ -325,7 +325,7 @@ void main() { platform: platform, ); FlutterInformation.instance = flutterInformation; - var mockSnippetGenerator = MockSnippetGenerator(); + final mockSnippetGenerator = MockSnippetGenerator(); snippets_main.snippetGenerator = mockSnippetGenerator; var errorMessage = ''; errorExit = (String message) { @@ -369,16 +369,6 @@ void main() { expect(errorMessage, equals('')); errorMessage = ''; - mockSnippetGenerator = MockSnippetGenerator(); - snippets_main.snippetGenerator = mockSnippetGenerator; - snippets_main.main([ - '--input=${input.absolute.path}', - '--type=snippet', - '--no-format-output', - ]); - expect(mockSnippetGenerator.formatOutput, equals(false)); - errorMessage = ''; - input.deleteSync(); snippets_main.main(['--input=${input.absolute.path}']); expect(errorMessage, equals('The input file ${input.absolute.path} does not exist.')); @@ -392,8 +382,6 @@ class MockSnippetGenerator extends SnippetGenerator { File? output; String? copyright; String? description; - late bool formatOutput; - late bool addSectionMarkers; late bool includeAssumptions; @override @@ -402,16 +390,12 @@ class MockSnippetGenerator extends SnippetGenerator { File? output, String? copyright, String? description, - bool formatOutput = true, - bool addSectionMarkers = false, bool includeAssumptions = false, }) { this.sample = sample; this.output = output; this.copyright = copyright; this.description = description; - this.formatOutput = formatOutput; - this.addSectionMarkers = addSectionMarkers; this.includeAssumptions = includeAssumptions; return ''; From dad484bc1e2245b0215624d72a2e552a239a5e85 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 5 Aug 2026 18:13:15 +0000 Subject: [PATCH 091/330] Migrate the embedder tests from the legacy Dart native function format to FFI (#190615) See https://github.com/flutter/flutter/issues/190154 --- .../shell/common/animator_unittests.cc | 2 +- .../shell/common/dart_native_benchmarks.cc | 2 +- .../shell/common/engine_animator_unittests.cc | 2 +- .../shell/common/input_events_unittests.cc | 2 +- .../flutter/shell/common/shell_unittests.cc | 2 +- .../framework/Source/FlutterEngineTest.mm | 75 ++- .../framework/Source/FlutterEngineTestUtils.h | 1 + .../Source/FlutterEngineTestUtils.mm | 4 + .../Source/FlutterWindowControllerTest.mm | 8 +- .../Source/fixtures/flutter_desktop_test.dart | 7 +- .../platform/embedder/fixtures/main.dart | 44 +- .../embedder/tests/embedder_a11y_unittests.cc | 377 ++++++------- .../embedder/tests/embedder_gl_unittests.cc | 257 ++++----- .../tests/embedder_metal_unittests.mm | 22 +- .../embedder/tests/embedder_test_context.cc | 5 + .../embedder/tests/embedder_test_context.h | 2 + .../embedder/tests/embedder_unittests.cc | 498 ++++++++---------- .../embedder/tests/embedder_vk_unittests.cc | 2 +- .../shell/platform/windows/fixtures/main.dart | 13 +- .../flutter_windows_engine_unittests.cc | 27 +- .../windows/flutter_windows_unittests.cc | 72 ++- .../windows/testing/windows_test_context.cc | 5 + .../windows/testing/windows_test_context.h | 9 + .../windows/window_manager_unittests.cc | 9 +- 24 files changed, 630 insertions(+), 817 deletions(-) diff --git a/engine/src/flutter/shell/common/animator_unittests.cc b/engine/src/flutter/shell/common/animator_unittests.cc index dc8e202bafeb7..3ae6fa6117445 100644 --- a/engine/src/flutter/shell/common/animator_unittests.cc +++ b/engine/src/flutter/shell/common/animator_unittests.cc @@ -17,7 +17,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter { diff --git a/engine/src/flutter/shell/common/dart_native_benchmarks.cc b/engine/src/flutter/shell/common/dart_native_benchmarks.cc index 65e6922595c40..15d614a8d1b96 100644 --- a/engine/src/flutter/shell/common/dart_native_benchmarks.cc +++ b/engine/src/flutter/shell/common/dart_native_benchmarks.cc @@ -12,7 +12,7 @@ #include "fml/synchronization/count_down_latch.h" #include "runtime/dart_vm_lifecycle.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter::testing { diff --git a/engine/src/flutter/shell/common/engine_animator_unittests.cc b/engine/src/flutter/shell/common/engine_animator_unittests.cc index a9d041670b6ac..d9010dc27869c 100644 --- a/engine/src/flutter/shell/common/engine_animator_unittests.cc +++ b/engine/src/flutter/shell/common/engine_animator_unittests.cc @@ -13,7 +13,7 @@ #include "gmock/gmock.h" #include "impeller/core/runtime_types.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter { diff --git a/engine/src/flutter/shell/common/input_events_unittests.cc b/engine/src/flutter/shell/common/input_events_unittests.cc index 54fdbb25bd89d..fd7823df51d64 100644 --- a/engine/src/flutter/shell/common/input_events_unittests.cc +++ b/engine/src/flutter/shell/common/input_events_unittests.cc @@ -5,7 +5,7 @@ #include "flutter/shell/common/shell_test.h" #include "flutter/testing/testing.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter { diff --git a/engine/src/flutter/shell/common/shell_unittests.cc b/engine/src/flutter/shell/common/shell_unittests.cc index 95d38893c15a4..371e824d7b04b 100644 --- a/engine/src/flutter/shell/common/shell_unittests.cc +++ b/engine/src/flutter/shell/common/shell_unittests.cc @@ -57,7 +57,7 @@ #include "flutter/vulkan/vulkan_application.h" // nogncheck #endif -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter { diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index 6d49a588958f4..fd17dd493a96c 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -33,7 +33,7 @@ #include "flutter/testing/test_dart_native_resolver.h" #include "gtest/gtest.h" -// CREATE_NATIVE_ENTRY and MOCK_ENGINE_PROC are leaky by design +// CREATE_FFI_LAMBDA and MOCK_ENGINE_PROC are leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) @interface FlutterEngine (Test) @@ -153,12 +153,12 @@ @implementation MockableFlutterEngine // Block until notified by the Dart test of the value of Platform.executable. BOOL signaled = NO; - AddNativeCallback("NotifyStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - const auto dart_string = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - EXPECT_EQ(executable_name, dart_string); - signaled = YES; - })); + AddFfiNativeCallback("NotifyStringValue", CREATE_FFI_LAMBDA([&](Dart_Handle value) { + const auto dart_string = + tonic::DartConverter::FromDart(value); + EXPECT_EQ(executable_name, dart_string); + signaled = YES; + })); // Launch the test entrypoint. EXPECT_TRUE([engine runWithEntrypoint:@"executableNameNotNull"]); @@ -216,8 +216,7 @@ @implementation MockableFlutterEngine TEST_F(FlutterEngineTest, CanLogToStdout) { // Block until completion of print statement. BOOL signaled = NO; - AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { signaled = YES; })); + AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { signaled = YES; })); // Replace stdout stream buffer with our own. FlutterStringOutputWriter* writer = [[FlutterStringOutputWriter alloc] init]; @@ -242,16 +241,16 @@ @implementation MockableFlutterEngine // Latch to ensure the entire layer tree has been generated and presented. BOOL signaled = NO; - AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - CALayer* rootLayer = engine.viewController.flutterView.layer; - EXPECT_TRUE(rootLayer.backgroundColor != nil); - if (rootLayer.backgroundColor != nil) { - NSColor* actualBackgroundColor = - [NSColor colorWithCGColor:rootLayer.backgroundColor]; - EXPECT_EQ(actualBackgroundColor, [NSColor blackColor]); - } - signaled = YES; - })); + AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + CALayer* rootLayer = engine.viewController.flutterView.layer; + EXPECT_TRUE(rootLayer.backgroundColor != nil); + if (rootLayer.backgroundColor != nil) { + NSColor* actualBackgroundColor = + [NSColor colorWithCGColor:rootLayer.backgroundColor]; + EXPECT_EQ(actualBackgroundColor, [NSColor blackColor]); + } + signaled = YES; + })); // Launch the test entrypoint. EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]); @@ -273,16 +272,16 @@ @implementation MockableFlutterEngine // Latch to ensure the entire layer tree has been generated and presented. BOOL signaled = NO; - AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - CALayer* rootLayer = engine.viewController.flutterView.layer; - EXPECT_TRUE(rootLayer.backgroundColor != nil); - if (rootLayer.backgroundColor != nil) { - NSColor* actualBackgroundColor = - [NSColor colorWithCGColor:rootLayer.backgroundColor]; - EXPECT_EQ(actualBackgroundColor, [NSColor whiteColor]); - } - signaled = YES; - })); + AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + CALayer* rootLayer = engine.viewController.flutterView.layer; + EXPECT_TRUE(rootLayer.backgroundColor != nil); + if (rootLayer.backgroundColor != nil) { + NSColor* actualBackgroundColor = + [NSColor colorWithCGColor:rootLayer.backgroundColor]; + EXPECT_EQ(actualBackgroundColor, [NSColor whiteColor]); + } + signaled = YES; + })); // Launch the test entrypoint. EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]); @@ -512,8 +511,7 @@ @implementation MockableFlutterEngine TEST_F(FlutterEngineTest, NativeCallbacks) { BOOL latch_called = NO; - AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch_called = YES; })); + AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { latch_called = YES; })); FlutterEngine* engine = GetFlutterEngine(); EXPECT_TRUE([engine runWithEntrypoint:@"nativeCallback"]); @@ -937,14 +935,13 @@ @implementation MockableFlutterEngine BOOL signaled = NO; std::optional engineId; - AddNativeCallback("NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - const auto argument = Dart_GetNativeArgument(args, 0); - if (!Dart_IsNull(argument)) { - const auto id = tonic::DartConverter::FromDart(argument); - engineId = id; - } - signaled = YES; - })); + AddFfiNativeCallback("NotifyEngineId", CREATE_FFI_LAMBDA([&](Dart_Handle argument) { + if (!Dart_IsNull(argument)) { + const auto id = tonic::DartConverter::FromDart(argument); + engineId = id; + } + signaled = YES; + })); EXPECT_TRUE([engine runWithEntrypoint:@"testEngineId"]); while (!signaled) { diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h index d9d78f88d1072..475d1121b1715 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h @@ -25,6 +25,7 @@ class FlutterEngineTest : public AutoreleasePoolTest { void TearDown() override; void AddNativeCallback(const char* name, Dart_NativeFunction function); + void AddFfiNativeCallback(const char* name, void* function); static void IsolateCreateCallback(void* user_data); diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.mm index b2ff26cab0d60..a2bf5cebcb4c4 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.mm @@ -73,6 +73,10 @@ - (BOOL)setString:(NSString*)string forType:(NSPasteboardType)dataType { native_resolver_->AddNativeCallback({name}, function); } +void FlutterEngineTest::AddFfiNativeCallback(const char* name, void* function) { + native_resolver_->AddFfiNativeCallback({name}, function); +} + id CreateMockFlutterEngine(NSString* pasteboardString) { NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm index 078192149b45c..0d49ae4051815 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm @@ -26,10 +26,10 @@ void SetUp() { signalled_ = false; - AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - isolate_ = Isolate::Current(); - signalled_ = true; - })); + AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + isolate_ = Isolate::Current(); + signalled_ = true; + })); while (!signalled_) { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false); diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart index a58f2eecb94e6..587e3726029e8 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart @@ -4,11 +4,12 @@ // ignore_for_file: avoid_print +import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; -@pragma('vm:external-name', 'SignalNativeTest') +@Native(symbol: 'SignalNativeTest') external void signalNativeTest(); void main() {} @@ -20,7 +21,7 @@ void empty() {} /// /// This is used to notify the native side of the test of a string value from /// the Dart fixture under test. -@pragma('vm:external-name', 'NotifyStringValue') +@Native(symbol: 'NotifyStringValue') external void notifyStringValue(String s); @pragma('vm:entry-point') @@ -86,7 +87,7 @@ void sendFooMessage() { PlatformDispatcher.instance.sendPlatformMessage('foo', null, (ByteData? result) {}); } -@pragma('vm:external-name', 'NotifyEngineId') +@Native(symbol: 'NotifyEngineId') external void notifyEngineId(int? engineId); @pragma('vm:entry-point') diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart index 2bafc60c41f64..ef903c5c4fcb9 100644 --- a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart @@ -20,7 +20,7 @@ void customEntrypoint() { sayHiFromCustomEntrypoint(); } -@pragma('vm:external-name', 'SayHiFromCustomEntrypoint') +@ffi.Native(symbol: 'SayHiFromCustomEntrypoint') external void sayHiFromCustomEntrypoint(); @pragma('vm:entry-point') @@ -30,11 +30,11 @@ void customEntrypoint1() { sayHiFromCustomEntrypoint3(); } -@pragma('vm:external-name', 'SayHiFromCustomEntrypoint1') +@ffi.Native(symbol: 'SayHiFromCustomEntrypoint1') external void sayHiFromCustomEntrypoint1(); -@pragma('vm:external-name', 'SayHiFromCustomEntrypoint2') +@ffi.Native(symbol: 'SayHiFromCustomEntrypoint2') external void sayHiFromCustomEntrypoint2(); -@pragma('vm:external-name', 'SayHiFromCustomEntrypoint3') +@ffi.Native(symbol: 'SayHiFromCustomEntrypoint3') external void sayHiFromCustomEntrypoint3(); @pragma('vm:entry-point') @@ -52,9 +52,9 @@ void implicitViewNotNull() { notifyBoolValue(PlatformDispatcher.instance.implicitView != null); } -@pragma('vm:external-name', 'NotifyStringValue') +@ffi.Native(symbol: 'NotifyStringValue') external void notifyStringValue(String value); -@pragma('vm:external-name', 'NotifyBoolValue') +@ffi.Native(symbol: 'NotifyBoolValue') external void notifyBoolValue(bool value); @pragma('vm:entry-point') @@ -104,17 +104,17 @@ Float64List kTestTransform = () { return values; }(); -@pragma('vm:external-name', 'SignalNativeTest') +@ffi.Native(symbol: 'SignalNativeTest') external void signalNativeTest(); -@pragma('vm:external-name', 'SignalNativeCount') +@ffi.Native(symbol: 'SignalNativeCount') external void signalNativeCount(int count); -@pragma('vm:external-name', 'SignalNativeMessage') +@ffi.Native(symbol: 'SignalNativeMessage') external void signalNativeMessage(String message); -@pragma('vm:external-name', 'NotifySemanticsEnabled') +@ffi.Native(symbol: 'NotifySemanticsEnabled') external void notifySemanticsEnabled(bool enabled); -@pragma('vm:external-name', 'NotifyAccessibilityFeatures') +@ffi.Native(symbol: 'NotifyAccessibilityFeatures') external void notifyAccessibilityFeatures(bool reduceMotion); -@pragma('vm:external-name', 'NotifySemanticsAction') +@ffi.Native(symbol: 'NotifySemanticsAction') external void notifySemanticsAction(int nodeId, int action, List data); @ffi.Native(symbol: 'FFISignalNativeTest') @@ -689,7 +689,7 @@ void can_composite_platform_views_with_platform_layer_on_bottom() { PlatformDispatcher.instance.scheduleFrame(); } -@pragma('vm:external-name', 'SignalBeginFrame') +@ffi.Native(symbol: 'SignalBeginFrame') // ignore: unreachable_from_main external void signalBeginFrame(); @@ -772,7 +772,17 @@ Picture createGradientBox(Size size) { return baseRecorder.endRecording(); } -@pragma('vm:external-name', 'EchoKeyEvent') +@ffi.Native< + ffi.Void Function( + ffi.Uint64, + ffi.Uint64, + ffi.Uint64, + ffi.Uint64, + ffi.Uint64, + ffi.Bool, + ffi.Uint64, + ) +>(symbol: 'EchoKeyEvent') external void _echoKeyEvent( int change, int timestamp, @@ -1234,7 +1244,7 @@ void scene_builder_with_complex_clips() { PlatformDispatcher.instance.scheduleFrame(); } -@pragma('vm:external-name', 'SendObjectToNativeCode') +@ffi.Native(symbol: 'SendObjectToNativeCode') external void sendObjectToNativeCode(dynamic object); @pragma('vm:entry-point') @@ -1312,7 +1322,7 @@ void render_targets_are_in_stable_order() { PlatformDispatcher.instance.scheduleFrame(); } -@pragma('vm:external-name', 'NativeArgumentsCallback') +@ffi.Native(symbol: 'NativeArgumentsCallback') external void nativeArgumentsCallback(List args); @pragma('vm:entry-point') @@ -1327,7 +1337,7 @@ void dart_entrypoint_args(List args) { nativeArgumentsCallback(args); } -@pragma('vm:external-name', 'SnapshotsCallback') +@ffi.Native(symbol: 'SnapshotsCallback') external void snapshotsCallback(Image bigImage, Image smallImage); @pragma('vm:entry-point') diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_a11y_unittests.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_a11y_unittests.cc index 743db21a8205a..1e022c5e28bab 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_a11y_unittests.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_a11y_unittests.cc @@ -20,7 +20,7 @@ #include "gmock/gmock.h" // For EXPECT_THAT and matchers #include "gtest/gtest.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter { @@ -109,39 +109,41 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV3Callbacks) { // Called by the Dart text fixture on the UI thread to signal that the C++ // unittest should resume. - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY(([&signal_native_latch](Dart_NativeArguments) { - signal_native_latch.Signal(); - }))); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA(([&signal_native_latch]() { + signal_native_latch.Signal(); + }))); // Called by test fixture on UI thread to pass data back to this test. - NativeEntry notify_semantics_enabled_callback; - context.AddNativeCallback( + std::function notify_semantics_enabled_callback; + context.AddFfiNativeCallback( "NotifySemanticsEnabled", - CREATE_NATIVE_ENTRY( - ([¬ify_semantics_enabled_callback](Dart_NativeArguments args) { - ASSERT_NE(notify_semantics_enabled_callback, nullptr); - notify_semantics_enabled_callback(args); - }))); + CREATE_FFI_LAMBDA(([¬ify_semantics_enabled_callback](bool enabled) { + ASSERT_NE(notify_semantics_enabled_callback, nullptr); + notify_semantics_enabled_callback(enabled); + }))); - NativeEntry notify_accessibility_features_callback; - context.AddNativeCallback( + std::function notify_accessibility_features_callback; + context.AddFfiNativeCallback( "NotifyAccessibilityFeatures", - CREATE_NATIVE_ENTRY(( - [¬ify_accessibility_features_callback](Dart_NativeArguments args) { + CREATE_FFI_LAMBDA( + ([¬ify_accessibility_features_callback](bool reduce_motion) { ASSERT_NE(notify_accessibility_features_callback, nullptr); - notify_accessibility_features_callback(args); + notify_accessibility_features_callback(reduce_motion); }))); - NativeEntry notify_semantics_action_callback; - context.AddNativeCallback( + std::function)> + notify_semantics_action_callback; + context.AddFfiNativeCallback( "NotifySemanticsAction", - CREATE_NATIVE_ENTRY( - ([¬ify_semantics_action_callback](Dart_NativeArguments args) { - ASSERT_NE(notify_semantics_action_callback, nullptr); - notify_semantics_action_callback(args); - }))); + CREATE_FFI_LAMBDA(([¬ify_semantics_action_callback]( + int64_t node_id, int64_t action, + Dart_Handle data_handle) { + ASSERT_NE(notify_semantics_action_callback, nullptr); + std::vector data = + tonic::DartConverter>::FromDart(data_handle); + notify_semantics_action_callback(node_id, action, data); + }))); fml::AutoResetWaitableEvent semantics_update_latch; context.SetSemanticsUpdateCallback2( @@ -185,10 +187,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV3Callbacks) { // 1: Wait for initial notifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch.Signal(); }; @@ -196,20 +195,14 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV3Callbacks) { // Prepare notifyAccessibilityFeatures callback. fml::AutoResetWaitableEvent notify_features_latch; - notify_accessibility_features_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_FALSE(enabled); + notify_accessibility_features_callback = [&](bool reduce_motion) { + ASSERT_FALSE(reduce_motion); notify_features_latch.Signal(); }; // 2: Enable semantics. Wait for notifySemanticsEnabled(true). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_2; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_TRUE(enabled); notify_semantics_enabled_latch_2.Signal(); }; @@ -222,11 +215,8 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV3Callbacks) { // 4: Wait for notifyAccessibilityFeatures (reduce_motion == true) fml::AutoResetWaitableEvent notify_features_latch_2; - notify_accessibility_features_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_TRUE(enabled); + notify_accessibility_features_callback = [&](bool reduce_motion) { + ASSERT_TRUE(reduce_motion); notify_features_latch_2.Signal(); }; result = FlutterEngineUpdateAccessibilityFeatures( @@ -241,22 +231,15 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV3Callbacks) { // 6: Dispatch a tap to semantics node 42. Wait for NotifySemanticsAction. fml::AutoResetWaitableEvent notify_semantics_action_latch; - notify_semantics_action_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - int64_t node_id = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_EQ(42, node_id); - - int64_t action_id = - ::tonic::DartConverter::FromArguments(args, 1, exception); - ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), action_id); - - std::vector semantic_args = - ::tonic::DartConverter>::FromArguments(args, 2, - exception); - ASSERT_THAT(semantic_args, ElementsAre(2, 1)); - notify_semantics_action_latch.Signal(); - }; + notify_semantics_action_callback = + [&](int64_t node_id, int64_t action_id, + const std::vector& semantic_args) { + ASSERT_EQ(42, node_id); + ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), + action_id); + ASSERT_THAT(semantic_args, ElementsAre(2, 1)); + notify_semantics_action_latch.Signal(); + }; std::vector bytes({2, 1}); result = FlutterEngineDispatchSemanticsAction( engine.get(), 42, kFlutterSemanticsActionTap, &bytes[0], bytes.size()); @@ -265,10 +248,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV3Callbacks) { // 7: Disable semantics. Wait for NotifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_3; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch_3.Signal(); }; @@ -288,11 +268,10 @@ TEST_F(EmbedderA11yTest, A11yStringAttributes) { // Called by the Dart text fixture on the UI thread to signal that the C++ // unittest should resume. - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY(([&signal_native_latch](Dart_NativeArguments) { - signal_native_latch.Signal(); - }))); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA(([&signal_native_latch]() { + signal_native_latch.Signal(); + }))); fml::AutoResetWaitableEvent semantics_update_latch; context.SetSemanticsUpdateCallback2( @@ -412,39 +391,41 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV2Callbacks) { // Called by the Dart text fixture on the UI thread to signal that the C++ // unittest should resume. - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY(([&signal_native_latch](Dart_NativeArguments) { - signal_native_latch.Signal(); - }))); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA(([&signal_native_latch]() { + signal_native_latch.Signal(); + }))); // Called by test fixture on UI thread to pass data back to this test. - NativeEntry notify_semantics_enabled_callback; - context.AddNativeCallback( + std::function notify_semantics_enabled_callback; + context.AddFfiNativeCallback( "NotifySemanticsEnabled", - CREATE_NATIVE_ENTRY( - ([¬ify_semantics_enabled_callback](Dart_NativeArguments args) { - ASSERT_NE(notify_semantics_enabled_callback, nullptr); - notify_semantics_enabled_callback(args); - }))); + CREATE_FFI_LAMBDA(([¬ify_semantics_enabled_callback](bool enabled) { + ASSERT_NE(notify_semantics_enabled_callback, nullptr); + notify_semantics_enabled_callback(enabled); + }))); - NativeEntry notify_accessibility_features_callback; - context.AddNativeCallback( + std::function notify_accessibility_features_callback; + context.AddFfiNativeCallback( "NotifyAccessibilityFeatures", - CREATE_NATIVE_ENTRY(( - [¬ify_accessibility_features_callback](Dart_NativeArguments args) { + CREATE_FFI_LAMBDA( + ([¬ify_accessibility_features_callback](bool reduce_motion) { ASSERT_NE(notify_accessibility_features_callback, nullptr); - notify_accessibility_features_callback(args); + notify_accessibility_features_callback(reduce_motion); }))); - NativeEntry notify_semantics_action_callback; - context.AddNativeCallback( + std::function)> + notify_semantics_action_callback; + context.AddFfiNativeCallback( "NotifySemanticsAction", - CREATE_NATIVE_ENTRY( - ([¬ify_semantics_action_callback](Dart_NativeArguments args) { - ASSERT_NE(notify_semantics_action_callback, nullptr); - notify_semantics_action_callback(args); - }))); + CREATE_FFI_LAMBDA(([¬ify_semantics_action_callback]( + int64_t node_id, int64_t action, + Dart_Handle data_handle) { + ASSERT_NE(notify_semantics_action_callback, nullptr); + std::vector data = + tonic::DartConverter>::FromDart(data_handle); + notify_semantics_action_callback(node_id, action, data); + }))); fml::AutoResetWaitableEvent semantics_update_latch; context.SetSemanticsUpdateCallback([&](const FlutterSemanticsUpdate* update) { @@ -486,10 +467,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV2Callbacks) { // 1: Wait for initial notifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch.Signal(); }; @@ -497,20 +475,14 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV2Callbacks) { // Prepare notifyAccessibilityFeatures callback. fml::AutoResetWaitableEvent notify_features_latch; - notify_accessibility_features_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_FALSE(enabled); + notify_accessibility_features_callback = [&](bool reduce_motion) { + ASSERT_FALSE(reduce_motion); notify_features_latch.Signal(); }; // 2: Enable semantics. Wait for notifySemanticsEnabled(true). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_2; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_TRUE(enabled); notify_semantics_enabled_latch_2.Signal(); }; @@ -523,11 +495,8 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV2Callbacks) { // 4: Wait for notifyAccessibilityFeatures (reduce_motion == true) fml::AutoResetWaitableEvent notify_features_latch_2; - notify_accessibility_features_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_TRUE(enabled); + notify_accessibility_features_callback = [&](bool reduce_motion) { + ASSERT_TRUE(reduce_motion); notify_features_latch_2.Signal(); }; result = FlutterEngineUpdateAccessibilityFeatures( @@ -542,22 +511,15 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV2Callbacks) { // 6: Dispatch a tap to semantics node 42. Wait for NotifySemanticsAction. fml::AutoResetWaitableEvent notify_semantics_action_latch; - notify_semantics_action_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - int64_t node_id = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_EQ(42, node_id); - - int64_t action_id = - ::tonic::DartConverter::FromArguments(args, 1, exception); - ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), action_id); - - std::vector semantic_args = - ::tonic::DartConverter>::FromArguments(args, 2, - exception); - ASSERT_THAT(semantic_args, ElementsAre(2, 1)); - notify_semantics_action_latch.Signal(); - }; + notify_semantics_action_callback = + [&](int64_t node_id, int64_t action_id, + const std::vector& semantic_args) { + ASSERT_EQ(42, node_id); + ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), + action_id); + ASSERT_THAT(semantic_args, ElementsAre(2, 1)); + notify_semantics_action_latch.Signal(); + }; std::vector bytes({2, 1}); result = FlutterEngineDispatchSemanticsAction( engine.get(), 42, kFlutterSemanticsActionTap, &bytes[0], bytes.size()); @@ -566,10 +528,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV2Callbacks) { // 7: Disable semantics. Wait for NotifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_3; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch_3.Signal(); }; @@ -589,39 +548,41 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV1Callbacks) { // Called by the Dart text fixture on the UI thread to signal that the C++ // unittest should resume. - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY(([&signal_native_latch](Dart_NativeArguments) { - signal_native_latch.Signal(); - }))); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA(([&signal_native_latch]() { + signal_native_latch.Signal(); + }))); // Called by test fixture on UI thread to pass data back to this test. - NativeEntry notify_semantics_enabled_callback; - context.AddNativeCallback( + std::function notify_semantics_enabled_callback; + context.AddFfiNativeCallback( "NotifySemanticsEnabled", - CREATE_NATIVE_ENTRY( - ([¬ify_semantics_enabled_callback](Dart_NativeArguments args) { - ASSERT_NE(notify_semantics_enabled_callback, nullptr); - notify_semantics_enabled_callback(args); - }))); + CREATE_FFI_LAMBDA(([¬ify_semantics_enabled_callback](bool enabled) { + ASSERT_NE(notify_semantics_enabled_callback, nullptr); + notify_semantics_enabled_callback(enabled); + }))); - NativeEntry notify_accessibility_features_callback; - context.AddNativeCallback( + std::function notify_accessibility_features_callback; + context.AddFfiNativeCallback( "NotifyAccessibilityFeatures", - CREATE_NATIVE_ENTRY(( - [¬ify_accessibility_features_callback](Dart_NativeArguments args) { + CREATE_FFI_LAMBDA( + ([¬ify_accessibility_features_callback](bool reduce_motion) { ASSERT_NE(notify_accessibility_features_callback, nullptr); - notify_accessibility_features_callback(args); + notify_accessibility_features_callback(reduce_motion); }))); - NativeEntry notify_semantics_action_callback; - context.AddNativeCallback( + std::function)> + notify_semantics_action_callback; + context.AddFfiNativeCallback( "NotifySemanticsAction", - CREATE_NATIVE_ENTRY( - ([¬ify_semantics_action_callback](Dart_NativeArguments args) { - ASSERT_NE(notify_semantics_action_callback, nullptr); - notify_semantics_action_callback(args); - }))); + CREATE_FFI_LAMBDA(([¬ify_semantics_action_callback]( + int64_t node_id, int64_t action, + Dart_Handle data_handle) { + ASSERT_NE(notify_semantics_action_callback, nullptr); + std::vector data = + tonic::DartConverter>::FromDart(data_handle); + notify_semantics_action_callback(node_id, action, data); + }))); fml::AutoResetWaitableEvent semantics_node_latch; fml::AutoResetWaitableEvent semantics_action_latch; @@ -683,10 +644,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV1Callbacks) { // 1: Wait for initial notifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch.Signal(); }; @@ -694,20 +652,14 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV1Callbacks) { // Prepare notifyAccessibilityFeatures callback. fml::AutoResetWaitableEvent notify_features_latch; - notify_accessibility_features_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_FALSE(enabled); + notify_accessibility_features_callback = [&](bool reduce_motion) { + ASSERT_FALSE(reduce_motion); notify_features_latch.Signal(); }; // 2: Enable semantics. Wait for notifySemanticsEnabled(true). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_2; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_TRUE(enabled); notify_semantics_enabled_latch_2.Signal(); }; @@ -720,11 +672,8 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV1Callbacks) { // 4: Wait for notifyAccessibilityFeatures (reduce_motion == true) fml::AutoResetWaitableEvent notify_features_latch_2; - notify_accessibility_features_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_TRUE(enabled); + notify_accessibility_features_callback = [&](bool reduce_motion) { + ASSERT_TRUE(reduce_motion); notify_features_latch_2.Signal(); }; result = FlutterEngineUpdateAccessibilityFeatures( @@ -744,22 +693,15 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV1Callbacks) { // 6: Dispatch a tap to semantics node 42. Wait for NotifySemanticsAction. fml::AutoResetWaitableEvent notify_semantics_action_latch; - notify_semantics_action_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - int64_t node_id = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_EQ(42, node_id); - - int64_t action_id = - ::tonic::DartConverter::FromArguments(args, 1, exception); - ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), action_id); - - std::vector semantic_args = - ::tonic::DartConverter>::FromArguments(args, 2, - exception); - ASSERT_THAT(semantic_args, ElementsAre(2, 1)); - notify_semantics_action_latch.Signal(); - }; + notify_semantics_action_callback = + [&](int64_t node_id, int64_t action_id, + const std::vector& semantic_args) { + ASSERT_EQ(42, node_id); + ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), + action_id); + ASSERT_THAT(semantic_args, ElementsAre(2, 1)); + notify_semantics_action_latch.Signal(); + }; std::vector bytes({2, 1}); result = FlutterEngineDispatchSemanticsAction( engine.get(), 42, kFlutterSemanticsActionTap, &bytes[0], bytes.size()); @@ -768,10 +710,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingV1Callbacks) { // 7: Disable semantics. Wait for NotifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_3; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch_3.Signal(); }; @@ -791,29 +730,27 @@ TEST_F(EmbedderA11yTest, A11yTreesAreConsistentWithMultipleViews) { // Called by the Dart text fixture on the UI thread to signal that the C++ // unittest should resume. - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY(([&signal_native_latch](Dart_NativeArguments) { - signal_native_latch.Signal(); - }))); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA(([&signal_native_latch]() { + signal_native_latch.Signal(); + }))); // Called by test fixture on UI thread to pass data back to this test. - NativeEntry notify_semantics_enabled_callback; - context.AddNativeCallback( + std::function notify_semantics_enabled_callback; + context.AddFfiNativeCallback( "NotifySemanticsEnabled", - CREATE_NATIVE_ENTRY( - ([¬ify_semantics_enabled_callback](Dart_NativeArguments args) { - ASSERT_NE(notify_semantics_enabled_callback, nullptr); - notify_semantics_enabled_callback(args); - }))); + CREATE_FFI_LAMBDA(([¬ify_semantics_enabled_callback](bool enabled) { + ASSERT_NE(notify_semantics_enabled_callback, nullptr); + notify_semantics_enabled_callback(enabled); + }))); - NativeEntry notify_accessibility_features_callback; - context.AddNativeCallback( + std::function notify_accessibility_features_callback; + context.AddFfiNativeCallback( "NotifyAccessibilityFeatures", - CREATE_NATIVE_ENTRY(( - [¬ify_accessibility_features_callback](Dart_NativeArguments args) { + CREATE_FFI_LAMBDA( + ([¬ify_accessibility_features_callback](bool reduce_motion) { ASSERT_NE(notify_accessibility_features_callback, nullptr); - notify_accessibility_features_callback(args); + notify_accessibility_features_callback(reduce_motion); }))); int num_times_set_semantics_update_callback2_called = 0; @@ -855,10 +792,7 @@ TEST_F(EmbedderA11yTest, A11yTreesAreConsistentWithMultipleViews) { // 1: Wait for initial notifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch.Signal(); }; @@ -912,20 +846,14 @@ TEST_F(EmbedderA11yTest, A11yTreesAreConsistentWithMultipleViews) { // Prepare notifyAccessibilityFeatures callback. fml::AutoResetWaitableEvent notify_features_latch; - notify_accessibility_features_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); - ASSERT_FALSE(enabled); + notify_accessibility_features_callback = [&](bool reduce_motion) { + ASSERT_FALSE(reduce_motion); notify_features_latch.Signal(); }; // 4: Enable semantics. Wait for notifySemanticsEnabled(true). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_2; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_TRUE(enabled); notify_semantics_enabled_latch_2.Signal(); }; @@ -947,10 +875,7 @@ TEST_F(EmbedderA11yTest, A11yTreesAreConsistentWithMultipleViews) { // 7: Disable semantics. Wait for NotifySemanticsEnabled(false). fml::AutoResetWaitableEvent notify_semantics_enabled_latch_3; - notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; - bool enabled = - ::tonic::DartConverter::FromArguments(args, 0, exception); + notify_semantics_enabled_callback = [&](bool enabled) { ASSERT_FALSE(enabled); notify_semantics_enabled_latch_3.Signal(); }; diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc index 1273b23c6c788..74c0d3a8d36a1 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc @@ -37,7 +37,7 @@ #include "third_party/skia/include/core/SkSurface.h" #include "third_party/tonic/converter/dart_converter.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter::testing { @@ -192,10 +192,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLFramebuffer) { latch.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -314,10 +312,8 @@ TEST_F(EmbedderTest, RasterCacheDisabledWithPlatformViews) { setup.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&setup](Dart_NativeArguments args) { setup.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&setup]() { setup.CountDown(); })); UniqueEngine engine = builder.LaunchEngine(); @@ -401,10 +397,8 @@ TEST_F(EmbedderTest, RasterCacheEnabled) { setup.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&setup](Dart_NativeArguments args) { setup.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&setup]() { setup.CountDown(); })); UniqueEngine engine = builder.LaunchEngine(); @@ -531,10 +525,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLTexture) { latch.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -653,10 +645,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToSoftwareBuffer) { latch.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -861,10 +851,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderKnownScene) { return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -1021,10 +1009,8 @@ TEST_F(EmbedderTest, CustomCompositorMustWorkWithCustomTaskRunner) { builder.SetPlatformTaskRunner(&task_runner_description); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); platform_task_runner->PostTask([&]() { std::scoped_lock lock(engine_mutex); @@ -1113,10 +1099,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithRootLayerOnly) { latch.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -1238,10 +1222,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithPlatformLayerOnBottom) { return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -1462,10 +1444,8 @@ TEST_F(EmbedderTest, return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -2462,11 +2442,10 @@ TEST_F(EmbedderTest, constexpr size_t frames_expected = 10; fml::CountDownLatch frame_latch(frames_expected); std::atomic_size_t frames_seen = 0; - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - frames_seen++; - frame_latch.CountDown(); - })); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + frames_seen++; + frame_latch.CountDown(); + })); frame_latch.Wait(); ASSERT_GE(frames_seen, frames_expected); @@ -2503,11 +2482,10 @@ TEST_F(EmbedderTest, constexpr size_t frames_expected = 10; fml::CountDownLatch frame_latch(frames_expected); std::atomic_size_t frames_seen = 0; - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - frames_seen++; - frame_latch.CountDown(); - })); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + frames_seen++; + frame_latch.CountDown(); + })); frame_latch.Wait(); ASSERT_GE(frames_seen, frames_expected); @@ -2863,9 +2841,8 @@ TEST_F(EmbedderTest, EmptySceneIsAcceptable) { builder.SetCompositor(); builder.SetDartEntrypoint("empty_scene"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -2891,9 +2868,8 @@ TEST_F(EmbedderTest, SceneWithNoRootContainerIsAcceptable) { EmbedderTestBackingStoreProducer::RenderTargetType::kOpenGLFramebuffer); builder.SetDartEntrypoint("scene_with_no_container"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -3124,12 +3100,11 @@ TEST_F(EmbedderTest, ObjectsCanBePostedViaPorts) { // for inspection. FlutterEngineDartPort port = 0; fml::AutoResetWaitableEvent event; - context.AddNativeCallback("SignalNativeCount", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - port = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - event.Signal(); - })); + context.AddFfiNativeCallback("SignalNativeCount", + CREATE_FFI_LAMBDA([&](int64_t count) { + port = count; + event.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); event.Wait(); @@ -3138,13 +3113,13 @@ TEST_F(EmbedderTest, ObjectsCanBePostedViaPorts) { using Trampoline = std::function; Trampoline trampoline; - context.AddNativeCallback("SendObjectToNativeCode", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - FML_CHECK(trampoline); - auto trampoline_copy = trampoline; - trampoline = nullptr; - trampoline_copy(Dart_GetNativeArgument(args, 0)); - })); + context.AddFfiNativeCallback("SendObjectToNativeCode", + CREATE_FFI_LAMBDA([&](Dart_Handle object) { + FML_CHECK(trampoline); + auto trampoline_copy = trampoline; + trampoline = nullptr; + trampoline_copy(object); + })); // Check null. { @@ -3422,10 +3397,8 @@ TEST_F(EmbedderTest, CompositorRenderTargetsAreRecycled) { fml::CountDownLatch latch(2); - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - latch.CountDown(); - })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.CountDown(); })); context.GetCompositor().SetNextPresentCallback( [&](FlutterViewId view_id, const FlutterLayer** layers, @@ -3468,10 +3441,8 @@ TEST_F(EmbedderTest, CompositorRenderTargetsAreInStableOrder) { fml::CountDownLatch latch(2); - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - latch.CountDown(); - })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.CountDown(); })); size_t frame_count = 0; std::vector first_frame_backing_store_user_data; @@ -3550,10 +3521,9 @@ TEST_F(EmbedderTest, FrameInfoContainsValidWidthAndHeight) { static fml::CountDownLatch frame_latch(10); - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - /* Nothing to do. */ - })); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + /* Nothing to do. */ + })); context.SetGLGetFBOCallback([](FlutterFrameInfo frame_info) { // width and height are rotated by 90 deg @@ -3675,10 +3645,9 @@ TEST_F(EmbedderTest, PresentInfoContainsValidFBOId) { static fml::CountDownLatch frame_latch(10); - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - /* Nothing to do. */ - })); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + /* Nothing to do. */ + })); const uint32_t window_fbo_id = context.GetWindowFBOId(); context.SetGLPresentCallback( @@ -3985,10 +3954,9 @@ TEST_F(EmbedderTest, PopulateExistingDamageReceivesInvalidID) { auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - /* Nothing to do. */ - })); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + /* Nothing to do. */ + })); const uint32_t window_fbo_id = context.GetWindowFBOId(); context.SetGLPopulateExistingDamageCallback( @@ -4017,9 +3985,8 @@ TEST_F(EmbedderTest, SetSingleDisplayConfigurationWithDisplayId) { builder.SetCompositor(); builder.SetDartEntrypoint("empty_scene"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -4059,9 +4026,8 @@ TEST_F(EmbedderTest, SetSingleDisplayConfigurationWithoutDisplayId) { builder.SetCompositor(); builder.SetDartEntrypoint("empty_scene"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -4101,9 +4067,8 @@ TEST_F(EmbedderTest, SetValidMultiDisplayConfiguration) { builder.SetCompositor(); builder.SetDartEntrypoint("empty_scene"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -4150,9 +4115,8 @@ TEST_F(EmbedderTest, MultipleDisplaysWithSingleDisplayTrueIsInvalid) { builder.SetCompositor(); builder.SetDartEntrypoint("empty_scene"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -4196,9 +4160,8 @@ TEST_F(EmbedderTest, MultipleDisplaysWithSameDisplayIdIsInvalid) { builder.SetCompositor(); builder.SetDartEntrypoint("empty_scene"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -4249,10 +4212,8 @@ TEST_F(EmbedderTest, CompositorRenderTargetsNotRecycledWhenAvoidsCacheSet) { const unsigned num_backing_stores = num_frames * num_engine_layers; fml::CountDownLatch latch(1 + num_frames); // 1 for native test signal. - context.AddNativeCallback("SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - latch.CountDown(); - })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.CountDown(); })); context.GetCompositor().SetPresentCallback( [&](FlutterViewId view_id, const FlutterLayer** layers, @@ -4298,21 +4259,21 @@ TEST_F(EmbedderTest, SnapshotRenderTargetScalesDownToDriverMax) { }); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SnapshotsCallback", CREATE_NATIVE_ENTRY(([&](Dart_NativeArguments args) { - auto get_arg = [&args](int index) { - Dart_Handle dart_image = Dart_GetNativeArgument(args, index); + context.AddFfiNativeCallback( + "SnapshotsCallback", + CREATE_FFI_LAMBDA(([&](Dart_Handle big_handle, Dart_Handle small_handle) { + auto get_arg = [](Dart_Handle dart_image) { Dart_Handle internal_image = Dart_GetField(dart_image, tonic::ToDart("_image")); return tonic::DartConverter::FromDart( internal_image); }; - CanvasImage* big_image = get_arg(0); + CanvasImage* big_image = get_arg(big_handle); ASSERT_EQ(big_image->width(), max_size); ASSERT_EQ(big_image->height(), max_size / 2); - CanvasImage* small_image = get_arg(1); + CanvasImage* small_image = get_arg(small_handle); ASSERT_TRUE(ImageMatchesFixture( "snapshot_large_scene.png", small_image->image()->asSkiaImage()->skia_image())); @@ -4337,12 +4298,11 @@ TEST_F(EmbedderTest, ObjectsPostedViaPortsServicedOnSecondaryTaskHeap) { // for inspection. FlutterEngineDartPort port = 0; fml::AutoResetWaitableEvent event; - context.AddNativeCallback("SignalNativeCount", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - port = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - event.Signal(); - })); + context.AddFfiNativeCallback("SignalNativeCount", + CREATE_FFI_LAMBDA([&](int64_t count) { + port = count; + event.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); event.Wait(); @@ -4351,13 +4311,13 @@ TEST_F(EmbedderTest, ObjectsPostedViaPortsServicedOnSecondaryTaskHeap) { using Trampoline = std::function; Trampoline trampoline; - context.AddNativeCallback("SendObjectToNativeCode", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - FML_CHECK(trampoline); - auto trampoline_copy = trampoline; - trampoline = nullptr; - trampoline_copy(Dart_GetNativeArgument(args, 0)); - })); + context.AddFfiNativeCallback("SendObjectToNativeCode", + CREATE_FFI_LAMBDA([&](Dart_Handle object) { + FML_CHECK(trampoline); + auto trampoline_copy = trampoline; + trampoline = nullptr; + trampoline_copy(object); + })); // Send a boolean value and assert that it's received by the right heap. { @@ -4420,10 +4380,8 @@ TEST_F(EmbedderTest, CreateInvalidBackingstoreOpenGLTexture) { return true; }; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -4483,10 +4441,8 @@ TEST_F(EmbedderTest, CreateInvalidBackingstoreOpenGLFramebuffer) { return true; }; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -4542,10 +4498,8 @@ TEST_F(EmbedderTest, CreateInvalidBackingstoreOpenGLSurface) { return true; }; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -5022,12 +4976,11 @@ TEST_F(EmbedderTest, ImpellerOpenGLImageSnapshot) { bool result = false; fml::AutoResetWaitableEvent latch; - context.AddNativeCallback("NotifyBoolValue", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - result = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - latch.Signal(); - })); + context.AddFfiNativeCallback("NotifyBoolValue", + CREATE_FFI_LAMBDA([&](bool value) { + result = value; + latch.Signal(); + })); EmbedderConfigBuilder builder(context); builder.AddCommandLineArgument("--enable-impeller"); @@ -5140,10 +5093,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLSurface) { latch.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -5345,10 +5296,8 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderKnownSceneToOpenGLSurfaces) { return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_metal_unittests.mm b/engine/src/flutter/shell/platform/embedder/tests/embedder_metal_unittests.mm index b8482653ebc4d..b064902b01aea 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_metal_unittests.mm +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_metal_unittests.mm @@ -27,7 +27,7 @@ #include "third_party/skia/include/gpu/ganesh/mtl/GrMtlBackendSurface.h" #include "third_party/skia/include/gpu/ganesh/mtl/GrMtlTypes.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter { @@ -219,9 +219,8 @@ latch.CountDown(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -487,9 +486,8 @@ return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -546,9 +544,8 @@ void Collect() { return true; }; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); auto engine = builder.LaunchEngine(); @@ -704,9 +701,8 @@ void Collect() { fml::CountDownLatch latch(3); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); context.GetCompositor().SetPlatformViewRendererCallback( [&](const FlutterLayer& layer, GrDirectContext* context) -> sk_sp { diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc index 4e7c84a664303..14b608438d4de 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc @@ -125,6 +125,11 @@ void EmbedderTestContext::AddNativeCallback(const char* name, native_resolver_->AddNativeCallback({name}, function); } +void EmbedderTestContext::AddFfiNativeCallback(const char* name, + void* function) { + native_resolver_->AddFfiNativeCallback({name}, function); +} + void EmbedderTestContext::SetSemanticsUpdateCallback2( SemanticsUpdateCallback2 update_semantics_callback) { update_semantics_callback2_ = std::move(update_semantics_callback); diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h index acc52a90e0ced..01c659c5070c6 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h @@ -85,6 +85,8 @@ class EmbedderTestContext { void AddNativeCallback(const char* name, Dart_NativeFunction function); + void AddFfiNativeCallback(const char* name, void* function); + void SetSemanticsNodeCallback(SemanticsNodeCallback update_semantics_node); void SetSemanticsCustomActionCallback( 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 364764f2e16a9..7c7e6dfe7ddaa 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc @@ -38,7 +38,7 @@ #include #endif -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace { @@ -92,10 +92,9 @@ TEST_F(EmbedderTest, DISABLED_CanLaunchAndShutdownMultipleTimes) { TEST_F(EmbedderTest, CanInvokeCustomEntrypoint) { auto& context = GetEmbedderContext(); static fml::AutoResetWaitableEvent latch; - Dart_NativeFunction entrypoint = [](Dart_NativeArguments args) { - latch.Signal(); - }; - context.AddNativeCallback("SayHiFromCustomEntrypoint", entrypoint); + auto entrypoint = []() { latch.Signal(); }; + context.AddFfiNativeCallback("SayHiFromCustomEntrypoint", + reinterpret_cast(+entrypoint)); EmbedderConfigBuilder builder(context); builder.SetSurface(DlISize(1, 1)); builder.SetDartEntrypoint("customEntrypoint"); @@ -112,28 +111,27 @@ TEST_F(EmbedderTest, CanInvokeCustomEntrypointMacro) { fml::AutoResetWaitableEvent latch3; // Can be defined separately. - auto entry1 = [&latch1](Dart_NativeArguments args) { + auto entry1 = [&latch1]() { FML_LOG(INFO) << "In Callback 1"; latch1.Signal(); }; - auto native_entry1 = CREATE_NATIVE_ENTRY(entry1); - context.AddNativeCallback("SayHiFromCustomEntrypoint1", native_entry1); + auto native_entry1 = CREATE_FFI_LAMBDA(entry1); + context.AddFfiNativeCallback("SayHiFromCustomEntrypoint1", native_entry1); // Can be wrapped in the args. - auto entry2 = [&latch2](Dart_NativeArguments args) { + auto entry2 = [&latch2]() { FML_LOG(INFO) << "In Callback 2"; latch2.Signal(); }; - context.AddNativeCallback("SayHiFromCustomEntrypoint2", - CREATE_NATIVE_ENTRY(entry2)); + context.AddFfiNativeCallback("SayHiFromCustomEntrypoint2", + CREATE_FFI_LAMBDA(entry2)); // Everything can be inline. - context.AddNativeCallback( - "SayHiFromCustomEntrypoint3", - CREATE_NATIVE_ENTRY([&latch3](Dart_NativeArguments args) { - FML_LOG(INFO) << "In Callback 3"; - latch3.Signal(); - })); + context.AddFfiNativeCallback("SayHiFromCustomEntrypoint3", + CREATE_FFI_LAMBDA([&latch3]() { + FML_LOG(INFO) << "In Callback 3"; + latch3.Signal(); + })); EmbedderConfigBuilder builder(context); builder.SetSurface(DlISize(1, 1)); @@ -160,10 +158,10 @@ TEST_F(EmbedderTest, ExecutableNameNotNull) { // Supply a callback to Dart for the test fixture to pass Platform.executable // back to us. fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "NotifyStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - const auto dart_string = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); + context.AddFfiNativeCallback( + "NotifyStringValue", CREATE_FFI_LAMBDA([&](Dart_Handle value) { + const auto dart_string = + tonic::DartConverter::FromDart(value); EXPECT_EQ("/path/to/binary", dart_string); latch.Signal(); })); @@ -184,12 +182,11 @@ TEST_F(EmbedderTest, ImplicitViewNotNull) { bool implicitViewNotNull = false; fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "NotifyBoolValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - implicitViewNotNull = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - latch.Signal(); - })); + context.AddFfiNativeCallback("NotifyBoolValue", + CREATE_FFI_LAMBDA([&](bool value) { + implicitViewNotNull = value; + latch.Signal(); + })); EmbedderConfigBuilder builder(context); builder.SetSurface(DlISize(1, 1)); @@ -245,8 +242,8 @@ TEST_F(EmbedderTest, CanSpecifyCustomUITaskRunner) { fml::AutoResetWaitableEvent signal_latch_ui; fml::AutoResetWaitableEvent signal_latch_platform; - context.AddNativeCallback( - "SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&]() { // Assert that the UI isolate is running on platform thread. ASSERT_TRUE(ui_task_runner->RunsTasksOnCurrentThread()); signal_latch_ui.Signal(); @@ -370,8 +367,8 @@ TEST_F(EmbedderTest, MergedPlatformUIThread) { fml::AutoResetWaitableEvent signal_latch_ui; fml::AutoResetWaitableEvent signal_latch_platform; - context.AddNativeCallback( - "SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&]() { // Assert that the UI isolate is running on platform thread. ASSERT_TRUE(task_runner->RunsTasksOnCurrentThread()); signal_latch_ui.Signal(); @@ -420,8 +417,8 @@ TEST_F(EmbedderTest, UITaskRunnerFlushesMicrotasks) { fml::AutoResetWaitableEvent signal_latch; - context.AddNativeCallback( - "SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&]() { ASSERT_TRUE(ui_task_runner->RunsTasksOnCurrentThread()); signal_latch.Signal(); })); @@ -586,9 +583,7 @@ TEST_F(EmbedderTest, CanCreateAndCollectCallbacks) { EmbedderConfigBuilder builder(context); builder.SetSurface(DlISize(1, 1)); builder.SetDartEntrypoint("platform_messages_response"); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([](Dart_NativeArguments args) {})); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([]() {})); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -626,10 +621,8 @@ TEST_F(EmbedderTest, PlatformMessagesCanReceiveResponse) { builder.SetDartEntrypoint("platform_messages_response"); fml::AutoResetWaitableEvent ready; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready](Dart_NativeArguments args) { ready.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&ready]() { ready.Signal(); })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -684,19 +677,16 @@ TEST_F(EmbedderTest, PlatformMessagesCanBeSentWithoutResponseHandles) { const std::string message_data = "Hello but don't call me back."; fml::AutoResetWaitableEvent ready, message; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready](Dart_NativeArguments args) { ready.Signal(); })); - context.AddNativeCallback( + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&ready]() { ready.Signal(); })); + context.AddFfiNativeCallback( "SignalNativeMessage", - CREATE_NATIVE_ENTRY( - ([&message, &message_data](Dart_NativeArguments args) { - auto received_message = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - ASSERT_EQ(received_message, message_data); - message.Signal(); - }))); + CREATE_FFI_LAMBDA(([&message, &message_data](Dart_Handle message_handle) { + auto received_message = + tonic::DartConverter::FromDart(message_handle); + ASSERT_EQ(received_message, message_data); + message.Signal(); + }))); auto engine = builder.LaunchEngine(); @@ -727,15 +717,13 @@ TEST_F(EmbedderTest, NullPlatformMessagesCanBeSent) { builder.SetDartEntrypoint("null_platform_messages"); fml::AutoResetWaitableEvent ready, message; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready](Dart_NativeArguments args) { ready.Signal(); })); - context.AddNativeCallback( + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&ready]() { ready.Signal(); })); + context.AddFfiNativeCallback( "SignalNativeMessage", - CREATE_NATIVE_ENTRY(([&message](Dart_NativeArguments args) { - auto received_message = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); + CREATE_FFI_LAMBDA(([&message](Dart_Handle message_handle) { + auto received_message = + tonic::DartConverter::FromDart(message_handle); ASSERT_EQ("true", received_message); message.Signal(); }))); @@ -855,15 +843,13 @@ TEST_F(EmbedderTest, DartEntrypointArgs) { fml::AutoResetWaitableEvent callback_latch; std::vector callback_args; auto nativeArgumentsCallback = [&callback_args, - &callback_latch](Dart_NativeArguments args) { - Dart_Handle exception = nullptr; + &callback_latch](Dart_Handle args) { callback_args = - tonic::DartConverter>::FromArguments( - args, 0, exception); + tonic::DartConverter>::FromDart(args); callback_latch.Signal(); }; - context.AddNativeCallback("NativeArgumentsCallback", - CREATE_NATIVE_ENTRY(nativeArgumentsCallback)); + context.AddFfiNativeCallback("NativeArgumentsCallback", + CREATE_FFI_LAMBDA(nativeArgumentsCallback)); auto engine = builder.LaunchEngine(); callback_latch.Wait(); ASSERT_EQ(callback_args[0], "foo"); @@ -1157,10 +1143,8 @@ TEST_F(EmbedderTest, return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -1288,10 +1272,8 @@ TEST_F(EmbedderTest, NoLayerCreatedForTransparentOverlayOnTopOfPlatformLayer) { return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -1425,10 +1407,8 @@ TEST_F(EmbedderTest, NoLayerCreatedForNoOverlayOnTopOfPlatformLayer) { return surface->makeImageSnapshot(); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.CountDown(); })); auto engine = builder.LaunchEngine(); @@ -1528,19 +1508,16 @@ TEST_F(EmbedderTest, CanAddView) { builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); fml::AutoResetWaitableEvent ready_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); std::string message; - context.AddNativeCallback("SignalNativeMessage", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - message = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - message_latch.Signal(); - })); + context.AddFfiNativeCallback( + "SignalNativeMessage", CREATE_FFI_LAMBDA([&](Dart_Handle message_handle) { + message = tonic::DartConverter::FromDart(message_handle); + message_latch.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -1575,16 +1552,13 @@ TEST_F(EmbedderTest, AddViewSchedulesFrame) { builder.SetSurface(DlISize(1, 1)); builder.SetDartEntrypoint("add_view_schedules_frame"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); fml::AutoResetWaitableEvent check_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeCount", - CREATE_NATIVE_ENTRY( - [&check_latch](Dart_NativeArguments args) { check_latch.Signal(); })); + CREATE_FFI_LAMBDA([&check_latch](int count) { check_latch.Signal(); })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -1621,19 +1595,16 @@ TEST_F(EmbedderTest, CanRemoveView) { builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); fml::AutoResetWaitableEvent ready_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); std::string message; - context.AddNativeCallback("SignalNativeMessage", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - message = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - message_latch.Signal(); - })); + context.AddFfiNativeCallback( + "SignalNativeMessage", CREATE_FFI_LAMBDA([&](Dart_Handle message_handle) { + message = tonic::DartConverter::FromDart(message_handle); + message_latch.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -1693,10 +1664,9 @@ TEST_F(EmbedderTest, RemoveViewCallbackIsInvokedAfterRasterThreadIsDone) { &render_task_runner.GetFlutterTaskRunnerDescription()); fml::AutoResetWaitableEvent ready_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); { std::scoped_lock lock(engine_mutex); @@ -1798,19 +1768,16 @@ TEST_F(EmbedderTest, CannotAddDuplicateViews) { builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); fml::AutoResetWaitableEvent ready_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); std::string message; - context.AddNativeCallback("SignalNativeMessage", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - message = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - message_latch.Signal(); - })); + context.AddFfiNativeCallback( + "SignalNativeMessage", CREATE_FFI_LAMBDA([&](Dart_Handle message_handle) { + message = tonic::DartConverter::FromDart(message_handle); + message_latch.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -1870,19 +1837,16 @@ TEST_F(EmbedderTest, CanReuseViewIds) { builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); fml::AutoResetWaitableEvent ready_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); std::string message; - context.AddNativeCallback("SignalNativeMessage", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - message = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - message_latch.Signal(); - })); + context.AddFfiNativeCallback( + "SignalNativeMessage", CREATE_FFI_LAMBDA([&](Dart_Handle message_handle) { + message = tonic::DartConverter::FromDart(message_handle); + message_latch.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -1961,16 +1925,15 @@ TEST_F(EmbedderTest, ViewOperationsOrdered) { builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); fml::AutoResetWaitableEvent ready_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); std::atomic message_count = 0; - context.AddNativeCallback("SignalNativeMessage", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - message_count.fetch_add(1); - })); + context.AddFfiNativeCallback( + "SignalNativeMessage", CREATE_FFI_LAMBDA([&](Dart_Handle message_handle) { + message_count.fetch_add(1); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -2235,18 +2198,15 @@ TEST_F(EmbedderTest, CanSendViewFocusEvent) { fml::AutoResetWaitableEvent latch; std::string last_event; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.Signal(); })); - context.AddNativeCallback("NotifyStringValue", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - const auto message_from_dart = - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - last_event = message_from_dart; - latch.Signal(); - })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); + context.AddFfiNativeCallback( + "NotifyStringValue", CREATE_FFI_LAMBDA([&](Dart_Handle value) { + const auto message_from_dart = + tonic::DartConverter::FromDart(value); + last_event = message_from_dart; + latch.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -2458,20 +2418,15 @@ TEST_F(EmbedderTest, CanUpdateLocales) { builder.SetSurface(DlISize(1, 1)); builder.SetDartEntrypoint("can_receive_locale_updates"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); fml::AutoResetWaitableEvent check_latch; - context.AddNativeCallback( - "SignalNativeCount", - CREATE_NATIVE_ENTRY([&check_latch](Dart_NativeArguments args) { - ASSERT_EQ(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)), - 2); - check_latch.Signal(); - })); + context.AddFfiNativeCallback("SignalNativeCount", + CREATE_FFI_LAMBDA([&check_latch](int count) { + ASSERT_EQ(count, 2); + check_latch.Signal(); + })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -3466,24 +3421,17 @@ TEST_F(EmbedderTest, KeyDataIsCorrectlySerialized) { FlutterKeyEvent echoed_event; echoed_event.struct_size = sizeof(FlutterKeyEvent); - auto native_echo_event = [&](Dart_NativeArguments args) { - echoed_event.type = - UnserializeKeyEventType(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0))); - echoed_event.timestamp = - static_cast(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 1))); - echoed_event.physical = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 2)); - echoed_event.logical = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 3)); - echoed_char = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 4)); - echoed_event.synthesized = - tonic::DartConverter::FromDart(Dart_GetNativeArgument(args, 5)); - echoed_event.device_type = - UnserializeKeyEventDeviceType(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 6))); + auto native_echo_event = [&](uint64_t change, uint64_t timestamp, + uint64_t physical, uint64_t logical, + uint64_t char_code, bool synthesized, + uint64_t device_type) { + echoed_event.type = UnserializeKeyEventType(change); + echoed_event.timestamp = static_cast(timestamp); + echoed_event.physical = physical; + echoed_event.logical = logical; + echoed_char = char_code; + echoed_event.synthesized = synthesized; + echoed_event.device_type = UnserializeKeyEventDeviceType(device_type); message_latch->Signal(); }; @@ -3502,13 +3450,11 @@ TEST_F(EmbedderTest, KeyDataIsCorrectlySerialized) { FlutterEngineSendPlatformMessageResponse( engine.get(), message->response_handle, nullptr, 0); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready](Dart_NativeArguments args) { ready.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&ready]() { ready.Signal(); })); - context.AddNativeCallback("EchoKeyEvent", - CREATE_NATIVE_ENTRY(native_echo_event)); + context.AddFfiNativeCallback("EchoKeyEvent", + CREATE_FFI_LAMBDA(native_echo_event)); engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -3587,23 +3533,17 @@ TEST_F(EmbedderTest, KeyDataAreBuffered) { auto message_latch = std::make_shared(); std::vector echoed_events; - auto native_echo_event = [&](Dart_NativeArguments args) { + auto native_echo_event = [&](uint64_t change, uint64_t timestamp, + uint64_t physical, uint64_t logical, + uint64_t char_code, bool synthesized, + uint64_t device_type) { echoed_events.push_back(FlutterKeyEvent{ - .timestamp = - static_cast(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 1))), - .type = - UnserializeKeyEventType(tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0))), - .physical = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 2)), - .logical = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 3)), - .synthesized = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 5)), - .device_type = UnserializeKeyEventDeviceType( - tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 6))), + .timestamp = static_cast(timestamp), + .type = UnserializeKeyEventType(change), + .physical = physical, + .logical = logical, + .synthesized = synthesized, + .device_type = UnserializeKeyEventDeviceType(device_type), }); message_latch->Signal(); @@ -3623,13 +3563,11 @@ TEST_F(EmbedderTest, KeyDataAreBuffered) { FlutterEngineSendPlatformMessageResponse( engine.get(), message->response_handle, nullptr, 0); }); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready](Dart_NativeArguments args) { ready.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&ready]() { ready.Signal(); })); - context.AddNativeCallback("EchoKeyEvent", - CREATE_NATIVE_ENTRY(native_echo_event)); + context.AddFfiNativeCallback("EchoKeyEvent", + CREATE_FFI_LAMBDA(native_echo_event)); engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -3718,12 +3656,14 @@ TEST_F(EmbedderTest, KeyDataResponseIsCorrectlyInvoked) { EmbedderConfigBuilder builder(context); builder.SetSurface(DlISize(1, 1)); builder.SetDartEntrypoint("key_data_echo"); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready](Dart_NativeArguments args) { ready.Signal(); })); - context.AddNativeCallback( - "EchoKeyEvent", CREATE_NATIVE_ENTRY([](Dart_NativeArguments args) {})); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&ready]() { ready.Signal(); })); + context.AddFfiNativeCallback( + "EchoKeyEvent", + CREATE_FFI_LAMBDA([](uint64_t change, uint64_t timestamp, + uint64_t physical, uint64_t logical, + uint64_t char_code, bool synthesized, + uint64_t device_type) {})); engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -3791,13 +3731,15 @@ TEST_F(EmbedderTest, BackToBackKeyEventResponsesCorrectlyInvoked) { EmbedderConfigBuilder builder(context); builder.SetSurface(DlISize(1, 1)); builder.SetDartEntrypoint("key_data_echo"); - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready](Dart_NativeArguments args) { ready.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&ready]() { ready.Signal(); })); - context.AddNativeCallback( - "EchoKeyEvent", CREATE_NATIVE_ENTRY([](Dart_NativeArguments args) {})); + context.AddFfiNativeCallback( + "EchoKeyEvent", + CREATE_FFI_LAMBDA([](uint64_t change, uint64_t timestamp, + uint64_t physical, uint64_t logical, + uint64_t char_code, bool synthesized, + uint64_t device_type) {})); engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -3880,10 +3822,9 @@ TEST_F(EmbedderTest, VsyncCallbackPostedIntoFuture) { vsync_latch.Signal(); }); }); - context.AddNativeCallback( - "SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - present_latch.Signal(); - })); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + present_latch.Signal(); + })); EmbedderConfigBuilder builder(context); builder.SetSurface(DlISize(1, 1)); @@ -3920,16 +3861,13 @@ TEST_F(EmbedderTest, CanScheduleFrame) { builder.SetSurface(DlISize(1, 1)); builder.SetDartEntrypoint("can_schedule_frame"); fml::AutoResetWaitableEvent latch; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddFfiNativeCallback( + "SignalNativeTest", CREATE_FFI_LAMBDA([&latch]() { latch.Signal(); })); fml::AutoResetWaitableEvent check_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeCount", - CREATE_NATIVE_ENTRY( - [&check_latch](Dart_NativeArguments args) { check_latch.Signal(); })); + CREATE_FFI_LAMBDA([&check_latch](int count) { check_latch.Signal(); })); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -4036,23 +3974,19 @@ TEST_F(EmbedderTest, CanSendPointer) { builder.SetDartEntrypoint("pointer_data_packet"); fml::AutoResetWaitableEvent ready_latch, count_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); - context.AddNativeCallback( - "SignalNativeCount", - CREATE_NATIVE_ENTRY([&count_latch](Dart_NativeArguments args) { - int count = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - ASSERT_EQ(count, 1); - count_latch.Signal(); - })); - context.AddNativeCallback( + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeCount", + CREATE_FFI_LAMBDA([&count_latch](int count) { + ASSERT_EQ(count, 1); + count_latch.Signal(); + })); + context.AddFfiNativeCallback( "SignalNativeMessage", - CREATE_NATIVE_ENTRY([&message_latch](Dart_NativeArguments args) { - auto message = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); + CREATE_FFI_LAMBDA([&message_latch](Dart_Handle message_handle) { + auto message = + tonic::DartConverter::FromDart(message_handle); ASSERT_EQ("PointerData(viewId: 0, x: 123.0, y: 456.0)", message); message_latch.Signal(); })); @@ -4086,23 +4020,19 @@ TEST_F(EmbedderTest, CanSendStylusPointerButtons) { builder.SetDartEntrypoint("pointer_data_packet_stylus_buttons"); fml::AutoResetWaitableEvent ready_latch, count_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); - context.AddNativeCallback( - "SignalNativeCount", - CREATE_NATIVE_ENTRY([&count_latch](Dart_NativeArguments args) { - int count = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); - EXPECT_EQ(count, 1); - count_latch.Signal(); - })); - context.AddNativeCallback( + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeCount", + CREATE_FFI_LAMBDA([&count_latch](int count) { + EXPECT_EQ(count, 1); + count_latch.Signal(); + })); + context.AddFfiNativeCallback( "SignalNativeMessage", - CREATE_NATIVE_ENTRY([&message_latch](Dart_NativeArguments args) { - auto message = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); + CREATE_FFI_LAMBDA([&message_latch](Dart_Handle message_handle) { + auto message = + tonic::DartConverter::FromDart(message_handle); EXPECT_EQ("buttons: 3", message); message_latch.Signal(); })); @@ -4140,15 +4070,14 @@ TEST_F(EmbedderTest, CanSendPointerEventWithViewId) { builder.SetDartEntrypoint("pointer_data_packet_view_id"); fml::AutoResetWaitableEvent ready_latch, add_view_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); - context.AddNativeCallback( + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); + context.AddFfiNativeCallback( "SignalNativeMessage", - CREATE_NATIVE_ENTRY([&message_latch](Dart_NativeArguments args) { - auto message = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); + CREATE_FFI_LAMBDA([&message_latch](Dart_Handle message_handle) { + auto message = + tonic::DartConverter::FromDart(message_handle); ASSERT_EQ("ViewID: 2", message); message_latch.Signal(); })); @@ -4202,15 +4131,14 @@ TEST_F(EmbedderTest, WindowMetricsEventDefaultsToImplicitView) { builder.SetDartEntrypoint("window_metrics_event_view_id"); fml::AutoResetWaitableEvent ready_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); - context.AddNativeCallback( + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); + context.AddFfiNativeCallback( "SignalNativeMessage", - CREATE_NATIVE_ENTRY([&message_latch](Dart_NativeArguments args) { - auto message = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); + CREATE_FFI_LAMBDA([&message_latch](Dart_Handle message_handle) { + auto message = + tonic::DartConverter::FromDart(message_handle); ASSERT_EQ("Changed: [0]", message); message_latch.Signal(); })); @@ -4243,16 +4171,15 @@ TEST_F(EmbedderTest, IgnoresWindowMetricsEventForUnknownView) { builder.SetDartEntrypoint("window_metrics_event_view_id"); fml::AutoResetWaitableEvent ready_latch, message_latch; - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeTest", - CREATE_NATIVE_ENTRY( - [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + CREATE_FFI_LAMBDA([&ready_latch]() { ready_latch.Signal(); })); - context.AddNativeCallback( + context.AddFfiNativeCallback( "SignalNativeMessage", - CREATE_NATIVE_ENTRY([&message_latch](Dart_NativeArguments args) { - auto message = tonic::DartConverter::FromDart( - Dart_GetNativeArgument(args, 0)); + CREATE_FFI_LAMBDA([&message_latch](Dart_Handle message_handle) { + auto message = + tonic::DartConverter::FromDart(message_handle); // Message latch should only be signaled once as the bad // view metric should be dropped by the engine. ASSERT_FALSE(message_latch.IsSignaledForTest()); @@ -4298,9 +4225,8 @@ TEST_F(EmbedderTest, RegisterChannelListener) { fml::AutoResetWaitableEvent latch; fml::AutoResetWaitableEvent latch2; bool listening = false; - context.AddNativeCallback( - "SignalNativeTest", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments) { latch.Signal(); })); + context.AddFfiNativeCallback("SignalNativeTest", + CREATE_FFI_LAMBDA([&]() { latch.Signal(); })); context.SetChannelUpdateCallback([&](const FlutterChannelUpdate* update) { EXPECT_STREQ(update->channel, "test/listen"); EXPECT_TRUE(update->listening); @@ -4344,10 +4270,10 @@ TEST_F(EmbedderTest, PlatformThreadIsolatesWithCustomPlatformTaskRunner) { // The test's Dart code will call this native function which overrides the // FFI resolver. After that, the Dart code will invoke the FFI function // using runOnPlatformThread. - context.AddNativeCallback( - "SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - Dart_SetFfiNativeResolver(Dart_RootLibrary(), ffi_resolver); - })); + context.AddFfiNativeCallback("SignalNativeTest", CREATE_FFI_LAMBDA([&]() { + Dart_SetFfiNativeResolver(Dart_RootLibrary(), + ffi_resolver); + })); auto platform_task_runner = CreateNewThread("test_platform_thread"); diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_vk_unittests.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_vk_unittests.cc index f1a4a0d5ff88c..f8737da4e5765 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_vk_unittests.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_vk_unittests.cc @@ -18,7 +18,7 @@ #include "flutter/shell/platform/embedder/tests/embedder_unittests_util.h" #include "flutter/testing/testing.h" -// CREATE_NATIVE_ENTRY is leaky by design +// CREATE_FFI_LAMBDA is leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) namespace flutter { diff --git a/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index 85f4bf0c07d46..4a83ad15c8cbf 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -4,28 +4,29 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ffi' as ffi; import 'dart:io' as io; import 'dart:typed_data' show ByteData, Float64List, Int32List, Uint8List; import 'dart:ui' as ui; // Signals a waiting latch in the native test. -@pragma('vm:external-name', 'Signal') +@ffi.Native(symbol: 'Signal') external void signal(); // Signals a waiting latch in the native test, passing a boolean value. -@pragma('vm:external-name', 'SignalBoolValue') +@ffi.Native(symbol: 'SignalBoolValue') external void signalBoolValue(bool value); // Signals a waiting latch in the native test, passing a string value. -@pragma('vm:external-name', 'SignalStringValue') +@ffi.Native(symbol: 'SignalStringValue') external void signalStringValue(String value); // Signals a waiting latch in the native test, which returns a value to the fixture. -@pragma('vm:external-name', 'SignalBoolReturn') +@ffi.Native(symbol: 'SignalBoolReturn') external bool signalBoolReturn(); // Notify the native test that the first frame has been scheduled. -@pragma('vm:external-name', 'NotifyFirstFrameScheduled') +@ffi.Native(symbol: 'NotifyFirstFrameScheduled') external void notifyFirstFrameScheduled(); void main() {} @@ -399,7 +400,7 @@ void mergedUIThread() { signal(); } -@pragma('vm:external-name', 'NotifyEngineId') +@ffi.Native(symbol: 'NotifyEngineId') external void notifyEngineId(int? handle); @pragma('vm:entry-point') diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc index 23daedc82ed14..3932b5e790f05 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -1023,9 +1023,8 @@ TEST_F(FlutterWindowsEngineTest, AccessibilityAnnouncement) { builder.SetDartEntrypoint("sendAccessibilityAnnouncement"); bool done = false; - auto native_entry = - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = CREATE_FFI_LAMBDA([&]() { done = true; }); + context.AddFfiNativeFunction("Signal", native_entry); EnginePtr engine{builder.RunHeadless()}; ASSERT_NE(engine, nullptr); @@ -1061,9 +1060,8 @@ TEST_F(FlutterWindowsEngineTest, AccessibilityAnnouncementHeadless) { builder.SetDartEntrypoint("sendAccessibilityAnnouncement"); bool done = false; - auto native_entry = - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = CREATE_FFI_LAMBDA([&]() { done = true; }); + context.AddFfiNativeFunction("Signal", native_entry); EnginePtr engine{builder.RunHeadless()}; ASSERT_NE(engine, nullptr); @@ -1087,9 +1085,8 @@ TEST_F(FlutterWindowsEngineTest, AccessibilityTooltip) { builder.SetDartEntrypoint("sendAccessibilityTooltipEvent"); bool done = false; - auto native_entry = - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = CREATE_FFI_LAMBDA([&]() { done = true; }); + context.AddFfiNativeFunction("Signal", native_entry); ViewControllerPtr controller{builder.Run()}; ASSERT_NE(controller, nullptr); @@ -1701,10 +1698,9 @@ TEST_F(FlutterWindowsEngineTest, MergedUIThread) { std::optional ui_thread_id; - auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - ui_thread_id = std::this_thread::get_id(); - }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = + CREATE_FFI_LAMBDA([&]() { ui_thread_id = std::this_thread::get_id(); }); + context.AddFfiNativeFunction("Signal", native_entry); EnginePtr engine{builder.RunHeadless()}; while (!ui_thread_id) { @@ -1737,9 +1733,8 @@ TEST_F(FlutterWindowsEngineTest, UpdateSemanticsMultiView) { // Setup: a signal for when we have send out all of our semantics updates bool done = false; - auto native_entry = - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = CREATE_FFI_LAMBDA([&]() { done = true; }); + context.AddFfiNativeFunction("Signal", native_entry); // Setup: Create the engine and two views + enable semantics EnginePtr engine{builder.RunHeadless()}; diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc index 14ede56af18ac..79ed380ffa6d9 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc @@ -133,9 +133,8 @@ TEST_F(WindowsTest, LaunchHeadlessEngine) { std::string view_ids; bool signaled = false; - context.AddNativeFunction( - "SignalStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto handle = Dart_GetNativeArgument(args, 0); + context.AddFfiNativeFunction( + "SignalStringValue", CREATE_FFI_LAMBDA([&](Dart_Handle handle) { ASSERT_FALSE(Dart_IsError(handle)); view_ids = tonic::DartConverter::FromDart(handle); signaled = true; @@ -219,9 +218,8 @@ TEST_F(WindowsTest, VerifyNativeFunction) { builder.SetDartEntrypoint("verifyNativeFunction"); bool signaled = false; - auto native_entry = - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { signaled = true; }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = CREATE_FFI_LAMBDA([&]() { signaled = true; }); + context.AddFfiNativeFunction("Signal", native_entry); ViewControllerPtr controller{builder.Run()}; ASSERT_NE(controller, nullptr); @@ -241,12 +239,11 @@ TEST_F(WindowsTest, VerifyNativeFunctionWithParameters) { bool bool_value = false; bool signaled = false; - auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto handle = Dart_GetNativeBooleanArgument(args, 0, &bool_value); - ASSERT_FALSE(Dart_IsError(handle)); + auto native_entry = CREATE_FFI_LAMBDA([&](bool value) { + bool_value = value; signaled = true; }); - context.AddNativeFunction("SignalBoolValue", native_entry); + context.AddFfiNativeFunction("SignalBoolValue", native_entry); ViewControllerPtr controller{builder.Run()}; ASSERT_NE(controller, nullptr); @@ -266,13 +263,12 @@ TEST_F(WindowsTest, PlatformExecutable) { std::string executable_name; bool signaled = false; - auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto handle = Dart_GetNativeArgument(args, 0); + auto native_entry = CREATE_FFI_LAMBDA([&](Dart_Handle handle) { ASSERT_FALSE(Dart_IsError(handle)); executable_name = tonic::DartConverter::FromDart(handle); signaled = true; }); - context.AddNativeFunction("SignalStringValue", native_entry); + context.AddFfiNativeFunction("SignalStringValue", native_entry); ViewControllerPtr controller{builder.Run()}; ASSERT_NE(controller, nullptr); @@ -293,19 +289,18 @@ TEST_F(WindowsTest, VerifyNativeFunctionWithReturn) { bool bool_value_to_return = true; int count = 2; - auto bool_return_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - Dart_SetBooleanReturnValue(args, bool_value_to_return); + auto bool_return_entry = CREATE_FFI_LAMBDA([&]() { --count; + return bool_value_to_return; }); - context.AddNativeFunction("SignalBoolReturn", bool_return_entry); + context.AddFfiNativeFunction("SignalBoolReturn", bool_return_entry); bool bool_value_passed = false; - auto bool_pass_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto handle = Dart_GetNativeBooleanArgument(args, 0, &bool_value_passed); - ASSERT_FALSE(Dart_IsError(handle)); + auto bool_pass_entry = CREATE_FFI_LAMBDA([&](bool value) { + bool_value_passed = value; --count; }); - context.AddNativeFunction("SignalBoolValue", bool_pass_entry); + context.AddFfiNativeFunction("SignalBoolValue", bool_pass_entry); ViewControllerPtr controller{builder.Run()}; ASSERT_NE(controller, nullptr); @@ -334,10 +329,9 @@ TEST_F(WindowsTest, NextFrameCallback) { WindowsConfigBuilder builder(context); builder.SetDartEntrypoint("drawHelloWorld"); - auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - captures.frame_scheduled_latch.Signal(); - }); - context.AddNativeFunction("NotifyFirstFrameScheduled", native_entry); + auto native_entry = + CREATE_FFI_LAMBDA([&]() { captures.frame_scheduled_latch.Signal(); }); + context.AddFfiNativeFunction("NotifyFirstFrameScheduled", native_entry); ViewControllerPtr controller{builder.Run()}; EXPECT_NE(controller, nullptr); @@ -814,9 +808,8 @@ TEST_F(WindowsTest, GetKeyboardStateHeadless) { builder.SetDartEntrypoint("sendGetKeyboardState"); std::atomic done = false; - context.AddNativeFunction( - "SignalStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto handle = Dart_GetNativeArgument(args, 0); + context.AddFfiNativeFunction( + "SignalStringValue", CREATE_FFI_LAMBDA([&](Dart_Handle handle) { ASSERT_FALSE(Dart_IsError(handle)); auto value = tonic::DartConverter::FromDart(handle); EXPECT_EQ(value, "Success"); @@ -843,13 +836,11 @@ TEST_F(WindowsTest, AddRemoveView) { builder.SetDartEntrypoint("onMetricsChangedSignalViewIds"); bool ready = false; - context.AddNativeFunction( - "Signal", - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { ready = true; })); + context.AddFfiNativeFunction("Signal", + CREATE_FFI_LAMBDA([&]() { ready = true; })); - context.AddNativeFunction( - "SignalStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - auto handle = Dart_GetNativeArgument(args, 0); + context.AddFfiNativeFunction( + "SignalStringValue", CREATE_FFI_LAMBDA([&](Dart_Handle handle) { ASSERT_FALSE(Dart_IsError(handle)); std::scoped_lock lock{mutex}; @@ -901,9 +892,8 @@ TEST_F(WindowsTest, EngineId) { builder.SetDartEntrypoint("testEngineId"); std::optional engineId; - context.AddNativeFunction( - "NotifyEngineId", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - const auto argument = Dart_GetNativeArgument(args, 0); + context.AddFfiNativeFunction( + "NotifyEngineId", CREATE_FFI_LAMBDA([&](Dart_Handle argument) { if (!Dart_IsNull(argument)) { const auto handle = tonic::DartConverter::FromDart(argument); engineId = handle; @@ -930,9 +920,8 @@ TEST_F(WindowsTest, EnableIAccessible) { // Setup: a signal for when we have send out all of our semantics updates bool done = false; - auto native_entry = - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = CREATE_FFI_LAMBDA([&]() { done = true; }); + context.AddFfiNativeFunction("Signal", native_entry); // Setup: Create a view ViewControllerPtr controller{builder.Run()}; @@ -978,9 +967,8 @@ TEST_F(WindowsTest, EnableIAccessibleEx) { // Setup: a signal for when we have send out all of our semantics updates bool done = false; - auto native_entry = - CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); - context.AddNativeFunction("Signal", native_entry); + auto native_entry = CREATE_FFI_LAMBDA([&]() { done = true; }); + context.AddFfiNativeFunction("Signal", native_entry); // Setup: Create a view ViewControllerPtr controller{builder.Run()}; diff --git a/engine/src/flutter/shell/platform/windows/testing/windows_test_context.cc b/engine/src/flutter/shell/platform/windows/testing/windows_test_context.cc index 30b62b59d7dbe..b0c3b07bfbb5c 100644 --- a/engine/src/flutter/shell/platform/windows/testing/windows_test_context.cc +++ b/engine/src/flutter/shell/platform/windows/testing/windows_test_context.cc @@ -40,6 +40,11 @@ void WindowsTestContext::AddNativeFunction(std::string_view name, native_resolver_->AddNativeCallback(std::string{name}, function); } +void WindowsTestContext::AddFfiNativeFunction(std::string_view name, + void* function) { + native_resolver_->AddFfiNativeCallback(std::string{name}, function); +} + fml::closure WindowsTestContext::GetRootIsolateCallback() { return [this]() { for (auto closure : this->isolate_create_callbacks_) { diff --git a/engine/src/flutter/shell/platform/windows/testing/windows_test_context.h b/engine/src/flutter/shell/platform/windows/testing/windows_test_context.h index f6e37ba2b49c0..14fb1b81fbbdb 100644 --- a/engine/src/flutter/shell/platform/windows/testing/windows_test_context.h +++ b/engine/src/flutter/shell/platform/windows/testing/windows_test_context.h @@ -44,6 +44,15 @@ class WindowsTestContext { // where `IdentifyingName` matches the |name| parameter to this method. void AddNativeFunction(std::string_view name, Dart_NativeFunction function); + // Registers an FFI function callable from Dart code in test fixtures. In + // the Dart test fixture, the associated function can be declared as: + // + // @Native(symbol: 'IdentifyingName') + // external ReturnType functionName(); + // + // where `IdentifyingName` matches the |name| parameter to this method. + void AddFfiNativeFunction(std::string_view name, void* function); + // Returns the root isolate create callback to register with the Flutter // runtime. fml::closure GetRootIsolateCallback(); diff --git a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc index 9c33e261b5c1b..55c255ab737d0 100644 --- a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc @@ -55,11 +55,10 @@ class WindowManagerTest : public WindowsTest { ASSERT_TRUE(engine_->Run("testWindowController")); bool signalled = false; - context.AddNativeFunction( - "Signal", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { - isolate_ = flutter::Isolate::Current(); - signalled = true; - })); + context.AddFfiNativeFunction("Signal", CREATE_FFI_LAMBDA([&]() { + isolate_ = flutter::Isolate::Current(); + signalled = true; + })); while (!signalled) { engine_->task_runner()->ProcessTasks(); } From 7c417dbcb1caf50d54fae28b1e9e16666035820f Mon Sep 17 00:00:00 2001 From: b-luk <97480502+b-luk@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:32:48 -0700 Subject: [PATCH 092/330] Rename and restructure the primitive_shape_test test to engine_integration_golden_test (#190581) Part of https://github.com/flutter/flutter/issues/190301 Renames the specific primitive_shape_test to a more generic engine_integration_golden_test. The original lib/main.dart of the test is renamed lib/primitive_shape_main.dart. We anticipate adding further tests with their own main.dart in lib/, and importing and using each one in integration_test/engine_integration_golden_test.dart. *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .ci.yaml | 4 ++-- TESTOWNERS | 2 +- ...=> windows_engine_integration_golden_test.dart} | 2 +- dev/devicelab/lib/tasks/integration_tests.dart | 6 +++--- .../engine_integration_golden_test/README.md | 14 ++++++++++++++ .../engine_integration_golden_test.dart} | 5 ++--- .../integration_test/flutter_test_config.dart | 2 +- .../lib/primitive_shape_main.dart} | 6 +++--- .../pubspec.yaml | 4 ++-- .../primitive_shape_test/README.md | 14 -------------- pubspec.yaml | 2 +- 11 files changed, 30 insertions(+), 31 deletions(-) rename dev/devicelab/bin/tasks/{windows_primitive_shape_golden_test.dart => windows_engine_integration_golden_test.dart} (89%) create mode 100644 dev/integration_tests/engine_integration_golden_test/README.md rename dev/integration_tests/{primitive_shape_test/integration_test/primitive_shape_test.dart => engine_integration_golden_test/integration_test/engine_integration_golden_test.dart} (79%) rename dev/integration_tests/{primitive_shape_test => engine_integration_golden_test}/integration_test/flutter_test_config.dart (86%) rename dev/integration_tests/{primitive_shape_test/lib/main.dart => engine_integration_golden_test/lib/primitive_shape_main.dart} (95%) rename dev/integration_tests/{primitive_shape_test => engine_integration_golden_test}/pubspec.yaml (66%) delete mode 100644 dev/integration_tests/primitive_shape_test/README.md diff --git a/.ci.yaml b/.ci.yaml index 31e8c2da76eeb..dd0ebf6faa1e6 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -7296,7 +7296,7 @@ targets: ] task_name: windows_desktop_impeller - - name: Windows windows_primitive_shape_golden_test + - name: Windows windows_engine_integration_golden_test recipe: devicelab/devicelab_drone presubmit: false bringup: true @@ -7309,7 +7309,7 @@ targets: {"dependency": "vs_build", "version": "version:vs2019"}, {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"} ] - task_name: windows_primitive_shape_golden_test + task_name: windows_engine_integration_golden_test - name: Windows texture_impeller_windows recipe: devicelab/devicelab_drone diff --git a/TESTOWNERS b/TESTOWNERS index 5af7f17425d06..027cfc027cf33 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -331,8 +331,8 @@ /dev/devicelab/bin/tasks/windowing_test_macos.dart @knopp @flutter/desktop /dev/devicelab/bin/tasks/windowing_test_windows.dart @mattkae @flutter/desktop /dev/devicelab/bin/tasks/windows_desktop_impeller.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/windows_engine_integration_golden_test.dart @b-luk @flutter/engine /dev/devicelab/bin/tasks/windows_home_scroll_perf__timeline_summary.dart @jonahwilliams @flutter/engine -/dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart @b-luk @flutter/engine /dev/devicelab/bin/tasks/windows_startup_test.dart @loic-sharma @flutter/desktop ## Host only framework tests diff --git a/dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart b/dev/devicelab/bin/tasks/windows_engine_integration_golden_test.dart similarity index 89% rename from dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart rename to dev/devicelab/bin/tasks/windows_engine_integration_golden_test.dart index 62e2559b9b354..804761cb74d33 100644 --- a/dev/devicelab/bin/tasks/windows_primitive_shape_golden_test.dart +++ b/dev/devicelab/bin/tasks/windows_engine_integration_golden_test.dart @@ -8,5 +8,5 @@ import 'package:flutter_devicelab/tasks/integration_tests.dart'; Future main() async { deviceOperatingSystem = DeviceOperatingSystem.windows; - await task(createPrimitiveShapeTest()); + await task(createEngineIntegrationGoldenTest()); } diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index 78a56b8a14965..e307cf20a1276 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -230,10 +230,10 @@ TaskFunction createWindowsStartupDriverTest({String? deviceIdOverride}) { ).call; } -TaskFunction createPrimitiveShapeTest() { +TaskFunction createEngineIntegrationGoldenTest() { return IntegrationTest( - '${flutterDirectory.path}/dev/integration_tests/primitive_shape_test', - 'integration_test/primitive_shape_test.dart', + '${flutterDirectory.path}/dev/integration_tests/engine_integration_golden_test', + 'integration_test/engine_integration_golden_test.dart', createPlatforms: ['windows'], ).call; } diff --git a/dev/integration_tests/engine_integration_golden_test/README.md b/dev/integration_tests/engine_integration_golden_test/README.md new file mode 100644 index 0000000000000..786c8b769045b --- /dev/null +++ b/dev/integration_tests/engine_integration_golden_test/README.md @@ -0,0 +1,14 @@ +# Engine Integration Golden Test + +This integration test suite validates engine related rendering tests using Skia Gold. + +## Running Locally +```sh +flutter test dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart -d +``` + +## Running via Devicelab on Windows +```sh +cd dev/devicelab +dart bin/run.dart -t windows_engine_integration_golden_test +``` diff --git a/dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart b/dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart similarity index 79% rename from dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart rename to dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart index da35e8d662e40..e097a0f28be33 100644 --- a/dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart +++ b/dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart @@ -2,19 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:engine_integration_golden_test/primitive_shape_main.dart' as primitive_shape; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:primitive_shape_test/main.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('renders primitive shapes', (WidgetTester tester) async { - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const primitive_shape.PrimitiveShapeApp()); await tester.pumpAndSettle(); - // Take a screenshot of the test canvas widget. await expectLater( find.byKey(const Key('primitive_shape_canvas')), matchesGoldenFile('primitive_shape_canvas_snapshot.png'), diff --git a/dev/integration_tests/primitive_shape_test/integration_test/flutter_test_config.dart b/dev/integration_tests/engine_integration_golden_test/integration_test/flutter_test_config.dart similarity index 86% rename from dev/integration_tests/primitive_shape_test/integration_test/flutter_test_config.dart rename to dev/integration_tests/engine_integration_golden_test/integration_test/flutter_test_config.dart index 2057b6ef9f76c..1098433c9a187 100644 --- a/dev/integration_tests/primitive_shape_test/integration_test/flutter_test_config.dart +++ b/dev/integration_tests/engine_integration_golden_test/integration_test/flutter_test_config.dart @@ -10,5 +10,5 @@ import 'package:flutter_goldens/flutter_goldens.dart' as flutter_goldens; /// On CI/LUCI, if `GOLDCTL` environment variable is present, screenshots taken with /// `matchesGoldenFile` will be uploaded to Skia Gold. Future testExecutable(FutureOr Function() testMain) async { - return flutter_goldens.testExecutable(testMain, namePrefix: 'primitive_shape'); + return flutter_goldens.testExecutable(testMain, namePrefix: 'engine_integration_golden'); } diff --git a/dev/integration_tests/primitive_shape_test/lib/main.dart b/dev/integration_tests/engine_integration_golden_test/lib/primitive_shape_main.dart similarity index 95% rename from dev/integration_tests/primitive_shape_test/lib/main.dart rename to dev/integration_tests/engine_integration_golden_test/lib/primitive_shape_main.dart index c69007b522a72..b8e306cf81197 100644 --- a/dev/integration_tests/primitive_shape_test/lib/main.dart +++ b/dev/integration_tests/engine_integration_golden_test/lib/primitive_shape_main.dart @@ -5,11 +5,11 @@ import 'package:flutter/material.dart'; void main() { - runApp(const MyApp()); + runApp(const PrimitiveShapeApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class PrimitiveShapeApp extends StatelessWidget { + const PrimitiveShapeApp({super.key}); @override Widget build(BuildContext context) { diff --git a/dev/integration_tests/primitive_shape_test/pubspec.yaml b/dev/integration_tests/engine_integration_golden_test/pubspec.yaml similarity index 66% rename from dev/integration_tests/primitive_shape_test/pubspec.yaml rename to dev/integration_tests/engine_integration_golden_test/pubspec.yaml index 1199f4e05d7f0..9f88f6084a79b 100644 --- a/dev/integration_tests/primitive_shape_test/pubspec.yaml +++ b/dev/integration_tests/engine_integration_golden_test/pubspec.yaml @@ -1,5 +1,5 @@ -name: primitive_shape_test -description: Integration test to capture primitive shape rendering snapshots and upload to Skia Gold. +name: engine_integration_golden_test +description: Integration tests that capture rendering snapshots and upload them to Skia Gold for engine-based render tests. publish_to: none environment: diff --git a/dev/integration_tests/primitive_shape_test/README.md b/dev/integration_tests/primitive_shape_test/README.md deleted file mode 100644 index 09b656b4e4340..0000000000000 --- a/dev/integration_tests/primitive_shape_test/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Primitive Shape Integration Test - -This integration test suite validates rendering of primitive canvas shapes. - -## Running Locally -```sh -flutter test dev/integration_tests/primitive_shape_test/integration_test/primitive_shape_test.dart -d -``` - -## Running via Devicelab on Windows -```sh -cd dev/devicelab -dart bin/run.dart -t windows_primitive_shape_golden_test -``` diff --git a/pubspec.yaml b/pubspec.yaml index f7b7952208d4c..d19d75af74729 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ workspace: - dev/integration_tests/data_asset_package - dev/integration_tests/deferred_components_test - dev/integration_tests/display_cutout_rotation + - dev/integration_tests/engine_integration_golden_test - dev/integration_tests/external_textures - dev/integration_tests/flavors - dev/integration_tests/flutter_gallery @@ -41,7 +42,6 @@ workspace: - dev/integration_tests/link_hook - dev/integration_tests/new_gallery - dev/integration_tests/platform_interaction - - dev/integration_tests/primitive_shape_test - dev/integration_tests/record_use_test_app - dev/integration_tests/record_use_test_package - dev/integration_tests/release_smoke_test From 3a1a7457ad7fa7f556a065d0f8cccc32400196ff Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Wed, 5 Aug 2026 15:11:04 -0400 Subject: [PATCH 093/330] fix(desktop): Keep an open log stream on desktop for integration testing. (#189192) When attempting to test multiple integration tests, the `DesktopLogReader` would cause a failure because `DesktopLogReader._inputController` was closed, so a new process couldn't use it. `DesktopLogReader` now keeps a persistent log reader per device that isn't closed on process termination, which fixes the issue for integration tests. However, `ProtocolDiscovery` requires the stream to close to detect that a launched process exited without providing a VM Service. This introduces `SingleLaunchLogReader`, which is specifically used for VM discovery, to satisfy that requirement. This is in line with how Android deals with the same issue. refs: #135673 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Ben Konyi --- .../flutter_tools/lib/src/desktop_device.dart | 85 +++++++--- .../lib/src/tester/flutter_tester.dart | 4 +- .../general.shard/desktop_device_test.dart | 152 ++++++++++++++++++ 3 files changed, 220 insertions(+), 21 deletions(-) diff --git a/packages/flutter_tools/lib/src/desktop_device.dart b/packages/flutter_tools/lib/src/desktop_device.dart index b622e3b34df79..d2ce93dc36c69 100644 --- a/packages/flutter_tools/lib/src/desktop_device.dart +++ b/packages/flutter_tools/lib/src/desktop_device.dart @@ -135,12 +135,12 @@ abstract class DesktopDevice extends Device { _runningProcesses.add(process); unawaited(process.exitCode.then((_) => _runningProcesses.remove(process))); - _deviceLogReader.initializeProcess(process); + _deviceLogReader.listenToProcessOutput(process); if (debuggingOptions.buildInfo.isRelease) { return LaunchResult.succeeded(); } final vmServiceDiscovery = ProtocolDiscovery.vmService( - _deviceLogReader, + SingleLaunchLogReader(_deviceLogReader.logLines, process.exitCode), devicePort: debuggingOptions.deviceVmServicePort, hostPort: debuggingOptions.hostVmServicePort, ipv6: debuggingOptions.ipv6, @@ -342,26 +342,29 @@ abstract class DesktopDevice extends Device { /// A log reader for desktop applications that delegates to a [Process] stdout /// and stderr streams. +/// +/// A single instance of this reader is kept for the lifetime of a +/// [DesktopDevice], returned by [DesktopDevice.getLogReader], so that +/// external callers (e.g. `flutter drive`, `flutter logs`) can subscribe to +/// [logLines] once and keep receiving output across multiple `startApp` +/// launches on the same device. Because of that, [logLines] is never closed +/// when a given `process` exits: `Device.dispose()` (and therefore +/// [dispose]) is called far more often than "the device is really done" — +/// e.g. once per test file for desktop integration tests — so this reader +/// deliberately has nothing for [dispose] to do, exactly like the upstream +/// implementation this is based on. (Mirrors +/// `CustomDeviceLogReader.listenToProcessOutput`, though that reader's +/// [dispose] closes it — its callers dispose it at true end-of-life only.) +/// +/// This reader is intentionally *not* used for a single launch's VM Service +/// discovery — see [SingleLaunchLogReader] for that. class DesktopLogReader extends DeviceLogReader { final _inputController = StreamController>.broadcast(); - /// Begin listening to the stdout and stderr streams of the provided [process]. - void initializeProcess(Process process) { - final StreamSubscription> stdoutSub = process.stdout.listen(_inputController.add); - final StreamSubscription> stderrSub = process.stderr.listen(_inputController.add); - final Future stdioFuture = Future.wait(>[ - stdoutSub.asFuture(), - stderrSub.asFuture(), - ]); - process.exitCode.whenComplete(() async { - // Wait for output to be fully processed. - await stdioFuture; - // The streams have already completed, so waiting for the stream - // cancellation to complete is not needed. - unawaited(stdoutSub.cancel()); - unawaited(stderrSub.cancel()); - await _inputController.close(); - }); + /// Adds the stdout and stderr streams of the provided [process] to [logLines]. + void listenToProcessOutput(Process process) { + process.stdout.listen(_inputController.add, onError: _inputController.addError); + process.stderr.listen(_inputController.add, onError: _inputController.addError); } @override @@ -380,3 +383,47 @@ class DesktopLogReader extends DeviceLogReader { @override Future provideVmService(FlutterVmService connectedVmService) async {} } + +/// A [DeviceLogReader] that mirrors `source` but closes [logLines] as soon +/// as `scope` completes. +/// +/// [ProtocolDiscovery] relies on [logLines] reaching "done" to detect that a +/// launched process exited without ever exposing a VM Service, so it can +/// give up instead of waiting forever. The device-scoped [DesktopLogReader] +/// returned by `getLogReader()` can't provide that signal — it must survive +/// across relaunches — so a fresh, throwaway [SingleLaunchLogReader] is +/// created for each [DesktopDevice.startApp] call instead, scoped to that +/// single process via `scope` (typically `process.exitCode`). This mirrors +/// how `AndroidDevice.startApp` avoids reusing its cached `getLogReader()` +/// singleton for the same reason, constructing a fresh `AdbLogReader` for VM +/// Service discovery on each launch. +class SingleLaunchLogReader extends DeviceLogReader { + SingleLaunchLogReader(Stream source, Future scope) { + _subscription = source.listen(_controller.add, onError: _controller.addError); + // Ignore how `scope` completed — only that it did — so an error from it + // (e.g. an unexpected failure reading `process.exitCode`) can't escape + // as an unhandled Future error. + scope.catchError((Object _, StackTrace _) {}).whenComplete(() { + unawaited(_subscription.cancel()); + unawaited(_controller.close()); + }); + } + + final _controller = StreamController.broadcast(); + late final StreamSubscription _subscription; + + @override + Stream get logLines => _controller.stream; + + @override + String get name => 'desktop (single launch)'; + + @override + void dispose() { + unawaited(_subscription.cancel()); + unawaited(_controller.close()); + } + + @override + Future provideVmService(FlutterVmService connectedVmService) async {} +} diff --git a/packages/flutter_tools/lib/src/tester/flutter_tester.dart b/packages/flutter_tools/lib/src/tester/flutter_tester.dart index f87c6573369a6..387f1030a0173 100644 --- a/packages/flutter_tools/lib/src/tester/flutter_tester.dart +++ b/packages/flutter_tools/lib/src/tester/flutter_tester.dart @@ -187,14 +187,14 @@ class FlutterTesterDevice extends Device { return LaunchResult.succeeded(); } + _logReader.listenToProcessOutput(_process!); vmServiceDiscovery = ProtocolDiscovery.vmService( - getLogReader(), + SingleLaunchLogReader(_logReader.logLines, _process!.exitCode), hostPort: debuggingOptions.hostVmServicePort, devicePort: debuggingOptions.deviceVmServicePort, ipv6: debuggingOptions.ipv6, logger: _logger, ); - _logReader.initializeProcess(_process!); final Uri? vmServiceUri = await vmServiceDiscovery.uri; if (vmServiceUri != null) { diff --git a/packages/flutter_tools/test/general.shard/desktop_device_test.dart b/packages/flutter_tools/test/general.shard/desktop_device_test.dart index 382c94131383d..3b0d792f5f201 100644 --- a/packages/flutter_tools/test/general.shard/desktop_device_test.dart +++ b/packages/flutter_tools/test/general.shard/desktop_device_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:fake_async/fake_async.dart'; import 'package:file/memory.dart'; @@ -22,6 +23,7 @@ import 'package:test/fake.dart'; import '../src/common.dart'; import '../src/context.dart'; +import '../src/fakes.dart'; void main() { group('Basic info', () { @@ -427,6 +429,156 @@ void main() { }); }, ); + + group('DesktopLogReader', () { + testWithoutContext('does not close logLines when a process exits', () async { + final logReader = DesktopLogReader(); + final receivedLines = []; + final StreamSubscription subscription = logReader.logLines.listen(receivedLines.add); + + final firstProcess = FakeProcess( + stdout: Stream>.fromIterable(>[utf8.encode('first line\n')]), + ); + logReader.listenToProcessOutput(firstProcess); + await firstProcess.exitCode; + await pumpEventQueue(); + + final secondProcess = FakeProcess( + stdout: Stream>.fromIterable(>[utf8.encode('second line\n')]), + ); + logReader.listenToProcessOutput(secondProcess); + await secondProcess.exitCode; + await pumpEventQueue(); + + expect(receivedLines, ['first line', 'second line']); + + await subscription.cancel(); + logReader.dispose(); + }); + + testWithoutContext('dispose does not close logLines', () async { + // We should not close loglines on dispose, since Device.dispose() is called + // once per launch (e.g. once per integration test file), not once at true end-of-life + final logReader = DesktopLogReader(); + final receivedLines = []; + final StreamSubscription subscription = logReader.logLines.listen(receivedLines.add); + + logReader.dispose(); + + final process = FakeProcess( + stdout: Stream>.fromIterable(>[utf8.encode('still here\n')]), + ); + logReader.listenToProcessOutput(process); + await process.exitCode; + await pumpEventQueue(); + + expect(receivedLines, ['still here']); + + await subscription.cancel(); + }); + }); + + group('SingleLaunchLogReader', () { + testWithoutContext('mirrors the source stream until scope completes', () async { + final sourceController = StreamController.broadcast(); + final scopeCompleter = Completer(); + final reader = SingleLaunchLogReader(sourceController.stream, scopeCompleter.future); + + final receivedLines = []; + final StreamSubscription subscription = reader.logLines.listen(receivedLines.add); + + sourceController.add('hello'); + await pumpEventQueue(); + expect(receivedLines, ['hello']); + + scopeCompleter.complete(); + await pumpEventQueue(); + + // No longer relayed once scope has completed. + sourceController.add('goodbye'); + await pumpEventQueue(); + expect(receivedLines, ['hello']); + + await subscription.cancel(); + await sourceController.close(); + }); + + testWithoutContext('closes logLines when scope completes without closing the source', () async { + final sourceController = StreamController.broadcast(); + final scopeCompleter = Completer(); + final reader = SingleLaunchLogReader(sourceController.stream, scopeCompleter.future); + + final Future done = reader.logLines.listen((String _) {}).asFuture(); + scopeCompleter.complete(); + await done; + + expect(sourceController.isClosed, false); + await sourceController.close(); + }); + }); + + testWithoutContext('getLogReader() observes multiple launches of startApp', () async { + final firstProcessCompleter = Completer(); + final processManager = FakeProcessManager.list([ + FakeCommand( + command: const ['debug'], + stdout: + 'The Dart VM service is listening on http://127.0.0.1/0\n' + 'first app output\n', + completer: firstProcessCompleter, + ), + FakeCommand( + command: const ['debug'], + stdout: + 'The Dart VM service is listening on http://127.0.0.1/1\n' + 'second app output\n', + completer: Completer(), + ), + ]); + final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); + final package = FakeApplicationPackage(); + + // Subscribe to the device's log reader up front, before any launch — + // this mirrors how `flutter drive`/`flutter logs` observe device + // output, and should keep working across the relaunch below. + final logLines = []; + final StreamSubscription subscription = device.getLogReader().logLines.listen( + logLines.add, + ); + + final LaunchResult firstResult = await device.startApp( + package, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + expect(firstResult.started, true); + expect(firstResult.vmServiceUri, Uri.parse('http://127.0.0.1/0')); + + // Regression test for https://github.com/flutter/flutter/issues/135673: + // let the first process actually exit before the second launch + // begins, which previously crashed with "Bad state: Cannot add new + // events after calling close". + firstProcessCompleter.complete(); + await pumpEventQueue(); + + // `flutter test`'s IntegrationTestTestDevice calls `device.dispose()` + // once per test file, not once at the very end of the whole run — + // this must not tear down anything the next launch needs. + await device.dispose(); + + final LaunchResult secondResult = await device.startApp( + package, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + expect(secondResult.started, true); + expect(secondResult.vmServiceUri, Uri.parse('http://127.0.0.1/1')); + + await pumpEventQueue(); + expect(logLines, containsAll(['first app output', 'second app output'])); + + await subscription.cancel(); + }); } FakeDesktopDevice setUpDesktopDevice({ From dcb306f347838e2ab02a068f7562743d96b29e1c Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 5 Aug 2026 15:29:05 -0400 Subject: [PATCH 094/330] [flutter_tools] Fix impellerc Windows Unicode crash and improve diagnostics (#190112) Fix impellerc crash on Windows when paths contain Unicode characters, and improve diagnostics by capturing stdout/stderr and adding helpful hints. Fixes #190233 --- .../src/flutter/impeller/compiler/switches.cc | 2 +- .../build_system/tools/shader_compiler.dart | 72 +++++- .../targets/shader_compiler_test.dart | 218 ++++++++++++++++++ 3 files changed, 285 insertions(+), 7 deletions(-) diff --git a/engine/src/flutter/impeller/compiler/switches.cc b/engine/src/flutter/impeller/compiler/switches.cc index 13369bc783659..ca845b57cd195 100644 --- a/engine/src/flutter/impeller/compiler/switches.cc +++ b/engine/src/flutter/impeller/compiler/switches.cc @@ -239,7 +239,7 @@ Switches::Switches(const fml::CommandLine& command_line) } auto dir = std::make_shared(fml::OpenDirectoryReadOnly( - *working_directory, include_dir_absolute.string().c_str())); + *working_directory, Utf8FromPath(include_dir_absolute).c_str())); if (!dir || !dir->is_valid()) { continue; } diff --git a/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart b/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart index 04db5b6d03f8a..1fa5cc91fa776 100644 --- a/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart +++ b/packages/flutter_tools/lib/src/build_system/tools/shader_compiler.dart @@ -265,7 +265,7 @@ class ShaderCompiler { final List shaderTargets = _shaderTargetsFromTargetPlatform(targetPlatform); final List cmd = makeImpellercCommand(shaderTargets); _logger.printTrace('impellerc command: $cmd'); - final ProcessResult result = await _runCommand(cmd); + ProcessResult result = await _runCommand(cmd); if (result.exitCode != 0) { // Maybe retry impellerc command without --sksl. if (!(shaderTargets.length > 1 && shaderTargets.contains('--sksl'))) { @@ -286,6 +286,7 @@ class ShaderCompiler { if (retryResult.exitCode != 0) { // Retry failed. _logger.printError('impellerc failure: ${retryResult.stderr}'); + result = retryResult; failure = true; } else { // Retry succeeded. Don't fail, but log a warning message and the sksl @@ -302,9 +303,15 @@ class ShaderCompiler { if (failure) { if (fatal) { + final String? hint = _getHelpfulHint(result, input, outputPath, impellerc.path); + final message = + 'Shader compilation of "${input.path}" to "$outputPath" ' + 'failed with exit code ${result.exitCode}.' + '${hint != null ? '\n$hint' : ''}'; throw ShaderCompilerException._( - 'Shader compilation of "${input.path}" to "$outputPath" ' - 'failed with exit code ${result.exitCode}.', + message, + stdout: result.stdout as String?, + stderr: result.stderr as String?, ); } return false; @@ -320,9 +327,46 @@ class ShaderCompiler { return true; } + String? _getHelpfulHint( + ProcessResult result, + File input, + String outputPath, + String impellercPath, + ) { + final int exitCode = result.exitCode; + if (_platform.isMacOS && exitCode == -9) { + return 'The shader compiler (impellerc) may have been blocked by macOS Gatekeeper or run out of memory (OOM).\n' + 'To resolve Gatekeeper issues, try running:\n' + ' xattr -d com.apple.quarantine "$impellercPath"'; + } + + final bool isWindowsAbort = _platform.isWindows && exitCode == 3; + final bool isPosixAbort = (_platform.isMacOS || _platform.isLinux) && exitCode == -6; + + if (isWindowsAbort || isPosixAbort) { + var hint = 'The shader compiler (impellerc) aborted during compilation.'; + if (_platform.isWindows) { + final bool hasNonAscii = + RegExp(r'[^\x00-\x7F]').hasMatch(input.path) || + RegExp(r'[^\x00-\x7F]').hasMatch(outputPath); + if (hasNonAscii) { + hint += + '\nWarning: The path contains non-ASCII characters, which is known to cause crashes on Windows.\n' + 'Try moving your project to a path containing only ASCII characters.'; + } + } + return hint; + } + return null; + } + Future _runCommand(List command) async { try { - return await _processManager.run(command, stderrEncoding: utf8); + return await _processManager.run( + command, + stdoutEncoding: utf8AllowMalformed, + stderrEncoding: utf8AllowMalformed, + ); } on ProcessException catch (e) { if (_isBlockedBySecurityPolicy(e)) { throw _SecurityPolicyBlockException(e); @@ -365,10 +409,26 @@ class _SecurityPolicyBlockException implements Exception { } class ShaderCompilerException implements Exception { - ShaderCompilerException._(this.message); + ShaderCompilerException._(this.message, {this.stdout, this.stderr}); final String message; + final String? stdout; + final String? stderr; @override - String toString() => 'ShaderCompilerException: $message\n\n'; + String toString() { + final buffer = StringBuffer(); + buffer.write('ShaderCompilerException: $message\n'); + final String? stdout = this.stdout; + if (stdout != null && stdout.trim().isNotEmpty) { + buffer.writeln('Stdout:'); + buffer.writeln(stdout.trim()); + } + final String? stderr = this.stderr; + if (stderr != null && stderr.trim().isNotEmpty) { + buffer.writeln('Stderr:'); + buffer.writeln(stderr.trim()); + } + return buffer.toString(); + } } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart index 94c17c233a0f1..600828695aa20 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/shader_compiler_test.dart @@ -244,6 +244,8 @@ void main() { '"/output/shaders/my_shader.frag" failed with exit code 1.', ), ); + expect(e.stdout, 'impellerc stdout'); + expect(e.stderr, 'impellerc stderr'); } expect(fileSystem.file(outputPath).existsSync(), false); @@ -1082,4 +1084,220 @@ void main() { expect(headerLine.allMatches(logger.errorText).length, 2); }); }); + + group('ShaderCompiler hints and diagnostics', () { + Future expectShaderCompilerException({ + required ShaderCompiler shaderCompiler, + required String inputPath, + required String outputPath, + required List matchers, + }) async { + await expectLater( + shaderCompiler.compileShader( + input: fileSystem.file(inputPath), + outputPath: outputPath, + targetPlatform: TargetPlatform.web_javascript, + ), + throwsA( + isA().having( + (ShaderCompilerException e) => e.toString(), + 'toString()', + allOf(matchers), + ), + ), + ); + } + + testWithoutContext('macOS and exit code -9 adds Gatekeeper/OOM hint', () async { + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=$outputPath', + '--spirv=$outputSpirvPath', + '--input=$notFragPath', + '--input-type=frag', + '--include=$fragDir', + '--include=$shaderLibDir', + ], + exitCode: -9, + ), + ]); + final shaderCompiler = ShaderCompiler( + processManager: processManager, + logger: logger, + fileSystem: fileSystem, + artifacts: artifacts, + platform: FakePlatform(operatingSystem: 'macos'), + ); + + await expectShaderCompilerException( + shaderCompiler: shaderCompiler, + inputPath: notFragPath, + outputPath: outputPath, + matchers: [ + contains('blocked by macOS Gatekeeper or run out of memory (OOM)'), + contains('xattr -d com.apple.quarantine'), + ], + ); + }); + + testWithoutContext('macOS and exit code -6 adds abort hint', () async { + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=$outputPath', + '--spirv=$outputSpirvPath', + '--input=$notFragPath', + '--input-type=frag', + '--include=$fragDir', + '--include=$shaderLibDir', + ], + exitCode: -6, + ), + ]); + final shaderCompiler = ShaderCompiler( + processManager: processManager, + logger: logger, + fileSystem: fileSystem, + artifacts: artifacts, + platform: FakePlatform(operatingSystem: 'macos'), + ); + + await expectShaderCompilerException( + shaderCompiler: shaderCompiler, + inputPath: notFragPath, + outputPath: outputPath, + matchers: [ + contains('The shader compiler (impellerc) aborted during compilation.'), + ], + ); + }); + + testWithoutContext('Linux and exit code -6 adds abort hint', () async { + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=$outputPath', + '--spirv=$outputSpirvPath', + '--input=$notFragPath', + '--input-type=frag', + '--include=$fragDir', + '--include=$shaderLibDir', + ], + exitCode: -6, + ), + ]); + final shaderCompiler = ShaderCompiler( + processManager: processManager, + logger: logger, + fileSystem: fileSystem, + artifacts: artifacts, + platform: FakePlatform(), + ); + + await expectShaderCompilerException( + shaderCompiler: shaderCompiler, + inputPath: notFragPath, + outputPath: outputPath, + matchers: [ + contains('The shader compiler (impellerc) aborted during compilation.'), + ], + ); + }); + + testWithoutContext('Windows and exit code 3 adds abort hint', () async { + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=$outputPath', + '--spirv=$outputSpirvPath', + '--input=$notFragPath', + '--input-type=frag', + '--include=$fragDir', + '--include=$shaderLibDir', + ], + exitCode: 3, + ), + ]); + final shaderCompiler = ShaderCompiler( + processManager: processManager, + logger: logger, + fileSystem: fileSystem, + artifacts: artifacts, + platform: FakePlatform(operatingSystem: 'windows'), + ); + + await expectShaderCompilerException( + shaderCompiler: shaderCompiler, + inputPath: notFragPath, + outputPath: outputPath, + matchers: [ + contains('The shader compiler (impellerc) aborted during compilation.'), + isNot(contains('Warning: The path contains non-ASCII characters')), + ], + ); + }); + + testWithoutContext( + 'Windows and exit code 3 with Unicode path adds Unicode path warning ' + '(regression test for https://github.com/flutter/flutter/issues/190233)', + () async { + const unicodeFragPath = '/shaders/my_shåder.frag'; + const unicodeOutputPath = '/output/shaders/my_shåder.frag'; + const unicodeOutputSpirvPath = '/output/shaders/my_shåder.frag.spirv'; + fileSystem.file(unicodeFragPath).createSync(recursive: true); + + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=$unicodeOutputPath', + '--spirv=$unicodeOutputSpirvPath', + '--input=$unicodeFragPath', + '--input-type=frag', + '--include=$fragDir', + '--include=$shaderLibDir', + ], + exitCode: 3, + ), + ]); + final shaderCompiler = ShaderCompiler( + processManager: processManager, + logger: logger, + fileSystem: fileSystem, + artifacts: artifacts, + platform: FakePlatform(operatingSystem: 'windows'), + ); + + await expectShaderCompilerException( + shaderCompiler: shaderCompiler, + inputPath: unicodeFragPath, + outputPath: unicodeOutputPath, + matchers: [ + contains('The shader compiler (impellerc) aborted during compilation.'), + contains('Warning: The path contains non-ASCII characters'), + ], + ); + }, + ); + }); } From b0868b355b25a2ee52aabb984a2a855fe37ddd08 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 5 Aug 2026 13:20:06 -0700 Subject: [PATCH 095/330] [Flutter GPU] Ignore redundant render pass state assignments (#190379) Related to #189899. `RenderPass` memoizes its built pipeline behind a dirty flag, but the fixed-function setters all assign through `GetPipelineDescriptor`, which dirties unconditionally. Callers re-send the same cull mode, winding order, primitive type, and polygon mode ahead of most draws, and rebind the same pipeline, so the memo was being invalidated on nearly every draw. These setters now compare before assigning and leave the state clean when nothing changed. --- engine/src/flutter/lib/gpu/render_pass.cc | 59 +++++++++++++----- engine/src/flutter/lib/gpu/render_pass.h | 12 ++++ .../flutter/lib/gpu/render_pass_unittests.cc | 60 ++++++++++++++++++- 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/engine/src/flutter/lib/gpu/render_pass.cc b/engine/src/flutter/lib/gpu/render_pass.cc index f085424e27f07..7a0347923b6ce 100644 --- a/engine/src/flutter/lib/gpu/render_pass.cc +++ b/engine/src/flutter/lib/gpu/render_pass.cc @@ -105,12 +105,53 @@ bool RenderPass::Begin(flutter::gpu::CommandBuffer& command_buffer) { } void RenderPass::SetPipeline(fml::RefPtr pipeline) { + if (render_pipeline_.get() == pipeline.get()) { + return; + } pipeline_state_dirty_ = true; // On debug this makes a difference, but not on release builds. // NOLINTNEXTLINE(performance-move-const-arg) render_pipeline_ = std::move(pipeline); } +// The setters below assign through the pipeline descriptor directly rather +// than through GetPipelineDescriptor, which would dirty the state +// unconditionally. Callers re-send the same fixed-function state ahead of +// most draws, and dirtying on a redundant assignment would rebuild the +// pipeline for every one of them. + +void RenderPass::SetCullMode(impeller::CullMode mode) { + if (pipeline_descriptor_.GetCullMode() == mode) { + return; + } + pipeline_descriptor_.SetCullMode(mode); + pipeline_state_dirty_ = true; +} + +void RenderPass::SetWindingOrder(impeller::WindingOrder order) { + if (pipeline_descriptor_.GetWindingOrder() == order) { + return; + } + pipeline_descriptor_.SetWindingOrder(order); + pipeline_state_dirty_ = true; +} + +void RenderPass::SetPrimitiveType(impeller::PrimitiveType type) { + if (pipeline_descriptor_.GetPrimitiveType() == type) { + return; + } + pipeline_descriptor_.SetPrimitiveType(type); + pipeline_state_dirty_ = true; +} + +void RenderPass::SetPolygonMode(impeller::PolygonMode mode) { + if (pipeline_descriptor_.GetPolygonMode() == mode) { + return; + } + pipeline_descriptor_.SetPolygonMode(mode); + pipeline_state_dirty_ = true; +} + void RenderPass::ClearBindings() { vertex_uniform_bindings.clear(); vertex_texture_bindings.clear(); @@ -728,36 +769,26 @@ void InternalFlutterGpu_RenderPass_SetStencilConfig( void InternalFlutterGpu_RenderPass_SetCullMode( flutter::gpu::RenderPass* wrapper, int cull_mode) { - impeller::PipelineDescriptor& pipeline_descriptor = - wrapper->GetPipelineDescriptor(); - pipeline_descriptor.SetCullMode(flutter::gpu::ToImpellerCullMode(cull_mode)); + wrapper->SetCullMode(flutter::gpu::ToImpellerCullMode(cull_mode)); } void InternalFlutterGpu_RenderPass_SetPrimitiveType( flutter::gpu::RenderPass* wrapper, int primitive_type) { - impeller::PipelineDescriptor& pipeline_descriptor = - wrapper->GetPipelineDescriptor(); - pipeline_descriptor.SetPrimitiveType( + wrapper->SetPrimitiveType( flutter::gpu::ToImpellerPrimitiveType(primitive_type)); } void InternalFlutterGpu_RenderPass_SetWindingOrder( flutter::gpu::RenderPass* wrapper, int winding_order) { - impeller::PipelineDescriptor& pipeline_descriptor = - wrapper->GetPipelineDescriptor(); - pipeline_descriptor.SetWindingOrder( - flutter::gpu::ToImpellerWindingOrder(winding_order)); + wrapper->SetWindingOrder(flutter::gpu::ToImpellerWindingOrder(winding_order)); } void InternalFlutterGpu_RenderPass_SetPolygonMode( flutter::gpu::RenderPass* wrapper, int polygon_mode) { - impeller::PipelineDescriptor& pipeline_descriptor = - wrapper->GetPipelineDescriptor(); - pipeline_descriptor.SetPolygonMode( - flutter::gpu::ToImpellerPolygonMode(polygon_mode)); + wrapper->SetPolygonMode(flutter::gpu::ToImpellerPolygonMode(polygon_mode)); } bool InternalFlutterGpu_RenderPass_Draw(flutter::gpu::RenderPass* wrapper, diff --git a/engine/src/flutter/lib/gpu/render_pass.h b/engine/src/flutter/lib/gpu/render_pass.h index 154558ab906ad..cf3914e8544bd 100644 --- a/engine/src/flutter/lib/gpu/render_pass.h +++ b/engine/src/flutter/lib/gpu/render_pass.h @@ -54,6 +54,18 @@ class RenderPass : public RefCountedDartWrappable { void SetPipeline(fml::RefPtr pipeline); + /// Set the face culling mode for subsequent draws. + void SetCullMode(impeller::CullMode mode); + + /// Set the front-face winding order for subsequent draws. + void SetWindingOrder(impeller::WindingOrder order); + + /// Set the primitive topology for subsequent draws. + void SetPrimitiveType(impeller::PrimitiveType type); + + /// Set the polygon fill mode for subsequent draws. + void SetPolygonMode(impeller::PolygonMode mode); + void ClearBindings(); /// Append a draw to the underlying render pass. [element_count] is the diff --git a/engine/src/flutter/lib/gpu/render_pass_unittests.cc b/engine/src/flutter/lib/gpu/render_pass_unittests.cc index 5a5b22045aea6..d453bc9b63841 100644 --- a/engine/src/flutter/lib/gpu/render_pass_unittests.cc +++ b/engine/src/flutter/lib/gpu/render_pass_unittests.cc @@ -6,11 +6,27 @@ #include "gtest/gtest.h" +#include "flutter/lib/gpu/render_pipeline.h" +#include "flutter/lib/gpu/shader.h" #include "fml/memory/ref_ptr.h" namespace flutter::gpu { namespace { +fml::RefPtr MakeShader(impeller::ShaderStage stage) { + return Shader::Make("library", "Entrypoint", stage, + /*code_mapping=*/nullptr, /*inputs=*/{}, /*layouts=*/{}, + /*uniform_structs=*/{}, /*uniform_textures=*/{}, + /*descriptor_set_layouts=*/{}); +} + +fml::RefPtr MakeRenderPipeline() { + auto vertex = MakeShader(impeller::ShaderStage::kVertex); + auto fragment = MakeShader(impeller::ShaderStage::kFragment); + return fml::MakeRefCounted(vertex, fragment, + vertex->CreateVertexDescriptor()); +} + // Regression test for https://github.com/flutter/flutter/issues/188712: // SetDepthWriteEnable must honor its argument. It previously ignored the // argument and always enabled depth writes, so disabling depth writes (for @@ -48,7 +64,49 @@ TEST(FlutterGpuRenderPassTest, PipelineStateMutationsMarkStateDirty) { EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); render_pass->ClearPipelineStateDirtyForTesting(); - render_pass->SetPipeline(nullptr); + render_pass->SetCullMode(impeller::CullMode::kBackFace); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + render_pass->SetWindingOrder(impeller::WindingOrder::kCounterClockwise); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + render_pass->SetPrimitiveType(impeller::PrimitiveType::kTriangleStrip); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + render_pass->SetPolygonMode(impeller::PolygonMode::kLine); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); + + render_pass->ClearPipelineStateDirtyForTesting(); + render_pass->SetPipeline(MakeRenderPipeline()); + EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); +} + +// Callers re-send the same fixed-function state and rebind the same pipeline +// ahead of most draws. Dirtying on those redundant assignments would rebuild +// the pipeline for every draw and leave the memoization doing nothing. +TEST(FlutterGpuRenderPassTest, RedundantPipelineStateAssignmentsAreIgnored) { + auto render_pass = fml::MakeRefCounted(); + auto pipeline = MakeRenderPipeline(); + + render_pass->SetCullMode(impeller::CullMode::kBackFace); + render_pass->SetWindingOrder(impeller::WindingOrder::kCounterClockwise); + render_pass->SetPrimitiveType(impeller::PrimitiveType::kTriangleStrip); + render_pass->SetPolygonMode(impeller::PolygonMode::kLine); + render_pass->SetPipeline(pipeline); + + render_pass->ClearPipelineStateDirtyForTesting(); + render_pass->SetCullMode(impeller::CullMode::kBackFace); + render_pass->SetWindingOrder(impeller::WindingOrder::kCounterClockwise); + render_pass->SetPrimitiveType(impeller::PrimitiveType::kTriangleStrip); + render_pass->SetPolygonMode(impeller::PolygonMode::kLine); + render_pass->SetPipeline(pipeline); + EXPECT_FALSE(render_pass->IsPipelineStateDirtyForTesting()); + + // A real change still dirties. + render_pass->SetCullMode(impeller::CullMode::kFrontFace); EXPECT_TRUE(render_pass->IsPipelineStateDirtyForTesting()); } From 07ab4d3f9b9cf22b1349ff6cc5560cec69df5261 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:51:47 -0700 Subject: [PATCH 096/330] Revert: Enable Gradle cache for a single test target (#190629) Reverts: [Enable Gradle cache for a single test target](https://github.com/flutter/flutter/pull/190474) Initiated by: @mboetger Reason for reverting: broke the tree with spaces. Original PR Author: @mboetger Reviewed By: @jtmcdole The original PR description is provided below: Enables Gradle CI Cache for single test target. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. --- .ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index dd0ebf6faa1e6..50e3a6036a943 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -7162,8 +7162,7 @@ targets: [ {"dependency": "android_sdk", "version": "version:36v4"}, {"dependency": "open_jdk", "version": "version:21"}, - {"dependency": "vs_build", "version": "version:vs2019"}, - {"dependency": "gradle_dists", "version": "8.4-bin, 8.13-rc-1-bin, 8.14-bin, 9.3.1-bin, 9.3.1-all"} + {"dependency": "vs_build", "version": "version:vs2019"} ] shard: tool_tests_commands subshard: 2_2 From 4c5bf76f361255b75ea7ad5b40471f188ede2905 Mon Sep 17 00:00:00 2001 From: Martin Kustermann Date: Wed, 5 Aug 2026 23:00:36 +0200 Subject: [PATCH 097/330] Use dart2wasm app.support.js expression with `js-string` builtin requirement (#183835) This changes flutter web's detection of wasm compatibility to be the expression emitted by dart2wasm. The expression was generated by passing `--require-js-string-builtin` to the dart2wasm compiler. Newer versions of browsers all support the `js-string` builtins nowadays. Apps running on browsers without `js-string` builtin support will not run in a performant way, so we'd not want to run the wasm version on them anyway. This is a preparation for dart2wasm to require the `js-string` builtin by default (i.e. we'll remove the `--require-js-string-builtin` flag and also remove the polyfill for `js-string` builtins). See [0] [0] https://dart-review.googlesource.com/c/sdk/+/488840 For reference of Safari testing on Dart: * [cl/347283](https://dart-review.googlesource.com/c/sdk/+/347283), [cl/348601](https://dart-review.googlesource.com/c/sdk/+/348601), [cl/348560](https://dart-review.googlesource.com/c/sdk/+/348560), [cl/371542](https://dart-review.googlesource.com/c/sdk/+/371542) added JavaScriptCore testing (command line version of Safari's JS engine) * [cl/426461](https://dart-review.googlesource.com/c/sdk/+/426461), [cl/501501](https://dart-review.googlesource.com/c/sdk/+/501501), [cl/501480](https://dart-review.googlesource.com/501480) added / will add Safari browser testing to Dart CI Co-authored-by: Mouad Debbar --- .../flutter_js/src/browser_environment.js | 20 +++++++++++-------- .../lib/web_ui/flutter_js/src/loader.js | 2 +- .../lib/web_ui/flutter_js/src/types.d.ts | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/flutter_js/src/browser_environment.js b/engine/src/flutter/lib/web_ui/flutter_js/src/browser_environment.js index 24d5e3d8de593..99d98aa3cdcf2 100644 --- a/engine/src/flutter/lib/web_ui/flutter_js/src/browser_environment.js +++ b/engine/src/flutter/lib/web_ui/flutter_js/src/browser_environment.js @@ -53,13 +53,17 @@ const hasTextCluster = () => { return (typeof window.TextCluster !== "undefined"); } -const supportsWasmGC = () => { - // This attempts to instantiate a wasm module that only will validate if the - // final WasmGC spec is implemented in the browser. - // - // Copied from https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js - const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]; - return WebAssembly.validate(new Uint8Array(bytes)); +const supportsDart2Wasm = () => { + // The `.support.js` expression emitted by + // ``` + // % dart compile wasm \ + // --extra-compiler-option=--require-js-string-builtin \ + // -o hello.wasm \ + // hello.dart + // % cat hello.support.js + // ``` + // It checks suport for Wasm GC, SIMD and `js-string` builtins. + return (WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,95,1,120,0]))&&WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]))&&!WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,23,1,14,119,97,115,109,58,106,115,45,115,116,114,105,110,103,4,99,97,115,116,0,0]),{"builtins":["js-string"]})); } const detectWebGLVersion = () => { @@ -88,7 +92,7 @@ export const browserEnvironment = { hasImageCodecs: hasImageCodecs(), hasChromiumBreakIterators: hasChromiumBreakIterators(), hasTextCluster: hasTextCluster(), - supportsWasmGC: supportsWasmGC(), + supportsDart2Wasm: supportsDart2Wasm(), crossOriginIsolated: window.crossOriginIsolated, webGLVersion: detectWebGLVersion(), isChromeExtension: isChromeExtension(), diff --git a/engine/src/flutter/lib/web_ui/flutter_js/src/loader.js b/engine/src/flutter/lib/web_ui/flutter_js/src/loader.js index 7fa689e7cb99d..b8a2fcd4a8968 100644 --- a/engine/src/flutter/lib/web_ui/flutter_js/src/loader.js +++ b/engine/src/flutter/lib/web_ui/flutter_js/src/loader.js @@ -10,7 +10,7 @@ import { loadCanvasKit } from './canvaskit_loader.js'; import { loadSkwasm } from './skwasm_loader.js'; import { getCanvaskitBaseUrl } from './utils.js'; -const supportsDart2Wasm = browserEnvironment.supportsWasmGC; +const supportsDart2Wasm = browserEnvironment.supportsDart2Wasm; /** * The public interface of _flutter.loader. Exposes two methods: diff --git a/engine/src/flutter/lib/web_ui/flutter_js/src/types.d.ts b/engine/src/flutter/lib/web_ui/flutter_js/src/types.d.ts index dd17c9b00d210..521a0beddc2e2 100644 --- a/engine/src/flutter/lib/web_ui/flutter_js/src/types.d.ts +++ b/engine/src/flutter/lib/web_ui/flutter_js/src/types.d.ts @@ -39,7 +39,7 @@ export interface BrowserEnvironment { browserEngine: BrowserEngine; hasImageCodecs: boolean; hasChromiumBreakIterators: boolean; - supportsWasmGC: boolean; + supportsDart2Wasm: boolean; crossOriginIsolated: boolean; webGLVersion: number; isChromeExtension: boolean; From 9b8cb9c4df6c38345e9b96ec70eab6afd83c7af4 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 5 Aug 2026 18:31:07 -0400 Subject: [PATCH 098/330] Remove --disable-dev-shm-usage from Chrome launch args (#190470) Testing CI stability without --disable-dev-shm-usage flag. --- packages/flutter_tools/lib/src/test/flutter_web_platform.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart index 5a31e32024443..ef53f5cd0b36c 100644 --- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart @@ -704,7 +704,6 @@ window.\$dartLoader.loader.nextAttempt(); completer.future, headless: !_config.pauseAfterLoad, logger: _logger, - webBrowserFlags: [if (useWasm) '--disable-dev-shm-usage'], ); } From c6bde29465d13c4e228c267b859e3715a67631ea Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:06:49 -0700 Subject: [PATCH 099/330] Migrate platform_view example to material_ui (#190309) Migrates the platform_view example to import material_ui. Related to https://github.com/flutter/flutter/issues/190093. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- dev/bots/check_examples_cross_imports.dart | 1 - examples/platform_view/lib/main.dart | 2 +- examples/platform_view/pubspec.yaml | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/bots/check_examples_cross_imports.dart b/dev/bots/check_examples_cross_imports.dart index c0bbe0a117a00..b19434b88aec2 100644 --- a/dev/bots/check_examples_cross_imports.dart +++ b/dev/bots/check_examples_cross_imports.dart @@ -585,7 +585,6 @@ class ExamplesCrossImportChecker { 'examples/multiple_windows/lib/app/window_settings_dialog.dart', 'examples/multiple_windows/lib/main.dart', 'examples/multiple_windows/test/multiple_windows_test.dart', - 'examples/platform_view/lib/main.dart', 'examples/splash/lib/main.dart', 'examples/splash/test/splash_test.dart', 'examples/texture/lib/main.dart', diff --git a/examples/platform_view/lib/main.dart b/examples/platform_view/lib/main.dart index d57ed3abbc789..2957fd40f9c5c 100644 --- a/examples/platform_view/lib/main.dart +++ b/examples/platform_view/lib/main.dart @@ -4,8 +4,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:material_ui/material_ui.dart'; void main() { runApp(const PlatformView()); diff --git a/examples/platform_view/pubspec.yaml b/examples/platform_view/pubspec.yaml index 8775a81f849c8..09307118be879 100644 --- a/examples/platform_view/pubspec.yaml +++ b/examples/platform_view/pubspec.yaml @@ -8,6 +8,7 @@ resolution: workspace dependencies: flutter: sdk: flutter + material_ui: ^0.0.2 flutter: @@ -16,4 +17,4 @@ flutter: assets: - assets/flutter-mark-square-64.png -# PUBSPEC CHECKSUM: h86dcv +# PUBSPEC CHECKSUM: dmrod6 From 2f715d63030a1cd423993f85c2100d5a36ce59a9 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:10:25 -0700 Subject: [PATCH 100/330] Make `InternalFlutterSwift` & `InternalFlutterSwiftCommon` internal for real (#190494) Marking types and type members internal prevents the swift compiler from marking the corresponding symbols (in the case of `@objc`, both the Swift and Objc symbols) visible. ~Unfortunately since the CI is still on Swift 6.2, so the ` -emit-clang-header-min-access` flag is not available. As a result the `engine/src/out/host_debug_unopt_arm64/gen/flutter/shell/platform/darwin/common/test_utils_swift/test_utils_swift.h` file swift compiler emits doesn't actually include the interface of `StringOutputWriter` since it's internal. The only workaround I found was to manually add the interface.~ Adding a bridging header fixed that. With these changes, when I build the iOS `Flutter.framework` with `enable-testing` set to false, the only visible swift symbols are from a class marked `@usableFromInline` (`_FlutterTraceScope`). And as gemini suggested I removed the `@inlinable` from that class. So no Swift symbols or internal objc symbols: ```bash nm -gUm out/ios_debug_sim_unopt_arm64/Flutter.framework/Flutter 00000000038d64b0 (__DATA,__common) external _FlutterEndOfEventStream 00000000038d64a8 (__DATA,__common) external _FlutterMethodNotImplemented 00000000037d5838 (__DATA_CONST,__const) external _FlutterSemanticsUpdateNotification 00000000037d5848 (__DATA_CONST,__const) external _FlutterViewControllerHideHomeIndicator 00000000037d5850 (__DATA_CONST,__const) external _FlutterViewControllerShowHomeIndicator 00000000037d5840 (__DATA_CONST,__const) external _FlutterViewControllerWillDealloc 00000000019bc8c4 (__TEXT,__text) external _InternalFlutterGpu_CommandBuffer_CopyBufferToTexture 00000000019bc9a8 (__TEXT,__text) external _InternalFlutterGpu_CommandBuffer_CopyTextureToBuffer 00000000019bca50 (__TEXT,__text) external _InternalFlutterGpu_CommandBuffer_CopyTextureToTexture 00000000019bc410 (__TEXT,__text) external _InternalFlutterGpu_CommandBuffer_Initialize 00000000019bc5b0 (__TEXT,__text) external _InternalFlutterGpu_CommandBuffer_Submit 00000000019c6df8 (__TEXT,__text) external _InternalFlutterGpu_Context_GetDefaultColorFormat 00000000019c7048 (__TEXT,__text) external _InternalFlutterGpu_Context_GetDefaultDepthStencilFormat 00000000019c7004 (__TEXT,__text) external _InternalFlutterGpu_Context_GetDefaultStencilFormat 00000000019c7174 (__TEXT,__text) external _InternalFlutterGpu_Context_GetMaxSamplerAnisotropy 00000000019c708c (__TEXT,__text) external _InternalFlutterGpu_Context_GetMinimumUniformByteAlignment 00000000019c70f4 (__TEXT,__text) external _InternalFlutterGpu_Context_GetSupportsFramebufferRenderMipmap 00000000019c7134 (__TEXT,__text) external _InternalFlutterGpu_Context_GetSupportsManuallyMippedTextures 00000000019c70cc (__TEXT,__text) external _InternalFlutterGpu_Context_GetSupportsOffscreenMSAA 00000000019c6c30 (__TEXT,__text) external _InternalFlutterGpu_Context_InitializeDefault 00000000019c71b4 (__TEXT,__text) external _InternalFlutterGpu_Context_SupportsTextureCompression 00000000019c7230 (__TEXT,__text) external _InternalFlutterGpu_Context_SupportsTextureFormat 00000000019c9ca8 (__TEXT,__text) external _InternalFlutterGpu_DeviceBuffer_Flush 00000000019c9720 (__TEXT,__text) external _InternalFlutterGpu_DeviceBuffer_Initialize 00000000019c99f8 (__TEXT,__text) external _InternalFlutterGpu_DeviceBuffer_InitializeWithHostData 00000000019c9c48 (__TEXT,__text) external _InternalFlutterGpu_DeviceBuffer_Overwrite 00000000019cc474 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_Begin 00000000019cc6ec (__TEXT,__text) external _InternalFlutterGpu_RenderPass_BindIndexBufferDevice 00000000019cc4cc (__TEXT,__text) external _InternalFlutterGpu_RenderPass_BindPipeline 00000000019ccb6c (__TEXT,__text) external _InternalFlutterGpu_RenderPass_BindTexture 00000000019cceb8 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_BindTextureIndexed 00000000019cc7fc (__TEXT,__text) external _InternalFlutterGpu_RenderPass_BindUniformDevice 00000000019ccac4 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_BindUniformDeviceIndexed 00000000019cc598 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_BindVertexBufferDevice 00000000019ccf54 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_ClearBindings 00000000019cd660 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_Draw 00000000019cd6d4 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_DrawIndexed 00000000019cbf98 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_Initialize 00000000019cc0e4 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetColorAttachment 00000000019ccf78 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetColorBlendEnable 00000000019ccfbc (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetColorBlendEquation 00000000019cd4b0 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetCullMode 00000000019cd0e8 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetDepthCompareOperation 00000000019cc2f0 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetDepthStencilAttachment 00000000019cd0ac (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetDepthWriteEnable 00000000019cd5f4 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetPolygonMode 00000000019cd51c (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetPrimitiveType 00000000019cd168 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetScissor 00000000019cd3b4 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetStencilConfig 00000000019cd148 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetStencilReference 00000000019cd2a0 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetViewport 00000000019cd588 (__TEXT,__text) external _InternalFlutterGpu_RenderPass_SetWindingOrder 00000000019d6208 (__TEXT,__text) external _InternalFlutterGpu_RenderPipeline_Initialize 00000000019e4898 (__TEXT,__text) external _InternalFlutterGpu_ShaderLibrary_GetShader 00000000019e4190 (__TEXT,__text) external _InternalFlutterGpu_ShaderLibrary_InitializeWithAsset 00000000019e44d8 (__TEXT,__text) external _InternalFlutterGpu_ShaderLibrary_InitializeWithBytes 00000000019e43a0 (__TEXT,__text) external _InternalFlutterGpu_ShaderLibrary_ReinitializeWithAsset 00000000019e473c (__TEXT,__text) external _InternalFlutterGpu_ShaderLibrary_ReinitializeWithBytes 00000000019dc244 (__TEXT,__text) external _InternalFlutterGpu_Shader_DebugIsDirty 00000000019dc170 (__TEXT,__text) external _InternalFlutterGpu_Shader_GetUniformMemberOffset 00000000019dc0d0 (__TEXT,__text) external _InternalFlutterGpu_Shader_GetUniformStructIndex 00000000019dc04c (__TEXT,__text) external _InternalFlutterGpu_Shader_GetUniformStructSize 00000000019dc120 (__TEXT,__text) external _InternalFlutterGpu_Shader_GetUniformTextureIndex 00000000019f2f64 (__TEXT,__text) external _InternalFlutterGpu_Surface_AcquireNextFrame 00000000019f2ff0 (__TEXT,__text) external _InternalFlutterGpu_Surface_DiscardFrame 00000000019f312c (__TEXT,__text) external _InternalFlutterGpu_Surface_GetBackingTextureCount 00000000019f3030 (__TEXT,__text) external _InternalFlutterGpu_Surface_GetCurrentImage 00000000019f2d4c (__TEXT,__text) external _InternalFlutterGpu_Surface_Initialize 00000000019f2f90 (__TEXT,__text) external _InternalFlutterGpu_Surface_PresentFrame 00000000019f3054 (__TEXT,__text) external _InternalFlutterGpu_Surface_Resize 00000000019f7650 (__TEXT,__text) external _InternalFlutterGpu_Texture_AsImage 00000000019f7674 (__TEXT,__text) external _InternalFlutterGpu_Texture_ImageTextureInfo 00000000019f7214 (__TEXT,__text) external _InternalFlutterGpu_Texture_Initialize 00000000019f7aec (__TEXT,__text) external _InternalFlutterGpu_Texture_InitializeFromImage 00000000019f75a0 (__TEXT,__text) external _InternalFlutterGpu_Texture_Overwrite 000000000388da18 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterAppDelegate 0000000003890a20 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterBasicMessageChannel 0000000003890bb0 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterBinaryCodec 00000000038909d0 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterBinaryMessengerRelay 000000000388dae0 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterCallbackCache 000000000388da90 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterCallbackInformation 000000000388db58 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterDartProject 000000000388dd88 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterEngine 000000000388df18 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterEngineGroup 000000000388def0 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterEngineGroupOptions 0000000003890a70 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterError 0000000003890b60 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterEventChannel 000000000388df68 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterHeadlessDartRunner 0000000003890c50 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterJSONMessageCodec 0000000003890ca0 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterJSONMethodCodec 0000000003890ac0 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterMethodCall 0000000003890b10 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterMethodChannel 000000000388e4b8 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterPluginAppLifeCycleDelegate 000000000388e5a8 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterPluginSceneLifeCycleDelegate 000000000388e558 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterSceneDelegate 0000000003890d68 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterStandardMessageCodec 0000000003890db8 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterStandardMethodCodec 0000000003890ea8 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterStandardReader 0000000003890d40 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterStandardReaderWriter 0000000003890e08 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterStandardTypedData 0000000003890e58 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterStandardWriter 0000000003890c00 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterStringCodec 000000000388e828 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterTextInputView 000000000388eaa8 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterTextureRegistryRelay 000000000388ebe8 (__DATA,__objc_data) external _OBJC_CLASS_$_FlutterViewController 000000000388da40 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterAppDelegate 0000000003890a48 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterBasicMessageChannel 0000000003890bd8 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterBinaryCodec 00000000038909f8 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterBinaryMessengerRelay 000000000388dab8 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterCallbackCache 000000000388da68 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterCallbackInformation 000000000388db80 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterDartProject 000000000388de00 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterEngine 000000000388df40 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterEngineGroup 000000000388dec8 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterEngineGroupOptions 0000000003890a98 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterError 0000000003890b88 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterEventChannel 000000000388df90 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterHeadlessDartRunner 0000000003890c78 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterJSONMessageCodec 0000000003890cc8 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterJSONMethodCodec 0000000003890ae8 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterMethodCall 0000000003890b38 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterMethodChannel 000000000388e4e0 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterPluginAppLifeCycleDelegate 000000000388e5d0 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterPluginSceneLifeCycleDelegate 000000000388e580 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterSceneDelegate 0000000003890d90 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterStandardMessageCodec 0000000003890de0 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterStandardMethodCodec 0000000003890ed0 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterStandardReader 0000000003890ef8 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterStandardReaderWriter 0000000003890e30 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterStandardTypedData 0000000003890e80 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterStandardWriter 0000000003890c28 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterStringCodec 000000000388e968 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterTextInputView 000000000388ead0 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterTextureRegistryRelay 000000000388ec10 (__DATA,__objc_data) external _OBJC_METACLASS_$_FlutterViewController 00000000022b91c0 (__TEXT,__const) external _kDartSnapshotData 0000000001eb0440 (__TEXT,__text) external _kDartSnapshotText 0000000002dd0e40 (__TEXT,__const) external _kPlatformStrongDill 00000000037b76a0 (__TEXT,__const) external _kPlatformStrongDillSize ``` ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../shell/platform/darwin/common/BUILD.gn | 8 ++- .../darwin/common/SwiftTestingRunner.swift | 6 +- .../common/framework/Source/Logger.swift | 44 +++++++-------- .../framework/Source/LoggerTestUtils.swift | 18 +++--- .../common/framework/Source/LoggerTests.swift | 4 +- .../framework/Source/Tracing+TraceScope.swift | 11 ++-- .../framework/Source/TracingTests.swift | 2 +- .../Source/AccessibilityFeatures.swift | 50 ++++++++--------- .../Source/ConnectionCollection.swift | 10 ++-- .../Source/ConnectionCollectionTests.swift | 2 +- .../framework/Source/DisplayLinkManager.swift | 12 ++-- .../framework/Source/FakeUIPressProxy.swift | 12 ++-- .../ios/framework/Source/FlutterEngineTest.mm | 1 + .../Source/KeyboardInsetManager.swift | 56 +++++++++---------- .../ios/framework/Source/LaunchEngine.swift | 6 +- .../framework/Source/LaunchEngineTests.swift | 2 +- .../Source/SplashScreenManager.swift | 14 ++--- .../framework/Source/TaskRunnerTests.swift | 2 +- .../ios/framework/Source/UIPressProxy.swift | 14 ++--- .../ios/framework/Source/VSyncClient.swift | 20 +++---- .../framework/Source/FlutterEngineTest.mm | 1 + .../framework/Source/FlutterRunLoop.swift | 12 ++-- .../framework/Source/ResizeSynchronizer.swift | 8 +-- .../Source/ResizeSynchronizerTests.swift | 2 +- 24 files changed, 162 insertions(+), 155 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/common/BUILD.gn b/engine/src/flutter/shell/platform/darwin/common/BUILD.gn index fb3805429953b..24e9e35a36448 100644 --- a/engine/src/flutter/shell/platform/darwin/common/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/common/BUILD.gn @@ -17,9 +17,11 @@ config("config") { lib_dirs = mac_swift_lib_paths } + swiftflags = [] + # Allow use of @testable imports in debug builds and when tests are enabled. if ((is_ios && enable_ios_unittests) || (is_mac && enable_unittests)) { - swiftflags = [ "-enable-testing" ] + swiftflags += [ "-enable-testing" ] } } @@ -217,6 +219,10 @@ source_set("test_utils_swift") { ":config", ":test_config", ] + + # Specifying a bridging header forces the swift compiler to add even internal + # members of this library in test_utils_swift.h + bridge_header = "InternalFlutterSwiftCommon-Bridging-Header.h" sources = [ "framework/Source/LoggerTestUtils.swift" ] deps = [ ":framework_common" ] } diff --git a/engine/src/flutter/shell/platform/darwin/common/SwiftTestingRunner.swift b/engine/src/flutter/shell/platform/darwin/common/SwiftTestingRunner.swift index 6c9db43337026..07a8ddb54e9c5 100644 --- a/engine/src/flutter/shell/platform/darwin/common/SwiftTestingRunner.swift +++ b/engine/src/flutter/shell/platform/darwin/common/SwiftTestingRunner.swift @@ -61,13 +61,13 @@ private func resolveSwiftTestingEntrypoint() -> SwiftTestingEntryPointFunc { } /// A test runner for Swift Testing tests. -public struct SwiftTestingRunner { - public init() {} +struct SwiftTestingRunner { + init() {} /// Runs all Swift Testing tests (annotated with `@Test`) in the current executable. /// /// Returns 0 on pass, non-zero on failure. - public func run() async -> CInt { + func run() async -> CInt { let testRunnerEntryPoint = resolveSwiftTestingEntrypoint() do { let result = try await testRunnerEntryPoint(nil) { _ in diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift index 4f0c55b176d0e..a4db8d8fab978 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift @@ -9,7 +9,7 @@ import Foundation /// /// These levels are used by `Logger` to determine if a message should be output. /// They are ordered by increasing severity. -@objc(FlutterLogLevel) public enum LogLevel: Int, Sendable { +@objc(FlutterLogLevel) enum LogLevel: Int, Sendable { /// Informational messages that are helpful for tracing application flow. case info @@ -38,13 +38,13 @@ import Foundation /// Logger.logLevel = .warning // Only show warnings and above /// Logger.logError("Failed to load asset: \(assetKey)") /// ``` -@objc(FlutterLogger) public final class Logger: NSObject, @unchecked Sendable { +@objc(FlutterLogger) final class Logger: NSObject, @unchecked Sendable { private static let shared = Logger() private let lock = NSLock() private var _outputWriter: OutputWriter private var _logLevel: LogLevel - public var outputWriter: OutputWriter { + var outputWriter: OutputWriter { get { return lock.withLock { _outputWriter } } @@ -55,7 +55,7 @@ import Foundation } } - public var logLevel: LogLevel { + var logLevel: LogLevel { get { return lock.withLock { _logLevel } } @@ -66,13 +66,13 @@ import Foundation } } - public init(outputWriter: OutputWriter, logLevel: LogLevel) { + init(outputWriter: OutputWriter, logLevel: LogLevel) { self._outputWriter = outputWriter self._logLevel = logLevel super.init() } - public override convenience init() { + override convenience init() { #if os(iOS) // On iOS, the user has no access to stdout. // Output can be read from the log by the user or the `flutter` tool. @@ -83,7 +83,7 @@ import Foundation #endif } - public func log(level: LogLevel, _ message: @autoclosure () -> String) { + func log(level: LogLevel, _ message: @autoclosure () -> String) { guard level.rawValue >= logLevel.rawValue else { return } // Evaluate outside the lock keep lock time minimal and to guard against the possibility of @@ -94,7 +94,7 @@ import Foundation } } - public func logDirect(_ message: String) { + func logDirect(_ message: String) { lock.withLock { _outputWriter.writeLine(level: .important, message) } @@ -103,82 +103,82 @@ import Foundation extension Logger { /// Sets the minimum log level. - @objc public static var outputWriter: OutputWriter { + @objc static var outputWriter: OutputWriter { get { return shared.outputWriter } set(newValue) { shared.outputWriter = newValue } } /// Sets the minimum log level. - @objc public static var logLevel: LogLevel { + @objc static var logLevel: LogLevel { get { return shared.logLevel } set(newValue) { shared.logLevel = newValue } } /// Logs a message at `LogLevel.info`. @available(swift, obsoleted: 1.0) - @objc(logInfo:) public static func objcLogInfo(_ message: String) { + @objc(logInfo:) static func objcLogInfo(_ message: String) { shared.log(level: .info, message) } /// Logs a message at `LogLevel.info`. - public static func logInfo(_ message: @autoclosure () -> String) { + static func logInfo(_ message: @autoclosure () -> String) { shared.log(level: .info, message()) } /// Logs a message at `LogLevel.important`. @available(swift, obsoleted: 1.0) - @objc(logImportant:) public static func objcLogImportant(_ message: String) { + @objc(logImportant:) static func objcLogImportant(_ message: String) { shared.log(level: .important, message) } /// Logs a message at `LogLevel.important`. - public static func logImportant(_ message: @autoclosure () -> String) { + static func logImportant(_ message: @autoclosure () -> String) { shared.log(level: .important, message()) } /// Logs a message at `LogLevel.warning`. @available(swift, obsoleted: 1.0) - @objc(logWarning:) public static func objcLogWarning(_ message: String) { + @objc(logWarning:) static func objcLogWarning(_ message: String) { shared.log(level: .warning, message) } /// Logs a message at `LogLevel.warning`. - public static func logWarning(_ message: @autoclosure () -> String) { + static func logWarning(_ message: @autoclosure () -> String) { shared.log(level: .warning, message()) } /// Logs a message at `LogLevel.error`. @available(swift, obsoleted: 1.0) - @objc(logError:) public static func objcLogError(_ message: String) { + @objc(logError:) static func objcLogError(_ message: String) { shared.log(level: .error, message) } /// Logs a message at `LogLevel.error`. - public static func logError(_ message: @autoclosure () -> String) { + static func logError(_ message: @autoclosure () -> String) { shared.log(level: .error, message()) } /// Logs a message at `LogLevel.fatal` and immediately terminates the application. @available(swift, obsoleted: 1.0) - @objc(logFatal:) public static func objcLogFatal(_ message: String) { + @objc(logFatal:) static func objcLogFatal(_ message: String) { shared.log(level: .fatal, message) abort() } /// Logs a message at `LogLevel.fatal` and immediately terminates the application. - public static func logFatal(_ message: @autoclosure () -> String) { + static func logFatal(_ message: @autoclosure () -> String) { shared.log(level: .fatal, message()) abort() } /// Logs a message unconditionally. - @objc public static func logDirect(_ message: String) { + @objc static func logDirect(_ message: String) { shared.logDirect(message) } } @objc(FlutterOutputWriter) -public protocol OutputWriter: Sendable { +protocol OutputWriter: Sendable { func writeLine(level: LogLevel, _ message: String) } diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift index 7b73808299773..6e3111c4d6df7 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTestUtils.swift @@ -3,18 +3,18 @@ // found in the LICENSE file. import Foundation -import InternalFlutterSwiftCommon +@testable import InternalFlutterSwiftCommon /// An `OutputWriter` that stores the most recently logged output in a string. @objc(FlutterStringOutputWriter) -public final class StringOutputWriter: NSObject, OutputWriter, @unchecked Sendable { - @objc public var didLog = false - public var lastLevel: LogLevel! - @objc public var lastLine: String! - @objc public var expectedOutput: String? - @objc public var gotExpectedOutput = false +final class StringOutputWriter: NSObject, OutputWriter, @unchecked Sendable { + @objc var didLog = false + var lastLevel: LogLevel! + @objc var lastLine: String! + @objc var expectedOutput: String? + @objc var gotExpectedOutput = false - public func writeLine(level: LogLevel, _ message: String) { + func writeLine(level: LogLevel, _ message: String) { didLog = true lastLevel = level lastLine = message @@ -23,7 +23,7 @@ public final class StringOutputWriter: NSObject, OutputWriter, @unchecked Sendab } } - @objc public func reset() { + @objc func reset() { didLog = false lastLevel = nil lastLine = nil diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift index 9d020789c3ffd..9cfa2e317fe79 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift @@ -3,9 +3,9 @@ // found in the LICENSE file. import Foundation -import InternalFlutterSwiftCommon +@testable import InternalFlutterSwiftCommon import Testing -import test_utils_swift +@testable import test_utils_swift @Suite struct LoggerTests { diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Tracing+TraceScope.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Tracing+TraceScope.swift index 37d545c042b70..de4e7ac5f94d7 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Tracing+TraceScope.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Tracing+TraceScope.swift @@ -13,11 +13,11 @@ import Foundation /// will fire in debug builds, and the scope will be ended automatically to /// prevent timeline corruption. @objc(FlutterTraceScope) -public final class TraceScope: NSObject { +final class TraceScope: NSObject { private let name: String private var isEnded = false - internal init(name: String) { + init(name: String) { self.name = name Tracing.beginSection(name) } @@ -25,7 +25,7 @@ public final class TraceScope: NSObject { /// Ends the tracing scope. /// /// This method must be called exactly once. Calling it multiple times has no effect. - @objc public func end() { + @objc func end() { guard !isEnded else { return } isEnded = true Tracing.endSection(name) @@ -68,8 +68,7 @@ extension Tracing { /// - name: The name of the tracing scope. /// - work: The block of work to synchronously execute, optionally returning a value. /// - Returns: The value returned by the `work` block. - @inlinable - public static func withTrace(_ name: String, _ work: () throws -> T) rethrows -> T { + static func withTrace(_ name: String, _ work: () throws -> T) rethrows -> T { let scope = beginScope(name) defer { scope.end() } return try work() @@ -86,7 +85,7 @@ extension Tracing { /// /// - Parameter name: The name of the tracing scope. /// - Returns: A `TraceScope` token that must be ended. - public static func beginScope(_ name: String) -> TraceScope { + static func beginScope(_ name: String) -> TraceScope { return TraceScope(name: name) } } diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/TracingTests.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/TracingTests.swift index 7efb53c35bafb..760aab8deaeb4 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/TracingTests.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/TracingTests.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import Foundation -import InternalFlutterSwiftCommon +@testable import InternalFlutterSwiftCommon import Testing @Suite struct TracingTests { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/AccessibilityFeatures.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/AccessibilityFeatures.swift index 46b3b70babbf7..de21e82711472 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/AccessibilityFeatures.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/AccessibilityFeatures.swift @@ -28,9 +28,9 @@ struct AccessibilityFeatureFlag: OptionSet { /// A wrapper for native iOS accessibility settings. @objc(FlutterAccessibilityFeatures) -public class AccessibilityFeatures: NSObject { +class AccessibilityFeatures: NSObject { /// Returns the current accessibility flags as a bitmask. - @objc public var flags: Int32 { + @objc var flags: Int32 { var flags: AccessibilityFeatureFlag = [] if self.isVoiceOverRunning() || self.isSwitchControlRunning() { @@ -66,7 +66,7 @@ public class AccessibilityFeatures: NSObject { /// Returns an array of notification names to observe for accessibility /// changes. - @objc public var observedNotificationNames: [String] { + @objc var observedNotificationNames: [String] { var names: [String] = [ AccessibilityFeatures.voiceOverStatusDidChangeNotification, AccessibilityFeatures.switchControlStatusDidChangeNotification, @@ -90,95 +90,95 @@ public class AccessibilityFeatures: NSObject { } /// Notification name for changes to `VoiceOver` status. - @objc public static var voiceOverStatusDidChangeNotification: String { + @objc static var voiceOverStatusDidChangeNotification: String { return UIAccessibility.voiceOverStatusDidChangeNotification.rawValue } /// Whether `VoiceOver` is running. - @objc public func isVoiceOverRunning() -> Bool { + @objc func isVoiceOverRunning() -> Bool { return UIAccessibility.isVoiceOverRunning } /// Notification name for changes to `Switch Control` status. - @objc public static var switchControlStatusDidChangeNotification: String { + @objc static var switchControlStatusDidChangeNotification: String { return UIAccessibility.switchControlStatusDidChangeNotification.rawValue } /// Whether `Switch Control` is running. - @objc public func isSwitchControlRunning() -> Bool { + @objc func isSwitchControlRunning() -> Bool { return UIAccessibility.isSwitchControlRunning } /// Notification name for changes to `Speak Screen` setting. - @objc public static var speakScreenStatusDidChangeNotification: String { + @objc static var speakScreenStatusDidChangeNotification: String { return UIAccessibility.speakScreenStatusDidChangeNotification.rawValue } /// Whether `Speak Screen` setting is enabled. - @objc public func isSpeakScreenEnabled() -> Bool { + @objc func isSpeakScreenEnabled() -> Bool { return UIAccessibility.isSpeakScreenEnabled } /// Notification name for changes to `Classic Invert` setting. - @objc public static var invertColorsStatusDidChangeNotification: String { + @objc static var invertColorsStatusDidChangeNotification: String { return UIAccessibility.invertColorsStatusDidChangeNotification.rawValue } /// Whether `Classic Invert` setting is enabled. - @objc public func isInvertColorsEnabled() -> Bool { + @objc func isInvertColorsEnabled() -> Bool { return UIAccessibility.isInvertColorsEnabled } /// Notification name for changes to `Reduce Motion` setting. - @objc public static var reduceMotionStatusDidChangeNotification: String { + @objc static var reduceMotionStatusDidChangeNotification: String { return UIAccessibility.reduceMotionStatusDidChangeNotification.rawValue } /// Whether `Reduce Motion` setting is enabled. - @objc public func isReduceMotionEnabled() -> Bool { + @objc func isReduceMotionEnabled() -> Bool { return UIAccessibility.isReduceMotionEnabled } /// Notification name for changes to `Bold Text` setting. - @objc public static var boldTextStatusDidChangeNotification: String { + @objc static var boldTextStatusDidChangeNotification: String { return UIAccessibility.boldTextStatusDidChangeNotification.rawValue } /// Whether `Bold Text` setting is enabled. - @objc public func isBoldTextEnabled() -> Bool { + @objc func isBoldTextEnabled() -> Bool { return UIAccessibility.isBoldTextEnabled } /// Notification name for changes to `Increase Contrast` setting. - @objc public static var darkerSystemColorsStatusDidChangeNotification: String { + @objc static var darkerSystemColorsStatusDidChangeNotification: String { return UIAccessibility.darkerSystemColorsStatusDidChangeNotification.rawValue } /// Whether `Increase Contrast` setting is enabled. - @objc public func isDarkerSystemColorsEnabled() -> Bool { + @objc func isDarkerSystemColorsEnabled() -> Bool { return UIAccessibility.isDarkerSystemColorsEnabled } /// Notification name for changes to `On/Off Labels` setting. - @objc public static var onOffSwitchLabelsDidChangeNotification: String { + @objc static var onOffSwitchLabelsDidChangeNotification: String { return UIAccessibility.onOffSwitchLabelsDidChangeNotification.rawValue } /// Whether `On/Off Labels` setting is enabled. - @objc public func isOnOffSwitchLabelsEnabled() -> Bool { + @objc func isOnOffSwitchLabelsEnabled() -> Bool { return UIAccessibility.isOnOffSwitchLabelsEnabled } /// Notification name for changes to `Auto-Play Animated Images` setting. @available(iOS 18.0, *) - @objc public static var animatedImagesAutoPlayStatusDidChangeNotification: String { + @objc static var animatedImagesAutoPlayStatusDidChangeNotification: String { return AccessibilitySettings.animatedImagesEnabledDidChangeNotification.rawValue } /// Whether `Auto-Play Animated Images` setting is enabled. /// /// Defaults to `true` on iOS versions earlier than 18. - @objc public func isAnimatedImagesAutoPlayEnabled() -> Bool { + @objc func isAnimatedImagesAutoPlayEnabled() -> Bool { if #available(iOS 18.0, *) { return AccessibilitySettings.animatedImagesEnabled } @@ -186,25 +186,25 @@ public class AccessibilityFeatures: NSObject { } /// Notification name for changes to `Auto-Play Video Previews` setting. - @objc public static var videosAutoPlayStatusDidChangeNotification: String { + @objc static var videosAutoPlayStatusDidChangeNotification: String { return UIAccessibility.videoAutoplayStatusDidChangeNotification.rawValue } /// Whether `Auto-Play Video Previews` setting is enabled. - @objc public func isVideosAutoPlayEnabled() -> Bool { + @objc func isVideosAutoPlayEnabled() -> Bool { return UIAccessibility.isVideoAutoplayEnabled } /// Notification name for changes to `Prefer Non-Blinking Cursor` setting. @available(iOS 18.0, *) - @objc public static var deterministicCursorStatusDidChangeNotification: String { + @objc static var deterministicCursorStatusDidChangeNotification: String { return AccessibilitySettings.prefersNonBlinkingTextInsertionIndicatorDidChangeNotification.rawValue } /// Whether `Prefer Non-Blinking Cursor` setting is enabled. /// /// Defaults to `false` on iOS versions earlier than 18. - @objc public func isDeterministicCursorEnabled() -> Bool { + @objc func isDeterministicCursorEnabled() -> Bool { if #available(iOS 18.0, *) { return AccessibilitySettings.prefersNonBlinkingTextInsertionIndicator } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollection.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollection.swift index 6950f8024c220..686bd7d8c278f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollection.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollection.swift @@ -11,8 +11,8 @@ import Foundation /// This class is not thread-safe. All accesses should happen from the /// platform thread. @objc(FlutterConnectionCollection) -public class ConnectionCollection: NSObject { - public typealias ConnectionID = Int64 +class ConnectionCollection: NSObject { + typealias ConnectionID = Int64 // The connection ID of the most recently used connection, or 0 if none. private var counter: ConnectionID = 0 @@ -21,7 +21,7 @@ public class ConnectionCollection: NSObject { private var connections: [String: ConnectionID] = [:] /// Acquires a new connection for the specified channel. - @objc public func acquireConnection(forChannel channel: String) -> ConnectionID { + @objc func acquireConnection(forChannel channel: String) -> ConnectionID { counter += 1 connections[channel] = counter return counter @@ -31,7 +31,7 @@ public class ConnectionCollection: NSObject { /// /// Returns the name of the associated channel if successful, otherwise the /// empty string. - @objc public func cleanupConnection(withID connectionID: ConnectionID) -> String { + @objc func cleanupConnection(withID connectionID: ConnectionID) -> String { guard connectionID > 0, let entry = connections.first(where: { $0.value == connectionID }) @@ -42,7 +42,7 @@ public class ConnectionCollection: NSObject { } /// Creates an error connection from an error code. - @objc public static func makeErrorConnection(errorCode: Int64) -> ConnectionID { + @objc static func makeErrorConnection(errorCode: Int64) -> ConnectionID { return abs(errorCode) } } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollectionTests.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollectionTests.swift index 0f400a195b7ee..3360591cecfb5 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollectionTests.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/ConnectionCollectionTests.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import InternalFlutterSwift +@testable import InternalFlutterSwift import Testing struct ConnectionCollectionTests { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift index 15d8e484e35ee..515ea18383b5e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift @@ -31,7 +31,7 @@ import UIKit /// while holding a lock; every other stored property is immutable, so instances are safe to share /// and read from any thread once constructed. @objc(FlutterDisplayLinkManager) -public final class DisplayLinkManager: NSObject, @unchecked Sendable { +final class DisplayLinkManager: NSObject, @unchecked Sendable { /// The shared DisplayLinkManager. /// @@ -40,13 +40,13 @@ public final class DisplayLinkManager: NSObject, @unchecked Sendable { /// callers remain responsible for calling from the main thread themselves. @MainActor @objc - public static let shared = DisplayLinkManager() + static let shared = DisplayLinkManager() /// Info.plist key enabling the full range of ProMotion refresh rates for CADisplayLink callbacks /// and CAAnimation animations in the app. /// /// - SeeAlso: https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro#3885321 - internal static let disableMinimumFrameDurationOnPhoneKey = "CADisableMinimumFrameDurationOnPhone" + static let disableMinimumFrameDurationOnPhoneKey = "CADisableMinimumFrameDurationOnPhone" /// Whether the max refresh rate on iPhone ProMotion devices is enabled. /// @@ -56,11 +56,11 @@ public final class DisplayLinkManager: NSObject, @unchecked Sendable { /// /// - Returns: `true` if the max refresh rate on ProMotion devices is enabled. @objc - public let maxRefreshRateEnabledOnIPhone: Bool + let maxRefreshRateEnabledOnIPhone: Bool /// The maximum display refresh rate, in frames per second. @objc - public internal(set) var displayRefreshRate: Double { + var displayRefreshRate: Double { get { // We cache the refresh rate rather than query from UIKit on every read, since this can be // read from background engine threads. The value is kept up-to-date by observing @@ -111,7 +111,7 @@ public final class DisplayLinkManager: NSObject, @unchecked Sendable { /// Testing initializer that injects configuration values. /// /// Unlike the standard initializer, this does not start observing system notifications. - internal init(maxRefreshRateEnabled: Bool, refreshRate: Double) { + init(maxRefreshRateEnabled: Bool, refreshRate: Double) { self.maxRefreshRateEnabledOnIPhone = maxRefreshRateEnabled self._displayRefreshRate = refreshRate super.init() diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FakeUIPressProxy.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FakeUIPressProxy.swift index 9e57f2631349c..aa291c52a9865 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FakeUIPressProxy.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FakeUIPressProxy.swift @@ -9,29 +9,29 @@ import UIKit /// UIPressProxy subclass for use to create fake UIPress events in tests. @available(iOS 13.4, *) @objc -public class FakeUIPressProxy: UIPressProxy { +class FakeUIPressProxy: UIPressProxy { private let dataPhase: UIPress.Phase private let dataKey: UIKey // Store the copied key private let dataType: UIEvent.EventType private let dataTimestamp: TimeInterval - @objc override public var phase: UIPress.Phase { + @objc override var phase: UIPress.Phase { return dataPhase } - @objc override public var key: UIKey? { + @objc override var key: UIKey? { return dataKey } - @objc override public var type: UIEvent.EventType { + @objc override var type: UIEvent.EventType { return dataType } - @objc override public var timestamp: TimeInterval { + @objc override var timestamp: TimeInterval { return dataTimestamp } - @objc public init( + @objc init( phase: UIPress.Phase, key: UIKey, type: UIEvent.EventType, diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 028339d0ab032..75ba57588289a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -10,6 +10,7 @@ #import "flutter/common/settings.h" #include "flutter/fml/synchronization/sync_switch.h" +#import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h" #import "flutter/shell/platform/darwin/common/test_utils_swift/test_utils_swift.h" diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/KeyboardInsetManager.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/KeyboardInsetManager.swift index 41599687f7d13..f21fd8df6b9b5 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/KeyboardInsetManager.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/KeyboardInsetManager.swift @@ -4,13 +4,13 @@ import UIKit -@objc public enum FlutterKeyboardMode: Int { +@objc enum FlutterKeyboardMode: Int { case hidden case docked case floating } -public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterval) -> Void +typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterval) -> Void /// @brief Coordinates the animation of the bottom viewport inset in response to system keyboard /// visibility changes. @@ -53,7 +53,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva /// active view controller. /// /// @see [FlutterViewController], which owns this manager and acts as its delegate. -@objc public protocol FlutterKeyboardInsetManagerDelegate: NSObjectProtocol { +@objc protocol FlutterKeyboardInsetManagerDelegate: NSObjectProtocol { @objc(updateViewportMetricsWithInset:) func updateViewportMetrics(withInset inset: CGFloat) func physicalViewInsetBottom() -> CGFloat @@ -67,7 +67,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva func isViewLoaded() -> Bool } -@objc public protocol FlutterKeyboardInsetManagerProtocol: NSObjectProtocol { +@objc protocol FlutterKeyboardInsetManagerProtocol: NSObjectProtocol { var delegate: FlutterKeyboardInsetManagerDelegate? { get set } var targetViewInsetBottom: CGFloat { get set } var isKeyboardInOrTransitioningFromBackground: Bool { get set } @@ -87,21 +87,21 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva func ensureViewportMetricsIsCorrect() } -@objc open class FlutterKeyboardInsetManager: NSObject, FlutterKeyboardInsetManagerProtocol { - @objc public weak var delegate: FlutterKeyboardInsetManagerDelegate? - @objc public var targetViewInsetBottom: CGFloat = 0 +@objc class FlutterKeyboardInsetManager: NSObject, FlutterKeyboardInsetManagerProtocol { + @objc weak var delegate: FlutterKeyboardInsetManagerDelegate? + @objc var targetViewInsetBottom: CGFloat = 0 private var originalViewInsetBottom: CGFloat = 0 - @objc public var keyboardAnimationVSyncClient: VSyncClient? - @objc public var keyboardAnimationIsShowing: Bool = false + @objc var keyboardAnimationVSyncClient: VSyncClient? + @objc var keyboardAnimationIsShowing: Bool = false private var keyboardAnimationStartTime: CFTimeInterval = 0 - @objc public var keyboardAnimationView: UIView? - @objc public var keyboardSpringAnimation: SpringAnimation? - @objc public var isKeyboardInOrTransitioningFromBackground: Bool = false + @objc var keyboardAnimationView: UIView? + @objc var keyboardSpringAnimation: SpringAnimation? + @objc var isKeyboardInOrTransitioningFromBackground: Bool = false private let displayLinkManager: DisplayLinkManager - @objc public init( + @objc init( delegate: FlutterKeyboardInsetManagerDelegate, displayLinkManager: DisplayLinkManager ) { self.delegate = delegate @@ -109,7 +109,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva super.init() } - @objc public func handleKeyboardNotification(_ notification: Notification) { + @objc func handleKeyboardNotification(_ notification: Notification) { // See https://flutter.dev/go/ios-keyboard-calculating-inset for more details on why // notifications are used and how things are calculated. guard delegate != nil else { return } @@ -162,7 +162,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva } } - @objc public func shouldIgnoreKeyboardNotification(_ notification: Notification) -> Bool { + @objc func shouldIgnoreKeyboardNotification(_ notification: Notification) -> Bool { // Don't ignore UIKeyboardWillHideNotification notifications. // Even if the notification is triggered in the background or by a different app/view // controller, we want to always handle this notification to avoid inaccurate inset when in a @@ -199,7 +199,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva return false } - @objc public func isKeyboardNotificationForDifferentView(_ notification: Notification) -> Bool { + @objc func isKeyboardNotificationForDifferentView(_ notification: Notification) -> Bool { let info = notification.userInfo // Keyboard notifications related to other apps (e.g. in split view mode on iPad). // If the UIKeyboardIsLocalUserInfoKey key doesn't exist (this should not happen after iOS 8), @@ -211,7 +211,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva return delegate.engine()?.viewController !== (delegate as AnyObject) } - @objc public func calculateKeyboardAttachMode(_ notification: Notification) -> FlutterKeyboardMode + @objc func calculateKeyboardAttachMode(_ notification: Notification) -> FlutterKeyboardMode { // There are multiple types of keyboard: docked, undocked, split, split docked, // floating, expanded shortcuts bar, minimized shortcuts bar. @@ -273,7 +273,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva return .hidden } - @objc public func calculateMultitaskingAdjustment(_ screenRect: CGRect, keyboardFrame: CGRect) + @objc func calculateMultitaskingAdjustment(_ screenRect: CGRect, keyboardFrame: CGRect) -> CGFloat { guard let delegate else { return 0 } @@ -302,7 +302,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva return 0 } - @objc public func calculateKeyboardInset( + @objc func calculateKeyboardInset( _ keyboardFrame: CGRect, keyboardMode: FlutterKeyboardMode ) -> CGFloat { // Only docked keyboards will have an inset. @@ -322,7 +322,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva return portionOfKeyboardInView * scale } - @objc public func startKeyBoardAnimation(_ duration: TimeInterval) { + @objc func startKeyBoardAnimation(_ duration: TimeInterval) { guard let delegate, delegate.isViewLoaded() else { return } let view = delegate.view() @@ -385,7 +385,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva }) } - @objc public func handleKeyboardAnimationCallback(withTargetTime targetTime: CFTimeInterval) { + @objc func handleKeyboardAnimationCallback(withTargetTime targetTime: CFTimeInterval) { guard let delegate else { return } if !delegate.isViewLoaded() { return } @@ -411,7 +411,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva delegate.updateViewportMetrics(withInset: currentInset) } - @objc public func hideKeyboardImmediately() { + @objc func hideKeyboardImmediately() { invalidateKeyboardAnimationVSyncClient() if let keyboardAnimationView = keyboardAnimationView { keyboardAnimationView.layer.removeAllAnimations() @@ -423,23 +423,23 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva ensureViewportMetricsIsCorrect() } - @objc public func invalidate() { + @objc func invalidate() { invalidateKeyboardAnimationVSyncClient() removeKeyboardAnimationView() } - @objc public func invalidateKeyboardAnimationVSyncClient() { + @objc func invalidateKeyboardAnimationVSyncClient() { keyboardAnimationVSyncClient?.invalidate() keyboardAnimationVSyncClient = nil } - @objc public func removeKeyboardAnimationView() { + @objc func removeKeyboardAnimationView() { if keyboardAnimationView?.superview != nil { keyboardAnimationView?.removeFromSuperview() } } - @objc public func setUpKeyboardSpringAnimationIfNeeded(_ keyboardAnimation: CAAnimation?) { + @objc func setUpKeyboardSpringAnimationIfNeeded(_ keyboardAnimation: CAAnimation?) { // If keyboard animation is nil or not a spring animation, fallback to DisplayLink tracking. guard let keyboardCASpringAnimation = keyboardAnimation as? CASpringAnimation else { keyboardSpringAnimation = nil @@ -457,7 +457,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva ) } - @objc public func setUpKeyboardAnimationVsyncClient( + @objc func setUpKeyboardAnimationVsyncClient( _ animationCallback: FlutterKeyboardAnimationCallback? ) { guard let animationCallback = animationCallback else { return } @@ -485,7 +485,7 @@ public typealias FlutterKeyboardAnimationCallback = (_ targetTime: CFTimeInterva keyboardAnimationVSyncClient?.await() } - @objc public func ensureViewportMetricsIsCorrect() { + @objc func ensureViewportMetricsIsCorrect() { delegate?.updateViewportMetrics(withInset: targetViewInsetBottom) } } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngine.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngine.swift index b6038a16e59cb..0d0f2fb725718 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngine.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngine.swift @@ -29,7 +29,7 @@ import Foundation /// `FlutterImplicitEngineDelegate` and implementing `didInitializeImplicitFlutterEngine` instead of /// relying on `FlutterAppDelegate`'s automatic registration. @objc(FlutterLaunchEngine) -public final class LaunchEngine: NSObject { +final class LaunchEngine: NSObject { /// The lifecycle state of the contained engine. private enum State { @@ -49,7 +49,7 @@ public final class LaunchEngine: NSObject { /// /// In cases where `takeEngine` has not yet been called, this will lazily allocate and return an /// engine. - @objc public func acquireEngine() -> FlutterEngine? { + @objc func acquireEngine() -> FlutterEngine? { switch state { case .uninitialized: let newEngine = FlutterEngine( @@ -73,7 +73,7 @@ public final class LaunchEngine: NSObject { /// Take ownership of the launch engine. /// /// After this is called `acquireEngine` and `takeEngine` will always return nil. - @objc public func takeEngine() -> FlutterEngine? { + @objc func takeEngine() -> FlutterEngine? { let result: FlutterEngine? switch state { case .created(let engine): diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngineTests.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngineTests.swift index 73896f1660588..753253cd13e70 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngineTests.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/LaunchEngineTests.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import InternalFlutterSwift +@testable import InternalFlutterSwift import Testing @MainActor diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/SplashScreenManager.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/SplashScreenManager.swift index c8b51762a2875..fea08d6651700 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/SplashScreenManager.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/SplashScreenManager.swift @@ -9,7 +9,7 @@ import UIKit /// Handles loading from storyboards or XIBs based on the `UILaunchStoryboardName` in the app's /// Info.plist. Performs an animated fade-out transition when the splash screen is removed. @objc(FlutterSplashScreenManager) -public final class SplashScreenManager: NSObject { +final class SplashScreenManager: NSObject { /// The default duration for the splash screen fade-out animation. private static let defaultAnimationDuration: TimeInterval = 0.2 @@ -23,7 +23,7 @@ public final class SplashScreenManager: NSObject { /// Setting this property to a new view will update view and apply flexible width/height /// autoresizing masks. Setting to `nil` will trigger the removal of the current splash screen /// with a fade-out animation. - @objc public var splashScreenView: UIView? { + @objc var splashScreenView: UIView? { get { return _splashScreenView } set { guard newValue !== _splashScreenView else { return } @@ -40,20 +40,20 @@ public final class SplashScreenManager: NSObject { /// Initializes a new manager with the specified bundle. /// /// The bundle is used to look up the launch storyboard name and resources. - @objc public init(bundle: Bundle = .main) { + @objc init(bundle: Bundle = .main) { self.bundle = bundle super.init() } /// Initializes a new manager with the main bundle. - @objc public override convenience init() { + @objc override convenience init() { self.init(bundle: .main) } /// Attempts to load the splash screen view specified by `UILaunchStoryboardName` in Info.plist. /// - Returns: `true` if successful, `false` otherwise. @discardableResult - @objc public func loadDefaultSplashScreenView() -> Bool { + @objc func loadDefaultSplashScreenView() -> Bool { guard let launchscreenName = bundle.infoDictionary?["UILaunchStoryboardName"] as? String else { return false } @@ -93,7 +93,7 @@ public final class SplashScreenManager: NSObject { /// /// The completion block is invoked after the animation completes and the view is removed from its /// superview. - @objc public func removeSplashScreen(completion: (() -> Void)?) { + @objc func removeSplashScreen(completion: (() -> Void)?) { // If no splash screen, bail out immediately and invoke the completion handler. guard let splashScreen = _splashScreenView else { completion?() @@ -115,7 +115,7 @@ public final class SplashScreenManager: NSObject { /// Installs the splash screen view into the specified parent view, if it is not already added. /// /// The view's frame is set to match the parent view's bounds. - @objc public func installSplashScreenView(asSubviewOf parentView: UIView) { + @objc func installSplashScreenView(asSubviewOf parentView: UIView) { guard let splashScreen = splashScreenView else { return } splashScreen.frame = parentView.bounds diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/TaskRunnerTests.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/TaskRunnerTests.swift index 9eae6d1329809..7b6696ef73148 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/TaskRunnerTests.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/TaskRunnerTests.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import InternalFlutterSwift +@testable import InternalFlutterSwift import QuartzCore import Testing diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/UIPressProxy.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/UIPressProxy.swift index 6b0810f995180..b3c81d3171a4c 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/UIPressProxy.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/UIPressProxy.swift @@ -9,7 +9,7 @@ import UIKit /// UIEvent or UIPress directly. @available(iOS 13.4, *) @objc(FlutterUIPressProxy) -public class UIPressProxy: NSObject { +class UIPressProxy: NSObject { private var press: UIPress? private var event: UIEvent? @@ -18,7 +18,7 @@ public class UIPressProxy: NSObject { * - Parameter press: The UIPress object to wrap. * - Parameter event: The UIEvent object to wrap. */ - @objc public init(press: UIPress, event: UIEvent) { + @objc init(press: UIPress, event: UIEvent) { self.press = press self.event = event super.init() // Call superclass initializer @@ -29,7 +29,7 @@ public class UIPressProxy: NSObject { * Subclasses using this MUST override the properties below as needed, * as the internal press/event objects will be nil. */ - @objc override public init() { + @objc override init() { self.press = nil self.event = nil super.init() @@ -40,7 +40,7 @@ public class UIPressProxy: NSObject { } /// The phase of the press event. - @objc public var phase: UIPress.Phase { + @objc var phase: UIPress.Phase { guard let press = press else { fatalError("nil UIPress") } @@ -49,12 +49,12 @@ public class UIPressProxy: NSObject { /// The key associated with the press event, if any. /// Note: In Swift, `UIPress.key` is optional. - @objc public var key: UIKey? { + @objc var key: UIKey? { return press?.key } /// The type of the event. - @objc public var type: UIEvent.EventType { + @objc var type: UIEvent.EventType { guard let event = event else { fatalError("nil UIEvent") } @@ -63,7 +63,7 @@ public class UIPressProxy: NSObject { /// The time at which the event occurred. /// NSTimeInterval is typealiased to TimeInterval in Swift. - @objc public var timestamp: TimeInterval { + @objc var timestamp: TimeInterval { guard let event = event else { fatalError("nil UIEvent") } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClient.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClient.swift index 281be1ca1e85a..ae1aec59ae18d 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClient.swift +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClient.swift @@ -7,7 +7,7 @@ import QuartzCore import UIKit @objc(FlutterVSyncClient) -public class VSyncClient: NSObject { +class VSyncClient: NSObject { private static let defaultRefreshRate: Double = 60.0 private let taskRunner: TaskRunner @@ -16,7 +16,7 @@ public class VSyncClient: NSObject { /// The display link used to coordinate vsync callbacks. @objc - internal private(set) var displayLink: CADisplayLink? + private(set) var displayLink: CADisplayLink? private var _refreshRate: Double = defaultRefreshRate @@ -29,7 +29,7 @@ public class VSyncClient: NSObject { /// If the current refresh rate is unknown or invalid (e.g., during startup or a paused transition), /// this property falls back to `defaultRefreshRate` (60Hz). @objc - public var refreshRate: Double { + var refreshRate: Double { return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate } @@ -37,7 +37,7 @@ public class VSyncClient: NSObject { /// signal. Setting this property to `false` can avoid this and vsync client will trigger vsync /// callback continuously. @objc - public var allowPauseAfterVsync: Bool = true + var allowPauseAfterVsync: Bool = true /// Initializes the vsync client. /// @@ -47,7 +47,7 @@ public class VSyncClient: NSObject { /// - maxRefreshRate: The maximum refresh rate to configure the display link with. /// - callback: The callback to invoke when a vsync signal is received. @objc - public init( + init( taskRunner: TaskRunner, isVariableRefreshRateEnabled: Bool, maxRefreshRate: Double, @@ -86,7 +86,7 @@ public class VSyncClient: NSObject { /// /// - Parameter requestedRate: The target maximum refresh rate in Hertz. @objc - public func setMaxRefreshRate(_ requestedRate: Double) { + func setMaxRefreshRate(_ requestedRate: Double) { guard isVariableRefreshRateEnabled else { return } guard let link = displayLink else { return } @@ -109,7 +109,7 @@ public class VSyncClient: NSObject { /// Calling this method unpauses the underlying `CADisplayLink`, allowing it to trigger /// `onDisplayLink(_:)` on the next vsync event. @objc - public func await() { + func await() { displayLink?.isPaused = false } @@ -118,7 +118,7 @@ public class VSyncClient: NSObject { /// Calling this method pauses the underlying `CADisplayLink`, preventing it from triggering /// any subsequent vsync events until `await()` is called. @objc - public func pause() { + func pause() { displayLink?.isPaused = true } @@ -127,7 +127,7 @@ public class VSyncClient: NSObject { /// This method must be called before releasing the `VSyncClient` instance to prevent memory leaks /// caused by the `CADisplayLink` retaining its target. @objc - public func invalidate() { + func invalidate() { guard let link = displayLink else { return } displayLink = nil @@ -152,7 +152,7 @@ public class VSyncClient: NSObject { /// /// - Parameter link: The display link triggering this event. @objc - internal func onDisplayLink(_ link: CADisplayLink) { + func onDisplayLink(_ link: CADisplayLink) { // CADisplayLink timestamps use the CACurrentMediaTime() monotonic clock (seconds since boot). // CACurrentMediaTime() is based on mach_absolute_time, whereas the core engine uses // fml::TimePoint, which is implemented with std::chrono::steady_clock, which uses diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index fd17dd493a96c..3413d650f996e 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -15,6 +15,7 @@ #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/lib/ui/window/platform_message.h" #include "flutter/shell/platform/common/accessibility_bridge.h" +#import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h" #import "flutter/shell/platform/darwin/common/test_utils_swift/test_utils_swift.h" diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.swift b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.swift index f5b01d48512b1..551b6da796824 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.swift +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.swift @@ -11,7 +11,7 @@ import Foundation /// schedules the task in both common run loop mode and a private run loop mode, /// which allows it to run in a mode where it only processes Flutter messages /// (`pollFlutterMessagesOnce()`). -@objc public final class FlutterRunLoop: NSObject { +@objc final class FlutterRunLoop: NSObject { private static let flutterRunLoopMode = CFRunLoopMode("FlutterRunLoopMode" as CFString) private static var _mainRunLoop: FlutterRunLoop? @@ -91,7 +91,7 @@ import Foundation // Ensures that the `FlutterRunLoop` for main thread is initialized. Only // needs to be called once and must be called on the main thread. - @objc public static func ensureMainLoopInitialized() { + @objc static func ensureMainLoopInitialized() { assert(Thread.isMainThread, "Must be called on the main thread.") if _mainRunLoop == nil { _mainRunLoop = FlutterRunLoop() @@ -99,7 +99,7 @@ import Foundation } // The `FlutterRunLoop` for the main thread. - @objc public static var mainRunLoop: FlutterRunLoop { + @objc static var mainRunLoop: FlutterRunLoop { assert( _mainRunLoop != nil, "Main run loop has not been initialized. Call ensureMainLoopInitialized() first." @@ -108,7 +108,7 @@ import Foundation } // Schedules a block to be executed on the main thread. - @objc public func perform(afterDelay delay: TimeInterval, block: @escaping () -> Void) { + @objc func perform(afterDelay delay: TimeInterval, block: @escaping () -> Void) { tasksLock.lock() defer { tasksLock.unlock() } @@ -123,7 +123,7 @@ import Foundation // Schedules a block to be executed on the main thread after a delay. @objc(performBlock:) - public func perform(_ block: @escaping () -> Void) { + func perform(_ block: @escaping () -> Void) { perform(afterDelay: 0, block: block) } @@ -158,7 +158,7 @@ import Foundation /// Executes single iteration of the run loop in the mode where only Flutter /// messages are processed. - @objc public func pollFlutterMessagesOnce() { + @objc func pollFlutterMessagesOnce() { CFRunLoopRunInMode(Self.flutterRunLoopMode, 0.1, true) } } diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizer.swift b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizer.swift index 21bb9ee0efea8..de4c3d13d8fc4 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizer.swift +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizer.swift @@ -41,7 +41,7 @@ import Foundation /// - Thread Safety: The class manages its internal state in a thread-safe manner to coordinate /// actions between the platform thread and the raster thread. @objc(FlutterResizeSynchronizer) -public final class ResizeSynchronizer: NSObject { +final class ResizeSynchronizer: NSObject { private static let invalidSize = CGSize(width: -1, height: -1) // Synchronizes access to _isInResize_unsafe: isInResize is accessed from multiple threads and @@ -75,7 +75,7 @@ public final class ResizeSynchronizer: NSObject { /// Blocks the thread until `performCommit(forSize:notify:delay:)` with the same size is called. /// While the thread is blocked, Flutter messages are being pumped. /// See `FlutterRunLoop.mainRunLoop.pollFlutterMessagesOnce()`. - @objc public func beginResize( + @objc func beginResize( forSize size: CGSize, notify: () -> Void, onTimeout: (() -> Void)? = nil @@ -121,7 +121,7 @@ public final class ResizeSynchronizer: NSObject { /// on the platform thread, if waiting for the surface during resize. /// /// Called from the raster thread on frame present. - @objc public func performCommit( + @objc func performCommit( forSize size: CGSize, afterDelay delay: TimeInterval, notify: @escaping () -> Void @@ -143,7 +143,7 @@ public final class ResizeSynchronizer: NSObject { /// Notifies the synchronizer that the Flutter view is being shut down. /// /// Unblocks the platform thread if blocked. - @objc public func shutDown() { + @objc func shutDown() { FlutterRunLoop.mainRunLoop.perform { self.isShuttingDown = true } diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizerTests.swift b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizerTests.swift index 47fdfa9d31fa8..ff50150b773ef 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizerTests.swift +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizerTests.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import InternalFlutterSwift +@testable import InternalFlutterSwift import Testing // Tests for `ResizeSynchronizer`. From 34c396450f07b82d7b5ef41a4042222b6c600fa2 Mon Sep 17 00:00:00 2001 From: "John \"codefu\" McDole" Date: Wed, 5 Aug 2026 17:13:53 -0700 Subject: [PATCH 101/330] ci: make tree-analyze workflow wait for linux_android_debug_engine (#190628) fixes: #190627 --- .github/workflows/tree-analyze.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tree-analyze.yml b/.github/workflows/tree-analyze.yml index 3f1ac02ccc92d..93f8fbc83eef6 100644 --- a/.github/workflows/tree-analyze.yml +++ b/.github/workflows/tree-analyze.yml @@ -33,6 +33,7 @@ jobs: uses: ./.github/actions/wait-for-engine-build with: github-token: ${{ secrets.GITHUB_TOKEN }} + check-name: 'Linux linux_host_engine,Linux linux_android_debug_engine' - uses: ./.github/actions/composite-flutter-setup From 09aafef6445b9b97ecc022efabecf9a9dc0fc833 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 6 Aug 2026 10:30:53 +0900 Subject: [PATCH 102/330] iOS: Switch logging from syslog to os_log (#190595) Previously, `Logger` on iOS defaulted to syslog which used C string interop and didn't record severity. Because `Settings::log_message_callback` is wired to use `Logger`, this migrates all iOS logging to `os_log`: both iOS embedder logging AND core engine (FML-based) logging, which includes Dart print statements in Flutter apps since those rely on the core engine log callback too. Manually tested using: * debug mode iOS simulator * debug mode iOS physical device * release mode iOS physical device This change does not affect macOS logging which previous used logging to stdout and still uses logging to stdout, but out of paranoia, since it also uses FlutterLogger, I manually tested using: * debug mode macOS app * release mode macOS app A followup patch will expose the logging subsystem via a `tag` parameter in `Logger`'s API so we don't need to do manual concatentation in the `log_message_callback`. Fixes https://github.com/flutter/flutter/issues/44030 (fingers crossed) ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../common/framework/Source/Logger.swift | 34 ++++++++++++++++--- .../common/framework/Source/LoggerTests.swift | 11 +++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift index a4db8d8fab978..ef2f06fa76939 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift @@ -4,6 +4,7 @@ import Darwin import Foundation +import os /// The level of logging severity. /// @@ -76,7 +77,7 @@ import Foundation #if os(iOS) // On iOS, the user has no access to stdout. // Output can be read from the log by the user or the `flutter` tool. - self.init(outputWriter: SyslogOutputWriter(), logLevel: .info) + self.init(outputWriter: OSLogOutputWriter(), logLevel: .info) #elseif os(macOS) // On macOS, both the user and the tool read from stdout. self.init(outputWriter: StdoutOutputWriter(), logLevel: .info) @@ -182,11 +183,34 @@ protocol OutputWriter: Sendable { func writeLine(level: LogLevel, _ message: String) } -final class SyslogOutputWriter: OutputWriter, Sendable { +private extension LogLevel { + /// The `OSLogType` used to emit a message at this level via `os_log`. + /// + /// `OSLogType` is not a strict severity ladder like `LogLevel`. It's a small set of categories + /// with differing persistence and display behavior. Each level therefore maps to the type with + /// the closest semantics rather than a matching severity. + /// + /// - `.info` is buffered in memory and not written to persistent store by default. + /// - `.warning` is written to persistent store. + /// - `.error` is logged with error metadata and is written to persistent store. + /// - `.important` is used by Dart `print` output, so is written to persistent store. + /// - `.fatal` is logged with fault metadata and is written to persistent store. + var osLogType: OSLogType { + switch self { + case .info: return .info + case .warning: return .default + case .error: return .error + case .important: return .default + case .fatal: return .fault + } + } +} + +final class OSLogOutputWriter: OutputWriter, Sendable { + private let osLog = OSLog(subsystem: "io.flutter.flutter", category: "flutter") + func writeLine(level: LogLevel, _ message: String) { - // TODO(cbracken): replace this with os_log-based approach. - // https://github.com/flutter/flutter/issues/44030 - message.withCString { vsyslog(LOG_ALERT, "%s", getVaList([$0])) } + os_log("%{public}@", log: osLog, type: level.osLogType, message) } } diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift index 9cfa2e317fe79..3075da055b047 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift @@ -7,7 +7,10 @@ import Foundation import Testing @testable import test_utils_swift -@Suite struct LoggerTests { +// Several tests mutate shared `Logger` singleton properties (`outputWriter`, `logLevel`). +// Even though those mutations are all lock-guarded and thread-safe, we serialize the tests to keep +// them from racing each other. +@Suite(.serialized) struct LoggerTests { @Test func testInitialization() { let writer = StringOutputWriter() @@ -135,4 +138,10 @@ import Testing // races or crashes. #expect(Logger.logLevel == .info || Logger.logLevel == .warning) } + + @Test func testDefaultInitialization() { + let logger = Logger() + #expect(logger.logLevel == .info) + logger.log(level: .info, "Test default logger") + } } From b345c5126925e9c7b48c5948d1270501f0a8fda0 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:11:41 -0700 Subject: [PATCH 103/330] Revert: iOS: Switch logging from syslog to os_log (#190643) Reverts: [iOS: Switch logging from syslog to os_log](https://github.com/flutter/flutter/pull/190595) Initiated by: @cbracken Reason for reverting: iOS devicelab bots failing to find VM service Original PR Author: @cbracken Reviewed By: @LongCatIsLooong The original PR description is provided below: Previously, `Logger` on iOS defaulted to syslog which used C string interop and didn't record severity. Because `Settings::log_message_callback` is wired to use `Logger`, this migrates all iOS logging to `os_log`: both iOS embedder logging AND core engine (FML-based) logging, which includes Dart print statements in Flutter apps since those rely on the core engine log callback too. Manually tested using: * debug mode iOS simulator * debug mode iOS physical device * release mode iOS physical device This change does not affect macOS logging which previous used logging to stdout and still uses logging to stdout, but out of paranoia, since it also uses FlutterLogger, I manually tested using: * debug mode macOS app * release mode macOS app A followup patch will expose the logging subsystem via a `tag` parameter in `Logger`'s API so we don't need to do manual concatentation in the `log_message_callback`. Fixes https://github.com/flutter/flutter/issues/44030 (fingers crossed) ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../common/framework/Source/Logger.swift | 34 +++---------------- .../common/framework/Source/LoggerTests.swift | 11 +----- 2 files changed, 6 insertions(+), 39 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift index ef2f06fa76939..a4db8d8fab978 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/Logger.swift @@ -4,7 +4,6 @@ import Darwin import Foundation -import os /// The level of logging severity. /// @@ -77,7 +76,7 @@ import os #if os(iOS) // On iOS, the user has no access to stdout. // Output can be read from the log by the user or the `flutter` tool. - self.init(outputWriter: OSLogOutputWriter(), logLevel: .info) + self.init(outputWriter: SyslogOutputWriter(), logLevel: .info) #elseif os(macOS) // On macOS, both the user and the tool read from stdout. self.init(outputWriter: StdoutOutputWriter(), logLevel: .info) @@ -183,34 +182,11 @@ protocol OutputWriter: Sendable { func writeLine(level: LogLevel, _ message: String) } -private extension LogLevel { - /// The `OSLogType` used to emit a message at this level via `os_log`. - /// - /// `OSLogType` is not a strict severity ladder like `LogLevel`. It's a small set of categories - /// with differing persistence and display behavior. Each level therefore maps to the type with - /// the closest semantics rather than a matching severity. - /// - /// - `.info` is buffered in memory and not written to persistent store by default. - /// - `.warning` is written to persistent store. - /// - `.error` is logged with error metadata and is written to persistent store. - /// - `.important` is used by Dart `print` output, so is written to persistent store. - /// - `.fatal` is logged with fault metadata and is written to persistent store. - var osLogType: OSLogType { - switch self { - case .info: return .info - case .warning: return .default - case .error: return .error - case .important: return .default - case .fatal: return .fault - } - } -} - -final class OSLogOutputWriter: OutputWriter, Sendable { - private let osLog = OSLog(subsystem: "io.flutter.flutter", category: "flutter") - +final class SyslogOutputWriter: OutputWriter, Sendable { func writeLine(level: LogLevel, _ message: String) { - os_log("%{public}@", log: osLog, type: level.osLogType, message) + // TODO(cbracken): replace this with os_log-based approach. + // https://github.com/flutter/flutter/issues/44030 + message.withCString { vsyslog(LOG_ALERT, "%s", getVaList([$0])) } } } diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift index 3075da055b047..9cfa2e317fe79 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/LoggerTests.swift @@ -7,10 +7,7 @@ import Foundation import Testing @testable import test_utils_swift -// Several tests mutate shared `Logger` singleton properties (`outputWriter`, `logLevel`). -// Even though those mutations are all lock-guarded and thread-safe, we serialize the tests to keep -// them from racing each other. -@Suite(.serialized) struct LoggerTests { +@Suite struct LoggerTests { @Test func testInitialization() { let writer = StringOutputWriter() @@ -138,10 +135,4 @@ import Testing // races or crashes. #expect(Logger.logLevel == .info || Logger.logLevel == .warning) } - - @Test func testDefaultInitialization() { - let logger = Logger() - #expect(logger.logLevel == .info) - logger.log(level: .info, "Test default logger") - } } From 710981c09ba17f22134671f7d8a6f372c7fb6baf Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 6 Aug 2026 15:02:27 +0900 Subject: [PATCH 104/330] iOS: Remove the IOSRenderingAPI selection code (#190636) Flutter iOS requires Metal. Skia support has been removed. Impeller has no software fallback. `IOSRenderingAPI` had one value (`kMetal`) and `GetRenderingAPIForProcess` only ever returned that value after asserting Metal was available. This removes the enum and the selection plumbing that was threaded through the engine, view, platform view, and context. `GetCoreAnimationLayerClassForRenderingAPI` still returns `FlutterMetalLayer` or `CAMetalLayer` depending on embedder settings. Issue: https://github.com/flutter/flutter/issues/190041 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/framework/Source/FlutterEngine+Test.h | 2 - .../ios/framework/Source/FlutterEngine.mm | 13 ++--- .../Source/FlutterEnginePlatformViewTest.mm | 1 - .../ios/framework/Source/FlutterEngineTest.mm | 8 --- .../Source/FlutterPlatformViewsTest.mm | 53 ------------------- .../Source/FlutterTextInputPluginTest.mm | 1 - .../ios/framework/Source/FlutterView.mm | 3 +- .../Source/accessibility_bridge_test.mm | 39 -------------- .../shell/platform/darwin/ios/ios_context.h | 8 --- .../shell/platform/darwin/ios/ios_context.mm | 9 +--- .../platform/darwin/ios/platform_view_ios.h | 2 - .../platform/darwin/ios/platform_view_ios.mm | 12 ++--- .../darwin/ios/platform_view_ios_test.mm | 2 - .../darwin/ios/rendering_api_selection.h | 10 ++-- .../darwin/ios/rendering_api_selection.mm | 34 ++++-------- 15 files changed, 25 insertions(+), 172 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine+Test.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine+Test.h index c3d64636e0712..a7045beb0be77 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine+Test.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine+Test.h @@ -9,7 +9,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" -#import "flutter/shell/platform/darwin/ios/rendering_api_selection.h" #include "flutter/shell/platform/embedder/embedder.h" @class FlutterBinaryMessengerRelay; @@ -30,7 +29,6 @@ class ThreadHost; - (flutter::PlatformViewIOS*)platformView; - (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger; -- (flutter::IOSRenderingAPI)platformViewsRenderingAPI; - (void)waitForFirstFrame:(NSTimeInterval)timeout callback:(void (^)(BOOL didTimeout))callback; - (FlutterEngine*)spawnWithEntrypoint:(/*nullable*/ NSString*)entrypoint libraryURI:(/*nullable*/ NSString*)libraryURI diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index b86edb4252d71..cf7dc0badc47e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -41,7 +41,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/profiler_metrics_ios.h" #import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" -#import "flutter/shell/platform/darwin/ios/rendering_api_selection.h" #include "flutter/shell/profiling/sampling_profiler.h" FLUTTER_ASSERT_ARC @@ -200,7 +199,6 @@ @implementation FlutterEngine { // -destroyContext must wait for this group to drain before it is safe to free _shell. dispatch_group_t _firstFrameWaiters; - flutter::IOSRenderingAPI _renderingApi; std::shared_ptr _profiler; FlutterBinaryMessengerRelay* _binaryMessenger; @@ -343,14 +341,9 @@ - (void)sceneWillConnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)) } - (void)recreatePlatformViewsController { - _renderingApi = flutter::GetRenderingAPIForProcess(); _platformViewsController = [[FlutterPlatformViewsController alloc] init]; } -- (flutter::IOSRenderingAPI)platformViewsRenderingAPI { - return _renderingApi; -} - - (void)dealloc { /// Notify plugins of dealloc. This should happen first in dealloc since the /// plugins may be talking to things like the binaryMessenger. @@ -926,9 +919,9 @@ - (BOOL)createShell:(NSString*)entrypoint [strongSelf recreatePlatformViewsController]; strongSelf.platformViewsController.taskRunner = [[FlutterFMLTaskRunner alloc] initWithTaskRunner:shell.GetTaskRunners().GetPlatformTaskRunner()]; - return std::make_unique( - shell, strongSelf->_renderingApi, strongSelf.platformViewsController, - shell.GetTaskRunners(), shell.GetIsGpuDisabledSyncSwitch()); + return std::make_unique(shell, strongSelf.platformViewsController, + shell.GetTaskRunners(), + shell.GetIsGpuDisabledSyncSwitch()); }; flutter::Shell::CreateCallback on_create_rasterizer = diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm index a0d1245aedcb8..6de5f50e2cc21 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm @@ -86,7 +86,6 @@ - (void)setUp { /*io=*/thread_task_runner); platform_view = std::make_unique( /*delegate=*/fake_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/sync_switch); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 75ba57588289a..60d78fe1efaac 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -285,14 +285,6 @@ - (void)testInitialRouteSettingsSendsNavigationMessage { message:encodedSetInitialRouteMethod]); } -- (void)testPlatformViewsControllerRenderingMetalBackend { - FlutterEngine* engine = [[FlutterEngine alloc] init]; - [engine run]; - flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI]; - - XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kMetal); -} - - (void)testWaitForFirstFrameTimeout { FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"]; [engine run]; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index d40b5a8f9f9c2..7e1242b03e02a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -378,7 +378,6 @@ - (void)testFlutterViewOnlyCreateOnceInOneFrame { CreateTestPlatformViewsController(self.name); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -431,7 +430,6 @@ - (void)testCanCreatePlatformViewWithoutFlutterView { CreateTestPlatformViewsController(self.name); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -525,7 +523,6 @@ - (void)testApplyBackdropFilter { CreateTestPlatformViewsController(self.name); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -602,7 +599,6 @@ - (void)testApplyBackdropFilterWithCorrectFrame { CreateTestPlatformViewsController(self.name); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -679,7 +675,6 @@ - (void)testApplyMultipleBackdropFilters { CreateTestPlatformViewsController(self.name); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -758,7 +753,6 @@ - (void)testAddBackdropFilters { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -883,7 +877,6 @@ - (void)testRemoveBackdropFilters { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -1035,7 +1028,6 @@ - (void)testEditBackdropFilters { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -1335,7 +1327,6 @@ - (void)testApplyBackdropFilterNotDlBlurImageFilter { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -1647,7 +1638,6 @@ - (void)testApplyBackdropFilterRespectsClipRRect { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -1726,7 +1716,6 @@ - (void)testApplyBackdropFilterRespectsClipRSuperellipse { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -1830,7 +1819,6 @@ - (void)testCompositePlatformView { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -1890,7 +1878,6 @@ - (void)testBackdropFilterCorrectlyPushedAndReset { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -1995,7 +1982,6 @@ - (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2069,7 +2055,6 @@ - (void)testClipsDoNotInterceptWithPlatformViewShouldNotAddMaskView { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2141,7 +2126,6 @@ - (void)testClipRRectOnlyHasCornersInterceptWithPlatformViewShouldAddMaskView { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2212,7 +2196,6 @@ - (void)testClipRect { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2288,7 +2271,6 @@ - (void)testClipRect_multipleClips { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2384,7 +2366,6 @@ - (void)testClipRRect { flutterPlatformViewsController.taskRunner = flutter::testing::GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2488,7 +2469,6 @@ - (void)testClipRRect_multipleClips { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2609,7 +2589,6 @@ - (void)testClipPath { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2713,7 +2692,6 @@ - (void)testClipPath_multipleClips { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2834,7 +2812,6 @@ - (void)testSetFlutterViewControllerAfterCreateCanStillDispatchTouchEvents { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2888,7 +2865,6 @@ - (void)testSetFlutterViewControllerInTheMiddleOfTouchEventShouldStillAllowGestu flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -2999,7 +2975,6 @@ - (void)testSetFlutterViewControllerInTheMiddleOfTouchEventShouldStillAllowGestu flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3099,7 +3074,6 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3158,7 +3132,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3240,7 +3213,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3304,7 +3276,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3369,7 +3340,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3433,7 +3403,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3486,7 +3455,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3598,7 +3566,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3743,7 +3710,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3895,7 +3861,6 @@ - (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailThe flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3937,7 +3902,6 @@ - (void)testFlutterPlatformViewGestureBlockingPolicy_ShouldAddDelayingRecognizer flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -3984,7 +3948,6 @@ - (void)testFlutterPlatformViewHitTest_AcceptTouchIfInstructedByFramework { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4040,7 +4003,6 @@ - (void)testFlutterPlatformViewHitTest_RejectTouchIfInstructedByFramework { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4096,7 +4058,6 @@ - (void)testFlutterPlatformViewGestureBlockingPolicy_PolicyMappingIsCorrect { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4198,7 +4159,6 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4271,7 +4231,6 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4323,7 +4282,6 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4389,7 +4347,6 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4491,7 +4448,6 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4680,7 +4636,6 @@ - (void)testClipMaskViewIsReused { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4778,7 +4733,6 @@ - (void)testDifferentClipMaskViewIsUsedForEachView { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4863,7 +4817,6 @@ - (void)testMaskViewUsesCAShapeLayerAsTheBackingLayer { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -4956,7 +4909,6 @@ - (void)testDisposingViewInCompositionOrderDoNotCrash { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -5069,7 +5021,6 @@ - (void)testOnlyPlatformViewsAreRemovedWhenReset { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -5135,7 +5086,6 @@ - (void)testResetClearsPreviousCompositionOrder { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -5203,7 +5153,6 @@ - (void)testNilPlatformViewDoesntCrash { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -5297,7 +5246,6 @@ - (void)testFlutterPlatformViewControllerSubmitFramePreservingFrameDamage { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); @@ -5378,7 +5326,6 @@ - (void)testClipSuperellipse { flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners.taskRunners, /*is_gpu_disabled_jsync_switch=*/std::make_shared()); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index 56ab4eb1750f5..07920eb58f53e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -891,7 +891,6 @@ - (void)testHotRestart { thread_task_runner->PostTask([&] { auto platform_view = std::make_unique( /*delegate=*/mock_platform_view_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm index 4e93dd30b57d3..7f522f43af0a9 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -8,6 +8,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" +#import "flutter/shell/platform/darwin/ios/rendering_api_selection.h" FLUTTER_ASSERT_ARC @@ -191,7 +192,7 @@ - (void)layoutSubviews { } + (Class)layerClass { - return flutter::GetCoreAnimationLayerClassForRenderingAPI(flutter::GetRenderingAPIForProcess()); + return flutter::GetCoreAnimationLayerClass(); } - (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index 4db876e4250dd..d2ffde9532b4d 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -162,7 +162,6 @@ - (void)testCreate { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -183,7 +182,6 @@ - (void)testUpdateSemanticsEmpty { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -211,7 +209,6 @@ - (void)testUpdateSemanticsOneNode { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -256,7 +253,6 @@ - (void)testIsVoiceOverRunning { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -289,7 +285,6 @@ - (void)testSemanticsDeallocated { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -352,7 +347,6 @@ - (void)testSemanticsDeallocatedWithoutLoadingView { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -401,7 +395,6 @@ - (void)testReplacedSemanticsDoesNotCleanupChildren { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -497,7 +490,6 @@ - (void)testScrollableSemanticsDeallocated { [[FlutterFMLTaskRunner alloc] initWithTaskRunner:thread_task_runner]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -569,7 +561,6 @@ - (void)testBridgeReplacesSemanticsNode { [[FlutterPlatformViewsController alloc] init]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -640,7 +631,6 @@ - (void)testAnnouncesRouteChanges { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -707,7 +697,6 @@ - (void)testRadioButtonIsNotSwitchButton { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -752,7 +741,6 @@ - (void)testSemanticObjectWithNoAccessibilityFlagNotMarkedAsResponsiveToUserInte /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -794,7 +782,6 @@ - (void)testSemanticObjectWithAccessibilityFlagsMarkedAsResponsiveToUserInteract /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -842,7 +829,6 @@ - (void)testLabeledParentAndChildNotInteractive { [[FlutterPlatformViewsController alloc] init]; auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/flutterPlatformViewsController, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -899,7 +885,6 @@ - (void)testLayoutChangeWithNonAccessibilityElement { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -982,7 +967,6 @@ - (void)testLayoutChangeDoesCallNativeAccessibility { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1056,7 +1040,6 @@ - (void)testLayoutChangeDoesCallNativeAccessibilityWhenFocusChanged { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1129,7 +1112,6 @@ - (void)testScrollableSemanticsContainerReturnsCorrectChildren { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1185,7 +1167,6 @@ - (void)testAnnouncesRouteChangesAndLayoutChangeInOneUpdate { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1281,7 +1262,6 @@ - (void)testAnnouncesRouteChangesWhenAddAdditionalRoute { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1366,7 +1346,6 @@ - (void)testAnnouncesRouteChangesRemoveRouteInMiddle { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1457,7 +1436,6 @@ - (void)testHandleEvent { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1512,7 +1490,6 @@ - (void)testAccessibilityObjectDidBecomeFocused { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1550,7 +1527,6 @@ - (void)testAnnouncesRouteChangesWhenNoNamesRoute { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1619,7 +1595,6 @@ - (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1687,7 +1662,6 @@ - (void)testAnnouncesLayoutChangeWithTheSameItemFocused { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1761,7 +1735,6 @@ - (void)testAnnouncesLayoutChangeWhenFocusMovedOutside { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1837,7 +1810,6 @@ - (void)testAnnouncesScrollChangeWithLastFocused { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1909,7 +1881,6 @@ - (void)testAnnouncesScrollChangeDoesCallNativeAccessibility { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -1983,7 +1954,6 @@ - (void)testAnnouncesIgnoresRouteChangesWhenModal { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2039,7 +2009,6 @@ - (void)testAnnouncesIgnoresLayoutChangeWhenModal { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2100,7 +2069,6 @@ - (void)testAnnouncesIgnoresScrollChangeWhenModal { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2169,7 +2137,6 @@ - (void)testAccessibilityMessageAfterDeletion { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2201,7 +2168,6 @@ - (void)testFlutterSemanticsScrollViewManagedObjectLifecycleCorrectly { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2249,7 +2215,6 @@ - (void)testPlatformViewDestructorDoesNotCallSemanticsAPIs { thread_task_runner->PostTask([&] { auto platform_view = std::make_unique( /*delegate=*/test_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2294,7 +2259,6 @@ - (void)testResetsAccessibilityElementsOnHotRestart { thread_task_runner->PostTask([&] { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2322,7 +2286,6 @@ - (void)testWeakViewController { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2354,7 +2317,6 @@ - (void)testSemanticsObjectAndContainerAccessAfterBridgeDestruction { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -2423,7 +2385,6 @@ - (void)testAccessibilityChannelCallbackAfterBridgeDestruction { /*io=*/thread_task_runner); auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context.h b/engine/src/flutter/shell/platform/darwin/ios/ios_context.h index 88a96742412e0..1e966046c28b9 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context.h @@ -14,7 +14,6 @@ #include "flutter/fml/macros.h" #include "flutter/fml/synchronization/sync_switch.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterTexture.h" -#import "flutter/shell/platform/darwin/ios/rendering_api_selection.h" #include "impeller/display_list/aiks_context.h" namespace impeller { @@ -42,16 +41,9 @@ class IOSContext { /// @brief Create an iOS context object capable of creating the on-screen /// and off-screen GPU context for use by Impeller. /// - /// In case the engine does not support the specified client - /// rendering API, this a `nullptr` may be returned. - /// - /// @param[in] api A client rendering API supported by the - /// engine/platform. - /// /// @return A valid context on success. `nullptr` on failure. /// static std::unique_ptr Create( - IOSRenderingAPI api, const std::shared_ptr& is_gpu_disabled_sync_switch, const Settings& settings); diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm index 4b7304b4a444a..7ae306c6d8269 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_context.mm @@ -9,7 +9,6 @@ #include "flutter/fml/logging.h" #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #include "flutter/shell/platform/darwin/ios/ios_context_metal_impeller.h" -#include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" FLUTTER_ASSERT_ARC @@ -20,15 +19,9 @@ IOSContext::~IOSContext() = default; std::unique_ptr IOSContext::Create( - IOSRenderingAPI api, const std::shared_ptr& is_gpu_disabled_sync_switch, const Settings& settings) { - switch (api) { - case IOSRenderingAPI::kMetal: - return std::make_unique(settings, is_gpu_disabled_sync_switch); - } - FML_CHECK(false); - return nullptr; + return std::make_unique(settings, is_gpu_disabled_sync_switch); } std::shared_ptr IOSContext::GetImpellerContext() const { diff --git a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h index 51a387032cb7e..20df802942fe5 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h @@ -19,7 +19,6 @@ #import "flutter/shell/platform/darwin/ios/ios_external_view_embedder.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" #import "flutter/shell/platform/darwin/ios/platform_message_handler_ios.h" -#import "flutter/shell/platform/darwin/ios/rendering_api_selection.h" @class FlutterViewController; @@ -46,7 +45,6 @@ class PlatformViewIOS final : public PlatformView { explicit PlatformViewIOS( PlatformView::Delegate& delegate, - IOSRenderingAPI rendering_api, __weak FlutterPlatformViewsController* platform_views_controller, const flutter::TaskRunners& task_runners, const std::shared_ptr& is_gpu_disabled_sync_switch); diff --git a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm index 311c6fdfd903a..21e76981ef433 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm @@ -32,16 +32,14 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} PlatformViewIOS::PlatformViewIOS( PlatformView::Delegate& delegate, - IOSRenderingAPI rendering_api, __weak FlutterPlatformViewsController* platform_views_controller, const flutter::TaskRunners& task_runners, const std::shared_ptr& is_gpu_disabled_sync_switch) - : PlatformViewIOS(delegate, - IOSContext::Create(rendering_api, - is_gpu_disabled_sync_switch, - delegate.OnPlatformViewGetSettings()), - platform_views_controller, - task_runners) {} + : PlatformViewIOS( + delegate, + IOSContext::Create(is_gpu_disabled_sync_switch, delegate.OnPlatformViewGetSettings()), + platform_views_controller, + task_runners) {} PlatformViewIOS::~PlatformViewIOS() = default; diff --git a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm index 0852c3055a594..802afdde1ebda 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm @@ -88,7 +88,6 @@ - (void)testSetSemanticsTreeEnabled { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); @@ -127,7 +126,6 @@ - (void)testLocaleCanBeSetWithoutViewController { auto platform_view = std::make_unique( /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, /*platform_views_controller=*/nil, /*task_runners=*/runners, /*is_gpu_disabled_sync_switch=*/std::make_shared()); diff --git a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h index 67af7a65bcc05..4b737686e558c 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h +++ b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.h @@ -11,13 +11,9 @@ namespace flutter { -enum class IOSRenderingAPI { - kMetal, -}; - -IOSRenderingAPI GetRenderingAPIForProcess(); - -Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api); +// Returns the CoreAnimation layer class to back a Flutter view. Fails fast if +// no Metal device is available. +Class GetCoreAnimationLayerClass(); } // namespace flutter diff --git a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm index 68fdd21278cb0..1417099d26c3f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/rendering_api_selection.mm @@ -31,32 +31,20 @@ bool IsMetalAvailable() { } // namespace -IOSRenderingAPI GetRenderingAPIForProcess() { +Class GetCoreAnimationLayerClass() { static bool metal_available = IsMetalAvailable(); - if (metal_available) { - return IOSRenderingAPI::kMetal; - } - FML_CHECK(false) << "Metal is unavailable. On a simulator this means the host environment " - "does not expose a Metal device; enabling GPU passthrough may fix this."; - FML_UNREACHABLE(); -} + FML_CHECK(metal_available) + << "Metal is unavailable. On a simulator this means the host environment " + "does not expose a Metal device; enabling GPU passthrough may fix this."; -Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api) { - switch (rendering_api) { - case IOSRenderingAPI::kMetal: - if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { - if ([FlutterMetalLayer enabled]) { - return [FlutterMetalLayer class]; - } else { - return [CAMetalLayer class]; - } - } - FML_CHECK(false) << "Metal availability should already have been checked"; - break; - default: - break; + if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { + // FlutterMetalLayer reports itself as a CAMetalLayer via -isKindOfClass:. + if ([FlutterMetalLayer enabled]) { + return [FlutterMetalLayer class]; + } + return [CAMetalLayer class]; } - FML_CHECK(false) << "Unknown client rendering API"; + FML_CHECK(false) << "Metal availability should already have been checked"; return [CALayer class]; } From e26b384689b8f7ff1c98dd1da9935284516ad631 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 5 Aug 2026 23:22:24 -0700 Subject: [PATCH 105/330] [Impeller] Validate GLES texture units against the combined limit (#189332) Fixes #189331. The GLES backend allocates fragment stage texture units after the vertex stage's, but validated the running unit index against the per-stage maximum. On a driver reporting the minimum 16 fragment units (ANGLE on D3D11), a draw using all 16 fragment samplers plus a vertex stage texture failed validation and the render pass aborted, crashing skinned-mesh rendering in Flutter GPU apps on Windows. Texture units are a combined resource in GL; the per-stage limits bound only how many samplers one stage references. This validates the unit index against the combined limit and the per-stage sampler count against the per-stage limit. Adds unit tests for the previously rejected case and both overflow cases. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../backend/gles/buffer_bindings_gles.cc | 13 +- .../backend/gles/buffer_bindings_gles.h | 10 ++ .../gles/buffer_bindings_gles_unittests.cc | 137 ++++++++++++++++++ .../renderer/backend/gles/test/mock_gles.cc | 2 + 4 files changed, 161 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc index 2055e5164edd7..b5a6fad371a2a 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc @@ -493,6 +493,7 @@ std::optional BufferBindingsGLES::BindTextures( ShaderStage stage, size_t unit_start_index) { size_t active_index = unit_start_index; + size_t stage_texture_count = 0; for (auto i = 0u; i < texture_range.length; i++) { const TextureAndSampler& data = bound_textures[texture_range.offset + i]; if (data.stage != stage) { @@ -513,11 +514,21 @@ std::optional BufferBindingsGLES::BindTextures( //-------------------------------------------------------------------------- /// Set the active texture unit. /// - if (active_index >= gl.GetCapabilities()->GetMaxTextureUnits(stage)) { + /// Units are a combined resource; the per-stage limits cap only how many + /// samplers one stage references, not the unit indices they bind to. + /// + stage_texture_count++; + if (stage_texture_count > gl.GetCapabilities()->GetMaxTextureUnits(stage)) { VALIDATION_LOG << "Texture units specified exceed the capabilities for " "this shader stage."; return std::nullopt; } + if (active_index >= + gl.GetCapabilities()->max_combined_texture_image_units) { + VALIDATION_LOG << "Texture units specified exceed the combined texture " + "unit limit."; + return std::nullopt; + } gl.ActiveTexture(GL_TEXTURE0 + active_index); //-------------------------------------------------------------------------- diff --git a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h index 12cef0328bc1b..c00b55e9c17be 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h +++ b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h @@ -21,6 +21,10 @@ FML_TEST_CLASS(BufferBindingsGLESTest, BindUniformData); FML_TEST_CLASS(BufferBindingsGLESTest, BindArrayData); FML_TEST_CLASS(BufferBindingsGLESTest, BindUniformDataVerticesAndMatrices); FML_TEST_CLASS(BufferBindingsGLESTest, BindUniformFailsWithoutFloatType); +FML_TEST_CLASS(BufferBindingsGLESTest, + BindsTexturesAcrossThePerStageUnitBoundary); +FML_TEST_CLASS(BufferBindingsGLESTest, RejectsTexturesBeyondThePerStageLimit); +FML_TEST_CLASS(BufferBindingsGLESTest, RejectsTexturesBeyondTheCombinedLimit); } // namespace testing //------------------------------------------------------------------------------ @@ -66,6 +70,12 @@ class BufferBindingsGLES { BindUniformDataVerticesAndMatrices); FML_FRIEND_TEST(testing::BufferBindingsGLESTest, BindUniformFailsWithoutFloatType); + FML_FRIEND_TEST(testing::BufferBindingsGLESTest, + BindsTexturesAcrossThePerStageUnitBoundary); + FML_FRIEND_TEST(testing::BufferBindingsGLESTest, + RejectsTexturesBeyondThePerStageLimit); + FML_FRIEND_TEST(testing::BufferBindingsGLESTest, + RejectsTexturesBeyondTheCombinedLimit); //---------------------------------------------------------------------------- /// @brief The arguments to glVertexAttribPointer. /// diff --git a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc index 9bae5b1beb717..494fc8d72e780 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc @@ -5,11 +5,14 @@ #include "flutter/testing/testing.h" // IWYU pragma: keep #include "gtest/gtest.h" #include "impeller/core/shader_types.h" +#include "impeller/core/texture_descriptor.h" #include "impeller/renderer/backend/gles/buffer_bindings_gles.h" #include "impeller/renderer/backend/gles/device_buffer_gles.h" #include "impeller/renderer/backend/gles/formats_gles.h" #include "impeller/renderer/backend/gles/reactor_gles.h" +#include "impeller/renderer/backend/gles/sampler_library_gles.h" #include "impeller/renderer/backend/gles/test/mock_gles.h" +#include "impeller/renderer/backend/gles/texture_gles.h" #include "impeller/renderer/command.h" namespace impeller { @@ -19,6 +22,7 @@ const GLint kBlockDataSize = 16; } using ::testing::_; +using ::testing::NiceMock; TEST(BufferBindingsGLESTest, ToVertexAttribTypeSupportedFormats) { EXPECT_EQ(ToVertexAttribType(VertexAttributeFormat::kFloat32x3), @@ -361,5 +365,138 @@ TEST(BufferBindingsGLESTest, /*expected_bound_size=*/32); } +namespace { + +// Owns the reactor, sampler, and metadata behind a set of texture bindings. +struct BoundTexturesFixture { + std::shared_ptr reactor; + std::shared_ptr worker; + std::unique_ptr sampler_library; + raw_ptr sampler; + std::vector> metadata; + std::vector bound_textures; + absl::flat_hash_map uniform_bindings; + + explicit BoundTexturesFixture(std::unique_ptr proc_table) { + reactor = std::make_shared(std::move(proc_table)); + worker = std::make_shared(); + reactor->AddWorker(worker); + sampler_library = std::make_unique( + /*supports_decal_sampler_address_mode=*/false); + sampler = sampler_library->GetSampler({}); + } + + void AddTextures(ShaderStage stage, size_t count) { + for (size_t i = 0; i < count; i++) { + TextureDescriptor desc; + desc.storage_mode = StorageMode::kDevicePrivate; + desc.type = TextureType::kTexture2D; + desc.format = PixelFormat::kR8G8B8A8UNormInt; + desc.size = {1, 1}; + desc.mip_count = 1u; + desc.usage = TextureUsage::kShaderRead; + auto texture = std::make_shared(reactor, desc); + const std::string name = "tex" + std::to_string(metadata.size()); + const std::string key = "TEX" + std::to_string(metadata.size()); + uniform_bindings[key] = static_cast(100 + metadata.size()); + auto meta = std::make_unique(); + meta->name = name; + TextureAndSampler data = {}; + data.stage = stage; + data.texture = TextureResource(meta.get(), std::move(texture)); + data.sampler = sampler; + metadata.push_back(std::move(meta)); + bound_textures.push_back(std::move(data)); + } + } +}; + +// Capabilities of a minimum-spec ES3 driver (16 per stage, 32 combined). +std::unique_ptr> MakeSixteenUnitMockImpl() { + auto impl = std::make_unique>(); + EXPECT_CALL(*impl, GetIntegerv(_, _)) + .WillRepeatedly([](GLenum name, GLint* value) { + switch (name) { + case GL_MAX_TEXTURE_IMAGE_UNITS: + case GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS: + *value = 16; + break; + case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: + *value = 32; + break; + default: + break; + } + }); + return impl; +} + +} // namespace + +// One vertex texture pushes the last of 16 fragment samplers onto unit 16, +// which must bind on a 16-per-stage driver since units are combined in GL. +TEST(BufferBindingsGLESTest, BindsTexturesAcrossThePerStageUnitBoundary) { + std::shared_ptr mock_gl = MockGLES::Init(MakeSixteenUnitMockImpl()); + BoundTexturesFixture fixture( + std::make_unique(kMockResolverGLES)); + fixture.AddTextures(ShaderStage::kVertex, 1); + fixture.AddTextures(ShaderStage::kFragment, 16); + ASSERT_TRUE(fixture.reactor->React()); + + BufferBindingsGLES bindings; + bindings.SetUniformBindings(std::move(fixture.uniform_bindings)); + std::vector bound_buffers; + EXPECT_TRUE(bindings.BindUniformData( + fixture.reactor->GetProcTable(), fixture.bound_textures, bound_buffers, + Range{0, fixture.bound_textures.size()}, Range{0, 0})); +} + +// More samplers in one stage than its limit is still rejected. +TEST(BufferBindingsGLESTest, RejectsTexturesBeyondThePerStageLimit) { + std::shared_ptr mock_gl = MockGLES::Init(MakeSixteenUnitMockImpl()); + BoundTexturesFixture fixture( + std::make_unique(kMockResolverGLES)); + fixture.AddTextures(ShaderStage::kFragment, 17); + ASSERT_TRUE(fixture.reactor->React()); + + BufferBindingsGLES bindings; + bindings.SetUniformBindings(std::move(fixture.uniform_bindings)); + std::vector bound_buffers; + EXPECT_FALSE(bindings.BindUniformData( + fixture.reactor->GetProcTable(), fixture.bound_textures, bound_buffers, + Range{0, fixture.bound_textures.size()}, Range{0, 0})); +} + +// Units past the combined limit are rejected even when each stage is within +// its per-stage limit. +TEST(BufferBindingsGLESTest, RejectsTexturesBeyondTheCombinedLimit) { + auto impl = std::make_unique>(); + EXPECT_CALL(*impl, GetIntegerv(_, _)) + .WillRepeatedly([](GLenum name, GLint* value) { + switch (name) { + case GL_MAX_TEXTURE_IMAGE_UNITS: + case GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS: + case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: + *value = 8; + break; + default: + break; + } + }); + std::shared_ptr mock_gl = MockGLES::Init(std::move(impl)); + BoundTexturesFixture fixture( + std::make_unique(kMockResolverGLES)); + fixture.AddTextures(ShaderStage::kVertex, 8); + fixture.AddTextures(ShaderStage::kFragment, 8); + ASSERT_TRUE(fixture.reactor->React()); + + BufferBindingsGLES bindings; + bindings.SetUniformBindings(std::move(fixture.uniform_bindings)); + std::vector bound_buffers; + EXPECT_FALSE(bindings.BindUniformData( + fixture.reactor->GetProcTable(), fixture.bound_textures, bound_buffers, + Range{0, fixture.bound_textures.size()}, Range{0, 0})); +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc index f4d48151f1537..dbd70248c3940 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc @@ -98,7 +98,9 @@ void mockGetIntegerv(GLenum name, int* value) { *value = g_extensions.size(); } break; case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: + // Minimum default; a registered mock may overwrite it. *value = 8; + CallMockMethod(&IMockGLESImpl::GetIntegerv, name, value); break; case GL_MAX_LABEL_LENGTH_KHR: *value = 64; From 8a18bbfab08082c4ffa62c2deb14465fbd784444 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 6 Aug 2026 21:10:33 +0900 Subject: [PATCH 106/330] iOS: remove unused OCMock imports (#190648) Removes OCMock imports from files where OCMock was not being used. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/framework/Source/FlutterEmbedderKeyResponderTest.mm | 1 - .../platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h | 1 - .../ios/framework/Source/availability_version_check_test.mm | 1 - 3 files changed, 3 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm index 929e96a9a3d5f..923b1aafc24d3 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm @@ -3,7 +3,6 @@ // found in the LICENSE file. #import -#import #import #include <_types/_uint64_t.h> diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h index 5001a2b453f13..b46019035cee9 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h @@ -6,7 +6,6 @@ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERFAKEKEYEVENTS_H_ #import -#import #import #import "flutter/shell/platform/darwin/ios/InternalFlutterSwift/InternalFlutterSwift.h" diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm index c843893c216e8..d2c783923ad8b 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm @@ -4,7 +4,6 @@ #import -#import #import #import "flutter/shell/platform/darwin/common/availability_version_check.h" From d38554a6db06023d9b089e109a6db76d8fcaa817 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 6 Aug 2026 21:54:06 +0900 Subject: [PATCH 107/330] iOS: Migrate the unit test host app to UIScene (#190646) Apps that haven't adopted the UIScene life cycle will not launch on iOS 27. The `IosUnitTests` host app was still using the pre-scene `UIApplicationMain` plus `UIMainStoryboardFile` setup, and failed with "Early unexpected exit ... The test runner crashed before establishing connection" when run with on an iOS 27 simulator. This adds a `UIApplicationSceneManifest` to the host app's `Info.plist` and a `SceneDelegate`. Migrating to scenes changes a couple things the tests were relying on. `FlutterSharedApplication.hasSceneDelegate` now returns `YES` in the host app, which suppresses forwarding of app lifecycle notifications to plugins. The `FlutterPluginAppLifeCycleDelegate` tests covering the pre-migration case stub this back out to `NO`. `[[UIWindow alloc] init]` no longer picks up a window scene so the window no longer has a screen. There were a few tests that need a screen, which now build their window with `initWithWindowScene:` instead. Without this we trip an `FML_DCHECK(self.screen)` in `FlutterView isWideGamutSupported]`. I'll send a followup to handle this more gracefully. Issue: https://github.com/flutter/flutter/issues/188336 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../FlutterPluginAppLifeCycleDelegateTest.mm | 8 +++++++ .../Source/FlutterViewControllerTest.mm | 14 +++++++++++-- .../ios/framework/Source/FlutterViewTest.mm | 9 ++++++-- .../testing/ios/IosUnitTests/App/Info.plist | 21 +++++++++++++++++++ .../ios/IosUnitTests/App/SceneDelegate.h | 16 ++++++++++++++ .../ios/IosUnitTests/App/SceneDelegate.m | 11 ++++++++++ .../IosUnitTests.xcodeproj/project.pbxproj | 6 ++++++ 7 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.h create mode 100644 engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.m diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm index b4490ca3324a1..b99173d097670 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm @@ -197,6 +197,8 @@ - (void)testDidEnterBackground { XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationDidEnterBackgroundNotification]; FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; + id mockApplication = OCMClassMock([FlutterSharedApplication class]); + OCMStub([mockApplication hasSceneDelegate]).andReturn(NO); id plugin = OCMProtocolMock(@protocol(FlutterPlugin)); [delegate addDelegate:plugin]; [[NSNotificationCenter defaultCenter] @@ -246,6 +248,8 @@ - (void)testWillEnterForeground { initWithName:UIApplicationWillEnterForegroundNotification]; FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; + id mockApplication = OCMClassMock([FlutterSharedApplication class]); + OCMStub([mockApplication hasSceneDelegate]).andReturn(NO); id plugin = OCMProtocolMock(@protocol(FlutterPlugin)); [delegate addDelegate:plugin]; [[NSNotificationCenter defaultCenter] @@ -294,6 +298,8 @@ - (void)testWillResignActive { [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationWillResignActiveNotification]; FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; + id mockApplication = OCMClassMock([FlutterSharedApplication class]); + OCMStub([mockApplication hasSceneDelegate]).andReturn(NO); id plugin = OCMProtocolMock(@protocol(FlutterPlugin)); [delegate addDelegate:plugin]; [[NSNotificationCenter defaultCenter] @@ -342,6 +348,8 @@ - (void)testDidBecomeActive { [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationDidBecomeActiveNotification]; FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; + id mockApplication = OCMClassMock([FlutterSharedApplication class]); + OCMStub([mockApplication hasSceneDelegate]).andReturn(NO); id plugin = OCMProtocolMock(@protocol(FlutterPlugin)); [delegate addDelegate:plugin]; [[NSNotificationCenter defaultCenter] diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 3df5a73f01582..54f5391b7e9f1 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -2310,7 +2310,12 @@ - (void)testLifeCycleNotificationApplicationBecameActive { [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - UIWindow* window = [[UIWindow alloc] init]; + // The window must be attached to the connected scene to have a screen, without which the + // viewport metrics stay empty and the surface is never updated. + UIWindowScene* windowScene = + (UIWindowScene*)UIApplication.sharedApplication.connectedScenes.anyObject; + XCTAssertNotNil(windowScene, @"The host app must have a connected scene for test"); + UIWindow* window = [[UIWindow alloc] initWithWindowScene:windowScene]; [window addSubview:flutterViewController.view]; flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100); [flutterViewController viewDidLayoutSubviews]; @@ -2348,7 +2353,12 @@ - (void)testLifeCycleNotificationSceneBecameActive { [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - UIWindow* window = [[UIWindow alloc] init]; + // The window must be attached to the connected scene to have a screen, without which the + // viewport metrics stay empty and the surface is never updated. + UIWindowScene* windowScene = + (UIWindowScene*)UIApplication.sharedApplication.connectedScenes.anyObject; + XCTAssertNotNil(windowScene, @"The host app must have a connected scene for test"); + UIWindow* window = [[UIWindow alloc] initWithWindowScene:windowScene]; [window addSubview:flutterViewController.view]; flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100); [flutterViewController viewDidLayoutSubviews]; 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 8ad9a7418396d..665f886b7ceb5 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 @@ -225,8 +225,13 @@ - (FlutterView*)createViewInWindowWithWideGamut:(BOOL)enableWideGamut { 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)]; + // Add to a real window so layoutSubviews has access to screen. The host app uses the UIScene + // life cycle, so the window only has a screen once it is attached to the connected scene. + UIWindowScene* windowScene = + (UIWindowScene*)UIApplication.sharedApplication.connectedScenes.anyObject; + XCTAssertNotNil(windowScene, @"The host app must have a connected scene for test"); + UIWindow* window = [[UIWindow alloc] initWithWindowScene:windowScene]; + window.frame = CGRectMake(0, 0, 100, 100); [window addSubview:view]; view.frame = window.bounds; [view layoutSubviews]; diff --git a/engine/src/flutter/testing/ios/IosUnitTests/App/Info.plist b/engine/src/flutter/testing/ios/IosUnitTests/App/Info.plist index e87b8d51863b9..6e8e327f945e4 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/App/Info.plist +++ b/engine/src/flutter/testing/ios/IosUnitTests/App/Info.plist @@ -20,6 +20,27 @@ 1 LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.h b/engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.h new file mode 100644 index 0000000000000..301f3cb92f5ec --- /dev/null +++ b/engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.h @@ -0,0 +1,16 @@ +// 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_TESTING_IOS_IOSUNITTESTS_APP_SCENEDELEGATE_H_ +#define FLUTTER_TESTING_IOS_IOSUNITTESTS_APP_SCENEDELEGATE_H_ + +#import + +@interface SceneDelegate : UIResponder + +@property(nonatomic, strong, nullable) UIWindow* window; + +@end + +#endif // FLUTTER_TESTING_IOS_IOSUNITTESTS_APP_SCENEDELEGATE_H_ diff --git a/engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.m b/engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.m new file mode 100644 index 0000000000000..281ab38c4332a --- /dev/null +++ b/engine/src/flutter/testing/ios/IosUnitTests/App/SceneDelegate.m @@ -0,0 +1,11 @@ +// 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 "SceneDelegate.h" + +@implementation SceneDelegate + +// The window is created from the Main storyboard and attached to the scene by UIKit. + +@end diff --git a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 6d35550aee0cf..719d93bbf4746 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 0D6AB6BE22BB05E200EEE540 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D6AB6BD22BB05E200EEE540 /* Assets.xcassets */; }; 0D6AB6C122BB05E200EEE540 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D6AB6BF22BB05E200EEE540 /* LaunchScreen.storyboard */; }; 0D6AB6C422BB05E200EEE540 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6C322BB05E200EEE540 /* main.m */; }; + 0D5CE1A22E4A000100AA0003 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D5CE1A12E4A000100AA0002 /* SceneDelegate.m */; }; F7521D7826BB68BC005F15C5 /* libios_test_flutter.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F7521D7926BB68BC005F15C5 /* libocmock_shared.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F77E081926FA9CE6003E6E4C /* Flutter.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = F77E081726FA9CE6003E6E4C /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -61,6 +62,8 @@ 0D6AB6B122BB05E100EEE540 /* IosUnitTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IosUnitTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6B422BB05E100EEE540 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 0D6AB6B522BB05E100EEE540 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 0D5CE1A02E4A000100AA0001 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; + 0D5CE1A12E4A000100AA0002 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 0D6AB6B722BB05E100EEE540 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 0D6AB6B822BB05E100EEE540 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 0D6AB6BB22BB05E100EEE540 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -164,6 +167,8 @@ children = ( 0D6AB6B422BB05E100EEE540 /* AppDelegate.h */, 0D6AB6B522BB05E100EEE540 /* AppDelegate.m */, + 0D5CE1A02E4A000100AA0001 /* SceneDelegate.h */, + 0D5CE1A12E4A000100AA0002 /* SceneDelegate.m */, 0D6AB6B722BB05E100EEE540 /* ViewController.h */, 0D6AB6B822BB05E100EEE540 /* ViewController.m */, 0D6AB6BA22BB05E100EEE540 /* Main.storyboard */, @@ -302,6 +307,7 @@ 0D6AB6B922BB05E100EEE540 /* ViewController.m in Sources */, 0D6AB6C422BB05E200EEE540 /* main.m in Sources */, 0D6AB6B622BB05E100EEE540 /* AppDelegate.m in Sources */, + 0D5CE1A22E4A000100AA0003 /* SceneDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 7849b6a9da0755fa4620280d6ae1b3d310233a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adil=20Burak=20=C5=9Een?= <56400880+adilburaksen@users.noreply.github.com> Date: Thu, 6 Aug 2026 18:20:41 +0300 Subject: [PATCH 108/330] [flutter_tools] Contain dependency-declared license paths within the package (#189159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package's `flutter: licenses:` entries are resolved against the package root and their contents appended to the app's generated NOTICES. Unlike dependency asset paths, license paths were not containment-checked, so a (transitive) dependency could declare `licenses: ["../secret"]` and have an arbitrary host file read during a normal `flutter build` and bundled into the shipped app. This mirrors the boundary enforced for asset paths in #187661. Fix: reject dependency license paths that escape the package directory (canonicalize + `isWithin`); the app's own licenses are unaffected. Verified with a real `flutter build bundle` — a dependency declaring `licenses: [../host_secret.txt]` previously bundled the host file into `flutter_assets/NOTICES.Z`; with this change the build is rejected. --------- Co-authored-by: Ben Konyi --- packages/flutter_tools/lib/src/asset.dart | 37 ++++++++++++++++--- .../asset_bundle_package_test.dart | 37 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 70af6b2afc9f1..e50eba9840611 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -458,12 +458,39 @@ class ManifestAssetBundle implements AssetBundle { continue; } // Collect any additional licenses from each package. + final isAppItself = packageFlutterManifest.appName == flutterManifest.appName; final licenseFiles = []; - for (final String relativeLicensePath in packageFlutterManifest.additionalLicenses) { - final String absoluteLicensePath = _fileSystem.path.fromUri( - package.root.resolve(relativeLicensePath), - ); - licenseFiles.add(_fileSystem.file(absoluteLicensePath).absolute); + // Most packages declare no additional licenses, so skip all of the work + // below (including canonicalization) when there is nothing to collect. + if (packageFlutterManifest.additionalLicenses.isNotEmpty) { + // The package root is constant for this package, so canonicalize it + // once instead of per license path. The app itself is exempt from the + // containment check, in which case this stays null. + final String? packageRoot = isAppItself + ? null + : _fileSystem.path.canonicalize(_fileSystem.path.fromUri(package.root)); + for (final String relativeLicensePath in packageFlutterManifest.additionalLicenses) { + final String absoluteLicensePath = _fileSystem.path.fromUri( + package.root.resolve(relativeLicensePath), + ); + // A dependency must not declare a license path that escapes its own + // package directory (e.g. '../secret'). Otherwise the build would read + // an arbitrary file outside the package and bundle its contents into + // the app's NOTICES. This mirrors the containment check applied to + // dependency-declared asset paths in `_ensureAssetPathIsValid`. + if (packageRoot != null) { + final String resolvedLicense = _fileSystem.path.canonicalize(absoluteLicensePath); + if (packageRoot != resolvedLicense && + !_fileSystem.path.isWithin(packageRoot, resolvedLicense)) { + throwToolExit( + 'Package "${packageFlutterManifest.appName}" specified a license path ' + '"$relativeLicensePath" that escapes its package directory. License paths ' + 'declared by a package must stay within that package.', + ); + } + } + licenseFiles.add(_fileSystem.file(absoluteLicensePath).absolute); + } } additionalLicenseFiles[packageFlutterManifest.appName] = licenseFiles; diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart index d1b2c97ac8509..d0e2d1e9e5889 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart @@ -164,6 +164,43 @@ $assetsSection }, ); + testUsingContext( + 'Package license path that escapes the package directory is rejected', + () async { + writePubspecFile('pubspec.yaml', 'test'); + writePackageConfigFiles( + directory: globals.fs.currentDirectory, + packages: {'test_package': 'p/p/'}, + mainLibName: 'test', + ); + // The dependency declares a license whose relative path escapes its own package + // directory. Without containment this would read a file outside the package on the + // build machine and bundle its contents into the consuming app's NOTICES, which the + // consuming app never declared. + globals.fs.file(fixPath('p/p/pubspec.yaml')) + ..createSync(recursive: true) + ..writeAsStringSync(''' +name: test_package +flutter: + licenses: + - ../../../escaped_secret +'''); + + final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); + expect( + () => bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + throwsToolExit(message: 'escapes its package directory'), + ); + }, + overrides: { + FileSystem: () => testFileSystem, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + testUsingContext( 'No assets are bundled when the package has no assets', () async { From 1abf8f42d372614c02ad78d1a00f3720f05af74c Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 6 Aug 2026 11:34:07 -0400 Subject: [PATCH 109/330] Roll Packages from b1424e248121 to 4e3f83d37f83 (2 revisions) (#190658) https://github.com/flutter/packages/compare/b1424e248121...4e3f83d37f83 2026-08-05 fluttergithubbot@gmail.com Sync release-go_router-17.4.0 to main (flutter/packages#12370) 2026-08-05 54941990+star4277@users.noreply.github.com [go_router] Add route metadata support to GoRouterState. (flutter/packages#11773) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- bin/internal/flutter_packages.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 33aba2ab4a198..4503cb3e69827 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -b1424e248121f15bb8f78a8fa6155e71f12bca79 +4e3f83d37f83af1c25a5006d13efce4b5d18d4b3 From b57e2af9a0b293798a008377486cdd6cf15c42d3 Mon Sep 17 00:00:00 2001 From: Mohellebi Abdessalem <116356835+AbdeMohlbi@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:49:10 +0100 Subject: [PATCH 110/330] Remove outdated `TODO`s about `toString` issues with`Offset` (#190138) Remove outdated TODOs about `toString` issues `withOffset`. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../test/painting/text_painter_rtl_test.dart | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/flutter/test/painting/text_painter_rtl_test.dart b/packages/flutter/test/painting/text_painter_rtl_test.dart index 07cd840e6e14d..85197cc61e1ff 100644 --- a/packages/flutter/test/painting/text_painter_rtl_test.dart +++ b/packages/flutter/test/painting/text_painter_rtl_test.dart @@ -548,78 +548,77 @@ void main() { ); painter.layout(); - // TODO(ianh): Remove the toString()s once https://github.com/flutter/engine/pull/4283 lands expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(0.0, 5.0)).toString(), - const TextPosition(offset: 0).toString(), + painter.getPositionForOffset(const Offset(0.0, 5.0)), + const TextPosition(offset: 0), ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(-100.0, 5.0)).toString(), - const TextPosition(offset: 0).toString(), + painter.getPositionForOffset(const Offset(-100.0, 5.0)), + const TextPosition(offset: 0), ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(4.0, 5.0)).toString(), - const TextPosition(offset: 0).toString(), + painter.getPositionForOffset(const Offset(4.0, 5.0)), + const TextPosition(offset: 0), ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(8.0, 5.0)).toString(), - const TextPosition(offset: 1, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(8.0, 5.0)), + const TextPosition(offset: 1, affinity: TextAffinity.upstream), ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(12.0, 5.0)).toString(), - const TextPosition(offset: 1).toString(), + painter.getPositionForOffset(const Offset(12.0, 5.0)), + const TextPosition(offset: 1), // currently we say upstream instead of downstream skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536 ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(28.0, 5.0)).toString(), - const TextPosition(offset: 3, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(28.0, 5.0)), + const TextPosition(offset: 3, affinity: TextAffinity.upstream), ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(32.0, 5.0)).toString(), - const TextPosition(offset: 6, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(32.0, 5.0)), + const TextPosition(offset: 6, affinity: TextAffinity.upstream), // this is part of https://github.com/flutter/flutter/issues/11375 skip: skipExpectsWithKnownBugs, ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(58.0, 5.0)).toString(), - const TextPosition(offset: 3).toString(), + painter.getPositionForOffset(const Offset(58.0, 5.0)), + const TextPosition(offset: 3), // this is part of https://github.com/flutter/flutter/issues/11375 skip: skipExpectsWithKnownBugs, ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(62.0, 5.0)).toString(), - const TextPosition(offset: 6).toString(), + painter.getPositionForOffset(const Offset(62.0, 5.0)), + const TextPosition(offset: 6), ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(88.0, 5.0)).toString(), - const TextPosition(offset: 9, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(88.0, 5.0)), + const TextPosition(offset: 9, affinity: TextAffinity.upstream), ); expect( // Aaa Bbb Ccc Gimel Bet Alef Ddd Eee Fff // ^ - painter.getPositionForOffset(const Offset(100.0, 5.0)).toString(), - const TextPosition(offset: 9, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(100.0, 5.0)), + const TextPosition(offset: 9, affinity: TextAffinity.upstream), ); painter.dispose(); }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536 @@ -633,40 +632,39 @@ void main() { ); painter.layout(); - // TODO(ianh): Remove the toString()s once https://github.com/flutter/engine/pull/4283 lands expect( // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef // ^ - painter.getPositionForOffset(const Offset(-4.0, 5.0)).toString(), - const TextPosition(offset: 9, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(-4.0, 5.0)), + const TextPosition(offset: 9, affinity: TextAffinity.upstream), ); expect( // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef // ^ - painter.getPositionForOffset(const Offset(28.0, 5.0)).toString(), - const TextPosition(offset: 6).toString(), + painter.getPositionForOffset(const Offset(28.0, 5.0)), + const TextPosition(offset: 6), ); expect( // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef // ^ - painter.getPositionForOffset(const Offset(32.0, 5.0)).toString(), - const TextPosition(offset: 3).toString(), + painter.getPositionForOffset(const Offset(32.0, 5.0)), + const TextPosition(offset: 3), // this is part of https://github.com/flutter/flutter/issues/11375 skip: skipExpectsWithKnownBugs, ); expect( // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef // ^ - painter.getPositionForOffset(const Offset(58.0, 5.0)).toString(), - const TextPosition(offset: 6, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(58.0, 5.0)), + const TextPosition(offset: 6, affinity: TextAffinity.upstream), // this is part of https://github.com/flutter/flutter/issues/11375 skip: skipExpectsWithKnownBugs, ); expect( // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef // ^ - painter.getPositionForOffset(const Offset(62.0, 5.0)).toString(), - const TextPosition(offset: 3, affinity: TextAffinity.upstream).toString(), + painter.getPositionForOffset(const Offset(62.0, 5.0)), + const TextPosition(offset: 3, affinity: TextAffinity.upstream), ); painter.dispose(); }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536 From ca8cdb0043c7d38b9e1ecf74e692b17f4766d5b6 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:39:34 -0700 Subject: [PATCH 111/330] Disallow Swift symbols in `verify_exported.dart` (#190645) Swift symbols are no longer exported in non-debug builds. Update `verify_exported.dart` so no new symbols get accidentally exported. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../testing/symbols/verify_exported.dart | 112 +----------------- 1 file changed, 6 insertions(+), 106 deletions(-) diff --git a/engine/src/flutter/testing/symbols/verify_exported.dart b/engine/src/flutter/testing/symbols/verify_exported.dart index 8e25e67b0e3e6..0be61adb67751 100644 --- a/engine/src/flutter/testing/symbols/verify_exported.dart +++ b/engine/src/flutter/testing/symbols/verify_exported.dart @@ -13,14 +13,9 @@ import 'package:path/path.dart' as p; // Android binaries (libflutter.so) should only export one symbol "JNI_OnLoad" // of type "T". // -// Ideally, iOS binaries (Flutter.framework/Flutter) should only export -// Objective-C Symbols from the Flutter namespace, of type "(__DATA,__common)" or -// "(__DATA,__objc_data)". However, to allow Swift symbols to be exported in -// an Objective-C bridging header, they must be public or open. The framework -// uses these types internally and never publishes these types in a public header. -// Like `_InternalFlutter` Obj-C symbols, we allow `InternalFlutterSwift` and -// `InternalFlutterSwiftCommon` symbols, as they are clearly marked as internal -// in the name. +// The iOS binaries (Flutter.framework/Flutter) should only export Objective-C +// Symbols from the Flutter namespace, of type "(__DATA,__common)" or +// "(__DATA,__objc_data)". /// Takes the path to the out directory as the first argument, and the path to /// the buildtools directory as the second argument. @@ -106,50 +101,10 @@ int _checkIos(String outPath, String nmPath, Iterable builds) { failures++; continue; } - final swiftEntries = []; - final unexpectedEntries = []; - - for (final NmEntry entry in NmEntry.parse(nmResult.stdout as String)) { - if (entry.isCInternalSymbol || entry.isAllowedCSymbol || entry.isAllowedObjCSymbol) { - continue; - } - final bool isSwiftSymbol = switch (entry.type) { - '(__TEXT,__text)' || - '(__TEXT,__const)' || - '(__TEXT,__constg_swiftt)' || - '(__DATA_CONST,__const)' || - '(__DATA,__data)' || - '(__DATA,__objc_data)' || - '(__DATA,__common)' => entry.name.startsWith(r'_$s'), - _ => false, - }; - - if (isSwiftSymbol) { - swiftEntries.add(entry); - } else { - unexpectedEntries.add(entry); - } - } - - final Map? symbolToModuleNameMap = _demangleSymbols( - swiftEntries.map((NmEntry entry) => entry.name), + final Iterable unexpectedEntries = NmEntry.parse(nmResult.stdout as String).where( + (NmEntry entry) => + !entry.isCInternalSymbol && !entry.isAllowedCSymbol && !entry.isAllowedObjCSymbol, ); - - if (symbolToModuleNameMap == null) { - print('ERROR: failed to execute "swift demangle"'); - failures++; - return failures; - } - - unexpectedEntries.addAll( - swiftEntries.where( - (NmEntry entry) => switch (symbolToModuleNameMap[entry.name]) { - 'InternalFlutterSwiftCommon' || 'InternalFlutterSwift' => false, - _ => true, - }, - ), - ); - if (unexpectedEntries.isNotEmpty) { print('ERROR: $libFlutter exports unexpected symbols:'); print( @@ -285,58 +240,3 @@ final class NmEntry { @override String toString() => '$name: $type'; } - -final RegExp moduleLinePattern = RegExp(r'kind=Module, text="(.+)"'); -// Demangles the given `symbols` and maps each mangled name to its Swift module name. -// -// Returns null if the `swift demangle` command failed entirely. -// Individual map values may be null if a symbol failed to demangle or did not belong to a Swift module. -Map? _demangleSymbols(Iterable symbols) { - if (symbols.isEmpty) { - return {}; - } - final ProcessResult demangledResult = Process.runSync('swift', [ - 'demangle', - '--tree-only', - ...symbols, - ]); - if (demangledResult.exitCode != 0) { - return null; - } - - final symbolToModule = {}; - final output = demangledResult.stdout as String; - - String trim(String string) => string.trim(); - - for (final String symbolTree in output.split('Demangling for ').map(trim)) { - final List lines = LineSplitter.split(symbolTree).toList(); - if (lines.isEmpty) { - continue; - } - final String mangledName = lines.first; - - // Parses the output from `swift demangle --tree-only` and extracts the module - // name of a single entry. - // - // Example `swift demangle --tree-only` output: - // - // Demangling for _$s26InternalFlutterSwiftCommon8LogLevelOSYAAMc - // kind=Global - // kind=ProtocolConformanceDescriptor - // kind=ProtocolConformance - // kind=Type - // kind=Enum - // kind=Module, text="InternalFlutterSwiftCommon" - // kind=Identifier, text="LogLevel" - // kind=Type - // kind=Protocol - // kind=Module, text="Swift" - // kind=Identifier, text="RawRepresentable" - // kind=Module, text="InternalFlutterSwiftCommon" - final String? moduleName = moduleLinePattern.firstMatch(symbolTree)?.group(1); - - symbolToModule[mangledName] = moduleName; - } - return symbolToModule; -} From d55d6f09cc66eede895e2386c72878d1c84343dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adil=20Burak=20=C5=9Een?= <56400880+adilburaksen@users.noreply.github.com> Date: Fri, 7 Aug 2026 01:09:52 +0300 Subject: [PATCH 112/330] [flutter_tools] Validate plugin class/package identifiers to prevent GeneratedPluginRegistrant injection (#189156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Plugin `pluginClass`/`dartPluginClass` and the Android `package` are interpolated **verbatim** into the generated `GeneratedPluginRegistrant` source files. `flutter_plugins.dart` renders these templates via `_renderTemplateToFile` → `templateRenderer.renderString(template, context)`, and the renderer's `htmlEscapeValues` defaults to `false`, so the values are emitted with no escaping into Java/Kotlin, Swift, Objective-C and C++ source (e.g. `new {{package}}.{{class}}()`, `{{prefix}}{{class}}.register(...)`, `#import <{{name}}/{{class}}.h>`). The per-platform `validate()` methods in `platform_plugins.dart` only checked that these fields were *strings*, not that they were valid identifiers. As a result a plugin declaration whose `pluginClass`/`package` contains arbitrary source (spaces, `;`, `{}`, `()`, newlines, …) passes validation and that source lands in the consuming app's `GeneratedPluginRegistrant` and is compiled into the app. Because plugins are collected over `computeTransitiveDependencies(...)` with no opt-in from the consuming app, a **transitive** dependency can use this to have arbitrary native code compiled into an app that merely depends on it (via a plain `flutter pub get` / `build` / `run`). This is the same "a package must not escape its declared boundary at build time" boundary enforced for asset paths in #187661 and for pub-cache extraction in CVE-2026-27704. Reproduced end-to-end: a dependency declaring ```yaml flutter: plugin: platforms: macos: pluginClass: "SomePlugin.register(...); ; if false { SomePlugin" ``` resulted, after `flutter pub get`, in the injected statements appearing verbatim in `macos/Flutter/GeneratedPluginRegistrant.swift` and `ios/Runner/GeneratedPluginRegistrant.m`. ## Fix Restrict `pluginClass`, `dartPluginClass` and the Android `package` to identifier characters (dot-separated identifiers) in each platform's `validate()`, rejecting the plugin specification otherwise. Legitimate class/package names are unaffected; a value that is not a plain identifier now fails with `Invalid plugin specification `. ## Tests - Added a regression test asserting a `pluginClass` containing injection characters is rejected. - Full `test/general.shard/plugins_test.dart` passes (77/77) — no legitimate plugin specification regresses. ## Pre-launch Checklist - [x] I added new tests to check the change I am making. - [x] All existing and new tests are passing. --------- Co-authored-by: Ben Konyi --- .../lib/src/platform_plugins.dart | 170 ++++++++++++++++-- packages/flutter_tools/lib/src/plugins.dart | 6 + .../test/general.shard/plugins_test.dart | 75 ++++++++ 3 files changed, 234 insertions(+), 17 deletions(-) diff --git a/packages/flutter_tools/lib/src/platform_plugins.dart b/packages/flutter_tools/lib/src/platform_plugins.dart index e82469cab6fe7..bc6e7ffaa8a3d 100644 --- a/packages/flutter_tools/lib/src/platform_plugins.dart +++ b/packages/flutter_tools/lib/src/platform_plugins.dart @@ -17,12 +17,42 @@ const kDartPluginClass = 'dartPluginClass'; /// Constant for 'dartPluginFile' key in plugin maps. const kDartFileName = 'dartFileName'; +/// Constant for 'fileName' key in plugin maps. +const kFileName = 'fileName'; + /// Constant for 'ffiPlugin' key in plugin maps. const kFfiPlugin = 'ffiPlugin'; // Constant for 'defaultPackage' key in plugin maps. const kDefaultPackage = 'default_package'; +/// Matches a valid native plugin class or dot-separated package identifier. +/// +/// Plugin `class`/`package` values are interpolated verbatim into the generated +/// GeneratedPluginRegistrant source files (Java/Kotlin, Swift, Objective-C, +/// C++). Restricting them to identifier characters prevents a (possibly +/// transitive) dependency from injecting arbitrary native code into the +/// consuming app's build via its pubspec plugin declaration. +final RegExp _pluginIdentifierPattern = RegExp( + r'^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$', +); + +/// Whether [value] is a valid native plugin class or dot-separated package +/// identifier. Callers first confirm the value is a String via the schema type +/// checks; absent fields are not validated here. +bool _isValidPluginIdentifier(String value) => _pluginIdentifierPattern.hasMatch(value); + +/// Matches a safe relative Dart source path (e.g. `src/foo_web.dart`) ending in +/// `.dart`. Plugin `fileName`/`dartFileName` values are interpolated into an +/// `import` in the generated registrant, so they must not contain quotes, +/// semicolons, whitespace or parent-directory segments. +final RegExp pluginDartFileNamePattern = RegExp(r'^\w[\w./-]*\.dart$'); + +/// Whether [value] is a safe relative Dart source path for a plugin. Callers +/// first confirm the value is a String via the schema type checks. +bool isValidPluginDartFileName(String value) => + pluginDartFileNamePattern.hasMatch(value) && !value.contains('..'); + /// Constant for 'sharedDarwinSource' key in plugin maps. /// Can be set for iOS and macOS plugins. const kSharedDarwinSource = 'sharedDarwinSource'; @@ -131,10 +161,34 @@ class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin { bool hasDart() => dartPluginClass != null; static bool validate(YamlMap yaml) { - return (yaml['package'] is String && yaml[kPluginClass] is String) || - yaml[kDartPluginClass] is String || + final Object? package = yaml['package']; + final Object? pluginClass = yaml[kPluginClass]; + final Object? dartPluginClass = yaml[kDartPluginClass]; + + final bool hasPluginDeclaration = + (package is String && pluginClass is String) || + dartPluginClass is String || yaml[kFfiPlugin] == true || yaml[kDefaultPackage] is String; + + if (!hasPluginDeclaration) { + return false; + } + + // Validate every identifier that is present, not just the ones that made + // the declaration above valid, so a plugin cannot smuggle an unsafe + // identifier in alongside an `ffiPlugin` or `default_package` entry. + if (package is String && !_isValidPluginIdentifier(package)) { + return false; + } + if (pluginClass is String && !_isValidPluginIdentifier(pluginClass)) { + return false; + } + if (dartPluginClass is String && !_isValidPluginIdentifier(dartPluginClass)) { + return false; + } + + return true; } static const kConfigKey = 'android'; @@ -288,11 +342,30 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlug } static bool validate(YamlMap yaml) { - return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || + final Object? pluginClass = yaml[kPluginClass]; + final Object? dartPluginClass = yaml[kDartPluginClass]; + + final bool hasPluginDeclaration = + pluginClass is String || + dartPluginClass is String || yaml[kFfiPlugin] == true || yaml[kSharedDarwinSource] == true || yaml[kDefaultPackage] is String; + + if (!hasPluginDeclaration) { + return false; + } + + // Validate every identifier that is present, not just the ones that made + // the declaration above valid. + if (pluginClass is String && !_isValidPluginIdentifier(pluginClass)) { + return false; + } + if (dartPluginClass is String && !_isValidPluginIdentifier(dartPluginClass)) { + return false; + } + + return true; } static const kConfigKey = 'ios'; @@ -381,11 +454,30 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPl } static bool validate(YamlMap yaml) { - return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || + final Object? pluginClass = yaml[kPluginClass]; + final Object? dartPluginClass = yaml[kDartPluginClass]; + + final bool hasPluginDeclaration = + pluginClass is String || + dartPluginClass is String || yaml[kFfiPlugin] == true || yaml[kSharedDarwinSource] == true || yaml[kDefaultPackage] is String; + + if (!hasPluginDeclaration) { + return false; + } + + // Validate every identifier that is present, not just the ones that made + // the declaration above valid. + if (pluginClass is String && !_isValidPluginIdentifier(pluginClass)) { + return false; + } + if (dartPluginClass is String && !_isValidPluginIdentifier(dartPluginClass)) { + return false; + } + + return true; } static const kConfigKey = 'macos'; @@ -488,10 +580,29 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, Varian } static bool validate(YamlMap yaml) { - return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || + final Object? pluginClass = yaml[kPluginClass]; + final Object? dartPluginClass = yaml[kDartPluginClass]; + + final bool hasPluginDeclaration = + pluginClass is String || + dartPluginClass is String || yaml[kFfiPlugin] == true || yaml[kDefaultPackage] is String; + + if (!hasPluginDeclaration) { + return false; + } + + // Validate every identifier that is present, not just the ones that made + // the declaration above valid. + if (pluginClass is String && !_isValidPluginIdentifier(pluginClass)) { + return false; + } + if (dartPluginClass is String && !_isValidPluginIdentifier(dartPluginClass)) { + return false; + } + + return true; } static const kConfigKey = 'windows'; @@ -575,10 +686,29 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin { } static bool validate(YamlMap yaml) { - return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || + final Object? pluginClass = yaml[kPluginClass]; + final Object? dartPluginClass = yaml[kDartPluginClass]; + + final bool hasPluginDeclaration = + pluginClass is String || + dartPluginClass is String || yaml[kFfiPlugin] == true || yaml[kDefaultPackage] is String; + + if (!hasPluginDeclaration) { + return false; + } + + // Validate every identifier that is present, not just the ones that made + // the declaration above valid. + if (pluginClass is String && !_isValidPluginIdentifier(pluginClass)) { + return false; + } + if (dartPluginClass is String && !_isValidPluginIdentifier(dartPluginClass)) { + return false; + } + + return true; } static const kConfigKey = 'linux'; @@ -622,19 +752,25 @@ class WebPlugin extends PluginPlatform { const WebPlugin({required this.name, required this.pluginClass, required this.fileName}); factory WebPlugin.fromYaml(String name, YamlMap yaml) { - if (yaml['pluginClass'] is! String) { + final Object? pluginClass = yaml[kPluginClass]; + if (pluginClass is! String) { throwToolExit( 'The plugin `$name` is missing the required field `pluginClass` in pubspec.yaml', ); } - if (yaml['fileName'] is! String) { + final Object? fileName = yaml[kFileName]; + if (fileName is! String) { throwToolExit('The plugin `$name` is missing the required field `fileName` in pubspec.yaml'); } - return WebPlugin( - name: name, - pluginClass: yaml['pluginClass'] as String, - fileName: yaml['fileName'] as String, - ); + if (!_isValidPluginIdentifier(pluginClass)) { + throwToolExit( + 'The plugin `$name` has an invalid `pluginClass` in its web plugin declaration.', + ); + } + if (!isValidPluginDartFileName(fileName)) { + throwToolExit('The plugin `$name` has an invalid `fileName` in its web plugin declaration.'); + } + return WebPlugin(name: name, pluginClass: pluginClass, fileName: fileName); } static const kConfigKey = 'web'; diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 0628199b54c83..4f2427a123fe9 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -389,6 +389,12 @@ class Plugin { final dartClass = (platformsYaml[platformKey] as YamlMap)[kDartPluginClass] as String; final String dartFileName = (platformsYaml[platformKey] as YamlMap)[kDartFileName] as String? ?? '$pluginName.dart'; + if (!isValidPluginDartFileName(dartFileName)) { + throwToolExit( + 'The plugin `$pluginName` has an invalid `dartFileName` for platform `$platformKey` ' + 'in pubspec.yaml.', + ); + } return (dartClass: dartClass, dartFileName: dartFileName); } return null; diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index 1d2e0632488dd..5f9d3c7e5d970 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -2020,6 +2020,81 @@ flutter: ); }); + testUsingContext( + 'Plugin.fromYaml rejects a plugin class with code-injection characters', + () async { + // A (possibly transitive) dependency must not be able to smuggle + // arbitrary source into the generated GeneratedPluginRegistrant by + // declaring a pluginClass that is not a plain identifier. + const maliciousYaml = ''' +platforms: + macos: + pluginClass: "SomePlugin(); evilInjectedCall(); if (false) { SomePlugin" +'''; + expect( + () => Plugin.fromYaml( + 'evil_plugin', + '', + loadYaml(maliciousYaml) as YamlMap, + null, + const [], + fileSystem: globals.fs, + isDevDependency: false, + ), + throwsToolExit(message: 'Invalid plugin specification evil_plugin'), + ); + }, + ); + + testUsingContext( + 'Plugin.fromYaml rejects a web plugin whose pluginClass/fileName contain injection', + () async { + const maliciousYaml = ''' +platforms: + web: + pluginClass: "P; void pwn() {} //" + fileName: some_file.dart +'''; + expect( + () => Plugin.fromYaml( + 'evil_web_plugin', + '', + loadYaml(maliciousYaml) as YamlMap, + null, + const [], + fileSystem: globals.fs, + isDevDependency: false, + ), + throwsToolExit(), + ); + }, + ); + + testUsingContext( + 'Plugin.fromYaml rejects a dartPluginClass with code-injection characters', + () async { + // A dart plugin class is also interpolated into generated registrant + // source, so it must be a plain identifier like the native one. + const maliciousYaml = ''' +platforms: + android: + dartPluginClass: "Evil(); evilInjectedCall(); class Evil" +'''; + expect( + () => Plugin.fromYaml( + 'evil_dart_plugin', + '', + loadYaml(maliciousYaml) as YamlMap, + null, + const [], + fileSystem: globals.fs, + isDevDependency: false, + ), + throwsToolExit(message: 'Invalid plugin specification evil_dart_plugin'), + ); + }, + ); + testUsingContext('createPlatformsYamlMap should create the correct map', () async { final YamlMap map = Plugin.createPlatformsYamlMap( ['ios', 'android', 'linux'], From 8732c18de2c4634d7f41e03f83d25f511ae2e6be Mon Sep 17 00:00:00 2001 From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:49:02 -0700 Subject: [PATCH 113/330] [web] Provide Content-Length header for main.dart.wasm in flutter test (#190584) This adds the `Content-Length` header for `main.dart.wasm` when it's served in the `shelf` test server. This complements PR #190155 which fixed this for `canvaskit.wasm`. By providing the `Content-Length` header, `shelf` avoids falling back to `Transfer-Encoding: chunked`, which prevents a bug in Chrome 145's WebAssembly compilation from hanging Skwasm tests. Fixes https://github.com/flutter/flutter/issues/189275 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md Co-authored-by: Mouad Debbar --- .../flutter_tools/lib/src/test/flutter_web_platform.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart index ef53f5cd0b36c..568e8a1f5dc28 100644 --- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart @@ -449,9 +449,13 @@ window.\$dartLoader.loader.nextAttempt(); headers: {'Content-Type': 'text/javascript'}, ); } else if (request.requestedUri.path.contains('main.dart.wasm')) { + final File wasmFile = _buildDirectory.childFile('main.dart.wasm'); return shelf.Response.ok( - _buildDirectory.childFile('main.dart.wasm').openRead(), - headers: {'Content-Type': 'application/wasm'}, + wasmFile.openRead(), + headers: { + HttpHeaders.contentTypeHeader: 'application/wasm', + HttpHeaders.contentLengthHeader: wasmFile.lengthSync().toString(), + }, ); } else { return shelf.Response.notFound('Not Found'); From b7853980f150249e0cbcb12e36842a576b4a34c5 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:55:20 -0700 Subject: [PATCH 114/330] Migrate examples/splash to package:material_ui (#190248) Work towards https://github.com/flutter/flutter/issues/190093 Migrates `examples/splash` to `material_ui` ## Pre-Review Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] page, which explains my responsibilities. - [x] I read and followed the [relevant style guides] and ran [the auto-formatter]. - [x] I signed the [CLA]. - [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` - [x] I [linked to at least one issue that this PR fixes] in the description above. - [x] I followed [the version and CHANGELOG instructions], using [semantic versioning] and the [repository CHANGELOG style], or I have commented below to indicate which documented exception this PR falls under[^1]. - [x] I updated/added any relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under[^1]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. [Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md [relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style [the auto-formatter]: https://github.com/flutter/packages/blob/main/script/tool/README.md#format-code [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [linked to at least one issue that this PR fixes]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview [the version and CHANGELOG instructions]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#version-and-changelog-updates [semantic versioning]: https://dart.dev/tools/pub/versioning#semantic-versions [repository CHANGELOG style]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog-style [test exemption]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests --- **Test exemption:** Migration only. --- dev/bots/check_examples_cross_imports.dart | 2 -- examples/splash/lib/main.dart | 2 +- examples/splash/pubspec.yaml | 3 ++- examples/splash/test/splash_test.dart | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dev/bots/check_examples_cross_imports.dart b/dev/bots/check_examples_cross_imports.dart index b19434b88aec2..25e2cb1c33d25 100644 --- a/dev/bots/check_examples_cross_imports.dart +++ b/dev/bots/check_examples_cross_imports.dart @@ -585,8 +585,6 @@ class ExamplesCrossImportChecker { 'examples/multiple_windows/lib/app/window_settings_dialog.dart', 'examples/multiple_windows/lib/main.dart', 'examples/multiple_windows/test/multiple_windows_test.dart', - 'examples/splash/lib/main.dart', - 'examples/splash/test/splash_test.dart', 'examples/texture/lib/main.dart', }; diff --git a/examples/splash/lib/main.dart b/examples/splash/lib/main.dart index 6062b6cb4d8ad..921be96c4dd34 100644 --- a/examples/splash/lib/main.dart +++ b/examples/splash/lib/main.dart @@ -2,7 +2,7 @@ // 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:material_ui/material_ui.dart'; void main() { runApp( diff --git a/examples/splash/pubspec.yaml b/examples/splash/pubspec.yaml index 7e4601d1e97f7..e804c61870f72 100644 --- a/examples/splash/pubspec.yaml +++ b/examples/splash/pubspec.yaml @@ -8,6 +8,7 @@ resolution: workspace dependencies: flutter: sdk: flutter + material_ui: ^0.0.2 dev_dependencies: @@ -15,4 +16,4 @@ dev_dependencies: sdk: flutter -# PUBSPEC CHECKSUM: 60tfp7 +# PUBSPEC CHECKSUM: oescuq diff --git a/examples/splash/test/splash_test.dart b/examples/splash/test/splash_test.dart index 96363b8ffb93a..9cf112e5d486a 100644 --- a/examples/splash/test/splash_test.dart +++ b/examples/splash/test/splash_test.dart @@ -2,9 +2,8 @@ // 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_test/flutter_test.dart'; - +import 'package:material_ui/material_ui.dart'; import 'package:splash/main.dart' as entrypoint; void main() { From 68bd1165db0a354a8e58d2ef48a777c73cfa0b7c Mon Sep 17 00:00:00 2001 From: Matt Boetger Date: Thu, 6 Aug 2026 17:14:08 -0700 Subject: [PATCH 115/330] [Reland] Enable Gradle cache for CI on single test target (#190635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a reland of #190474. Now using ':' - which should not break the validate luci test. In https://cs.opensource.google/flutter/infra/+/main:config/lib/ci_yaml/ci_yaml.star;l=93-95 the swarming cache names are created. This is what broke on the last go - the cache names did not fit the [regex](https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8674277389276279873/+/u/luci_validate/stdout): `^[a-z0-9_]+$` Before: `flutter_main_gradle_dists_8_4_bin, 8_13_rc_1_bin, 8_14_bin, 9_3_1_bin, 9_3_1_all` ❌ does not match After: `flutter_main_gradle_dists_8_4_bin_8_13_rc_1_bin_8_14_bin_9_3_1_bin_9_3_1_all` ✅ does match ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. --- .ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci.yaml b/.ci.yaml index 50e3a6036a943..984a31865c725 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -7162,7 +7162,8 @@ targets: [ {"dependency": "android_sdk", "version": "version:36v4"}, {"dependency": "open_jdk", "version": "version:21"}, - {"dependency": "vs_build", "version": "version:vs2019"} + {"dependency": "vs_build", "version": "version:vs2019"}, + {"dependency": "gradle_dists", "version": "8.4-bin:8.13-rc-1-bin:8.14-bin:9.3.1-bin:9.3.1-all"} ] shard: tool_tests_commands subshard: 2_2 From 198e1cefd26aa963f9a22d9295d4f265562a04f7 Mon Sep 17 00:00:00 2001 From: Hannah Jin Date: Thu, 6 Aug 2026 17:16:56 -0700 Subject: [PATCH 116/330] Add service extension getSemanticsTree (#189635) tracking issue: https://github.com/flutter/devtools/issues/9893 Add a service extension ,it will be used to visualize semantics tree in devtool. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. tracking issue: https://github.com/flutter/devtools/issues/9893 It will used to visualize semantics tree in devtool - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/src/semantics/semantics.dart | 55 +++++++ .../src/widgets/accessibility_inspector.dart | 113 +++++++++++++ packages/flutter/lib/src/widgets/binding.dart | 3 +- .../lib/src/widgets/service_extensions.dart | 44 +++++ .../foundation/service_extensions_test.dart | 16 +- .../test/semantics/semantics_test.dart | 41 +++++ .../widgets/accessibility_inspector_test.dart | 152 ++++++++++++++++++ 7 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/accessibility_inspector.dart create mode 100644 packages/flutter/test/widgets/accessibility_inspector_test.dart diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 5cc2d64d6d0e1..13c34fb94e5fc 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -1377,6 +1377,38 @@ class SemanticsData with Diagnosticable { /// Whether [actions] contains the given action. bool hasAction(SemanticsAction action) => (actions & action.index) != 0; + /// Returns a JSON-compatible map representation of this object. + /// + /// Used by debugging tools and VM service extensions such as + /// `ext.flutter.accessibility.getSemanticsTree`. + Map toJson() { + final flagsList = [ + for (final SemanticsFlag flag in SemanticsFlag.values) + if (hasFlag(flag)) flag.name, + ]; + final actionsList = [ + for (final SemanticsAction action in SemanticsAction.values) + if (hasAction(action)) action.name, + ]; + return { + 'label': label, + 'value': value, + 'hint': hint, + 'tooltip': tooltip, + 'increasedValue': increasedValue, + 'decreasedValue': decreasedValue, + 'flags': flagsList, + 'actions': actionsList, + 'rect': { + 'left': rect.left, + 'top': rect.top, + 'width': rect.width, + 'height': rect.height, + }, + 'transform': ?transform?.storage.toList(), + }; + } + @override String toStringShort() => objectRuntimeType(this, 'SemanticsData'); @@ -4566,6 +4598,29 @@ class SemanticsNode with DiagnosticableTreeMixin { DebugSemanticsDumpOrder.traversalOrder => _childrenInTraversalOrder(), }; } + + /// Returns a JSON-compatible map representation of this node and its children + /// identifiers. + /// + /// Used by debugging tools and VM service extensions such as + /// `ext.flutter.accessibility.getSemanticsTree`. + Map toJson() { + final SemanticsData data = getSemanticsData(); + final List traversalChildren = debugListChildrenInOrder( + DebugSemanticsDumpOrder.traversalOrder, + ); + final List hitTestChildren = debugListChildrenInOrder( + DebugSemanticsDumpOrder.inverseHitTest, + ); + return { + 'id': id, + ...data.toJson(), + 'childrenInTraversalOrder': [ + for (final SemanticsNode child in traversalChildren) child.id, + ], + 'childrenInHitTestOrder': [for (final SemanticsNode child in hitTestChildren) child.id], + }; + } } /// An edge of a box, such as top, bottom, left or right, used to compute diff --git a/packages/flutter/lib/src/widgets/accessibility_inspector.dart b/packages/flutter/lib/src/widgets/accessibility_inspector.dart new file mode 100644 index 0000000000000..76a74b64163a3 --- /dev/null +++ b/packages/flutter/lib/src/widgets/accessibility_inspector.dart @@ -0,0 +1,113 @@ +// 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/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'service_extensions.dart'; + +/// Service that handles accessibility and semantics inspection. +class AccessibilityInspector { + AccessibilityInspector._(); + + /// The active [AccessibilityInspector] instance. + static final AccessibilityInspector instance = AccessibilityInspector._(); + + SemanticsHandle? _semanticsHandle; + + /// Registers accessibility-related VM service extensions. + void initServiceExtensions( + void Function({required String name, required ServiceExtensionCallback callback}) + registerServiceExtension, + ) { + registerServiceExtension( + name: AccessibilityServiceExtensions.getSemanticsTree.extensionName, + callback: _getSemanticsTree, + ); + registerServiceExtension( + name: AccessibilityServiceExtensions.enableSemantics.extensionName, + callback: _enableSemantics, + ); + registerServiceExtension( + name: AccessibilityServiceExtensions.disposeSemantics.extensionName, + callback: _disposeSemantics, + ); + } + + /// Reset the helper state (primarily used in tests). + @visibleForTesting + void resetAllState() { + _semanticsHandle?.dispose(); + _semanticsHandle = null; + } + + Future> _enableSemantics(Map parameters) async { + _semanticsHandle ??= SemanticsBinding.instance.ensureSemantics(); + return const {}; + } + + Future> _disposeSemantics(Map parameters) async { + resetAllState(); + return const {}; + } + + Future> _getSemanticsTree(Map parameters) async { + if (!SemanticsBinding.instance.semanticsEnabled) { + return {'error': 'Semantics not enabled.'}; + } + final PipelineOwner? pipelineOwner = _findPipelineOwner(); + final SemanticsOwner? semanticsOwner = pipelineOwner?.semanticsOwner; + if (semanticsOwner == null) { + return {'error': 'No PipelineOwner with SemanticsOwner found'}; + } + final SemanticsNode? root = semanticsOwner.rootSemanticsNode; + if (root == null) { + RendererBinding.instance.ensureVisualUpdate(); + return {'error': 'rootSemanticsNode is null', 'needsFrame': true}; + } + + final nodes = {}; + final visited = {}; + final queue = [root]; + while (queue.isNotEmpty) { + final SemanticsNode node = queue.removeLast(); + if (!visited.add(node.id)) { + continue; + } + nodes[node.id.toString()] = node.toJson(); + for (final SemanticsNode child in node.debugListChildrenInOrder( + DebugSemanticsDumpOrder.traversalOrder, + )) { + if (!visited.contains(child.id)) { + queue.add(child); + } + } + for (final SemanticsNode child in node.debugListChildrenInOrder( + DebugSemanticsDumpOrder.inverseHitTest, + )) { + if (!visited.contains(child.id)) { + queue.add(child); + } + } + } + + return {'data': nodes}; + } + + // TODO(hannahjin): This returns the first SemanticsOwner of any RenderView. + // This getSemanticsTree feature is used in DevTools, which currently only supports + // single-view inspection. Add multi-view support when DevTools needs it. + PipelineOwner? _findPipelineOwner() { + for (final RenderView renderView in RendererBinding.instance.renderViews) { + if (renderView.owner?.semanticsOwner != null) { + return renderView.owner; + } + } + final PipelineOwner deprecatedOwner = RendererBinding.instance.pipelineOwner; + if (deprecatedOwner.semanticsOwner != null) { + return deprecatedOwner; + } + return null; + } +} diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 4bf52688e0df7..0e49026ee0d16 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -38,6 +38,7 @@ import 'package:flutter/services.dart'; import '../foundation/_features.dart'; import '_accessibility_evaluations.dart'; import '_window.dart'; +import 'accessibility_inspector.dart'; import 'app.dart'; import 'debug.dart'; import 'focus_manager.dart'; @@ -807,7 +808,7 @@ mixin WidgetsBinding return _forceRebuild(); }, ); - + AccessibilityInspector.instance.initServiceExtensions(registerServiceExtension); WidgetInspectorService.instance.initServiceExtensions(registerServiceExtension); return true; diff --git a/packages/flutter/lib/src/widgets/service_extensions.dart b/packages/flutter/lib/src/widgets/service_extensions.dart index ca0749ea8cf85..6474cd20f36fb 100644 --- a/packages/flutter/lib/src/widgets/service_extensions.dart +++ b/packages/flutter/lib/src/widgets/service_extensions.dart @@ -6,6 +6,7 @@ /// @docImport 'package:flutter/foundation.dart'; /// @docImport 'package:flutter/rendering.dart'; /// +/// @docImport 'accessibility_inspector.dart'; /// @docImport 'app.dart'; /// @docImport 'binding.dart'; /// @docImport 'debug.dart'; @@ -537,3 +538,46 @@ enum WidgetInspectorServiceExtensions { /// extension is registered. setFlexProperties, } + +/// Service extension constants for accessibility and semantics. +/// +/// These constants will be used when registering service extensions in the +/// framework, and they will also be used by tools and services that call these +/// service extensions. +/// +/// The String value for each of these extension names should be accessed by +/// calling the [extensionName] property on the enum value. +enum AccessibilityServiceExtensions { + /// Name of service extension that, when called, returns the JSON serialized + /// semantics tree. + /// + /// This extension should only be called after semantics has been enabled + /// (for example, by calling [enableSemantics]). + /// + /// See also: + /// + /// * [AccessibilityInspector.initServiceExtensions], where the service + /// extension is registered. + getSemanticsTree, + + /// Name of service extension that, when called, enables semantics in the app + /// by creating a [SemanticsHandle]. + /// + /// See also: + /// + /// * [AccessibilityInspector.initServiceExtensions], where the service + /// extension is registered. + enableSemantics, + + /// Name of service extension that, when called, disposes the [SemanticsHandle] + /// created by [enableSemantics]. + /// + /// See also: + /// + /// * [AccessibilityInspector.initServiceExtensions], where the service + /// extension is registered. + disposeSemantics; + + /// The full name of the service extension, including the `accessibility.` prefix. + String get extensionName => 'accessibility.$name'; +} diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index e79a999612767..379fdf19fbd79 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -194,6 +194,14 @@ void main() { hasLength(widgetInspectorExtensionCount), ); + // See accessibility_inspector_test.dart for tests of the ext.flutter.accessibility + // service extensions included in this count. + const accessibilityExtensionCount = 3; + expect( + binding.extensions.keys.where((String name) => name.startsWith('accessibility.')), + hasLength(accessibilityExtensionCount), + ); + // The following service extensions are disabled in web: // 1. exit // 2. showPerformanceOverlay @@ -202,7 +210,8 @@ void main() { // The expected number of registered service extensions in the Flutter // framework, excluding any that are for the widget inspector (see // widget_inspector_test.dart for tests of the ext.flutter.inspector service - // extensions). Any test counted here must be tested in this file! + // extensions) or accessibility inspector (see accessibility_inspector_test.dart). + // Any test counted here must be tested in this file! const serviceExtensionCount = 31; // The tests are in the widgets/accessibility_evaluations_service_extension_test.dart @@ -211,7 +220,10 @@ void main() { expect( binding.extensions.length, - serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions, + serviceExtensionCount + + widgetInspectorExtensionCount + + accessibilityExtensionCount - + disabledExtensions, ); expect(testedExtensions, hasLength(serviceExtensionCount)); diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart index 743e6ab05f965..5225f629dce29 100644 --- a/packages/flutter/test/semantics/semantics_test.dart +++ b/packages/flutter/test/semantics/semantics_test.dart @@ -1342,6 +1342,47 @@ void main() { expect(label, 'Emoji: 😀🎉 Math: ∑∆π Currency: €£¥'); }); }); + + test('SemanticsData.toJson and SemanticsNode.toJson generate expected maps', () { + final node = SemanticsNode() + ..rect = const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0) + ..updateWith( + config: SemanticsConfiguration() + ..label = 'Test Label' + ..textDirection = TextDirection.ltr + ..value = 'Test Value' + ..hint = 'Test Hint' + ..isButton = true, + ); + + final SemanticsData data = node.getSemanticsData(); + final Map dataJsonMap = data.toJson(); + expect(dataJsonMap['label'], 'Test Label'); + expect(dataJsonMap['value'], 'Test Value'); + expect(dataJsonMap['hint'], 'Test Hint'); + expect(dataJsonMap['flags'], contains('isButton')); + expect(dataJsonMap['rect'], { + 'left': 0.0, + 'top': 0.0, + 'width': 100.0, + 'height': 50.0, + }); + + final Map nodeJsonMap = node.toJson(); + expect(nodeJsonMap['id'], node.id); + expect(nodeJsonMap['label'], 'Test Label'); + expect(nodeJsonMap['value'], 'Test Value'); + expect(nodeJsonMap['hint'], 'Test Hint'); + expect(nodeJsonMap['flags'], contains('isButton')); + expect(nodeJsonMap['rect'], { + 'left': 0.0, + 'top': 0.0, + 'width': 100.0, + 'height': 50.0, + }); + expect(nodeJsonMap['childrenInTraversalOrder'], isEmpty); + expect(nodeJsonMap['childrenInHitTestOrder'], isEmpty); + }); } class TestRender extends RenderProxyBox { diff --git a/packages/flutter/test/widgets/accessibility_inspector_test.dart b/packages/flutter/test/widgets/accessibility_inspector_test.dart new file mode 100644 index 0000000000000..95ae92f8587d4 --- /dev/null +++ b/packages/flutter/test/widgets/accessibility_inspector_test.dart @@ -0,0 +1,152 @@ +// 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']) +@TestOn('!chrome') +library; + +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/src/widgets/accessibility_inspector.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('ext.flutter.accessibility.getSemanticsTree', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( + label: 'Root Node', + container: true, + explicitChildNodes: true, + child: Column( + children: [ + Semantics( + label: 'Child Node 1', + button: true, + tooltip: 'This is a tooltip', + child: const Text('Button 1'), + ), + Semantics( + label: 'Child Node 2', + value: '42', + increasedValue: '43', + decreasedValue: '41', + onIncrease: () {}, + onDecrease: () {}, + child: const Text('Value 2'), + ), + Transform.scale( + scale: 2.0, + child: Semantics(label: 'Child Node 3', child: const Text('Scaled')), + ), + ], + ), + ), + ), + ); + + final accessibilityExtensions = {}; + AccessibilityInspector.instance.initServiceExtensions(({ + required String name, + required ServiceExtensionCallback callback, + }) { + accessibilityExtensions[name] = callback; + }); + + Future> callExtension(String name) async { + return json.decode( + json.encode(await accessibilityExtensions[name]!(const {})), + ) + as Map; + } + + // Calling getSemanticsTree before semantics is enabled returns an error. + final Map disabledResult = await callExtension( + AccessibilityServiceExtensions.getSemanticsTree.extensionName, + ); + expect(disabledResult['error'], equals('Semantics not enabled.')); + expect(disabledResult['needsFrame'], isNull); + + // Calling enableSemantics enables semantics without returning the tree. + final Map enableResult = await callExtension( + AccessibilityServiceExtensions.enableSemantics.extensionName, + ); + expect(enableResult, isEmpty); + + // Calling getSemanticsTree schedules a frame and returns an error map indicating root is null. + final Map result1 = await callExtension( + AccessibilityServiceExtensions.getSemanticsTree.extensionName, + ); + + expect(result1['error'], equals('rootSemanticsNode is null')); + expect(result1['needsFrame'], isTrue); + + // Pump a frame to build/flush the semantics tree. + await tester.pump(); + + // The second call returns the populated semantics tree. + final Map result2 = await callExtension( + AccessibilityServiceExtensions.getSemanticsTree.extensionName, + ); + + expect(result2['error'], isNull); + expect(result2['data'], isA>()); + final nodes = result2['data']! as Map; + expect(nodes, isNotEmpty); + + Map findNodeWithLabel(Map nodes, String label) { + for (final Object? value in nodes.values) { + final node = value! as Map; + if ((node['label']! as String).contains(label)) { + return node; + } + } + return const {}; + } + + final Map rootNode = findNodeWithLabel(nodes, 'Root Node'); + expect(rootNode, isNotEmpty); + expect(rootNode['id'], isNotNull); + + final Map child1 = findNodeWithLabel(nodes, 'Child Node 1'); + expect(child1, isNotEmpty); + expect(child1['flags']! as List, contains('isButton')); + expect(child1['tooltip'], equals('This is a tooltip')); + + final Map child2 = findNodeWithLabel(nodes, 'Child Node 2'); + expect(child2, isNotEmpty); + expect(child2['value'], equals('42')); + expect(child2['increasedValue'], equals('43')); + expect(child2['decreasedValue'], equals('41')); + expect(child2['actions']! as List, contains('increase')); + expect(child2['actions']! as List, contains('decrease')); + + final Map child3 = findNodeWithLabel(nodes, 'Child Node 3'); + expect(child3, isNotEmpty); + expect(child3['transform'], isNotNull); + final transform = child3['transform']! as List; + expect(transform, hasLength(16)); + expect(transform[0], equals(2.0)); + + expect( + rootNode['childrenInTraversalOrder']! as List, + containsAll([child1['id'], child2['id'], child3['id']]), + ); + expect( + rootNode['childrenInHitTestOrder']! as List, + containsAll([child1['id'], child2['id'], child3['id']]), + ); + + // Calling disposeSemantics succeeds and cleans up semantics handle. + final Map disposeResult = await callExtension( + AccessibilityServiceExtensions.disposeSemantics.extensionName, + ); + expect(disposeResult, isEmpty); + + AccessibilityInspector.instance.resetAllState(); + }, semanticsEnabled: false); +} From 2a469b880c19cbbec6aaa6329d4a4a9d1db22a4e Mon Sep 17 00:00:00 2001 From: Vincent Ong <256906086+mvincentong@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:09:21 +0800 Subject: [PATCH 117/330] Preserve SelectableRegion scope across context menu rebuilds (#186553) `SelectableRegion` conditionally wraps its selection subtree in `PlatformSelectableRegionContextMenu` on desktop web. When `BrowserContextMenu.enabled` changes, that wrapper can be removed while the existing root `SelectionContainer` is still registered, causing a replacement container to register before the old one unregisters. This keeps the selection status scope keyed from `SelectableRegionState`, so the selection subtree is reparented across the optional wrapper change instead of recreated. The browser regression covers enable/disable transitions with the widgets-layer test harness and current selection controls. Fixes #186459. Tests: - `../../bin/flutter test --no-pub --platform chrome test/widgets/selectable_region_context_menu_test.dart` - `./bin/flutter analyze --no-pub packages/flutter/lib/src/widgets/selectable_region.dart packages/flutter/test/widgets/selectable_region_context_menu_test.dart` - `./bin/dart format --output=none --set-exit-if-changed packages/flutter/test/widgets/selectable_region_context_menu_test.dart` - `git diff --check` ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/src/widgets/selectable_region.dart | 6 +++ .../selectable_region_context_menu_test.dart | 51 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart index ca7f777d8a507..fd52fc649416f 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart @@ -448,6 +448,10 @@ class SelectableRegionState extends State final _SelectableRegionSelectionStatusNotifier _selectionStatusNotifier = _SelectableRegionSelectionStatusNotifier._(); + /// Preserves the selection status scope and root selection container when the + /// desktop web context menu wrapper is added or removed. + final GlobalKey _selectionStatusScopeKey = GlobalKey(debugLabel: 'selectionStatusScopeKey'); + @protected @override void initState() { @@ -1959,6 +1963,7 @@ class SelectableRegionState extends State Widget build(BuildContext context) { assert(debugCheckHasOverlay(context)); Widget result = SelectableRegionSelectionStatusScope._( + key: _selectionStatusScopeKey, selectionStatusNotifier: _selectionStatusNotifier, child: SelectionContainer(registrar: this, delegate: _selectionDelegate, child: widget.child), ); @@ -3516,6 +3521,7 @@ final class _SelectableRegionSelectionStatusNotifier extends ChangeNotifier /// does not change. final class SelectableRegionSelectionStatusScope extends InheritedWidget { const SelectableRegionSelectionStatusScope._({ + super.key, required this.selectionStatusNotifier, required super.child, }); diff --git a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart index bc4fccc21c1c9..5298257ec194a 100644 --- a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart +++ b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart @@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:web/web.dart' as web; +import 'editable_text_tester.dart'; import 'web_platform_view_registry_utils.dart'; extension on web.HTMLCollection { @@ -43,11 +44,23 @@ void main() { fakePlatformViewRegistry = FakePlatformViewRegistry(); PlatformSelectableRegionContextMenu.debugOverrideRegisterViewFactory = fakePlatformViewRegistry.registerViewFactory; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.contextMenu, + (MethodCall call) { + // Just complete successfully, so that BrowserContextMenu thinks that + // the engine successfully received its call. + return Future.value(); + }, + ); }); tearDown(() { PlatformSelectableRegionContextMenu.debugOverrideRegisterViewFactory = null; PlatformSelectableRegionContextMenu.debugResetRegistry(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.contextMenu, + null, + ); }); testWidgets('DOM element is set up correctly', (WidgetTester tester) async { @@ -429,6 +442,44 @@ void main() { expect(event.defaultPrevented, isTrue); } }, variant: _browserContextMenuEnabledVariants); + + // Regression test for https://github.com/flutter/flutter/issues/186459 + testWidgets('can rebuild SelectableRegion as browser context menu toggles', ( + WidgetTester tester, + ) async { + await BrowserContextMenu.enableContextMenu(); + addTearDown(BrowserContextMenu.enableContextMenu); + + late StateSetter rebuild; + await tester.pumpWidget( + TestWidgetsApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + rebuild = setState; + return SelectableRegion( + selectionControls: testTextSelectionHandleControls, + child: const Text('How are you?'), + ); + }, + ), + ), + ); + + await BrowserContextMenu.disableContextMenu(); + rebuild(() {}); + await tester.pump(); + expect(tester.takeException(), isNull); + + await BrowserContextMenu.enableContextMenu(); + rebuild(() {}); + await tester.pump(); + expect(tester.takeException(), isNull); + + await BrowserContextMenu.disableContextMenu(); + rebuild(() {}); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + }, variant: _browserContextMenuEnabledVariants); } void removeAllStyleElements() { From 7b6a1edd3b78e7d49cb1f8d921a9b5e939eee9a4 Mon Sep 17 00:00:00 2001 From: TtangKong Date: Fri, 7 Aug 2026 11:15:06 +0900 Subject: [PATCH 118/330] Fix Hero size changes during Navigator resize (#190025) Fixes #134647 ## Description Hero flights were positioned by converting the evaluated flight Rect into a RelativeRect using the Navigator size captured when the flight started. This caused the Hero's size or position to become distorted if the Navigator resized during the transition. It was especially visible with nested Navigators because they can be resized by their parent layout, for example when a Scaffold shrinks in response to the keyboard. The root Navigator's Overlay typically keeps the full viewport size, so the issue was less likely to occur there. This change positions the Hero directly from the evaluated Rect, avoiding calculations based on stale Navigator dimensions. A regression test was added that resizes a nested Navigator during a Hero flight and verifies that the shuttle preserves its size. ## Demonstration

Previous / Current Video [previous](https://github.com/user-attachments/assets/b72c2c25-b732-4704-b6f0-6d97a0947305) [current](https://github.com/user-attachments/assets/d9430346-5b81-4232-a023-753bd600c306)
## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Victor Sanni --- packages/flutter/lib/src/widgets/heroes.dart | 22 +---- .../flutter/test/widgets/heroes_test.dart | 97 +++++++++++++++++++ 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index d4951873d7f8c..6c31edc4914e0 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -443,7 +443,6 @@ class _HeroFlightManifest { _HeroFlightManifest({ required this.type, required this.overlay, - required this.navigatorSize, required this.fromRoute, required this.toRoute, required this.fromHero, @@ -456,7 +455,6 @@ class _HeroFlightManifest { final HeroFlightDirection type; final OverlayState overlay; - final Size navigatorSize; final PageRoute fromRoute; final PageRoute toRoute; final _HeroState fromHero; @@ -584,12 +582,8 @@ class _HeroFlight { child: shuttle, builder: (BuildContext context, Widget? child) { final Rect rect = heroRectTween.evaluate(_proxyAnimation)!; - final offsets = RelativeRect.fromSize(rect, manifest.navigatorSize); - return Positioned( - top: offsets.top, - right: offsets.right, - bottom: offsets.bottom, - left: offsets.left, + return Positioned.fromRect( + rect: rect, child: IgnorePointer( child: FadeTransition(opacity: _heroOpacity, child: child), ), @@ -996,17 +990,6 @@ class HeroController extends NavigatorObserver { return; } - final RenderObject? navigatorRenderObject = navigator.context.findRenderObject(); - - if (navigatorRenderObject is! RenderBox) { - assert( - false, - 'Navigator $navigator has an invalid RenderObject type ${navigatorRenderObject.runtimeType}.', - ); - return; - } - assert(navigatorRenderObject.hasSize); - // At this point, the toHeroes may have been built and laid out for the first time. // // If `fromSubtreeContext` is null, call endFlight on all toHeroes, for good measure. @@ -1030,7 +1013,6 @@ class HeroController extends NavigatorObserver { : _HeroFlightManifest( type: flightType, overlay: overlay, - navigatorSize: navigatorRenderObject.size, fromRoute: from, toRoute: to, fromHero: fromHero, diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index c714f4c159470..4b61883e7f6e4 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -3920,6 +3920,103 @@ Future main() async { ); expect(tester.getSize(find.byType(Hero)), Size.zero); }); + + testWidgets('Hero does not resize when its Navigator resizes during flight', ( + WidgetTester tester, + ) async { + final heroController = HeroController( + createRectTween: (begin, end) => RectTween(begin: begin, end: end), + ); + + addTearDown(heroController.dispose); + + final navigatorKey = GlobalKey(); + const shuttleKey = ValueKey('hero-shuttle'); + + late StateSetter updateHost; + var navigatorHeight = 600.0; + + Widget buildHero(Alignment alignment) { + return Align( + alignment: alignment, + child: Hero( + tag: 'hero', + flightShuttleBuilder: (_, _, _, _, _) { + return const SizedBox(key: shuttleKey, width: 100.0, height: 100.0); + }, + child: const SizedBox(width: 100.0, height: 100.0), + ), + ); + } + + Widget buildSourcePage() { + return buildHero(Alignment.topLeft); + } + + Widget buildDestinationPage() { + return buildHero(Alignment.topRight); + } + + await tester.pumpWidget( + TestWidgetsApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + updateHost = setState; + + return Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400.0, + height: navigatorHeight, + child: HeroControllerScope( + controller: heroController, + child: Navigator( + key: navigatorKey, + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + transitionDuration: const Duration(seconds: 1), + pageBuilder: (_, _, _) => buildSourcePage(), + ); + }, + ), + ), + ), + ); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + // Push the destination route to start the Hero transition. + navigatorKey.currentState!.push( + PageRouteBuilder( + transitionDuration: const Duration(seconds: 1), + pageBuilder: (_, _, _) => buildDestinationPage(), + ), + ); + + // Build the destination route, then advance into the middle of the Hero flight. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + final Finder shuttle = find.byKey(shuttleKey); + + // Verify the shuttle starts with the expected size. + expect(shuttle, findsOneWidget); + expect(tester.getSize(shuttle), const Size(100.0, 100.0)); + + // Resize the Navigator while the Hero is still in flight. + updateHost(() => navigatorHeight -= 100.0); + await tester.pump(); + + // Verify that resizing the Navigator does not alter the shuttle's size. + expect(shuttle, findsOneWidget); + expect(tester.getSize(shuttle), const Size(100.0, 100.0)); + + await tester.pumpAndSettle(); + }); } class TestDependencies extends StatelessWidget { From ddf82665468b8c74b4527ab6f9d52e6031de271e Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 6 Aug 2026 22:43:22 -0400 Subject: [PATCH 119/330] Roll Dart SDK from bb17f25f176f to 941e2ec4ff23 (9 revisions) (#190688) https://dart.googlesource.com/sdk.git/+log/bb17f25f176f..941e2ec4ff23 2026-08-07 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-104.0.dev 2026-08-06 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-103.0.dev 2026-08-06 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-102.0.dev 2026-08-06 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-101.0.dev 2026-08-06 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-100.0.dev 2026-08-06 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-99.0.dev 2026-08-05 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-98.0.dev 2026-08-05 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-97.0.dev 2026-08-05 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-96.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC codefu@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DEPS b/DEPS index 142cb94b30cb8..cdfd12bf41c74 100644 --- a/DEPS +++ b/DEPS @@ -55,7 +55,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': 'bb17f25f176f5209179b23d51aab9725ffb5146d', + 'dart_revision': '941e2ec4ff23b3c8f4292ddf802a91c026937195', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -68,7 +68,7 @@ vars = { 'dart_i18n_rev': 'e1b5a798f8922bb27bbc6d858748ece6f9a19f02', 'dart_perfetto_rev': '13ce0c9e13b0940d2476cd0cff2301708a9a2e2b', 'dart_protobuf_rev': '91efb90f437bb6a30e6726c3369a2fcb9bba06e7', - 'dart_pub_rev': 'ec276d10a7fa0f6c6ec005340fb9ad29f3b012d0', + 'dart_pub_rev': '7654d523a42e764fad77c9e7b63a9686b88c9323', 'dart_sync_http_rev': '6666fff944221891182e1f80bf56569338164d72', 'dart_tools_rev': 'b827a6e38b934232c7f7b8728a5aef6165e7df2d', 'dart_vector_math_rev': 'cf3b5db7340d317dd3489e5a35434b408020a852', @@ -372,7 +372,7 @@ deps = { Var('dart_git') + '/external/github.com/simolus3/tar.git@13479f7c2a18f499e840ad470cfcca8c579f6909', 'engine/src/flutter/third_party/dart/third_party/pkg/test': - Var('dart_git') + '/test.git@bd92e633e7f05edc3301865bdc00d1ae181cb1f1', + Var('dart_git') + '/test.git@4838365e7fb3a2d0302cdbbd8330b7884f7d3c0d', 'engine/src/flutter/third_party/dart/third_party/pkg/tools': Var('dart_git') + '/tools.git' + '@' + Var('dart_tools_rev'), From afc82b8e1eee147e91ee4e1b977404eea957feae Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 6 Aug 2026 21:52:51 -0500 Subject: [PATCH 120/330] Fix runIf path filtering in .ci.yaml to include all example projects (#190687) Noticed in https://github.com/flutter/flutter/pull/188488 the tests were not being run in CI. This fixes that! :) ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 984a31865c725..2a059bf39e0de 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -876,7 +876,7 @@ targets: ["framework", "hostonly", "shard", "linux"] runIf: - dev/** - - examples/api/** + - examples/** - packages/flutter/** - packages/flutter_driver/** - packages/integration_test/** @@ -4345,7 +4345,7 @@ targets: ["framework", "hostonly", "shard", "mac"] runIf: - dev/** - - examples/api/** + - examples/** - packages/flutter/** - packages/flutter_driver/** - packages/integration_test/** @@ -4376,7 +4376,7 @@ targets: ["framework", "hostonly", "shard", "mac"] runIf: - dev/** - - examples/api/** + - examples/** - packages/flutter/** - packages/flutter_driver/** - packages/integration_test/** @@ -6410,7 +6410,7 @@ targets: ["framework", "hostonly", "shard", "windows"] runIf: - dev/** - - examples/api/** + - examples/** - packages/flutter/** - packages/flutter_driver/** - packages/integration_test/** From d4e7f9ae4c9d728718871d5929f47c8491df3976 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 7 Aug 2026 12:38:25 +0900 Subject: [PATCH 121/330] iOS: Migrate the scenario app to UIScene (#190649) Apps that haven't adopted the UIScene life cycle do not run on iOS 27. This updates the scenarios app to include a `UIApplicationSceneManifest` in the app's `Info.plist` and moves the window setup into a `SceneDelegate` that subclasses `FlutterSceneDelegate`, similar to what we do in `dev/integration_tests/ios_add2app_uiscene`. We create the window from the connected `UIWindowScene` and make it key before calling to super, which is where the engine forwards the connection event on to plugins. I've kept the bits that aren't UI setup in `AppDelegate`: the guard against `--enable-software-rendering` and the `ContinuousTexture` registration, which we register against the implicit engine rather than the one the scenario builds. Since the window now belongs to the scene delegate rather than the application delegate, the tests that reached it via `UIApplication.sharedApplication.delegate.window` have been updated to use the new `SceneDelegate.mainWindow` instead. They assert that it is non-nil before using it: with no connected scene, presenting on the root view controller, assigning it, and rendering the layer are all silent no-ops that surface as an expectation timeout rather than as a missing window. Because it's still there, `Info_Skia.plist` gets the same manifest to stay in sync with `Info.plist`, from which it differs only in `FLTEnableImpeller`; that said we no longer support a Skia backend, so I'll delete this in a followup. Issue: https://github.com/flutter/flutter/issues/188336 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Scenarios.xcodeproj/project.pbxproj | 6 + .../ios/Scenarios/Scenarios/AppDelegate.m | 220 +--------------- .../ios/Scenarios/Scenarios/Info.plist | 19 ++ .../ios/Scenarios/Scenarios/Info_Skia.plist | 19 ++ .../ios/Scenarios/Scenarios/SceneDelegate.h | 24 ++ .../ios/Scenarios/Scenarios/SceneDelegate.m | 248 ++++++++++++++++++ .../ScenariosTests/AppLifecycleTests.m | 11 +- .../FlutterViewControllerInitialRouteTest.m | 7 +- .../FlutterViewControllerTest.m | 14 +- 9 files changed, 336 insertions(+), 232 deletions(-) create mode 100644 engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.h create mode 100644 engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index b10e571497114..e635155cb93bc 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 246B4E4222E3B5F700073EBF /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 246B4E4122E3B5F700073EBF /* App.framework */; }; 246B4E4622E3B61000073EBF /* ../../Flutter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 246B4E4522E3B61000073EBF /* ../../Flutter.xcframework */; }; 248D76CC22E388370012F0C1 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76CB22E388370012F0C1 /* AppDelegate.m */; }; + 0D5CE1B22E4A000200AA0003 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D5CE1B12E4A000200AA0002 /* SceneDelegate.m */; }; 248D76D422E388380012F0C1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 248D76D322E388380012F0C1 /* Assets.xcassets */; }; 248D76DA22E388380012F0C1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76D922E388380012F0C1 /* main.m */; }; 248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76EE22E388380012F0C1 /* PlatformViewUITests.m */; }; @@ -199,6 +200,8 @@ 248D76C722E388370012F0C1 /* Scenarios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Scenarios.app; sourceTree = BUILT_PRODUCTS_DIR; }; 248D76CA22E388370012F0C1 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 248D76CB22E388370012F0C1 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 0D5CE1B02E4A000200AA0001 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; + 0D5CE1B12E4A000200AA0002 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 248D76D322E388380012F0C1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 248D76D822E388380012F0C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 248D76D922E388380012F0C1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -339,6 +342,8 @@ 24F1FB87230B4579005ACE7C /* TextPlatformView.m */, 248D76CA22E388370012F0C1 /* AppDelegate.h */, 248D76CB22E388370012F0C1 /* AppDelegate.m */, + 0D5CE1B02E4A000200AA0001 /* SceneDelegate.h */, + 0D5CE1B12E4A000200AA0002 /* SceneDelegate.m */, 248D76D322E388380012F0C1 /* Assets.xcassets */, 248D76D822E388380012F0C1 /* Info.plist */, F72114B628EF99F500184A2D /* Info_Skia.plist */, @@ -688,6 +693,7 @@ 68D4017D2564859300ECD91A /* ContinuousTexture.m in Sources */, 24F1FB89230B4579005ACE7C /* TextPlatformView.m in Sources */, 248D76CC22E388370012F0C1 /* AppDelegate.m in Sources */, + 0D5CE1B22E4A000200AA0003 /* SceneDelegate.m in Sources */, 0A57B3BF2323C74200DD9521 /* FlutterEngine+ScenariosTest.m in Sources */, 0A57B3BD2323C4BD00DD9521 /* ScreenBeforeFlutter.m in Sources */, ); diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 41d032c6d8f1d..3b03bdbaef48c 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -5,37 +5,6 @@ #import "AppDelegate.h" #import "ContinuousTexture.h" -#import "FlutterEngine+ScenariosTest.h" -#import "ScreenBeforeFlutter.h" -#import "TextPlatformView.h" - -// A UIViewController that sets YES for its preferedStatusBarHidden property. -// StatusBar includes current time, which is non-deterministic. This ViewController -// removes the StatusBar to make the screenshot deterministic. -@interface NoStatusBarViewController : UIViewController - -@end - -@interface FlutterEngine () -@property(nonatomic, strong) FlutterMethodChannel* statusBarChannel; -@end - -@implementation NoStatusBarViewController -- (BOOL)prefersStatusBarHidden { - return YES; -} -@end - -// The FlutterViewController version of NoStatusBarViewController -@interface NoStatusBarFlutterViewController : FlutterViewController - -@end - -@implementation NoStatusBarFlutterViewController -- (BOOL)prefersStatusBarHidden { - return YES; -} -@end @implementation AppDelegate @@ -46,104 +15,7 @@ - (BOOL)application:(UIApplication*)application @throw @"--enable-software-rendering is unsupported in iOS scenario tests"; } - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; - if ([processArguments containsObject:@"--maskview-blocking"]) { - self.window.tintColor = UIColor.systemPinkColor; - } - NSDictionary* launchArgsMap = @{ - // The golden test args should match `GoldenTestManager`. - @"--locale-initialization" : @"locale_initialization", - @"--platform-view" : @"platform_view", - @"--platform-view-no-overlay-intersection" : @"platform_view_no_overlay_intersection", - @"--platform-view-two-intersecting-overlays" : @"platform_view_two_intersecting_overlays", - @"--platform-view-partial-intersection" : @"platform_view_partial_intersection", - @"--platform-view-one-overlay-two-intersecting-overlays" : - @"platform_view_one_overlay_two_intersecting_overlays", - @"--platform-view-multiple-without-overlays" : @"platform_view_multiple_without_overlays", - @"--platform-view-max-overlays" : @"platform_view_max_overlays", - @"--platform-view-surrounding-layers-fractional-coordinate" : - @"platform_view_surrounding_layers_fractional_coordinate", - @"--platform-view-partial-intersection-fractional-coordinate" : - @"platform_view_partial_intersection_fractional_coordinate", - @"--platform-view-multiple" : @"platform_view_multiple", - @"--platform-view-multiple-background-foreground" : - @"platform_view_multiple_background_foreground", - @"--platform-view-cliprect" : @"platform_view_cliprect", - @"--platform-view-cliprect-multiple-clips" : @"platform_view_cliprect_multiple_clips", - @"--platform-view-cliprrect" : @"platform_view_cliprrect", - @"--platform-view-cliprrect-multiple-clips" : @"platform_view_cliprrect_multiple_clips", - @"--platform-view-large-cliprrect" : @"platform_view_large_cliprrect", - @"--platform-view-large-cliprrect-multiple-clips" : - @"platform_view_large_cliprrect_multiple_clips", - @"--platform-view-clippath" : @"platform_view_clippath", - @"--platform-view-clippath-multiple-clips" : @"platform_view_clippath_multiple_clips", - @"--platform-view-cliprrect-with-transform" : @"platform_view_cliprrect_with_transform", - @"--platform-view-cliprrect-with-transform-multiple-clips" : - @"platform_view_cliprrect_with_transform_multiple_clips", - @"--platform-view-large-cliprrect-with-transform" : - @"platform_view_large_cliprrect_with_transform", - @"--platform-view-large-cliprrect-with-transform-multiple-clips" : - @"platform_view_large_cliprrect_with_transform_multiple_clips", - @"--platform-view-cliprect-with-transform" : @"platform_view_cliprect_with_transform", - @"--platform-view-cliprect-with-transform-multiple-clips" : - @"platform_view_cliprect_with_transform_multiple_clips", - @"--platform-view-clippath-with-transform" : @"platform_view_clippath_with_transform", - @"--platform-view-clippath-with-transform-multiple-clips" : - @"platform_view_clippath_with_transform_multiple_clips", - @"--platform-view-transform" : @"platform_view_transform", - @"--platform-view-opacity" : @"platform_view_opacity", - @"--platform-view-with-other-backdrop-filter" : @"platform_view_with_other_backdrop_filter", - @"--two-platform-views-with-other-backdrop-filter" : - @"two_platform_views_with_other_backdrop_filter", - @"--platform-view-with-negative-backdrop-filter" : - @"platform_view_with_negative_backdrop_filter", - @"--platform-view-rotate" : @"platform_view_rotate", - @"--non-full-screen-flutter-view-platform-view" : @"non_full_screen_flutter_view_platform_view", - @"--gesture-reject-after-touches-ended" : @"platform_view_gesture_reject_after_touches_ended", - @"--gesture-reject-eager" : @"platform_view_gesture_reject_eager", - @"--gesture-accept" : @"platform_view_gesture_accept", - @"--gesture-accept-with-overlapping-platform-views" : - @"platform_view_gesture_accept_with_overlapping_platform_views", - @"--tap-status-bar" : @"tap_status_bar", - @"--animated-color-square" : @"animated_color_square", - @"--solid-blue" : @"solid_blue", - @"--platform-view-with-continuous-texture" : @"platform_view_with_continuous_texture", - @"--bogus-font-text" : @"bogus_font_text", - @"--spawn-engine-works" : @"spawn_engine_works", - @"--pointer-events" : @"pointer_events", - @"--platform-view-scrolling-under-widget" : @"platform_view_scrolling_under_widget", - @"--platform-views-with-clips-scrolling" : @"platform_views_with_clips_scrolling", - @"--platform-views-with-clips-scrolling-multiple-clips" : - @"platform_views_with_clips_scrolling_multiple_clips", - @"--platform-view-cliprect-after-moved" : @"platform_view_cliprect_after_moved", - @"--platform-view-cliprect-after-moved-multiple-clips" : - @"platform_view_cliprect_after_moved_multiple_clips", - @"--two-platform-view-clip-rect" : @"two_platform_view_clip_rect", - @"--two-platform-view-clip-rect-multiple-clips" : @"two_platform_view_clip_rect_multiple_clips", - @"--two-platform-view-clip-rrect" : @"two_platform_view_clip_rrect", - @"--two-platform-view-clip-rrect-multiple-clips" : - @"two_platform_view_clip_rrect_multiple_clips", - @"--two-platform-view-clip-path" : @"two_platform_view_clip_path", - @"--two-platform-view-clip-path-multiple-clips" : @"two_platform_view_clip_path_multiple_clips", - @"--darwin-system-font" : @"darwin_system_font", - }; - __block NSString* flutterViewControllerTestName = nil; - [launchArgsMap - enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL* stop) { - if ([processArguments containsObject:argument]) { - flutterViewControllerTestName = testName; - *stop = YES; - } - }]; - if (flutterViewControllerTestName) { - [self setupFlutterViewControllerTest:flutterViewControllerTestName]; - } else if ([processArguments containsObject:@"--screen-before-flutter"]) { - self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; - } else { - self.window.rootViewController = [[UIViewController alloc] init]; - } - - [self.window makeKeyAndVisible]; + // The window and its root view controller are set up by SceneDelegate. if ([processArguments containsObject:@"--with-continuous-texture"]) { [ContinuousTexture registerWithRegistrar:[self registrarForPlugin:@"com.constant.firing.texture"]]; @@ -151,94 +23,4 @@ - (BOOL)application:(UIApplication*)application return [super application:application didFinishLaunchingWithOptions:launchOptions]; } -- (FlutterEngine*)engineForTest:(NSString*)scenarioIdentifier { - if ([scenarioIdentifier isEqualToString:@"spawn_engine_works"]) { - FlutterEngine* spawner = [[FlutterEngine alloc] initWithName:@"FlutterControllerTest" - project:nil]; - [spawner run]; - return [spawner spawnWithEntrypoint:nil libraryURI:nil initialRoute:nil entrypointArgs:nil]; - } else { - FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"FlutterControllerTest" - project:nil]; - [engine run]; - return engine; - } -} - -- (FlutterViewController*)flutterViewControllerForTest:(NSString*)scenarioIdentifier - withEngine:(FlutterEngine*)engine { - if ([scenarioIdentifier isEqualToString:@"tap_status_bar"]) { - return [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - } else { - return [[NoStatusBarFlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - } -} - -- (void)setupFlutterViewControllerTest:(NSString*)scenarioIdentifier { - FlutterEngine* engine = [self engineForTest:scenarioIdentifier]; - FlutterViewController* flutterViewController = - [self flutterViewControllerForTest:scenarioIdentifier withEngine:engine]; - flutterViewController.view.accessibilityIdentifier = @"flutter_view"; - - [engine.binaryMessenger - setMessageHandlerOnChannel:@"waiting_for_status" - binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { - FlutterMethodChannel* channel = [FlutterMethodChannel - methodChannelWithName:@"driver" - binaryMessenger:engine.binaryMessenger - codec:[FlutterJSONMethodCodec sharedInstance]]; - [channel invokeMethod:@"set_scenario" arguments:@{@"name" : scenarioIdentifier}]; - }]; - // Can be used to synchronize timing in the test for a signal from Dart. - [engine.binaryMessenger - setMessageHandlerOnChannel:@"display_data" - binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { - NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message - options:0 - error:nil]; - UITextField* text = [[UITextField alloc] initWithFrame:CGRectMake(0, 400, 300, 100)]; - text.text = dict[@"data"]; - [flutterViewController.view addSubview:text]; - }]; - - TextPlatformViewFactory* textPlatformViewFactory = - [[TextPlatformViewFactory alloc] initWithMessenger:engine.binaryMessenger]; - NSObject* registrar = - [engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; - [registrar registerViewFactory:textPlatformViewFactory - withId:@"scenarios/textPlatformView" - gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; - [registrar registerViewFactory:textPlatformViewFactory - withId:@"scenarios/textPlatformView_blockPolicyUntilTouchesEnded" - gestureRecognizersBlockingPolicy: - FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded]; - - UIViewController* rootViewController = flutterViewController; - if ([scenarioIdentifier isEqualToString:@"non_full_screen_flutter_view_platform_view"]) { - // Make Flutter View's origin x/y not 0. - rootViewController = [[NoStatusBarViewController alloc] init]; - [rootViewController.view addSubview:flutterViewController.view]; - flutterViewController.view.frame = CGRectMake(150, 150, 500, 500); - } else if ([scenarioIdentifier isEqualToString:@"tap_status_bar"]) { - [engine.binaryMessenger - setMessageHandlerOnChannel:@"flutter/status_bar" - binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { - NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message - options:0 - error:nil]; - FlutterBasicMessageChannel* channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"display_data" - binaryMessenger:engine.binaryMessenger - codec:[FlutterJSONMessageCodec sharedInstance]]; - [channel sendMessage:@{@"data" : dict}]; - UITextField* text = - [[UITextField alloc] initWithFrame:CGRectMake(0, 400, 300, 100)]; - text.text = dict[@"method"]; - [flutterViewController.view addSubview:text]; - }]; - } - - self.window.rootViewController = rootViewController; -} - @end diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist index 798a798b4766e..8d1e8db786072 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist @@ -20,6 +20,25 @@ 1 LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + + + + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist index 0605d9602d839..3f0ff4e8967e9 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist @@ -20,6 +20,25 @@ 1 LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + + + + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.h b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.h new file mode 100644 index 0000000000000..6f2a0943c8786 --- /dev/null +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.h @@ -0,0 +1,24 @@ +// 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_TESTING_IOS_SCENARIO_APP_IOS_SCENARIOS_SCENARIOS_SCENEDELEGATE_H_ +#define FLUTTER_TESTING_IOS_SCENARIO_APP_IOS_SCENARIOS_SCENARIOS_SCENEDELEGATE_H_ + +#import +#import + +@interface SceneDelegate : FlutterSceneDelegate + +/** + * The main window of the first connected scene, or nil if no scene is connected. + * + * Under the UIScene life cycle a window belongs to a scene rather than to the application + * delegate, and a single scene delegate may serve more than one scene. The scenario app only + * ever has one scene, so tests use this to reach its root view controller. + */ +@property(class, nonatomic, readonly, nullable) UIWindow* mainWindowOfFirstConnectedScene; + +@end + +#endif // FLUTTER_TESTING_IOS_SCENARIO_APP_IOS_SCENARIOS_SCENARIOS_SCENEDELEGATE_H_ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m new file mode 100644 index 0000000000000..e7627335b2ca6 --- /dev/null +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m @@ -0,0 +1,248 @@ +// 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 "SceneDelegate.h" + +#import "FlutterEngine+ScenariosTest.h" +#import "ScreenBeforeFlutter.h" +#import "TextPlatformView.h" + +// A UIViewController that sets YES for its preferedStatusBarHidden property. +// StatusBar includes current time, which is non-deterministic. This ViewController +// removes the StatusBar to make the screenshot deterministic. +@interface NoStatusBarViewController : UIViewController + +@end + +@interface FlutterEngine () +@property(nonatomic, strong) FlutterMethodChannel* statusBarChannel; +@end + +@implementation NoStatusBarViewController +- (BOOL)prefersStatusBarHidden { + return YES; +} +@end + +// The FlutterViewController version of NoStatusBarViewController +@interface NoStatusBarFlutterViewController : FlutterViewController + +@end + +@implementation NoStatusBarFlutterViewController +- (BOOL)prefersStatusBarHidden { + return YES; +} +@end + +@implementation SceneDelegate + ++ (UIWindow*)mainWindowOfFirstConnectedScene { + for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) { + if ([scene.delegate isKindOfClass:[SceneDelegate class]]) { + return ((SceneDelegate*)scene.delegate).window; + } + } + return nil; +} + +- (void)scene:(UIScene*)scene + willConnectToSession:(UISceneSession*)session + options:(UISceneConnectionOptions*)connectionOptions { + UIWindowScene* windowScene = (UIWindowScene*)scene; + self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; + + NSArray* processArguments = NSProcessInfo.processInfo.arguments; + if ([processArguments containsObject:@"--maskview-blocking"]) { + self.window.tintColor = UIColor.systemPinkColor; + } + NSDictionary* launchArgsMap = @{ + // The golden test args should match `GoldenTestManager`. + @"--locale-initialization" : @"locale_initialization", + @"--platform-view" : @"platform_view", + @"--platform-view-no-overlay-intersection" : @"platform_view_no_overlay_intersection", + @"--platform-view-two-intersecting-overlays" : @"platform_view_two_intersecting_overlays", + @"--platform-view-partial-intersection" : @"platform_view_partial_intersection", + @"--platform-view-one-overlay-two-intersecting-overlays" : + @"platform_view_one_overlay_two_intersecting_overlays", + @"--platform-view-multiple-without-overlays" : @"platform_view_multiple_without_overlays", + @"--platform-view-max-overlays" : @"platform_view_max_overlays", + @"--platform-view-surrounding-layers-fractional-coordinate" : + @"platform_view_surrounding_layers_fractional_coordinate", + @"--platform-view-partial-intersection-fractional-coordinate" : + @"platform_view_partial_intersection_fractional_coordinate", + @"--platform-view-multiple" : @"platform_view_multiple", + @"--platform-view-multiple-background-foreground" : + @"platform_view_multiple_background_foreground", + @"--platform-view-cliprect" : @"platform_view_cliprect", + @"--platform-view-cliprect-multiple-clips" : @"platform_view_cliprect_multiple_clips", + @"--platform-view-cliprrect" : @"platform_view_cliprrect", + @"--platform-view-cliprrect-multiple-clips" : @"platform_view_cliprrect_multiple_clips", + @"--platform-view-large-cliprrect" : @"platform_view_large_cliprrect", + @"--platform-view-large-cliprrect-multiple-clips" : + @"platform_view_large_cliprrect_multiple_clips", + @"--platform-view-clippath" : @"platform_view_clippath", + @"--platform-view-clippath-multiple-clips" : @"platform_view_clippath_multiple_clips", + @"--platform-view-cliprrect-with-transform" : @"platform_view_cliprrect_with_transform", + @"--platform-view-cliprrect-with-transform-multiple-clips" : + @"platform_view_cliprrect_with_transform_multiple_clips", + @"--platform-view-large-cliprrect-with-transform" : + @"platform_view_large_cliprrect_with_transform", + @"--platform-view-large-cliprrect-with-transform-multiple-clips" : + @"platform_view_large_cliprrect_with_transform_multiple_clips", + @"--platform-view-cliprect-with-transform" : @"platform_view_cliprect_with_transform", + @"--platform-view-cliprect-with-transform-multiple-clips" : + @"platform_view_cliprect_with_transform_multiple_clips", + @"--platform-view-clippath-with-transform" : @"platform_view_clippath_with_transform", + @"--platform-view-clippath-with-transform-multiple-clips" : + @"platform_view_clippath_with_transform_multiple_clips", + @"--platform-view-transform" : @"platform_view_transform", + @"--platform-view-opacity" : @"platform_view_opacity", + @"--platform-view-with-other-backdrop-filter" : @"platform_view_with_other_backdrop_filter", + @"--two-platform-views-with-other-backdrop-filter" : + @"two_platform_views_with_other_backdrop_filter", + @"--platform-view-with-negative-backdrop-filter" : + @"platform_view_with_negative_backdrop_filter", + @"--platform-view-rotate" : @"platform_view_rotate", + @"--non-full-screen-flutter-view-platform-view" : @"non_full_screen_flutter_view_platform_view", + @"--gesture-reject-after-touches-ended" : @"platform_view_gesture_reject_after_touches_ended", + @"--gesture-reject-eager" : @"platform_view_gesture_reject_eager", + @"--gesture-accept" : @"platform_view_gesture_accept", + @"--gesture-accept-with-overlapping-platform-views" : + @"platform_view_gesture_accept_with_overlapping_platform_views", + @"--tap-status-bar" : @"tap_status_bar", + @"--animated-color-square" : @"animated_color_square", + @"--solid-blue" : @"solid_blue", + @"--platform-view-with-continuous-texture" : @"platform_view_with_continuous_texture", + @"--bogus-font-text" : @"bogus_font_text", + @"--spawn-engine-works" : @"spawn_engine_works", + @"--pointer-events" : @"pointer_events", + @"--platform-view-scrolling-under-widget" : @"platform_view_scrolling_under_widget", + @"--platform-views-with-clips-scrolling" : @"platform_views_with_clips_scrolling", + @"--platform-views-with-clips-scrolling-multiple-clips" : + @"platform_views_with_clips_scrolling_multiple_clips", + @"--platform-view-cliprect-after-moved" : @"platform_view_cliprect_after_moved", + @"--platform-view-cliprect-after-moved-multiple-clips" : + @"platform_view_cliprect_after_moved_multiple_clips", + @"--two-platform-view-clip-rect" : @"two_platform_view_clip_rect", + @"--two-platform-view-clip-rect-multiple-clips" : @"two_platform_view_clip_rect_multiple_clips", + @"--two-platform-view-clip-rrect" : @"two_platform_view_clip_rrect", + @"--two-platform-view-clip-rrect-multiple-clips" : + @"two_platform_view_clip_rrect_multiple_clips", + @"--two-platform-view-clip-path" : @"two_platform_view_clip_path", + @"--two-platform-view-clip-path-multiple-clips" : @"two_platform_view_clip_path_multiple_clips", + @"--darwin-system-font" : @"darwin_system_font", + }; + __block NSString* flutterViewControllerTestName = nil; + [launchArgsMap + enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL* stop) { + if ([processArguments containsObject:argument]) { + flutterViewControllerTestName = testName; + *stop = YES; + } + }]; + if (flutterViewControllerTestName) { + [self setupFlutterViewControllerTest:flutterViewControllerTestName]; + } else if ([processArguments containsObject:@"--screen-before-flutter"]) { + self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; + } else { + self.window.rootViewController = [[UIViewController alloc] init]; + } + + [self.window makeKeyAndVisible]; + + [super scene:scene willConnectToSession:session options:connectionOptions]; +} + +- (FlutterEngine*)engineForTest:(NSString*)scenarioIdentifier { + if ([scenarioIdentifier isEqualToString:@"spawn_engine_works"]) { + FlutterEngine* spawner = [[FlutterEngine alloc] initWithName:@"FlutterControllerTest" + project:nil]; + [spawner run]; + return [spawner spawnWithEntrypoint:nil libraryURI:nil initialRoute:nil entrypointArgs:nil]; + } else { + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"FlutterControllerTest" + project:nil]; + [engine run]; + return engine; + } +} + +- (FlutterViewController*)flutterViewControllerForTest:(NSString*)scenarioIdentifier + withEngine:(FlutterEngine*)engine { + if ([scenarioIdentifier isEqualToString:@"tap_status_bar"]) { + return [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + } else { + return [[NoStatusBarFlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + } +} + +- (void)setupFlutterViewControllerTest:(NSString*)scenarioIdentifier { + FlutterEngine* engine = [self engineForTest:scenarioIdentifier]; + FlutterViewController* flutterViewController = + [self flutterViewControllerForTest:scenarioIdentifier withEngine:engine]; + flutterViewController.view.accessibilityIdentifier = @"flutter_view"; + + [engine.binaryMessenger + setMessageHandlerOnChannel:@"waiting_for_status" + binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"driver" + binaryMessenger:engine.binaryMessenger + codec:[FlutterJSONMethodCodec sharedInstance]]; + [channel invokeMethod:@"set_scenario" arguments:@{@"name" : scenarioIdentifier}]; + }]; + // Can be used to synchronize timing in the test for a signal from Dart. + [engine.binaryMessenger + setMessageHandlerOnChannel:@"display_data" + binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { + NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message + options:0 + error:nil]; + UITextField* text = [[UITextField alloc] initWithFrame:CGRectMake(0, 400, 300, 100)]; + text.text = dict[@"data"]; + [flutterViewController.view addSubview:text]; + }]; + + TextPlatformViewFactory* textPlatformViewFactory = + [[TextPlatformViewFactory alloc] initWithMessenger:engine.binaryMessenger]; + NSObject* registrar = + [engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; + [registrar registerViewFactory:textPlatformViewFactory + withId:@"scenarios/textPlatformView" + gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; + [registrar registerViewFactory:textPlatformViewFactory + withId:@"scenarios/textPlatformView_blockPolicyUntilTouchesEnded" + gestureRecognizersBlockingPolicy: + FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded]; + + UIViewController* rootViewController = flutterViewController; + if ([scenarioIdentifier isEqualToString:@"non_full_screen_flutter_view_platform_view"]) { + // Make Flutter View's origin x/y not 0. + rootViewController = [[NoStatusBarViewController alloc] init]; + [rootViewController.view addSubview:flutterViewController.view]; + flutterViewController.view.frame = CGRectMake(150, 150, 500, 500); + } else if ([scenarioIdentifier isEqualToString:@"tap_status_bar"]) { + [engine.binaryMessenger + setMessageHandlerOnChannel:@"flutter/status_bar" + binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { + NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message + options:0 + error:nil]; + FlutterBasicMessageChannel* channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"display_data" + binaryMessenger:engine.binaryMessenger + codec:[FlutterJSONMessageCodec sharedInstance]]; + [channel sendMessage:@{@"data" : dict}]; + UITextField* text = + [[UITextField alloc] initWithFrame:CGRectMake(0, 400, 300, 100)]; + text.text = dict[@"method"]; + [flutterViewController.view addSubview:text]; + }]; + } + + self.window.rootViewController = rootViewController; +} + +@end diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m index b6ca5a9d9eae2..86b44e1cbe3f4 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m @@ -5,6 +5,7 @@ #import #import +#import "SceneDelegate.h" #import "ScreenBeforeFlutter.h" FLUTTER_ASSERT_ARC @@ -71,8 +72,9 @@ - (void)skip_testDismissedFlutterViewControllerNotRespondingToApplicationLifecyc }]; [self waitForExpectationsWithTimeout:5 handler:nil]; - UIApplication* application = UIApplication.sharedApplication; - application.delegate.window.rootViewController = rootVC; + UIWindow* window = SceneDelegate.mainWindowOfFirstConnectedScene; + XCTAssertNotNil(window, @"The host app must have a connected scene for test"); + window.rootViewController = rootVC; FlutterEngine* engine = rootVC.engine; NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10]; @@ -208,8 +210,9 @@ - (void)skip_testFlutterViewControllerDetachingSendsApplicationLifecycle { [self waitForExpectationsWithTimeout:5 handler:nil]; - UIApplication* application = UIApplication.sharedApplication; - application.delegate.window.rootViewController = rootVC; + UIWindow* window = SceneDelegate.mainWindowOfFirstConnectedScene; + XCTAssertNotNil(window, @"The host app must have a connected scene for test"); + window.rootViewController = rootVC; FlutterEngine* engine = rootVC.engine; NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10]; diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m index 50f70f2a4635a..0de37005dd092 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m @@ -5,7 +5,7 @@ #import #import -#import "AppDelegate.h" +#import "SceneDelegate.h" FLUTTER_ASSERT_ARC @@ -72,8 +72,9 @@ - (void)testSettingInitialRoute { } }]; - AppDelegate* appDelegate = (AppDelegate*)UIApplication.sharedApplication.delegate; - UIViewController* rootVC = appDelegate.window.rootViewController; + UIWindow* window = SceneDelegate.mainWindowOfFirstConnectedScene; + XCTAssertNotNil(window, @"The host app must have a connected scene for test"); + UIViewController* rootVC = window.rootViewController; [rootVC presentViewController:self.flutterViewController animated:NO completion:nil]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m index 339e0d7c002b8..2040d54ef7a4b 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m @@ -5,7 +5,7 @@ #import #import -#import "AppDelegate.h" +#import "SceneDelegate.h" FLUTTER_ASSERT_ARC @@ -53,8 +53,9 @@ - (void)testFirstFrameCallback { [firstFrameRendered fulfill]; }]; - AppDelegate* appDelegate = (AppDelegate*)UIApplication.sharedApplication.delegate; - UIViewController* rootVC = appDelegate.window.rootViewController; + UIWindow* window = SceneDelegate.mainWindowOfFirstConnectedScene; + XCTAssertNotNil(window, @"The host app must have a connected scene for test"); + UIViewController* rootVC = window.rootViewController; [rootVC presentViewController:self.flutterViewController animated:NO completion:nil]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; @@ -86,8 +87,9 @@ - (void)testDrawLayer { [firstFrameRendered fulfill]; }]; - AppDelegate* appDelegate = (AppDelegate*)UIApplication.sharedApplication.delegate; - UIViewController* rootVC = appDelegate.window.rootViewController; + UIWindow* window = SceneDelegate.mainWindowOfFirstConnectedScene; + XCTAssertNotNil(window, @"The host app must have a connected scene for test"); + UIViewController* rootVC = window.rootViewController; [rootVC presentViewController:self.flutterViewController animated:NO completion:nil]; CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB(); @@ -96,7 +98,7 @@ - (void)testDrawLayer { CGContextRef context = CGBitmapContextCreate(nil, width, width, 8, 4 * width, color_space, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); - [appDelegate.window.layer renderInContext:context]; + [window.layer renderInContext:context]; uint32_t* image_data = (uint32_t*)CGBitmapContextGetData(context); if (image_data[20] == 0xFF0000FF) { [imageRendered fulfill]; From b710a6e0290f7cb9c3ad55666c5f8573aed2ee8f Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 7 Aug 2026 08:37:10 -0400 Subject: [PATCH 122/330] Roll Fuchsia Linux SDK from _J8wM3kyQpLN9wvRD... to zGBigY0YYrKHxPKN-... (#190709) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/fuchsia-linux-sdk-flutter Please CC codefu@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index cdfd12bf41c74..5ca275d23bd12 100644 --- a/DEPS +++ b/DEPS @@ -830,7 +830,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': '_J8wM3kyQpLN9wvRD5upBr9L1g5TCYF3Oc8P0m3QeZMC' + 'version': 'zGBigY0YYrKHxPKN-zAHWAJgqEUOHjPyJc3aZ8gSjT8C' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From 7ccae91ac1f6fd6c873f1104eefc8b823bd147a7 Mon Sep 17 00:00:00 2001 From: Bruno Corona Date: Fri, 7 Aug 2026 08:38:38 -0600 Subject: [PATCH 123/330] [web] Fix null-check crash when a platform view is disposed mid-frame (#190071) ## Description On Flutter Web, `PlatformViewEmbedder._getElement` force-unwrapped the clip chain for a platform view (`_viewClipChains[viewId]!.root`). If a view is disposed while still referenced by the frame being submitted (seen under memory pressure / hidden tab), the `!` threw `Null check operator used on a null value` and crashed the frame. This change makes the composition tolerate a missing clip chain: the view is skipped instead of crashing, consistent with the existing invalid-view handling in `submitFrame`. A regression test covers the dispose-mid-frame case. Note: the second crash reported in the issue (`_PlatformViewPlaceholderBox.performLayout`) is already fixed on master, so this PR only addresses the engine crash. ## Fixes Fixes flutter/flutter#190017 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/AI-contributions.md [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/wiki/Data-driven-Fixes [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Discord]: https://github.com/flutter/flutter/wiki/Chat Co-authored-by: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Co-authored-by: zhongliugo Co-authored-by: Mouad Debbar --- .../src/engine/platform_views/embedder.dart | 45 ++++++++++++++-- .../web_ui/test/ui/platform_view_test.dart | 53 +++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart index dddbb02efac5e..80ba7d30a11e8 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart @@ -517,6 +517,10 @@ class PlatformViewEmbedder { canvas.displayCanvas!.setIsOverlay(needsOverlayPositioning); } + // Platform views which were disposed while they were still being + // composited. They are left out of the DOM. + final missingViewIds = {}; + // At this point, the DOM contains the static elements and the elements from // the previous composition which need to move. We iterate over the static // elements and insert the elements which come before them into the DOM. @@ -524,16 +528,27 @@ class PlatformViewEmbedder { var nextCompositionIndex = 0; while (staticElementIndex < staticElements.length) { final int staticElementIndexInActiveComposition = staticElements[staticElementIndex]; - final DomElement staticDomElement = _getElement( + final DomElement? staticDomElement = _getElement( _activeComposition.entities[staticElementIndexInActiveComposition], ); + if (staticDomElement == null) { + // This view is not in the DOM, so it can't position other elements. + // Use the next static element instead. + staticElementIndex++; + continue; + } // Go through next composition elements until we reach the static element. while (indexMap[nextCompositionIndex] != staticElementIndexInActiveComposition) { final CompositionEntity nextEntity = composition.entities[nextCompositionIndex]; if (nextEntity is CompositionCanvas) { updateCompositionCanvasWithDisplay(nextEntity, nextCompositionIndex); } - sceneHost.insertBefore(_getElement(nextEntity), staticDomElement); + final DomElement? nextDomElement = _getElement(nextEntity); + if (nextDomElement != null) { + sceneHost.insertBefore(nextDomElement, staticDomElement); + } else if (nextEntity is CompositionPlatformView) { + missingViewIds.add(nextEntity.viewId); + } nextCompositionIndex++; } if (composition.entities[nextCompositionIndex] is CompositionCanvas) { @@ -554,15 +569,35 @@ class PlatformViewEmbedder { if (nextEntity is CompositionCanvas) { updateCompositionCanvasWithDisplay(nextEntity, nextCompositionIndex); } - sceneHost.append(_getElement(nextEntity)); + final DomElement? nextDomElement = _getElement(nextEntity); + if (nextDomElement != null) { + sceneHost.append(nextDomElement); + } else if (nextEntity is CompositionPlatformView) { + missingViewIds.add(nextEntity.viewId); + } nextCompositionIndex++; } + + if (missingViewIds.isNotEmpty) { + // Drop the skipped views, so the next frame doesn't expect them to be + // in the DOM. + composition.entities.removeWhere( + (CompositionEntity entity) => + entity is CompositionPlatformView && missingViewIds.contains(entity.viewId), + ); + printWarning( + 'Cannot render platform views: ${missingViewIds.join(', ')}. ' + 'These views were disposed while they were being composited.', + ); + } } - DomElement _getElement(CompositionEntity entity) { + /// Returns the DOM element for [entity], or null if the platform view no + /// longer has a clip chain because it was disposed. + DomElement? _getElement(CompositionEntity entity) { return switch (entity) { CompositionCanvas() => entity.displayCanvas!.hostElement, - CompositionPlatformView() => _viewClipChains[entity.viewId]!.root, + CompositionPlatformView() => _viewClipChains[entity.viewId]?.root, }; } diff --git a/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart b/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart index 518902f38ced1..00de1727c6845 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart @@ -824,6 +824,59 @@ Future testMain() async { expect(platformViewsHost.querySelector('flt-platform-view'), isNull); }); + test('does not crash when a platform view is disposed mid-frame', () async { + await createPlatformView(0, platformViewType); + await createPlatformView(1, platformViewType); + + final sb = ui.SceneBuilder() + ..pushOffset(0, 0) + ..addPlatformView(0, width: 10, height: 10) + ..addPlatformView(1, width: 10, height: 10) + ..pop(); + await renderScene(sb.build()); + _expectSceneMatches(<_EmbeddedViewMarker>[_platformView, _platformView]); + + // Build the next frame by hand so that a platform view can be disposed + // after the composition was created, but before the frame is submitted. + final ViewRasterizer rasterizer = renderer.rasterizers[implicitView.viewId]!; + final PlatformViewEmbedder embedder = rasterizer.viewEmbedder; + final rootLayer = RootLayer(); + // The views are composited in the opposite order, so the DOM needs to be + // updated for this composition. + rootLayer.children.add(PlatformViewLayer(1, ui.Offset.zero, 10, 10)); + rootLayer.children.add(PlatformViewLayer(0, ui.Offset.zero, 10, 10)); + + embedder.frameSize = rasterizer.currentFrameSize; + final Frame frame = rasterizer.context.acquireFrame(embedder); + frame.raster(LayerTree(rootLayer), rasterizer.currentFrameSize, null); + + // View 0 is disposed while it is still part of the composition which is + // about to be submitted. + embedder.disposeView(0); + + final warnings = []; + final void Function(String) originalPrintWarning = printWarning; + printWarning = (String warning) => warnings.add(warning); + try { + await expectLater(embedder.submitFrame(null), completes); + } finally { + printWarning = originalPrintWarning; + } + + // The disposed view is left out of the composition. + _expectSceneMatches(<_EmbeddedViewMarker>[_platformView]); + expect(embedder.debugActiveComposition.entities, hasLength(1)); + expect( + warnings, + contains( + contains( + 'Cannot render platform views: 0. ' + 'These views were disposed while they were being composited.', + ), + ), + ); + }); + test('preserves the DOM node of an unrendered platform view', () async { await createPlatformView(1, platformViewType); From 8425ba6d0dc7037fedb8c923f2366ec95aee201a Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 7 Aug 2026 11:43:57 -0400 Subject: [PATCH 124/330] Roll Dart SDK from 941e2ec4ff23 to 3928c443a387 (2 revisions) (#190715) https://dart.googlesource.com/sdk.git/+log/941e2ec4ff23..3928c443a387 2026-08-07 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-106.0.dev 2026-08-07 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-105.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC codefu@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/DEPS b/DEPS index 5ca275d23bd12..903a5f2267d88 100644 --- a/DEPS +++ b/DEPS @@ -55,24 +55,24 @@ 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': '941e2ec4ff23b3c8f4292ddf802a91c026937195', + 'dart_revision': '3928c443a387af72255c2bb324e989f36f4d1ec1', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py 'dart_binaryen_rev': '9926156a583cec3d22d521232b31c70fa9a87dc1', 'dart_boringssl_rev': '7515be5ccd601ae0433d3682ba7737592b402014', - 'dart_core_rev': 'fe516ee1b38cc60e7a8c6e082c337037a043d782', + 'dart_core_rev': '4a5ae2bc9db1f39fac071f1a6fade64bd155f734', 'dart_devtools_rev': '21f1838f3a9b138ac377efb953ca5a53c8832e75', - 'dart_ecosystem_rev': 'edfdb3b4063b9034b708144633a700204f865f43', - 'dart_http_rev': '5d94ef52582867e077bf41c3fa20fb8b1d1d834e', + 'dart_ecosystem_rev': 'ed9c592c1d35106c0a8a52044426515017a60646', + 'dart_http_rev': '9cb80c77562569705dd2089839a5526c5318c321', 'dart_i18n_rev': 'e1b5a798f8922bb27bbc6d858748ece6f9a19f02', 'dart_perfetto_rev': '13ce0c9e13b0940d2476cd0cff2301708a9a2e2b', 'dart_protobuf_rev': '91efb90f437bb6a30e6726c3369a2fcb9bba06e7', 'dart_pub_rev': '7654d523a42e764fad77c9e7b63a9686b88c9323', 'dart_sync_http_rev': '6666fff944221891182e1f80bf56569338164d72', - 'dart_tools_rev': 'b827a6e38b934232c7f7b8728a5aef6165e7df2d', + 'dart_tools_rev': 'f3ec9ed5a7b25dfef256bea9827dc1f81c8c7afc', 'dart_vector_math_rev': 'cf3b5db7340d317dd3489e5a35434b408020a852', - 'dart_web_rev': '12a9ca2ebc08f5a6f2d69aebc7daa1f5a2e6a431', + 'dart_web_rev': 'f1b9d561850355efd81b24fdbfd226dbe805d415', 'dart_webdriver_rev': '3a711ebb36871eac997c5d5d2429f7414873dc63', 'dart_webkit_inspection_protocol_rev': '762115a971d1968bc940454ad1e88d506d8c5640', @@ -339,7 +339,7 @@ deps = { Var('dart_git') + '/dart_style.git@dfdf6420c7ea923d28edef3f11e89b4ff23d03bf', 'engine/src/flutter/third_party/dart/third_party/pkg/dartdoc': - Var('dart_git') + '/dartdoc.git@ac96918074974dcd4ea20f764f17090f58c1e428', + Var('dart_git') + '/dartdoc.git@1fd02f73cfb2ade871f9ff9b96e7475fb81d5ce8', 'engine/src/flutter/third_party/dart/third_party/pkg/ecosystem': Var('dart_git') + '/ecosystem.git' + '@' + Var('dart_ecosystem_rev'), @@ -363,7 +363,7 @@ deps = { Var('dart_git') + '/pub.git' + '@' + Var('dart_pub_rev'), 'engine/src/flutter/third_party/dart/third_party/pkg/shelf': - Var('dart_git') + '/shelf.git@6918a7690946044b4098e9f6735439044c676e13', + Var('dart_git') + '/shelf.git@fb3f931d2c158d794e83c1b76b7be4b625db3c28', 'engine/src/flutter/third_party/dart/third_party/pkg/sync_http': Var('dart_git') + '/sync_http.git' + '@' + Var('dart_sync_http_rev'), @@ -372,7 +372,7 @@ deps = { Var('dart_git') + '/external/github.com/simolus3/tar.git@13479f7c2a18f499e840ad470cfcca8c579f6909', 'engine/src/flutter/third_party/dart/third_party/pkg/test': - Var('dart_git') + '/test.git@4838365e7fb3a2d0302cdbbd8330b7884f7d3c0d', + Var('dart_git') + '/test.git@dd426d439da9d399975a3455d4d507bce9fec01b', 'engine/src/flutter/third_party/dart/third_party/pkg/tools': Var('dart_git') + '/tools.git' + '@' + Var('dart_tools_rev'), From b9bc0122df927fb090ddb0f8db15843a2f7c59c1 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:18:59 -0700 Subject: [PATCH 125/330] Adds Impeller availability to the impeller README.md (#190671) source: https://docs.google.com/spreadsheets/d/1AebMvprRkxP-D6ndx920lbvDBbhg-sNNRJ64XY2P2t0/edit?gid=0#gid=0 We are going to deprecate that sheets doc and rely on this markdown section. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- engine/src/flutter/impeller/README.md | 65 +++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/engine/src/flutter/impeller/README.md b/engine/src/flutter/impeller/README.md index 28e70196b1ab5..2053f1d4c2ef6 100644 --- a/engine/src/flutter/impeller/README.md +++ b/engine/src/flutter/impeller/README.md @@ -171,6 +171,40 @@ flowchart TD Impeller is available under the `--enable-impeller` flag on iOS, Android, and macOS Desktop. This flag can be specified to `flutter run`. +### Availability Matrix + +> [!NOTE] +> This assumes availability in stable releases. The Embedder API statuses are tricky. + +| Releases | iOS | Android | Embedder API (Metal) | Embedder API (Vulkan) | Embedder API (OpenGL) | macOS | Windows | Linux | Web | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **main** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | ✅ | ✅ | ✅ | 🚫 | +| **3.47** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | ✅ | ✅ | ✅ | 🚫 | +| **3.44** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.41** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.38** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.35** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.32** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.29** | ⭐ | ✅ | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.27** | ✅ | ✅ | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.24** | ✅ | 🧪 | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.22** | ✅ | 🧪 | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.19** | ✅ | 🧪 | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.16** | ✅ | 🧪 | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.13** | ✅ | 🚫 | 🧪 | 🚧 | 🚧 | 🧪 | 🚧 | 🚧 | 🚫 | +| **3.10** | ✅ | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | +| **3.70** | 🧪 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | +| **3.30** | 🧪 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | +| **3.00** | 🧪 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | +| **2.10** | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | + +#### Key +- **⭐ Exclusive** — Only Impeller is available. +- **✅ Default** — Impeller is the default option, Skia is available. +- **🧪 Preview** — Can use Impeller with flags/manifest options. But, Skia is the default with no action. +- **🚧 Experimental** — Skia is the default. Impeller may or may not work. The team is not actively working on this and doesn't recommend using it. +- **🚫 Unavailable** — Only Skia is available. + If the application needs to be launched with Impeller enabled without using the Flutter tool, follow the platform specific steps below. @@ -210,15 +244,37 @@ to your `AndroidManifest.xml` file under the `` tag: android:value="opengles" /> ``` -### macOS Desktop +### macOS -Impeller is in preview on macOS Desktop. +Impeller is the **default** on macOS. -To your `Info.plist` file, add under the top-level `` tag: +To explicitly opt out of using Impeller, add the following to your `Info.plist` file under the top-level `` tag: ```xml FLTEnableImpeller - + +``` + +### Linux + +Impeller is the **default** on Linux. + +To disable Impeller on Linux when deploying your app, add the following setup to +your project in `linux/runner/my_application.cc`. + +```c +fl_dart_project_set_enable_impeller(project, FALSE); +``` + +### Windows + +Impeller is the **default** on Windows. + +To disable Impeller on Windows when deploying your app, add the following setup to +your project in `windows\runner\main.cpp`. + +```c++ +project.set_impeller_switch(flutter::ImpellerSwitch::Disabled); ``` ### Custom Embedders @@ -263,4 +319,5 @@ examples, are available](toolkit/interop/README.md). * [Android CPU Profiling](/docs/engine/impeller/docs/android_cpu_profile.md) * [Android Rendering Backend Selection](/docs/engine/impeller/docs/android.md) * [Using Impeller as a Standalone Rendering Library (with OpenGL ES)](/docs/engine/impeller/docs/standalone_gles.md) +* [Impeller Availability Matrix](#availability-matrix) * [Glossary](/docs/engine/impeller/docs/glossary.md) From 80234e1bc79499a7f8d0d072e8ab5c9b56a7e6de Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Sat, 8 Aug 2026 01:19:23 +0900 Subject: [PATCH 126/330] tools: Support --host-arch option in flutter precache (#190480) Adds a `--host-arch=` option to `flutter precache` and adds support for the `FLUTTER_HOST_ARCH` environment variable in `OperatingSystemUtils.hostPlatform`. When specified, this overrides the default hardware detection check (`sysctl hw.optional.arm64` on macOS), and causes `flutter precache` to download host engine artifacts for the specified architecture instead. This is required to allow arm64 macOS CI hosts to download and cache x64 host engine artifacts when cross-packaging x64 Flutter SDK release archives on an arm64 host in the `packaging/packaging` recipe in `packaging.py`. See: https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/packaging/packaging.py Issue: https://github.com/flutter/flutter/issues/189144 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter_tools/lib/src/base/os.dart | 53 ++++++++++++- packages/flutter_tools/lib/src/cache.dart | 1 + .../lib/src/commands/precache.dart | 20 +++++ .../hermetic/precache_test.dart | 16 ++++ .../test/general.shard/base/os_test.dart | 78 +++++++++++++++++++ packages/flutter_tools/test/src/fakes.dart | 3 + 6 files changed, 170 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart index 15bb4cda19027..190fae8883999 100644 --- a/packages/flutter_tools/lib/src/base/os.dart +++ b/packages/flutter_tools/lib/src/base/os.dart @@ -143,8 +143,40 @@ abstract class OperatingSystemUtils { return osNames[osName] ?? osName; } + /// Optional override for the host platform architecture. + HostPlatform? hostPlatformOverride; + /// Represents the platform of the host machine running the Flutter tool. + /// + /// The architecture may be overridden, in precedence order, by the + /// [hostPlatformOverride] property or the `FLUTTER_HOST_ARCH` environment + /// variable. When neither is set, or the environment variable does not match + /// an architecture supported on the current OS, the host platform is + /// determined by [defaultHostPlatform], which subclasses may override to + /// probe the hardware directly. HostPlatform get hostPlatform { + if (hostPlatformOverride case final HostPlatform override) { + return override; + } + if (_platform.environment['FLUTTER_HOST_ARCH'] case final String overrideArch) { + final HostPlatform? overridePlatform = HostPlatform.fromOsAndArch( + _platform.operatingSystem, + overrideArch, + ); + if (overridePlatform != null) { + return overridePlatform; + } + } + return defaultHostPlatform; + } + + /// The host platform detected when no architecture override is in effect. + /// + /// Defaults to the architecture the tool was compiled for. Subclasses may + /// override this to probe the underlying hardware (for example, to see + /// through Rosetta translation on macOS). + @protected + HostPlatform get defaultHostPlatform { return switch (_currentAbi) { Abi.macosX64 => HostPlatform.darwin_x64, Abi.macosArm64 => HostPlatform.darwin_arm64, @@ -365,7 +397,7 @@ class _MacOSUtils extends _PosixUtils { HostPlatform? _hostPlatform; @override - HostPlatform get hostPlatform { + HostPlatform get defaultHostPlatform { if (_hostPlatform == null) { String? sysctlPath; if (which('sysctl') == null) { @@ -612,6 +644,25 @@ enum HostPlatform { final String cliName; final String platformName; + + /// Returns the host platform for the specified OS and architecture. + /// + /// [os] is an operating system name as returned by + /// [Platform.operatingSystem]. [arch] is an architecture name matching the + /// [platformName] of one of the values of this enum. Returns null if no match + /// is found. + static HostPlatform? fromOsAndArch(String os, String arch) { + return switch ((os, arch.toLowerCase())) { + ('macos', 'x64') => darwin_x64, + ('macos', 'arm64') => darwin_arm64, + ('linux', 'x64') => linux_x64, + ('linux', 'arm64') => linux_arm64, + ('linux', 'riscv64') => linux_riscv64, + ('windows', 'x64') => windows_x64, + ('windows', 'arm64') => windows_arm64, + _ => null, + }; + } } // flutter_ignore: deprecation_syntax (see analyze.dart) diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index ec17a559048ae..5b5cbc44a5df6 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -217,6 +217,7 @@ class Cache { final Platform _platform; final FileSystem _fileSystem; final OperatingSystemUtils _osUtils; + OperatingSystemUtils get osUtils => _osUtils; final Directory? _rootOverride; final List _artifacts; final Stdio? _stdio; diff --git a/packages/flutter_tools/lib/src/commands/precache.dart b/packages/flutter_tools/lib/src/commands/precache.dart index 438836e8051ca..5f0c727b2de8d 100644 --- a/packages/flutter_tools/lib/src/commands/precache.dart +++ b/packages/flutter_tools/lib/src/commands/precache.dart @@ -4,6 +4,7 @@ import '../base/common.dart'; import '../base/logger.dart'; +import '../base/os.dart'; import '../base/platform.dart'; import '../cache.dart'; import '../features.dart'; @@ -83,6 +84,12 @@ class PrecacheCommand extends FlutterCommand { help: 'Precache the unsigned macOS binaries when available.', hide: !verboseHelp, ); + argParser.addOption( + 'host-arch', + allowed: const ['x64', 'arm64'], + help: 'Override the architecture of host artifacts to precache.', + hide: !verboseHelp, + ); } final Cache _cache; @@ -171,6 +178,19 @@ class PrecacheCommand extends FlutterCommand { if (boolArg('use-unsigned-mac-binaries')) { _cache.useUnsignedMacBinaries = true; } + final String? hostArch = stringArg('host-arch'); + if (hostArch != null) { + final HostPlatform? overridePlatform = HostPlatform.fromOsAndArch( + _platform.operatingSystem, + hostArch, + ); + if (overridePlatform == null) { + throwToolExit( + 'Unsupported host architecture "$hostArch" for OS "${_platform.operatingSystem}"', + ); + } + _cache.osUtils.hostPlatformOverride = overridePlatform; + } final Set explicitlyEnabled = _explicitArtifactSelections(); _cache.platformOverrideArtifacts = explicitlyEnabled; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart index 4aeb9ae3fd6b4..704439775113d 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/precache.dart'; @@ -504,6 +505,18 @@ void main() { }), ); }); + + testUsingContext('precache --host-arch overrides cache hostPlatformOverride', () async { + final command = PrecacheCommand( + cache: cache, + logger: BufferLogger.test(), + featureFlags: TestFeatureFlags(), + platform: FakePlatform(operatingSystem: 'macos', environment: {}), + ); + await createTestCommandRunner(command).run(const ['precache', '--host-arch=x64']); + + expect(cache.osUtils.hostPlatformOverride, HostPlatform.darwin_x64); + }); } class FakeCache extends Fake implements Cache { @@ -540,4 +553,7 @@ class FakeCache extends Fake implements Cache { @override bool includeAllPlatforms = false; + + @override + late final OperatingSystemUtils osUtils = FakeOperatingSystemUtils(); } diff --git a/packages/flutter_tools/test/general.shard/base/os_test.dart b/packages/flutter_tools/test/general.shard/base/os_test.dart index e105c8d183a3e..c7ebac9742960 100644 --- a/packages/flutter_tools/test/general.shard/base/os_test.dart +++ b/packages/flutter_tools/test/general.shard/base/os_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:ffi' show Abi; + import 'package:archive/archive.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -175,6 +176,61 @@ void main() { expect(utils.hostPlatform, HostPlatform.darwin_arm64); }); + testWithoutContext('macOS ARM64 with FLUTTER_HOST_ARCH=x64 override', () async { + final OperatingSystemUtils utils = createOSUtils( + FakePlatform( + operatingSystem: 'macos', + environment: {'FLUTTER_HOST_ARCH': 'x64'}, + ), + currentAbi: Abi.macosArm64, + ); + expect(utils.hostPlatform, HostPlatform.darwin_x64); + }); + + testWithoutContext('macOS x64 with FLUTTER_HOST_ARCH=arm64 override', () async { + final OperatingSystemUtils utils = createOSUtils( + FakePlatform( + operatingSystem: 'macos', + environment: {'FLUTTER_HOST_ARCH': 'arm64'}, + ), + currentAbi: Abi.macosX64, + ); + expect(utils.hostPlatform, HostPlatform.darwin_arm64); + }); + + testWithoutContext('Linux x64 with FLUTTER_HOST_ARCH=arm64 override', () async { + final OperatingSystemUtils utils = createOSUtils( + FakePlatform(environment: {'FLUTTER_HOST_ARCH': 'arm64'}), + currentAbi: Abi.linuxX64, + ); + expect(utils.hostPlatform, HostPlatform.linux_arm64); + }); + + testWithoutContext('FLUTTER_HOST_ARCH override is case-insensitive', () async { + final OperatingSystemUtils utils = createOSUtils( + FakePlatform(environment: {'FLUTTER_HOST_ARCH': 'ARM64'}), + currentAbi: Abi.linuxX64, + ); + expect(utils.hostPlatform, HostPlatform.linux_arm64); + }); + + testWithoutContext('unrecognized FLUTTER_HOST_ARCH override is ignored', () async { + final OperatingSystemUtils utils = createOSUtils( + FakePlatform(environment: {'FLUTTER_HOST_ARCH': 'sparc'}), + currentAbi: Abi.linuxX64, + ); + expect(utils.hostPlatform, HostPlatform.linux_x64); + }); + + testWithoutContext('hostPlatformOverride property takes precedence', () async { + final OperatingSystemUtils utils = createOSUtils( + FakePlatform(operatingSystem: 'macos'), + currentAbi: Abi.macosArm64, + ); + utils.hostPlatformOverride = HostPlatform.darwin_x64; + expect(utils.hostPlatform, HostPlatform.darwin_x64); + }); + testWithoutContext('unsupported throws', () async { final OperatingSystemUtils utils = createOSUtils(FakePlatform(), currentAbi: Abi.androidArm); expect(() => utils.hostPlatform, throwsUnsupportedError); @@ -422,6 +478,28 @@ void main() { ); }); + group('HostPlatform.fromOsAndArch', () { + testWithoutContext('maps supported OS and architecture combinations', () { + expect(HostPlatform.fromOsAndArch('macos', 'x64'), HostPlatform.darwin_x64); + expect(HostPlatform.fromOsAndArch('macos', 'arm64'), HostPlatform.darwin_arm64); + expect(HostPlatform.fromOsAndArch('linux', 'x64'), HostPlatform.linux_x64); + expect(HostPlatform.fromOsAndArch('linux', 'arm64'), HostPlatform.linux_arm64); + expect(HostPlatform.fromOsAndArch('linux', 'riscv64'), HostPlatform.linux_riscv64); + expect(HostPlatform.fromOsAndArch('windows', 'x64'), HostPlatform.windows_x64); + expect(HostPlatform.fromOsAndArch('windows', 'arm64'), HostPlatform.windows_arm64); + }); + + testWithoutContext('matches the architecture case-insensitively', () { + expect(HostPlatform.fromOsAndArch('macos', 'ARM64'), HostPlatform.darwin_arm64); + }); + + testWithoutContext('returns null for unsupported combinations', () { + expect(HostPlatform.fromOsAndArch('macos', 'riscv64'), isNull); + expect(HostPlatform.fromOsAndArch('fuchsia', 'x64'), isNull); + expect(HostPlatform.fromOsAndArch('linux', 'sparc'), isNull); + }); + }); + testWithoutContext('If unzip fails, include stderr in exception text', () { const exceptionMessage = 'Something really bad happened.'; final handler = FileExceptionHandler(); diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 4f327ebbc48da..c63d284fe04f6 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -685,6 +685,9 @@ class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils { @override void makeExecutable(File file) {} + @override + HostPlatform? hostPlatformOverride; + @override HostPlatform hostPlatform = HostPlatform.linux_x64; From 4adc4692feb32c62a77ec11a13c7642f90dac2a0 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 7 Aug 2026 12:25:54 -0400 Subject: [PATCH 127/330] Roll Skia from ad7abeecbb6d to 5dddf11de22a (32 revisions) (#190717) Roll Skia from ad7abeecbb6d to 5dddf11de22a (32 revisions) https://skia.googlesource.com/skia.git/+log/ad7abeecbb6d..5dddf11de22a 2026-08-07 nathanasanchez@google.com Add SkCaptureCanvas support for onDrawMesh 2026-08-07 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 67a94c725fff to e8c6af24af68 (4 revisions) 2026-08-07 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from cd1628319cf9 to 1c9c16c9ad1c (9 revisions) 2026-08-07 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Skia Infra from 82486f250514 to 2c6128eaca86 (8 revisions) 2026-08-07 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from c88eebd234b6 to acdd3c9e9e9f 2026-08-07 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-07 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-06 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from f14e1f12e719 to 67a94c725fff (6 revisions) 2026-08-06 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-06 nathanasanchez@google.com [Graphite] Add primitive color space option to PaintParams 2026-08-06 nathanasanchez@google.com Fix GM_custommesh_cs to use correct canvas for drawMesh 2026-08-06 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-06 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-06 michaelludwig@google.com [graphite] Support kSwapRB xfer op w/o relying on raster pipeline 2026-08-06 sergiog@microsoft.com [rust jpeg] Initial implementation of rust jpeg 2026-08-06 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 27cc24c48167 to f14e1f12e719 (9 revisions) 2026-08-06 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Skia Infra from 9898b1962de7 to 82486f250514 (12 revisions) 2026-08-06 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from da44df7daa86 to cd1628319cf9 (13 revisions) 2026-08-06 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from 096004527aba to c88eebd234b6 2026-08-06 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-05 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Manual roll ANGLE from 013e8a1af187 to 7b4238a1ad6c (10 revisions) 2026-08-05 nathanasanchez@google.com [Graphite] Plumb RootNodesInfo through all RenderStep SkSL functions 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from bb34a3ef6ad4 to 27cc24c48167 (1 revision) 2026-08-05 michaelludwig@google.com [ganesh] Check result inside GrResourceProvider::writePixels 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Roll ANGLE from d5c8131b66e3 to 013e8a1af187 (6 revisions) 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Skia Infra from 45efa0244bed to 9898b1962de7 (14 revisions) 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from a488846c0f17 to da44df7daa86 (8 revisions) 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Roll SwiftShader from a7c547b55474 to 26e6a4b84daf (2 revisions) 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from 1923fa1074e1 to 096004527aba 2026-08-05 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-05 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from b2accf345f13 to bb34a3ef6ad4 (6 revisions) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC alexisdavidc@google.com,codefu@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: ... --- DEPS | 2 +- engine/src/flutter/sky/packages/sky_engine/LICENSE | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 903a5f2267d88..7860cfbcfe050 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'ad7abeecbb6ddc1ceaaec7cb987fc893ca7e9a62', + 'skia_revision': '5dddf11de22a1c185766102e0c132a8887b7f7da', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds diff --git a/engine/src/flutter/sky/packages/sky_engine/LICENSE b/engine/src/flutter/sky/packages/sky_engine/LICENSE index 07be947142735..ff82575ac2026 100644 --- a/engine/src/flutter/sky/packages/sky_engine/LICENSE +++ b/engine/src/flutter/sky/packages/sky_engine/LICENSE @@ -18188,6 +18188,13 @@ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- +skia + +Copyright 2026 Google LLC. + +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- angle Copyright 2026 The ANGLE Project Authors. All rights reserved. From 55c7c8e111fdb66daf60f5827daf8d84fcc48d1d Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 7 Aug 2026 12:32:03 -0400 Subject: [PATCH 128/330] Roll Packages from 4e3f83d37f83 to fc22143c4490 (5 revisions) (#190719) https://github.com/flutter/packages/compare/4e3f83d37f83...fc22143c4490 2026-08-07 engine-flutter-autoroll@skia.org Roll Flutter (stable) from 058e0af2c2b5 to 6b182d2c7585 (4 revisions) (flutter/packages#12394) 2026-08-07 269567208+reidbaker-agent@users.noreply.github.com [camera_android_camerax][tool] Migrate complexity checks to package:cognitive_complexity (flutter/packages#12356) 2026-08-06 mdebbar@google.com [camera_web] Remove invalid @JS annotation on extension type constructors (flutter/packages#12384) 2026-08-06 tarrinneal@gmail.com [pigeon] add support for analyzer 13 and 14 (flutter/packages#12358) 2026-08-06 katelovett@google.com [cupertino_ui, material_ui] Fix bad doc references (flutter/packages#12381) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- bin/internal/flutter_packages.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 4503cb3e69827..b31b925e6988c 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -4e3f83d37f83af1c25a5006d13efce4b5d18d4b3 +fc22143c4490b61cc2aa925751ec246ef1a86ec1 From 9f48e447dbd91bc961b0768a7865ead974b884aa Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 7 Aug 2026 09:40:35 -0700 Subject: [PATCH 129/330] Migrate example image_list to material_ui (#190363) Migrates the image_list example to material_ui. Part of https://github.com/flutter/flutter/issues/190093. --- dev/bots/check_examples_cross_imports.dart | 1 - examples/image_list/lib/main.dart | 3 ++- examples/image_list/pubspec.yaml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev/bots/check_examples_cross_imports.dart b/dev/bots/check_examples_cross_imports.dart index 25e2cb1c33d25..728aacf3e77ac 100644 --- a/dev/bots/check_examples_cross_imports.dart +++ b/dev/bots/check_examples_cross_imports.dart @@ -569,7 +569,6 @@ class ExamplesCrossImportChecker { 'examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart', 'examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart', 'examples/flutter_view/lib/main.dart', - 'examples/image_list/lib/main.dart', 'examples/multiple_windows/lib/app/main_window.dart', 'examples/multiple_windows/lib/app/tooltip_button.dart', 'examples/multiple_windows/lib/app/tooltip_window_edit_dialog.dart', diff --git a/examples/image_list/lib/main.dart b/examples/image_list/lib/main.dart index 14b64e71a6391..3860145280f8e 100644 --- a/examples/image_list/lib/main.dart +++ b/examples/image_list/lib/main.dart @@ -5,8 +5,9 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:flutter/material.dart'; + import 'package:flutter/services.dart'; +import 'package:material_ui/material_ui.dart'; /// An example that sets up local http server for serving single /// image, creates single flutter widget with five copies of requested diff --git a/examples/image_list/pubspec.yaml b/examples/image_list/pubspec.yaml index 44cf49c558544..b7a95ee951c05 100644 --- a/examples/image_list/pubspec.yaml +++ b/examples/image_list/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. + material_ui: ^0.0.2 dev_dependencies: flutter_test: sdk: flutter @@ -37,4 +38,4 @@ flutter: assets: - images/coast.jpg -# PUBSPEC CHECKSUM: 60tfp7 +# PUBSPEC CHECKSUM: oescuq From 5b5f63a40bc14b95928e4153492e35abb94741ab Mon Sep 17 00:00:00 2001 From: Kercy Date: Sat, 8 Aug 2026 00:48:07 +0800 Subject: [PATCH 130/330] [Impeller] Fix hardcoded Vulkan validation state in C API (#186767) Make the standalone Impeller Vulkan interop layer respect `ImpellerContextVulkanSettings.enable_vulkan_validation`. The C API already exposes this setting and `ContextVK::Settings` reads it, but `ContextVK::Create` currently forces `impeller_settings.enable_validation = true`. This means callers cannot disable Vulkan validation through the public standalone SDK API. This PR changes the interop layer to pass through `settings.enable_validation`. Fixes flutter/flutter#186764 ## Tested See PR comment for the full local test output. Locally verified from `engine/src/flutter`: - `./bin/et build -c host_debug_unopt //flutter/impeller:impeller_unittests` - `../out/host_debug_unopt/impeller_unittests --gtest_filter="InteropObjectTest.*"` passed, 3 tests. - `../out/host_debug_unopt/impeller_unittests --gtest_filter="Play/InteropPlaygroundTest.*OpenGLES*"` passed, 18 tests. Vulkan coverage is left to CI because the local Vulkan playground run aborts in my Vulkan environment while initializing debug utils. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Co-authored-by: Andy Wolff --- .../renderer/backend/vulkan/debug_report_vk.cc | 3 ++- .../renderer/backend/vulkan/debug_report_vk.h | 2 +- .../toolkit/interop/backend/vulkan/context_vk.cc | 2 +- .../impeller/toolkit/interop/playground_test.cc | 11 ++++------- .../impeller/toolkit/interop/playground_test.h | 4 ++++ 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.cc index 118bb30674103..6a628d764e5a2 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.cc @@ -35,7 +35,8 @@ DebugReportVK::DebugReportVK(const CapabilitiesVK& caps, return; } - messenger_ = std::move(messenger.value); + messenger_ = std::make_unique( + std::move(messenger.value)); is_valid_ = true; } diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.h b/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.h index e3a247bb6008d..4b8b9b91f2506 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.h +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/debug_report_vk.h @@ -20,7 +20,7 @@ class DebugReportVK { bool IsValid() const; private: - vk::UniqueDebugUtilsMessengerEXT messenger_; + std::unique_ptr messenger_; bool is_valid_ = false; enum class Result { diff --git a/engine/src/flutter/impeller/toolkit/interop/backend/vulkan/context_vk.cc b/engine/src/flutter/impeller/toolkit/interop/backend/vulkan/context_vk.cc index c386827f24b85..aa280665d0152 100644 --- a/engine/src/flutter/impeller/toolkit/interop/backend/vulkan/context_vk.cc +++ b/engine/src/flutter/impeller/toolkit/interop/backend/vulkan/context_vk.cc @@ -49,7 +49,7 @@ ScopedObject ContextVK::Create(const Settings& settings) { impeller::ContextVK::Settings impeller_settings; impeller_settings.shader_libraries_data = CreateShaderLibraryMappings(); impeller_settings.cache_directory = fml::paths::GetCachesDirectory(); - impeller_settings.enable_validation = true; + impeller_settings.enable_validation = settings.enable_validation; sContextVKProcAddressCallback = settings.instance_proc_address_callback; impeller_settings.proc_address_callback = ContextVKGetInstanceProcAddress; impeller_settings.flags = impeller::Flags{}; diff --git a/engine/src/flutter/impeller/toolkit/interop/playground_test.cc b/engine/src/flutter/impeller/toolkit/interop/playground_test.cc index 0462eafd49ad5..17552eda56458 100644 --- a/engine/src/flutter/impeller/toolkit/interop/playground_test.cc +++ b/engine/src/flutter/impeller/toolkit/interop/playground_test.cc @@ -77,13 +77,10 @@ ScopedObject PlaygroundTest::CreateContext() const { } case PlaygroundBackend::kVulkan: ImpellerContextVulkanSettings settings = {}; - struct UserData { - Playground::VKProcAddressResolver resolver; - } user_data; - user_data.resolver = CreateVKProcAddressResolver(); - settings.user_data = &user_data; - settings.enable_vulkan_validation = - GetSwitches().enable_vulkan_validation; + user_data_ = std::make_unique(); + user_data_->resolver = CreateVKProcAddressResolver(); + settings.user_data = user_data_.get(); + settings.enable_vulkan_validation = true; settings.proc_address_callback = [](void* instance, // const char* proc_name, // void* user_data // diff --git a/engine/src/flutter/impeller/toolkit/interop/playground_test.h b/engine/src/flutter/impeller/toolkit/interop/playground_test.h index 07256028ce4d1..8a36557dad6d9 100644 --- a/engine/src/flutter/impeller/toolkit/interop/playground_test.h +++ b/engine/src/flutter/impeller/toolkit/interop/playground_test.h @@ -48,6 +48,10 @@ class PlaygroundTest : public ::impeller::PlaygroundTest { std::string asset_name) const; private: + struct UserData { + Playground::VKProcAddressResolver resolver; + }; + mutable std::unique_ptr user_data_; ScopedObject interop_context_; }; From 9ac6d73463a8bb7c520efd66f927f4a8e6f11686 Mon Sep 17 00:00:00 2001 From: b-luk <97480502+b-luk@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:25:09 -0700 Subject: [PATCH 131/330] Add text rendering test to engine_integration_golden_test (#190678) Add text rendering test to engine_integration_golden_test Part of https://github.com/flutter/flutter/issues/190301 Manual run on Windows: image Compare with a manual run that includes a revert of https://github.com/flutter/flutter/pull/190477: image ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../engine_integration_golden_test/README.md | 3 +- .../engine_integration_golden_test.dart | 16 +++- .../lib/text_main.dart | 86 +++++++++++++++++++ .../pubspec.yaml | 3 +- 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 dev/integration_tests/engine_integration_golden_test/lib/text_main.dart diff --git a/dev/integration_tests/engine_integration_golden_test/README.md b/dev/integration_tests/engine_integration_golden_test/README.md index 786c8b769045b..4675696f51ccf 100644 --- a/dev/integration_tests/engine_integration_golden_test/README.md +++ b/dev/integration_tests/engine_integration_golden_test/README.md @@ -4,7 +4,8 @@ This integration test suite validates engine related rendering tests using Skia ## Running Locally ```sh -flutter test dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart -d +cd dev/integration_tests/engine_integration_golden_test +flutter test integration_test/engine_integration_golden_test.dart -d ``` ## Running via Devicelab on Windows diff --git a/dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart b/dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart index e097a0f28be33..f9f3a6e9870ce 100644 --- a/dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart +++ b/dev/integration_tests/engine_integration_golden_test/integration_test/engine_integration_golden_test.dart @@ -2,9 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:engine_integration_golden_test/primitive_shape_main.dart' as primitive_shape; +import 'package:engine_integration_golden_test/primitive_shape_main.dart' + as primitive_shape; +import 'package:engine_integration_golden_test/text_main.dart' as text_main; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:integration_test/integration_test.dart'; void main() { @@ -19,4 +22,15 @@ void main() { matchesGoldenFile('primitive_shape_canvas_snapshot.png'), ); }); + + testWidgets('renders text', (WidgetTester tester) async { + await tester.pumpWidget(const text_main.TextRenderingApp()); + await GoogleFonts.pendingFonts(); + await tester.pumpAndSettle(); + + await expectLater( + find.byKey(const Key('text_rendering_canvas')), + matchesGoldenFile('text_rendering_canvas_snapshot.png'), + ); + }); } diff --git a/dev/integration_tests/engine_integration_golden_test/lib/text_main.dart b/dev/integration_tests/engine_integration_golden_test/lib/text_main.dart new file mode 100644 index 0000000000000..fef74f07fd650 --- /dev/null +++ b/dev/integration_tests/engine_integration_golden_test/lib/text_main.dart @@ -0,0 +1,86 @@ +// 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/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +void main() { + runApp(const TextRenderingApp()); +} + +/// A test application that renders text with various font weights and colors +/// to validate text rendering in integration tests. +class TextRenderingApp extends StatelessWidget { + /// Creates a [TextRenderingApp]. + const TextRenderingApp({super.key}); + + static const String _testText = + 'the quick brown fox jumped over the lazy dog!.?'; + + Widget _buildTextSection({ + required Color textColor, + required Color backgroundColor, + }) { + return Expanded( + child: Container( + color: backgroundColor, + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + for (final FontWeight weight in [ + FontWeight.w100, + FontWeight.normal, + FontWeight.bold, + ]) + Text( + _testText, + style: GoogleFonts.roboto( + color: textColor, + fontSize: 20.0, + fontWeight: weight, + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Container( + key: const Key('text_rendering_canvas'), + color: Colors.grey[850], + width: double.infinity, + height: double.infinity, + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildTextSection( + textColor: Colors.white, + backgroundColor: Colors.black, + ), + const SizedBox(height: 16.0), + _buildTextSection( + textColor: Colors.black, + backgroundColor: Colors.white, + ), + const SizedBox(height: 16.0), + _buildTextSection( + textColor: Colors.green, + backgroundColor: Colors.black, + ), + ], + ), + ), + ), + ); + } +} diff --git a/dev/integration_tests/engine_integration_golden_test/pubspec.yaml b/dev/integration_tests/engine_integration_golden_test/pubspec.yaml index 9f88f6084a79b..ea99b43d086cb 100644 --- a/dev/integration_tests/engine_integration_golden_test/pubspec.yaml +++ b/dev/integration_tests/engine_integration_golden_test/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: sdk: flutter flutter_driver: sdk: flutter + google_fonts: any integration_test: sdk: flutter path: any @@ -23,4 +24,4 @@ dev_dependencies: sdk: flutter test: any -# PUBSPEC CHECKSUM: fp4cre +# PUBSPEC CHECKSUM: 3hii4d From 1857274fcb87bf7b09fabbd3434ab30a475c93b2 Mon Sep 17 00:00:00 2001 From: Gray Mackall <34871572+gmackall@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:35:21 -0700 Subject: [PATCH 132/330] Fix an ANR in HCPP rotation due to platform <-> raster thread deadlock (#190638) Fixes a deadlock where we are waiting here https://github.com/flutter/flutter/blob/master/engine/src/flutter/shell/platform/android/platform_view_android.cc#L247 and at the same time here https://github.com/flutter/flutter/blob/master/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc#L292 by removing the call to `DestroySurfaces()` - we shouldn't be destroying in hcpp in the first place. I'm pretty sure this was an error in copying it from https://github.com/flutter/flutter/blob/master/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc#L226 in the first place, and it shouldn't have been included. --- .../external_view_embedder_2.cc | 6 +- .../external_view_embedder_unittests.cc | 246 ++++++++++++++++++ .../external_view_embedder/surface_pool.cc | 13 +- .../surface_pool_unittests.cc | 20 +- 4 files changed, 270 insertions(+), 15 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc index e90b1d30d509d..6eb0f9ba6faf8 100644 --- a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc +++ b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc @@ -239,11 +239,9 @@ void AndroidExternalViewEmbedder2::PrepareFlutterView( double device_pixel_ratio) { Reset(); - // The surface size changed. Therefore, destroy existing surfaces as - // the existing surfaces in the pool can't be recycled. + // The singular overlay surface is persistent, so it is resized in place by + // |SurfacePool::GetLayer| rather than destroyed and recreated here. if (frame_size_ != frame_size) { - DestroySurfaces(); - // This should not block to prevent deadlocks with // setViewportMetrics. task_runners_.GetPlatformTaskRunner()->PostTask(fml::MakeCopyable( diff --git a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index dc15f144864cf..dc2c89ad49e97 100644 --- a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -11,6 +11,7 @@ #include "flutter/flow/embedded_views.h" #include "flutter/flow/surface.h" +#include "flutter/fml/make_copyable.h" #include "flutter/fml/raster_thread_merger.h" #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/fml/thread.h" @@ -18,6 +19,7 @@ #include "flutter/shell/platform/android/jni/jni_mock.h" #include "flutter/shell/platform/android/surface/android_surface.h" #include "flutter/shell/platform/android/surface/android_surface_mock.h" +#include "flutter/testing/post_task_sync.h" #include "flutter/testing/testing.h" #include "gmock/gmock.h" @@ -1188,5 +1190,249 @@ TEST(AndroidExternalViewEmbedder2, embedder.reset(); } +TEST(AndroidExternalViewEmbedder2, FrameSizeChangeDoesNotDestroySurfaces) { + auto jni_mock = std::make_shared(); + auto android_context = + std::make_shared(AndroidRenderingAPI::kSoftware); + ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".", + ThreadHost::Type::kPlatform | ThreadHost::Type::kIo | + ThreadHost::Type::kUi | ThreadHost::Type::kRaster); + TaskRunners task_runners( + "test", + thread_host.platform_thread->GetTaskRunner(), // platform + thread_host.raster_thread->GetTaskRunner(), // raster + thread_host.ui_thread->GetTaskRunner(), // ui + thread_host.io_thread->GetTaskRunner() // io + ); + const DlISize frame_size1(100, 100); + const DlISize frame_size2(200, 200); + SurfaceFrame::FramebufferInfo framebuffer_info; + + auto surface_mock = std::make_unique(); + EXPECT_CALL(*surface_mock, AcquireFrame(frame_size1)) + .WillOnce(Return(ByMove(std::make_unique( + SkSurfaces::Null(100, 100), framebuffer_info, + [](const SurfaceFrame&, DlCanvas*) { return true; }, + [](const SurfaceFrame&) { return true; }, frame_size1)))); + EXPECT_CALL(*surface_mock, AcquireFrame(frame_size2)) + .WillOnce(Return(ByMove(std::make_unique( + SkSurfaces::Null(200, 200), framebuffer_info, + [](const SurfaceFrame&, DlCanvas*) { return true; }, + [](const SurfaceFrame&) { return true; }, frame_size2)))); + + auto surface_factory = std::make_shared( + fml::MakeCopyable([frame_size2, + surface_mock = std::move(surface_mock)]() mutable { + auto android_surface = std::make_unique(); + EXPECT_CALL(*android_surface, IsValid()).WillRepeatedly(Return(true)); + EXPECT_CALL(*android_surface, SetNativeWindow(_, _)) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*android_surface, CreateGPUSurface(_)) + .WillOnce(Return(ByMove(std::move(surface_mock)))); + EXPECT_CALL(*android_surface, OnScreenSurfaceResize(frame_size2)) + .Times(1); + return android_surface; + })); + + fml::RefPtr window = + fml::MakeRefCounted(nullptr); + EXPECT_CALL(*jni_mock, createOverlaySurface2()) + .Times(1) + .WillOnce(Return( + ByMove(std::make_unique( + 0, window)))); + EXPECT_CALL(*jni_mock, destroyOverlaySurface2()).Times(0); + + auto embedder = std::make_unique( + *android_context, jni_mock, surface_factory, task_runners); + + const int64_t view_id = 42; + MutatorsStack mutators; + DlMatrix matrix = DlMatrix::MakeTranslation({0, 0}); + DlPaint rect_paint; + rect_paint.setColor(DlColor::kCyan()); + rect_paint.setDrawStyle(DlDrawStyle::kFill); + + // First frame with size 100x100 + embedder->PrepareFlutterView(frame_size1, 1.0); + embedder->PrerollCompositeEmbeddedView( + view_id, + std::make_unique(matrix, DlSize(50, 50), mutators)); + auto canvas1 = embedder->CompositeEmbeddedView(view_id); + canvas1->DrawRect(DlRect::MakeXYWH(0, 0, 50, 50), rect_paint); + + EXPECT_CALL(*jni_mock, + onDisplayPlatformView2(view_id, 0, 0, 50, 50, 50, 50, mutators)); + EXPECT_CALL(*jni_mock, swapTransaction()); + EXPECT_CALL(*jni_mock, onEndFrame2()); + + auto surface_frame1 = std::make_unique( + SkSurfaces::Null(100, 100), framebuffer_info, + [](const SurfaceFrame& surface_frame, DlCanvas* canvas) { return true; }, + [](const SurfaceFrame& surface_frame) { return true; }, + /*frame_size=*/frame_size1); + + PostTaskSync(task_runners.GetRasterTaskRunner(), [&]() { + embedder->SubmitFlutterView(kImplicitViewId, nullptr, nullptr, + std::move(surface_frame1)); + }); + // Drain the work that SubmitFlutterView posted to the platform thread. + PostTaskSync(task_runners.GetPlatformTaskRunner(), []() {}); + + // Second frame with size 200x200 (simulating rotation/resize). + // PrepareFlutterView should NOT destroy overlay surfaces. + EXPECT_CALL(*jni_mock, MaybeResizeSurfaceView(200, 200)); + embedder->PrepareFlutterView(frame_size2, 1.0); + embedder->PrerollCompositeEmbeddedView( + view_id, + std::make_unique(matrix, DlSize(100, 100), mutators)); + auto canvas2 = embedder->CompositeEmbeddedView(view_id); + canvas2->DrawRect(DlRect::MakeXYWH(0, 0, 100, 100), rect_paint); + + EXPECT_CALL(*jni_mock, onDisplayPlatformView2(view_id, 0, 0, 100, 100, 100, + 100, mutators)); + EXPECT_CALL(*jni_mock, swapTransaction()); + EXPECT_CALL(*jni_mock, onEndFrame2()); + + auto surface_frame2 = std::make_unique( + SkSurfaces::Null(200, 200), framebuffer_info, + [](const SurfaceFrame& surface_frame, DlCanvas* canvas) { return true; }, + [](const SurfaceFrame& surface_frame) { return true; }, + /*frame_size=*/frame_size2); + + PostTaskSync(task_runners.GetRasterTaskRunner(), [&]() { + embedder->SubmitFlutterView(kImplicitViewId, nullptr, nullptr, + std::move(surface_frame2)); + }); + PostTaskSync(task_runners.GetPlatformTaskRunner(), []() {}); + + EXPECT_CALL(*jni_mock, destroyOverlaySurface2()).Times(1); + embedder->Teardown(); + embedder.reset(); +} + +// Regression test for the HCPP rotation ANR. +// +// The platform thread is unavailable while it is inside +// ViewRootImpl.performTraversals dispatching surfaceChanged, so a frame resize +// on the raster thread must not block waiting on it. Rather than reproduce both +// halves of the deadlock, this occupies the platform thread directly and +// asserts the raster thread still makes progress. +TEST(AndroidExternalViewEmbedder2, ResizeDoesNotBlockRasterOnPlatformThread) { + auto jni_mock = std::make_shared(); + auto android_context = + std::make_shared(AndroidRenderingAPI::kSoftware); + ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".", + ThreadHost::Type::kPlatform | ThreadHost::Type::kIo | + ThreadHost::Type::kUi | ThreadHost::Type::kRaster); + TaskRunners task_runners( + "test", + thread_host.platform_thread->GetTaskRunner(), // platform + thread_host.raster_thread->GetTaskRunner(), // raster + thread_host.ui_thread->GetTaskRunner(), // ui + thread_host.io_thread->GetTaskRunner() // io + ); + const DlISize frame_size1(100, 100); + const DlISize frame_size2(200, 200); + SurfaceFrame::FramebufferInfo framebuffer_info; + + auto surface_mock = std::make_unique(); + EXPECT_CALL(*surface_mock, AcquireFrame(frame_size1)) + .WillOnce(Return(ByMove(std::make_unique( + SkSurfaces::Null(100, 100), framebuffer_info, + [](const SurfaceFrame&, DlCanvas*) { return true; }, + [](const SurfaceFrame&) { return true; }, frame_size1)))); + + auto surface_factory = std::make_shared( + fml::MakeCopyable([surface_mock = std::move(surface_mock)]() mutable { + auto android_surface = std::make_unique(); + EXPECT_CALL(*android_surface, IsValid()).WillRepeatedly(Return(true)); + EXPECT_CALL(*android_surface, SetNativeWindow(_, _)) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*android_surface, CreateGPUSurface(_)) + .WillOnce(Return(ByMove(std::move(surface_mock)))); + return android_surface; + })); + + fml::RefPtr window = + fml::MakeRefCounted(nullptr); + EXPECT_CALL(*jni_mock, createOverlaySurface2()) + .Times(1) + .WillOnce(Return( + ByMove(std::make_unique( + 0, window)))); + + auto embedder = std::make_unique( + *android_context, jni_mock, surface_factory, task_runners); + + const int64_t view_id = 42; + MutatorsStack mutators; + DlMatrix matrix = DlMatrix::MakeTranslation({0, 0}); + DlPaint rect_paint; + rect_paint.setColor(DlColor::kCyan()); + rect_paint.setDrawStyle(DlDrawStyle::kFill); + + // Run one frame so the overlay layer exists. Without a layer, DestroySurfaces + // early-returns and there is nothing to deadlock on. + embedder->PrepareFlutterView(frame_size1, 1.0); + embedder->PrerollCompositeEmbeddedView( + view_id, + std::make_unique(matrix, DlSize(50, 50), mutators)); + auto canvas = embedder->CompositeEmbeddedView(view_id); + canvas->DrawRect(DlRect::MakeXYWH(0, 0, 50, 50), rect_paint); + + EXPECT_CALL(*jni_mock, + onDisplayPlatformView2(view_id, 0, 0, 50, 50, 50, 50, mutators)); + EXPECT_CALL(*jni_mock, swapTransaction()); + EXPECT_CALL(*jni_mock, onEndFrame2()); + + auto surface_frame = std::make_unique( + SkSurfaces::Null(100, 100), framebuffer_info, + [](const SurfaceFrame& surface_frame, DlCanvas* canvas) { return true; }, + [](const SurfaceFrame& surface_frame) { return true; }, + /*frame_size=*/frame_size1); + + PostTaskSync(task_runners.GetRasterTaskRunner(), [&]() { + embedder->SubmitFlutterView(kImplicitViewId, nullptr, nullptr, + std::move(surface_frame)); + }); + PostTaskSync(task_runners.GetPlatformTaskRunner(), []() {}); + + // Occupy the platform thread, standing in for a layout traversal. + fml::AutoResetWaitableEvent platform_occupied; + fml::AutoResetWaitableEvent release_platform; + task_runners.GetPlatformTaskRunner()->PostTask([&]() { + platform_occupied.Signal(); + release_platform.Wait(); + }); + platform_occupied.Wait(); + + EXPECT_CALL(*jni_mock, MaybeResizeSurfaceView(200, 200)); + + // The resize must complete without the platform thread running anything. + fml::AutoResetWaitableEvent resize_done; + task_runners.GetRasterTaskRunner()->PostTask([&]() { + embedder->PrepareFlutterView(frame_size2, 1.0); + resize_done.Signal(); + }); + const bool timed_out = + resize_done.WaitWithTimeout(fml::TimeDelta::FromSeconds(10)); + + // Release the platform thread before asserting so that a regression fails + // this test instead of hanging the suite. + release_platform.Signal(); + if (timed_out) { + resize_done.Wait(); + } + EXPECT_FALSE(timed_out) + << "PrepareFlutterView blocked the raster thread on the platform thread"; + + PostTaskSync(task_runners.GetPlatformTaskRunner(), []() {}); + + EXPECT_CALL(*jni_mock, destroyOverlaySurface2()).Times(1); + embedder->Teardown(); + embedder.reset(); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool.cc b/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool.cc index 07f1bdf4827e7..c0b91116a8264 100644 --- a/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool.cc +++ b/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool.cc @@ -28,9 +28,18 @@ std::shared_ptr SurfacePool::GetLayer( const std::shared_ptr& jni_facade, const std::shared_ptr& surface_factory) { std::lock_guard lock(mutex_); - // Destroy current layers in the pool if the frame size has changed. if (requested_frame_size_ != current_frame_size_) { - DestroyLayersLocked(jni_facade); + if (use_new_surface_methods_) { + // The overlay surface is persistent, so resize it in place. Nothing else + // resizes the swapchain: |Surface::AcquireFrame| ignores the size it is + // handed. + for (const std::shared_ptr& layer : layers_) { + layer->android_surface->OnScreenSurfaceResize(requested_frame_size_); + } + } else { + // Destroy current layers in the pool if the frame size has changed. + DestroyLayersLocked(jni_facade); + } } intptr_t gr_context_key = reinterpret_cast(gr_context); // Allocate a new surface if there isn't one available. diff --git a/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool_unittests.cc b/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool_unittests.cc index f052aedebc17f..dd07b3ee5cc0b 100644 --- a/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool_unittests.cc +++ b/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool_unittests.cc @@ -304,7 +304,7 @@ TEST(SurfacePool, DestroyLayersFrameSizeChanged) { ASSERT_TRUE(pool->HasLayers()); } -TEST(SurfacePool, DestroyLayersFrameSizeChangedNew) { +TEST(SurfacePool, DoesNotDestroyLayersFrameSizeChangedNew) { auto pool = std::make_unique(/*use_new_surface_methods=*/true); auto jni_mock = std::make_shared(); @@ -320,6 +320,9 @@ TEST(SurfacePool, DestroyLayersFrameSizeChangedNew) { EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())); EXPECT_CALL(*android_surface_mock, SetNativeWindow(window, _)); EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); + EXPECT_CALL(*android_surface_mock, + OnScreenSurfaceResize(DlISize(20, 20))) + .Times(1); return android_surface_mock; }); pool->SetFrameSize(DlISize(10, 10)); @@ -332,21 +335,20 @@ TEST(SurfacePool, DestroyLayersFrameSizeChangedNew) { ASSERT_FALSE(pool->HasLayers()); - pool->GetLayer(gr_context.get(), *android_context, jni_mock, surface_factory); + auto layer_1 = pool->GetLayer(gr_context.get(), *android_context, jni_mock, + surface_factory); ASSERT_TRUE(pool->HasLayers()); + ASSERT_NE(nullptr, layer_1); + pool->RecycleLayers(); pool->SetFrameSize(DlISize(20, 20)); - EXPECT_CALL(*jni_mock, destroyOverlaySurface2()).Times(1); - EXPECT_CALL(*jni_mock, createOverlaySurface2()) - .Times(1) - .WillOnce(Return( - ByMove(std::make_unique( - 1, window)))); - pool->GetLayer(gr_context.get(), *android_context, jni_mock, surface_factory); + auto layer_2 = pool->GetLayer(gr_context.get(), *android_context, jni_mock, + surface_factory); ASSERT_TRUE(pool->GetUnusedLayers().empty()); ASSERT_TRUE(pool->HasLayers()); + ASSERT_EQ(layer_1, layer_2); } } // namespace testing From d30fbd4a8ed30c34c0b2a5b07652c858bfd67e71 Mon Sep 17 00:00:00 2001 From: matheusccastro Date: Fri, 7 Aug 2026 14:36:58 -0300 Subject: [PATCH 133/330] Pass web-defines to the web builder in all run configurations (#189622) `--web-define` substitution of `{{PLACEHOLDER}}` tokens in `web/index.html` and `flutter_bootstrap.js` worked in debug `flutter run` (serve-time substitution in `WebAssetServer`) and in `flutter build web` (`webDefine:`-prefixed entries in the build `Environment` consumed by the `WebTemplatedFiles` target), but not in `flutter run --profile` or `--release`: `ResidentWebRunner` stored the parsed defines in `_webDefines` and then omitted them from both `WebBuilder.buildWeb()` calls in the non-debug branch, and `ReleaseAssetServer` serves the build output verbatim with no serve-time templating, so the placeholders were never replaced. This passes `webDefines` to both `buildWeb()` call sites (the initial build in `run()` and the rebuild on hot restart), matching how `flutter build web` already passes them. Debug `--wasm` runs take the same branch and are fixed as a side effect. `flutter drive` never forwarded web-defines in any mode, so the flag is now plumbed through `DriverService.start()` into `WebDriverService` and on to the web runner. Added unit tests assert that the build `Environment` receives the `webDefine:`-prefixed defines in profile mode, on the rebuild after a hot restart in release mode, and in debug `--wasm` mode; that `WebDriverService` forwards web-defines to the web runner; and that the drive command forwards `--web-define` values to `DriverService.start()`. Added end-to-end tests run the real tool against a fixture project whose `web/index.html` and `web/flutter_bootstrap.js` contain `{{MY_VERSION}}` and `{{API_URL}}` placeholders: `flutter build web` in debug/profile/release must substitute them in `build/web/index.html` and `build/web/flutter_bootstrap.js`, and `flutter run -d web-server` in debug/profile/release must serve substituted HTML, including after hot reload (debug) and hot restart (all three modes). Existing web-define coverage for `flutter build web` and debug-mode serving is unchanged and passing. Fixes #8885 (was closed, but the problem was actually this - the flag was only passed on specific run configurations). Also related to PR #175805 and issue #127853 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Ben Konyi --- .../flutter_tools/lib/src/commands/drive.dart | 1 + .../lib/src/drive/drive_service.dart | 2 + .../lib/src/drive/web_driver_service.dart | 2 + .../lib/src/isolated/resident_web_runner.dart | 2 + .../commands.shard/hermetic/drive_test.dart | 5 + .../drive/web_driver_service_test.dart | 21 ++++ .../resident_web_runner_test.dart | 111 +++++++++++++++++ .../test_data/web_define_project.dart | 85 +++++++++++++ .../web_define_build_test.dart | 62 ++++++++++ .../test/web.shard/web_define_run_test.dart | 117 ++++++++++++++++++ 10 files changed, 408 insertions(+) create mode 100644 packages/flutter_tools/test/integration.shard/test_data/web_define_project.dart create mode 100644 packages/flutter_tools/test/integration.shard/web_define_build_test.dart create mode 100644 packages/flutter_tools/test/web.shard/web_define_run_test.dart diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 8d45046b703f4..87f47ba68c367 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -354,6 +354,7 @@ class DriveCommand extends RunCommandBase { if (traceStartup) 'trace-startup': traceStartup, if (web) 'no-launch-chrome': true, }, + webDefines: extractWebDefines(), ); } else { final Uri? uri = Uri.tryParse(stringArg(_kUseExistingApp)!); diff --git a/packages/flutter_tools/lib/src/drive/drive_service.dart b/packages/flutter_tools/lib/src/drive/drive_service.dart index 8f519d549d74f..c9937bd68ed53 100644 --- a/packages/flutter_tools/lib/src/drive/drive_service.dart +++ b/packages/flutter_tools/lib/src/drive/drive_service.dart @@ -85,6 +85,7 @@ abstract class DriverService { String? userIdentifier, String? mainPath, Map platformArgs = const {}, + Map webDefines = const {}, }); /// If --use-existing-app is provided, configured the correct VM Service URI. @@ -160,6 +161,7 @@ class FlutterDriverService extends DriverService { String? userIdentifier, Map platformArgs = const {}, String? mainPath, + Map webDefines = const {}, }) async { if (buildInfo.isRelease) { throwToolExit( diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index d7aff791c05cf..0780d81735506 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -72,6 +72,7 @@ class WebDriverService extends DriverService { String? userIdentifier, String? mainPath, Map platformArgs = const {}, + Map webDefines = const {}, }) async { final FlutterDevice flutterDevice = await FlutterDevice.create( device, @@ -98,6 +99,7 @@ class WebDriverService extends DriverService { ), platformArgs: platformArgs, stayResident: true, + webDefines: webDefines, flutterProject: FlutterProject.current(), fileSystem: globals.fs, analytics: globals.analytics, diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 6f49a8bf4470d..69cd2dc942931 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -355,6 +355,7 @@ class ResidentWebRunner extends ResidentRunner { debuggingOptions.buildInfo, ServiceWorkerStrategy.none, compilerConfigs: [_compilerConfig], + webDefines: _webDefines, ); } final webDevFS = flutterDevice!.devFS! as WebDevFS; @@ -508,6 +509,7 @@ class ResidentWebRunner extends ResidentRunner { debuggingOptions.buildInfo, ServiceWorkerStrategy.none, compilerConfigs: [_compilerConfig], + webDefines: _webDefines, ); } on ToolExit { return OperationResult(1, 'Failed to recompile application.'); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart index 2271447a6d644..2e7284516b6d4 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart @@ -87,10 +87,12 @@ void main() { 'chrome', '--browser-name=chrome', '--chrome-binary=/tmp/custom-chrome', + '--web-define=FOO=bar', ]); expect(capturingDriverService.platformArgs, containsPair('no-launch-chrome', true)); expect(capturingDriverService.platformArgs, isNot(contains('--no-launch-chrome'))); + expect(capturingDriverService.webDefines, {'FOO': 'bar'}); }, overrides: { FileSystem: () => fileSystem, @@ -1045,6 +1047,7 @@ class FakeDriverService extends Fake implements DriverService { class CapturingDriverService extends Fake implements DriverService { Map? platformArgs; + Map? webDefines; @override Future start( @@ -1056,8 +1059,10 @@ class CapturingDriverService extends Fake implements DriverService { String? userIdentifier, String? mainPath, Map platformArgs = const {}, + Map webDefines = const {}, }) async { this.platformArgs = platformArgs; + this.webDefines = webDefines; } @override diff --git a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart index 60a6a7f980a31..99b191fa40061 100644 --- a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart +++ b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart @@ -285,6 +285,25 @@ void main() { }, ); + testUsingContext( + 'WebDriverService forwards web-defines to the web runner', + () async { + final WebDriverService service = setUpDriverService(); + final device = FakeDevice(); + await service.start( + BuildInfo.profile, + device, + DebuggingOptions.enabled(BuildInfo.profile, ipv6: true), + webDefines: {'VERSION': 'v1.2.3'}, + ); + await service.stop(); + expect(fakeWebRunnerFactory.lastWebDefines, {'VERSION': 'v1.2.3'}); + }, + overrides: { + WebRunnerFactory: () => fakeWebRunnerFactory = FakeWebRunnerFactory(), + }, + ); + testUsingContext('WebDriverService can start an app with a launch url provided', () async { final WebDriverService service = setUpDriverService(); final device = FakeDevice(); @@ -358,6 +377,7 @@ class FakeWebRunnerFactory implements WebRunnerFactory { final bool doResolveToError; Map? lastPlatformArgs; + Map? lastWebDefines; @override ResidentRunner createWebRunner( @@ -381,6 +401,7 @@ class FakeWebRunnerFactory implements WebRunnerFactory { }) { expect(stayResident, isTrue); lastPlatformArgs = platformArgs; + lastWebDefines = webDefines; return FakeResidentRunner( doResolveToError: doResolveToError, debuggingOptions: debuggingOptions, diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 754492f7ae180..e9deba25ed7c1 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -2183,6 +2183,117 @@ flutter: ProcessManager: () => processManager, }, ); + + group('web-defines', () { + testUsingContext( + 'passes web-defines to the build in profile mode', + () async { + fakeVmServiceHost = FakeVmServiceHost(requests: []); + setupMocks(); + final residentWebRunner = ResidentWebRunner( + flutterDevice, + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile), + stayResident: false, + fileSystem: fileSystem, + logger: BufferLogger.test(), + terminal: Terminal.test(), + platform: FakePlatform(), + outputPreferences: OutputPreferences.test(), + analytics: globals.analytics, + systemClock: globals.systemClock, + webDefines: const {'VERSION': 'v1.2.3'}, + ); + + expect(await residentWebRunner.run(), 0); + }, + overrides: { + BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), ( + Target target, + Environment environment, + ) { + expect(environment.defines['webDefine:VERSION'], 'v1.2.3'); + }), + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Pub: ThrowingPub.new, + }, + ); + + testUsingContext( + 'passes web-defines to the rebuild after a hot restart in release mode', + () async { + fakeVmServiceHost = FakeVmServiceHost(requests: []); + setupMocks(); + final residentWebRunner = ResidentWebRunner( + flutterDevice, + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.release), + fileSystem: fileSystem, + logger: BufferLogger.test(), + terminal: Terminal.test(), + platform: FakePlatform(), + outputPreferences: OutputPreferences.test(), + analytics: globals.analytics, + systemClock: globals.systemClock, + webDefines: const {'VERSION': 'v1.2.3'}, + ); + + final connectionInfoCompleter = Completer(); + unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter)); + await connectionInfoCompleter.future; + + final OperationResult result = await residentWebRunner.restart(); + expect(result.code, 0); + }, + overrides: { + BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), ( + Target target, + Environment environment, + ) { + expect(environment.defines['webDefine:VERSION'], 'v1.2.3'); + }), + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Pub: ThrowingPub.new, + }, + ); + + testUsingContext( + 'passes web-defines to the build in debug --wasm mode', + () async { + fakeVmServiceHost = FakeVmServiceHost(requests: []); + setupMocks(); + final residentWebRunner = ResidentWebRunner( + flutterDevice, + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, webUseWasm: true), + stayResident: false, + fileSystem: fileSystem, + logger: BufferLogger.test(), + terminal: Terminal.test(), + platform: FakePlatform(), + outputPreferences: OutputPreferences.test(), + analytics: globals.analytics, + systemClock: globals.systemClock, + webDefines: const {'VERSION': 'v1.2.3'}, + ); + + expect(await residentWebRunner.run(), 0); + }, + overrides: { + BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), ( + Target target, + Environment environment, + ) { + expect(environment.defines['webDefine:VERSION'], 'v1.2.3'); + }), + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Pub: ThrowingPub.new, + }, + ); + }); } ResidentRunner setUpResidentRunner( diff --git a/packages/flutter_tools/test/integration.shard/test_data/web_define_project.dart b/packages/flutter_tools/test/integration.shard/test_data/web_define_project.dart new file mode 100644 index 0000000000000..398de80d69089 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/web_define_project.dart @@ -0,0 +1,85 @@ +// 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:file/file.dart'; + +import '../test_utils.dart'; +import 'project.dart'; + +/// A project whose `web/index.html` and `web/flutter_bootstrap.js` contain user +/// `--web-define` placeholders (`{{MY_VERSION}}` and `{{API_URL}}`), used by the +/// end-to-end tests that verify `--web-define` substitution. +class WebDefineProject extends Project { + WebDefineProject() : super(indexHtml: _indexHtml); + + static const String kVersion = 'v9.9.9'; + static const String kApiUrl = 'https://example.invalid/api'; + + static const String _indexHtml = ''' + + + + Hello, World + + + + + + + +'''; + + // The default generated bootstrap has no user placeholder, so the fixture + // supplies a template with one to make bootstrap substitution observable. + static const String _flutterBootstrapJs = ''' +// build: {{MY_VERSION}} api: {{API_URL}} +{{flutter_js}} +{{flutter_build_config}} +_flutter.loader.load(); +'''; + + @override + String get pubspec => + ''' + name: $name + environment: + sdk: ^3.7.0-0 + + dependencies: + flutter: + sdk: flutter + '''; + + // Rebuild continuously so hot reload/restart have an observable effect. + @override + final main = r''' + import 'dart:async'; + + import 'package:flutter/material.dart'; + + Future main() async { + while (true) { + runApp(MyApp()); + await Future.delayed(const Duration(milliseconds: 50)); + } + } + + class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + home: Container(), + ); + } + } + '''; + + @override + Future setUpIn(Directory dir, {bool generateMain = true}) async { + await super.setUpIn(dir, generateMain: generateMain); + // Project.setUpIn does not write a flutter_bootstrap.js, so add the custom template here. + writeFile(fileSystem.path.join(dir.path, 'web', 'flutter_bootstrap.js'), _flutterBootstrapJs); + } +} diff --git a/packages/flutter_tools/test/integration.shard/web_define_build_test.dart b/packages/flutter_tools/test/integration.shard/web_define_build_test.dart new file mode 100644 index 0000000000000..976be4770a406 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/web_define_build_test.dart @@ -0,0 +1,62 @@ +// 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:file/file.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/base/io.dart'; + +import '../src/common.dart'; +import 'test_data/web_define_project.dart'; +import 'test_utils.dart'; + +/// Asserts --web-define placeholders were substituted and none remain. +void _expectSubstituted(String contents) { + expect(contents, contains(WebDefineProject.kVersion)); + expect(contents, contains(WebDefineProject.kApiUrl)); + expect(contents, isNot(contains('{{MY_VERSION}}'))); + expect(contents, isNot(contains('{{API_URL}}'))); +} + +void main() { + late Directory tempDir; + final project = WebDefineProject(); + + setUp(() async { + tempDir = createResolvedTempDirectorySync('web_define_build_test.'); + await project.setUpIn(tempDir); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + for (final mode in const ['--debug', '--profile', '--release']) { + testWithoutContext('flutter build web $mode substitutes --web-define in output', () async { + final ProcessResult result = processManager.runSync([ + flutterBin, + ...getLocalEngineArguments(), + 'build', + 'web', + '--no-pub', + '--no-web-resources-cdn', + '--web-define=MY_VERSION=${WebDefineProject.kVersion}', + '--web-define=API_URL=${WebDefineProject.kApiUrl}', + mode, + ], workingDirectory: tempDir.path); + expect(result, const ProcessResultMatcher()); + + final File indexHtml = fileSystem.file( + fileSystem.path.join(tempDir.path, 'build', 'web', 'index.html'), + ); + expect(indexHtml, exists); + _expectSubstituted(indexHtml.readAsStringSync()); + + final File bootstrapJs = fileSystem.file( + fileSystem.path.join(tempDir.path, 'build', 'web', 'flutter_bootstrap.js'), + ); + expect(bootstrapJs, exists); + _expectSubstituted(bootstrapJs.readAsStringSync()); + }); + } +} diff --git a/packages/flutter_tools/test/web.shard/web_define_run_test.dart b/packages/flutter_tools/test/web.shard/web_define_run_test.dart new file mode 100644 index 0000000000000..96c3af6b229a2 --- /dev/null +++ b/packages/flutter_tools/test/web.shard/web_define_run_test.dart @@ -0,0 +1,117 @@ +// 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(['flutter-test-driver']) +library; + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; + +import '../integration.shard/test_data/web_define_project.dart'; +import '../integration.shard/test_driver.dart'; +import '../integration.shard/test_utils.dart'; +import '../src/common.dart'; + +import 'test_data/web_server_test_common.dart'; + +/// Fetches the body served at [url]; the bare app URL serves index.html. +Future _fetch(String url) async { + final client = io.HttpClient(); + try { + final io.HttpClientRequest request = await client.getUrl(Uri.parse(url)); + final io.HttpClientResponse response = await request.close(); + final String body = await response.transform(utf8.decoder).join(); + if (response.statusCode != io.HttpStatus.ok) { + throw Exception('GET $url returned HTTP ${response.statusCode}, body:\n$body'); + } + return body; + } finally { + client.close(force: true); + } +} + +/// Asserts --web-define placeholders were substituted and none remain. +void _expectSubstituted(String body) { + expect(body, contains(WebDefineProject.kVersion)); + expect(body, contains(WebDefineProject.kApiUrl)); + expect(body, isNot(contains('{{MY_VERSION}}'))); + expect(body, isNot(contains('{{API_URL}}'))); +} + +void main() { + late Directory tempDir; + final project = WebDefineProject(); + late FlutterRunTestDriver flutter; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('web_define_run_test.'); + await project.setUpIn(tempDir); + flutter = FlutterRunTestDriver(tempDir); + }); + + tearDown(() async { + await flutter.stop(); + tryToDelete(tempDir); + }); + + const webDefineArgs = [ + '--no-web-resources-cdn', + '--web-define=MY_VERSION=${WebDefineProject.kVersion}', + '--web-define=API_URL=${WebDefineProject.kApiUrl}', + ]; + + testWithoutContext('flutter run (debug) substitutes --web-define in served index.html ' + 'and keeps it substituted across hot reload and hot restart', () async { + final testRunner = WebServerDeviceTestRunner(flutter); + try { + final String appUrl = await testRunner.runWebServerDevice( + additionalCommandArgs: webDefineArgs, + ); + _expectSubstituted(await _fetch(appUrl)); + + await testRunner.hotReload(); + _expectSubstituted(await _fetch(appUrl)); + + await testRunner.hotRestart(); + _expectSubstituted(await _fetch(appUrl)); + } finally { + await testRunner.cleanup(); + } + }); + + testWithoutContext('flutter run --profile substitutes --web-define in served index.html ' + 'and keeps it substituted across hot restart', () async { + final testRunner = WebServerDeviceTestRunner(flutter); + try { + final String appUrl = await testRunner.runWebServerDevice( + additionalCommandArgs: [...webDefineArgs, '--profile'], + ); + _expectSubstituted(await _fetch(appUrl)); + + // Hot reload is debug-only. + await testRunner.hotRestart(); + _expectSubstituted(await _fetch(appUrl)); + } finally { + await testRunner.cleanup(); + } + }); + + testWithoutContext('flutter run --release substitutes --web-define in served index.html ' + 'and keeps it substituted across hot restart', () async { + final testRunner = WebServerDeviceTestRunner(flutter); + try { + final String appUrl = await testRunner.runWebServerDevice( + additionalCommandArgs: [...webDefineArgs, '--release'], + ); + _expectSubstituted(await _fetch(appUrl)); + + await testRunner.hotRestart(); + _expectSubstituted(await _fetch(appUrl)); + } finally { + await testRunner.cleanup(); + } + }); +} From dba040a45935e4fe937c445728a8f3f1260ba96b Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 7 Aug 2026 13:49:06 -0400 Subject: [PATCH 134/330] [flutter_tools] Fix hot reload for workspace member packages in lib/ (#190284) (#190540) Fixes https://github.com/flutter/flutter/issues/190284 When a pub workspace member package is located under the workspace root package's `lib/` directory (e.g., `workspace_root/lib/member_pkg/`), standard `PackageConfig.toPackageUri(fileUri)` performs a prefix-tree lookup that matches shorter outer prefixes first (`workspace_root`). Modifying `workspace_root/lib/member_pkg/lib/foo.dart` resulted in `package:workspace_root/member_pkg/lib/foo.dart` instead of `package:member_pkg/foo.dart`. Because the incorrect URI does not exist in the active isolate's compilation graph, `ResidentCompiler` reported `Reloaded 0 of N libraries` and dropped the edit. This PR adds `PackageConfigWorkspaceExtension.toPackageUriForWorkspace` in `package_map.dart`, implementing longest-prefix matching across all configured packages so that nested workspace member packages resolve to their correct `package:` URIs. ## Testing - Added hermetic regression test `incremental compile sends correct package URI for pub workspace member package located under root lib/` in `packages/flutter_tools/test/general.shard/compile_incremental_test.dart`. - Added unit test in `packages/flutter_tools/test/general.shard/dart/package_map_test.dart`. --- .../lib/src/build_system/targets/web.dart | 2 +- packages/flutter_tools/lib/src/compile.dart | 13 ++-- .../lib/src/dart/package_map.dart | 35 +++++++++++ .../lib/src/isolated/resident_web_runner.dart | 3 +- .../lib/src/isolated/web_asset_server.dart | 5 +- .../compile_incremental_test.dart | 59 ++++++++++++++++++- .../general.shard/dart/package_map_test.dart | 32 ++++++++++ 7 files changed, 139 insertions(+), 10 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 98a05a5384dc4..41b89a8f65246 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -94,7 +94,7 @@ class WebEntrypointTarget extends Target { // does not have an entry for the user's application or if the main file is // outside of the lib/ directory. final String importedEntrypoint = - packageConfig.toPackageUri(importUri)?.toString() ?? importUri.toString(); + packageConfig.toPackageUriForWorkspace(importUri)?.toString() ?? importUri.toString(); await injectBuildTimePluginFilesForWebPlatform( flutterProject, diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index 51d2c2797c7d4..cf8ec8f8c97ef 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -23,6 +23,7 @@ import 'base/utils.dart'; import 'build_info.dart'; import 'bundle.dart'; import 'convert.dart'; +import 'dart/package_map.dart'; /// Opt-in changes to the dart compilers. const kDartCompilerExperiments = []; @@ -291,7 +292,7 @@ class KernelCompiler { final File mainFile = _fileSystem.file(mainPath); final Uri mainFileUri = mainFile.uri; if (packagesPath != null) { - mainUri = packageConfig.toPackageUri(mainFileUri)?.toString(); + mainUri = packageConfig.toPackageUriForWorkspace(mainFileUri)?.toString(); } mainUri ??= toMultiRootPath( mainFileUri, @@ -314,7 +315,7 @@ class KernelCompiler { if (dartPluginRegistrant != null && dartPluginRegistrant.existsSync()) { final Uri dartPluginRegistrantFileUri = dartPluginRegistrant.uri; dartPluginRegistrantUri = - packageConfig.toPackageUri(dartPluginRegistrantFileUri)?.toString() ?? + packageConfig.toPackageUriForWorkspace(dartPluginRegistrantFileUri)?.toString() ?? toMultiRootPath( dartPluginRegistrantFileUri, _fileSystemScheme, @@ -853,13 +854,15 @@ class DefaultResidentCompiler implements ResidentCompiler { _stdoutHandler._suppressCompilerMessages = request.suppressErrors; final String mainUri = - request.packageConfig.toPackageUri(request.mainUri)?.toString() ?? + request.packageConfig.toPackageUriForWorkspace(request.mainUri)?.toString() ?? toMultiRootPath(request.mainUri, fileSystemScheme, fileSystemRoots, _platform.isWindows); String? additionalSourceUri; if (request.additionalSourceUri != null) { additionalSourceUri = - request.packageConfig.toPackageUri(request.additionalSourceUri!)?.toString() ?? + request.packageConfig + .toPackageUriForWorkspace(request.additionalSourceUri!) + ?.toString() ?? toMultiRootPath( request.additionalSourceUri!, fileSystemScheme, @@ -898,7 +901,7 @@ class DefaultResidentCompiler implements ResidentCompiler { message = fileUri.toString(); } else { message = - request.packageConfig.toPackageUri(fileUri)?.toString() ?? + request.packageConfig.toPackageUriForWorkspace(fileUri)?.toString() ?? toMultiRootPath(fileUri, fileSystemScheme, fileSystemRoots, _platform.isWindows); } server.stdin.writeln(message); diff --git a/packages/flutter_tools/lib/src/dart/package_map.dart b/packages/flutter_tools/lib/src/dart/package_map.dart index b7ac029532197..78417c24c26cb 100644 --- a/packages/flutter_tools/lib/src/dart/package_map.dart +++ b/packages/flutter_tools/lib/src/dart/package_map.dart @@ -131,3 +131,38 @@ Future loadPackageConfigWithLogging( } return result; } + +extension PackageConfigWorkspaceExtension on PackageConfig { + /// Converts a [fileUri] to a `package:` URI, finding the most specific + /// package whose [Package.packageUriRoot] is a prefix of [fileUri]. + /// + /// The default [PackageConfig.toPackageUri] may match an outer package first + /// when pub workspace member packages are located under the workspace root + /// package's `lib/` directory. + Uri? toPackageUriForWorkspace(Uri fileUri) { + if (fileUri.isScheme('package')) { + return fileUri; + } + final path = fileUri.toString(); + Package? bestMatch; + String? bestMatchRoot; + + for (final Package package in packages) { + final rootPath = package.packageUriRoot.toString(); + final rootPathWithSlash = rootPath.endsWith('/') ? rootPath : '$rootPath/'; + if (path.startsWith(rootPathWithSlash)) { + if (bestMatchRoot == null || rootPathWithSlash.length > bestMatchRoot.length) { + bestMatch = package; + bestMatchRoot = rootPathWithSlash; + } + } + } + + if (bestMatch == null || bestMatchRoot == null) { + return null; + } + + final String rest = path.substring(bestMatchRoot.length); + return Uri(scheme: 'package', path: '${bestMatch.name}/$rest'); + } +} diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 69cd2dc942931..8f76a0842b708 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -26,6 +26,7 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; import '../dart/language_version.dart'; +import '../dart/package_map.dart'; import '../devfs.dart'; import '../device.dart'; import '../flutter_plugins.dart'; @@ -712,7 +713,7 @@ class ResidentWebRunner extends ResidentRunner { // the web_plugin_registrant.dart file alongside the generated main.dart const generatedImport = 'web_plugin_registrant.dart'; - Uri? importedEntrypoint = packageConfig!.toPackageUri(mainUri); + Uri? importedEntrypoint = packageConfig!.toPackageUriForWorkspace(mainUri); // Special handling for entrypoints that are not under lib, such as test scripts. if (importedEntrypoint == null) { final String parent = _fileSystem.file(mainUri).parent.path; diff --git a/packages/flutter_tools/lib/src/isolated/web_asset_server.dart b/packages/flutter_tools/lib/src/isolated/web_asset_server.dart index 8ba8c0f06a65a..640925b965f51 100644 --- a/packages/flutter_tools/lib/src/isolated/web_asset_server.dart +++ b/packages/flutter_tools/lib/src/isolated/web_asset_server.dart @@ -25,6 +25,7 @@ import '../base/platform.dart'; import '../build_info.dart'; import '../cache.dart'; import '../convert.dart'; +import '../dart/package_map.dart'; import '../globals.dart' as globals; import '../web/bootstrap.dart'; import '../web/chrome.dart'; @@ -347,7 +348,7 @@ class WebAssetServer implements AssetReader { PackageUriMapper(packageConfig), digestProvider, BuildSettings( - appEntrypoint: packageConfig.toPackageUri( + appEntrypoint: packageConfig.toPackageUriForWorkspace( fileSystem.file(entrypoint).absolute.uri, ), canaryFeatures: canaryFeatures, @@ -361,7 +362,7 @@ class WebAssetServer implements AssetReader { PackageUriMapper(packageConfig), digestProvider, BuildSettings( - appEntrypoint: packageConfig.toPackageUri( + appEntrypoint: packageConfig.toPackageUriForWorkspace( fileSystem.file(entrypoint).absolute.uri, ), canaryFeatures: canaryFeatures, diff --git a/packages/flutter_tools/test/general.shard/compile_incremental_test.dart b/packages/flutter_tools/test/general.shard/compile_incremental_test.dart index d1881a0e19018..0ec1a5998f11d 100644 --- a/packages/flutter_tools/test/general.shard/compile_incremental_test.dart +++ b/packages/flutter_tools/test/general.shard/compile_incremental_test.dart @@ -131,6 +131,62 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); }); + testWithoutContext( + 'incremental compile sends correct package URI for pub workspace member package located under root lib/', + () async { + final packageConfig = PackageConfig([ + Package( + 'root', + Uri.parse('file:///workspace/'), + packageUriRoot: Uri.parse('file:///workspace/lib/'), + ), + Package( + 'member', + Uri.parse('file:///workspace/lib/member/'), + packageUriRoot: Uri.parse('file:///workspace/lib/member/lib/'), + ), + ]); + + fakeProcessManager.addCommand( + FakeCommand( + command: const [ + ...frontendServerCommand, + '--initialize-from-dill', + expectedCachePath, + '--verbosity=error', + ], + stdout: 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0', + stdin: frontendServerStdIn, + ), + ); + + await generator.recompile( + Uri.parse('file:///workspace/lib/main.dart'), + null, + outputPath: '/build/', + packageConfig: packageConfig, + fs: MemoryFileSystem(), + projectRootPath: '', + ); + expect(frontendServerStdIn.getAndClear(), 'compile package:root/main.dart\n'); + + await _accept(generator, frontendServerStdIn, ''); + await _reject(generatorStdoutHandler, generator, frontendServerStdIn, '', ''); + + await _recompile( + generatorStdoutHandler, + generator, + frontendServerStdIn, + 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', + mainUri: Uri.parse('file:///workspace/lib/main.dart'), + expectedMainUri: 'package:root/main.dart', + updatedUris: [Uri.parse('file:///workspace/lib/member/lib/foo.dart')], + expectedUpdatedUris: ['package:member/foo.dart'], + packageConfig: packageConfig, + ); + }, + ); + testWithoutContext('incremental compile single dart compile with filesystem scheme', () async { fakeProcessManager.addCommand( FakeCommand( @@ -648,6 +704,7 @@ Future _recompile( String expectedMainUri = '/path/to/main.dart', List? updatedUris, List? expectedUpdatedUris, + PackageConfig? packageConfig, }) async { mainUri ??= Uri.parse('/path/to/main.dart'); updatedUris ??= [mainUri]; @@ -657,7 +714,7 @@ Future _recompile( mainUri, updatedUris, outputPath: '/build/', - packageConfig: PackageConfig.empty, + packageConfig: packageConfig ?? PackageConfig.empty, suppressErrors: suppressErrors, fs: MemoryFileSystem(), projectRootPath: '', diff --git a/packages/flutter_tools/test/general.shard/dart/package_map_test.dart b/packages/flutter_tools/test/general.shard/dart/package_map_test.dart index 3f695c9990014..bbee6d58eb4af 100644 --- a/packages/flutter_tools/test/general.shard/dart/package_map_test.dart +++ b/packages/flutter_tools/test/general.shard/dart/package_map_test.dart @@ -135,4 +135,36 @@ void main() { }, ); }); + + group('PackageConfigWorkspaceExtension', () { + testWithoutContext('toPackageUriForWorkspace finds most specific package in pub workspace', () { + final packageConfig = PackageConfig([ + Package( + 'root', + Uri.parse('file:///workspace/'), + packageUriRoot: Uri.parse('file:///workspace/lib/'), + ), + Package( + 'member', + Uri.parse('file:///workspace/lib/member/'), + packageUriRoot: Uri.parse('file:///workspace/lib/member/lib/'), + ), + ]); + + expect( + packageConfig.toPackageUriForWorkspace(Uri.parse('file:///workspace/lib/foo.dart')), + Uri.parse('package:root/foo.dart'), + ); + expect( + packageConfig.toPackageUriForWorkspace( + Uri.parse('file:///workspace/lib/member/lib/bar.dart'), + ), + Uri.parse('package:member/bar.dart'), + ); + expect( + packageConfig.toPackageUriForWorkspace(Uri.parse('package:member/bar.dart')), + Uri.parse('package:member/bar.dart'), + ); + }); + }); } From 0feeeab3a945c689d8a6c0e37e6176b7e3ae52a4 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 7 Aug 2026 12:39:26 -0700 Subject: [PATCH 135/330] Move `examples/api` to `packages/flutter/examples/api` and fix CI errors (#190481) This PR moves API examples from `/examples/api` to `/packages/flutter/examples/api`. This change is required because the standard dartdoc directive `{@example}` requires API examples to reside within the package directory. For more details, see issue https://github.com/flutter/flutter/issues/189629 (item 1 under "What's needed"). ### Summary of changes Moved all API example files to `packages/flutter/examples/api`. Updated related scripts and CI configurations to reflect the new directory structure. - Most changes involve simple path updates from `examples/api` to `packages/flutter/examples/api`. - Some adjustments were more complex because the examples, which previously resided in a single `/examples` directory, are now split across two separate locations. Existing paths in `"See code in "` directives are retained. These paths are now treated as relative to the package root rather than the repository root. For example, the following API doc comment remains valid despite the file move: ```dart /// Class definition. /// /// {@tool sample} /// An example. /// /// ** See code in examples/api/lib/widgets/foo/foo.0.dart ** /// {@end-tool} ``` Three API doc tests are now marked as `reduced-test-set` because they're now facing additional analysis for being under `packages/flutter`. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .github/labeler.yml | 4 +- .github/workflows/freeze.yml | 8 +- dev/bots/analyze.dart | 2 +- dev/bots/analyze_snippet_code.dart | 13 +- dev/bots/check_code_samples.dart | 14 +- dev/bots/check_examples_cross_imports.dart | 1124 +++++++++-------- dev/bots/cross_imports_checker_utils.dart | 3 +- .../suite_runners/run_framework_tests.dart | 11 +- .../analyze-test-input/ktlint-baseline.xml | 2 +- dev/bots/test/check_code_samples_test.dart | 38 +- .../check_examples_cross_imports_test.dart | 139 +- dev/snippets/lib/src/snippet_parser.dart | 2 +- dev/snippets/test/snippet_parser_test.dart | 9 +- dev/snippets/test/snippets_test.dart | 4 +- dev/tools/examples_smoke_test.dart | 14 +- .../Style-guide-for-Flutter-repo.md | 4 +- .../flutter/examples}/api/.gitignore | 0 .../flutter/examples}/api/.metadata | 0 .../flutter/examples}/api/README.md | 0 .../examples}/api/analysis_options.yaml | 0 .../animated_digit.0.dart | 0 .../api/lib/animation/curves/curve2_d.0.dart | 0 .../cupertino_activity_indicator.0.dart | 0 ...cupertino_linear_activity_indicator.0.dart | 0 .../bottom_tab_bar/cupertino_tab_bar.0.dart | 0 .../cupertino/button/cupertino_button.0.dart | 0 .../checkbox/cupertino_checkbox.0.dart | 0 .../cupertino_context_menu.0.dart | 0 .../cupertino_context_menu.1.dart | 0 .../date_picker/cupertino_date_picker.0.dart | 0 .../date_picker/cupertino_timer_picker.0.dart | 0 .../dialog/cupertino_action_sheet.0.dart | 0 .../dialog/cupertino_alert_dialog.0.dart | 0 .../dialog/cupertino_popup_surface.0.dart | 0 .../cupertino_expansion_tile.0.dart | 0 .../form_row/cupertino_form_row.0.dart | 0 .../list_section/list_section_base.0.dart | 0 .../list_section/list_section_inset.0.dart | 0 .../list_tile/cupertino_list_tile.0.dart | 0 .../magnifier/cupertino_magnifier.0.dart | 0 .../magnifier/cupertino_text_magnifier.0.dart | 0 .../cupertino/menu_anchor/menu_anchor.0.dart | 0 .../cupertino/menu_anchor/menu_anchor.1.dart | 0 .../nav_bar/cupertino_navigation_bar.0.dart | 0 .../nav_bar/cupertino_navigation_bar.1.dart | 0 .../nav_bar/cupertino_navigation_bar.2.dart | 0 .../nav_bar/cupertino_sliver_nav_bar.0.dart | 0 .../nav_bar/cupertino_sliver_nav_bar.1.dart | 0 .../nav_bar/cupertino_sliver_nav_bar.2.dart | 0 .../cupertino_page_scaffold.0.dart | 0 .../cupertino/picker/cupertino_picker.0.dart | 0 .../cupertino/radio/cupertino_radio.0.dart | 0 .../radio/cupertino_radio.toggleable.0.dart | 0 .../cupertino_sliver_refresh_control.0.dart | 0 .../route/show_cupertino_dialog.0.dart | 0 .../route/show_cupertino_modal_popup.0.dart | 0 .../scrollbar/cupertino_scrollbar.0.dart | 0 .../scrollbar/cupertino_scrollbar.1.dart | 0 .../cupertino_search_field.0.dart | 0 .../cupertino_search_field.1.dart | 0 .../cupertino_segmented_control.0.dart | 0 ...cupertino_sliding_segmented_control.0.dart | 0 .../cupertino/sheet/cupertino_sheet.0.dart | 0 .../cupertino/sheet/cupertino_sheet.1.dart | 0 .../cupertino/sheet/cupertino_sheet.2.dart | 0 .../cupertino/sheet/cupertino_sheet.3.dart | 0 .../cupertino/slider/cupertino_slider.0.dart | 0 .../cupertino/switch/cupertino_switch.0.dart | 0 .../cupertino_tab_controller.0.dart | 0 .../cupertino_tab_scaffold.0.dart | 0 .../text_field/cupertino_text_field.0.dart | 0 .../cupertino_text_form_field_row.1.dart | 0 .../api/lib/foundation/key/value_key.0.dart | 0 .../pointer_signal_resolver.0.dart | 0 .../gestures/tap_and_drag/tap_and_drag.0.dart | 0 .../lib/material/about/about_list_tile.0.dart | 0 .../action_buttons/action_icon_theme.0.dart | 0 .../material/action_chip/action_chip.0.dart | 0 .../animated_icon/animated_icon.0.dart | 0 .../animated_icon/animated_icons_data.0.dart | 0 .../examples}/api/lib/material/app/app.0.dart | 0 .../api/lib/material/app_bar/app_bar.0.dart | 0 .../api/lib/material/app_bar/app_bar.1.dart | 0 .../api/lib/material/app_bar/app_bar.2.dart | 0 .../api/lib/material/app_bar/app_bar.3.dart | 0 .../api/lib/material/app_bar/app_bar.4.dart | 0 .../material/app_bar/sliver_app_bar.1.dart | 0 .../material/app_bar/sliver_app_bar.2.dart | 0 .../material/app_bar/sliver_app_bar.3.dart | 0 .../material/app_bar/sliver_app_bar.4.dart | 0 .../material/autocomplete/autocomplete.0.dart | 0 .../material/autocomplete/autocomplete.1.dart | 0 .../material/autocomplete/autocomplete.2.dart | 0 .../material/autocomplete/autocomplete.3.dart | 0 .../material/autocomplete/autocomplete.4.dart | 0 .../api/lib/material/badge/badge.0.dart | 0 .../material/banner/material_banner.0.dart | 0 .../material/banner/material_banner.1.dart | 0 .../bottom_app_bar/bottom_app_bar.1.dart | 0 .../bottom_app_bar/bottom_app_bar.2.dart | 0 .../bottom_navigation_bar.0.dart | 0 .../bottom_navigation_bar.1.dart | 0 .../bottom_navigation_bar.2.dart | 0 .../bottom_sheet/show_bottom_sheet.0.dart | 0 .../show_modal_bottom_sheet.0.dart | 0 .../show_modal_bottom_sheet.1.dart | 0 .../show_modal_bottom_sheet.2.dart | 0 .../material/button_style/button_style.0.dart | 0 .../api/lib/material/card/card.0.dart | 0 .../api/lib/material/card/card.1.dart | 0 .../api/lib/material/card/card.2.dart | 0 .../api/lib/material/carousel/carousel.0.dart | 0 .../api/lib/material/carousel/carousel.1.dart | 0 .../api/lib/material/checkbox/checkbox.0.dart | 0 .../api/lib/material/checkbox/checkbox.1.dart | 0 .../checkbox_list_tile.0.dart | 0 .../checkbox_list_tile.1.dart | 0 .../custom_labeled_checkbox.0.dart | 0 .../custom_labeled_checkbox.1.dart | 0 ...p_attributes.avatar_box_constraints.0.dart | 0 ...hip_attributes.chip_animation_style.0.dart | 0 ...ributes.delete_icon_box_constraints.0.dart | 0 ...eletable_chip_attributes.on_deleted.0.dart | 0 .../material/choice_chip/choice_chip.0.dart | 0 .../material/color_scheme/color_scheme.0.dart | 0 .../color_scheme/dynamic_content_color.0.dart | 0 .../editable_text_toolbar_builder.2.dart | 0 .../selectable_region_toolbar_builder.0.dart | 0 .../lib/material/data_table/data_table.0.dart | 0 .../lib/material/data_table/data_table.1.dart | 0 .../custom_calendar_date_picker.0.dart | 0 .../date_picker_theme_day_shape.0.dart | 0 .../date_picker/show_date_picker.0.dart | 0 .../date_picker/show_date_picker.1.dart | 0 .../date_picker/show_date_range_picker.0.dart | 0 .../dialog/adaptive_alert_dialog.0.dart | 0 .../lib/material/dialog/alert_dialog.0.dart | 0 .../lib/material/dialog/alert_dialog.1.dart | 0 .../api/lib/material/dialog/dialog.0.dart | 0 .../lib/material/dialog/show_dialog.0.dart | 0 .../lib/material/dialog/show_dialog.1.dart | 0 .../lib/material/dialog/show_dialog.2.dart | 0 .../api/lib/material/divider/divider.0.dart | 0 .../api/lib/material/divider/divider.1.dart | 0 .../material/divider/vertical_divider.0.dart | 0 .../material/divider/vertical_divider.1.dart | 0 .../api/lib/material/drawer/drawer.0.dart | 0 .../material/dropdown/dropdown_button.0.dart | 0 ...opdown_button.selected_item_builder.0.dart | 0 .../dropdown/dropdown_button.style.0.dart | 0 .../dropdown_menu/dropdown_menu.0.dart | 0 .../dropdown_menu/dropdown_menu.1.dart | 0 .../dropdown_menu/dropdown_menu.2.dart | 0 .../dropdown_menu_entry_label_widget.0.dart | 0 .../elevated_button/elevated_button.0.dart | 0 .../expansion_panel_list.0.dart | 0 ...nel_list.expansion_panel_list_radio.0.dart | 0 .../expansion_tile/expansion_tile.0.dart | 0 .../expansion_tile/expansion_tile.1.dart | 0 .../expansion_tile/expansion_tile.2.dart | 0 .../filled_button/filled_button.0.dart | 0 .../material/filter_chip/filter_chip.0.dart | 0 .../flexible_space_bar.0.dart | 0 .../floating_action_button.0.dart | 0 .../floating_action_button.1.dart | 0 .../floating_action_button.2.dart | 0 .../standard_fab_location.0.dart | 0 .../icon_alignment/icon_alignment.0.dart | 0 .../material/icon_button/icon_button.0.dart | 0 .../material/icon_button/icon_button.1.dart | 0 .../material/icon_button/icon_button.2.dart | 0 .../material/icon_button/icon_button.3.dart | 0 .../lib/material/ink/ink.image_clip.0.dart | 0 .../lib/material/ink/ink.image_clip.1.dart | 0 .../api/lib/material/ink_well/ink_well.0.dart | 0 .../lib/material/input_chip/input_chip.0.dart | 0 .../lib/material/input_chip/input_chip.1.dart | 0 .../input_decorator/input_decoration.0.dart | 0 .../input_decorator/input_decoration.1.dart | 0 .../input_decorator/input_decoration.2.dart | 0 .../input_decorator/input_decoration.3.dart | 0 ...coration.floating_label_style_error.0.dart | 0 .../input_decoration.helper.0.dart | 0 .../input_decoration.label.0.dart | 0 .../input_decoration.label_style_error.0.dart | 0 .../input_decoration.prefix_icon.0.dart | 0 ..._decoration.prefix_icon_constraints.0.dart | 0 .../input_decoration.suffix_icon.0.dart | 0 ..._decoration.suffix_icon_constraints.0.dart | 0 .../input_decoration.widget_state.0.dart | 0 .../input_decoration.widget_state.1.dart | 0 .../list_tile/custom_list_item.0.dart | 0 .../list_tile/custom_list_item.1.dart | 0 .../lib/material/list_tile/list_tile.0.dart | 0 .../lib/material/list_tile/list_tile.1.dart | 0 .../lib/material/list_tile/list_tile.2.dart | 0 .../lib/material/list_tile/list_tile.3.dart | 0 .../lib/material/list_tile/list_tile.4.dart | 0 .../list_tile/list_tile.selected.0.dart | 0 .../material_state_border_side.0.dart | 0 .../material_state_mouse_cursor.0.dart | 0 .../material_state_property.0.dart | 0 .../menu_anchor/checkbox_menu_button.0.dart | 0 .../menu_anchor/menu_accelerator_label.0.dart | 0 .../material/menu_anchor/menu_anchor.0.dart | 0 .../material/menu_anchor/menu_anchor.1.dart | 0 .../material/menu_anchor/menu_anchor.2.dart | 0 .../material/menu_anchor/menu_anchor.3.dart | 0 .../lib/material/menu_anchor/menu_bar.0.dart | 0 .../menu_anchor/radio_menu_button.0.dart | 0 .../navigation_bar/navigation_bar.0.dart | 0 .../navigation_bar/navigation_bar.1.dart | 0 .../navigation_bar/navigation_bar.2.dart | 0 .../navigation_drawer.0.dart | 0 .../navigation_rail/navigation_rail.0.dart | 0 .../navigation_rail.extended_animation.0.dart | 0 .../outlined_button/outlined_button.0.dart | 0 .../page_transitions_theme.0.dart | 0 .../page_transitions_theme.1.dart | 0 .../page_transitions_theme.3.dart | 0 .../paginated_data_table.0.dart | 0 .../paginated_data_table.1.dart | 0 .../lib/material/popup_menu/popup_menu.0.dart | 0 .../lib/material/popup_menu/popup_menu.1.dart | 0 .../lib/material/popup_menu/popup_menu.2.dart | 0 .../circular_progress_indicator.0.dart | 0 .../circular_progress_indicator.1.dart | 0 .../circular_progress_indicator.2.dart | 0 .../linear_progress_indicator.0.dart | 0 .../linear_progress_indicator.1.dart | 0 .../api/lib/material/radio/radio.0.dart | 0 .../api/lib/material/radio/radio.1.dart | 0 .../material/radio/radio.toggleable.0.dart | 0 .../custom_labeled_radio.0.dart | 0 .../custom_labeled_radio.1.dart | 0 .../radio_list_tile/radio_list_tile.0.dart | 0 .../radio_list_tile/radio_list_tile.1.dart | 0 .../radio_list_tile.toggleable.0.dart | 0 .../material/range_slider/range_slider.0.dart | 0 .../refresh_indicator.0.dart | 0 .../refresh_indicator.1.dart | 0 .../refresh_indicator.2.dart | 0 .../reorderable_list_view.0.dart | 0 .../reorderable_list_view.1.dart | 0 .../reorderable_list_view.2.dart | 0 ...ist_view.build_default_drag_handles.0.dart | 0 ..._view.reorderable_list_view_builder.0.dart | 0 .../api/lib/material/scaffold/scaffold.0.dart | 0 .../api/lib/material/scaffold/scaffold.1.dart | 0 .../api/lib/material/scaffold/scaffold.2.dart | 0 .../material/scaffold/scaffold.drawer.0.dart | 0 .../scaffold/scaffold.end_drawer.0.dart | 0 ...old.floating_action_button_animator.0.dart | 0 .../lib/material/scaffold/scaffold.of.0.dart | 0 .../lib/material/scaffold/scaffold.of.1.dart | 0 .../scaffold/scaffold_messenger.0.dart | 0 .../scaffold/scaffold_messenger.of.0.dart | 0 .../scaffold/scaffold_messenger.of.1.dart | 0 ...essenger_state.show_material_banner.0.dart | 0 ...fold_messenger_state.show_snack_bar.0.dart | 0 ...fold_messenger_state.show_snack_bar.1.dart | 0 ...fold_messenger_state.show_snack_bar.2.dart | 0 .../scaffold_state.show_bottom_sheet.0.dart | 0 .../scaffold_state.show_bottom_sheet.1.dart | 0 .../lib/material/scrollbar/scrollbar.0.dart | 0 .../lib/material/scrollbar/scrollbar.1.dart | 0 .../search_anchor/search_anchor.0.dart | 0 .../search_anchor/search_anchor.1.dart | 0 .../search_anchor/search_anchor.2.dart | 0 .../search_anchor/search_anchor.3.dart | 0 .../search_anchor/search_anchor.4.dart | 0 .../material/search_anchor/search_bar.0.dart | 0 .../segmented_button/segmented_button.0.dart | 0 .../segmented_button/segmented_button.1.dart | 0 .../selection_area/selection_area.0.dart | 0 .../selection_area/selection_area.1.dart | 0 .../selection_area/selection_area.2.dart | 0 .../shaped_input_border.0.dart | 0 .../api/lib/material/slider/slider.0.dart | 0 .../api/lib/material/slider/slider.1.dart | 0 .../lib/material/snack_bar/snack_bar.0.dart | 0 .../lib/material/snack_bar/snack_bar.1.dart | 0 .../lib/material/snack_bar/snack_bar.2.dart | 0 .../lib/material/stepper/step_style.0.dart | 0 .../api/lib/material/stepper/stepper.0.dart | 0 .../stepper/stepper.controls_builder.0.dart | 0 .../api/lib/material/switch/switch.0.dart | 0 .../api/lib/material/switch/switch.1.dart | 0 .../api/lib/material/switch/switch.2.dart | 0 .../api/lib/material/switch/switch.3.dart | 0 .../api/lib/material/switch/switch.4.dart | 0 .../custom_labeled_switch.0.dart | 0 .../custom_labeled_switch.1.dart | 0 .../switch_list_tile/switch_list_tile.0.dart | 0 .../switch_list_tile/switch_list_tile.1.dart | 0 .../tab_controller/tab_controller.1.dart | 0 .../api/lib/material/tabs/tab_bar.0.dart | 0 .../api/lib/material/tabs/tab_bar.1.dart | 0 .../api/lib/material/tabs/tab_bar.2.dart | 0 .../api/lib/material/tabs/tab_bar.3.dart | 0 .../tabs/tab_bar.indicator_animation.0.dart | 0 .../material/tabs/tab_bar.onFocusChange.dart | 0 .../lib/material/tabs/tab_bar.onHover.dart | 0 .../material/text_button/text_button.0.dart | 0 .../material/text_button/text_button.1.dart | 0 .../lib/material/text_field/text_field.0.dart | 0 .../lib/material/text_field/text_field.1.dart | 0 .../lib/material/text_field/text_field.2.dart | 0 .../lib/material/text_field/text_field.3.dart | 0 .../text_form_field/text_form_field.1.dart | 0 .../text_form_field/text_form_field.2.dart | 0 .../lib/material/theme/theme_extension.1.dart | 0 .../lib/material/theme_data/theme_data.0.dart | 0 .../time_picker/show_time_picker.0.dart | 0 .../toggle_buttons/toggle_buttons.0.dart | 0 .../toggle_buttons/toggle_buttons.1.dart | 0 .../api/lib/material/tooltip/tooltip.0.dart | 0 .../api/lib/material/tooltip/tooltip.1.dart | 0 .../api/lib/material/tooltip/tooltip.2.dart | 0 .../api/lib/material/tooltip/tooltip.3.dart | 0 .../widget_state_input_border.0.dart | 0 .../axis_direction/axis_direction.0.dart | 0 .../borders/border_side.stroke_align.0.dart | 0 .../painting/gradient/linear_gradient.0.dart | 0 .../image_provider/image_provider.0.dart | 0 .../linear_border/linear_border.0.dart | 0 .../rounded_superellipse_border.0.dart | 0 .../painting/star_border/star_border.0.dart | 0 .../api/lib/rendering/box/parent_data.0.dart | 0 .../growth_direction/growth_direction.0.dart | 0 .../scroll_direction/scroll_direction.0.dart | 0 ...elegate_with_fixed_cross_axis_count.0.dart | 0 ...elegate_with_fixed_cross_axis_count.1.dart | 0 .../api/lib/sample_templates/cupertino.0.dart | 0 .../api/lib/sample_templates/material.0.dart | 0 .../api/lib/sample_templates/widgets.0.dart | 0 .../binding/handle_request_app_exit.0.dart | 0 .../keyboard_key/logical_keyboard_key.0.dart | 0 .../keyboard_key/physical_keyboard_key.0.dart | 0 .../services/mouse_cursor/mouse_cursor.0.dart | 0 ...chrome.set_system_u_i_overlay_style.0.dart | 0 ...chrome.set_system_u_i_overlay_style.1.dart | 0 .../text_input/text_input_control.0.dart | 0 .../api/lib/ui/text/font_feature.0.dart | 0 ...nt_feature.font_feature_alternative.0.dart | 0 ....font_feature_alternative_fractions.0.dart | 0 ...e.font_feature_case_sensitive_forms.0.dart | 0 ...ture.font_feature_character_variant.0.dart | 0 ....font_feature_contextual_alternates.0.dart | 0 ...nt_feature.font_feature_denominator.0.dart | 0 ...font_feature.font_feature_fractions.0.dart | 0 ...ature.font_feature_historical_forms.0.dart | 0 ...e.font_feature_historical_ligatures.0.dart | 0 ...feature.font_feature_lining_figures.0.dart | 0 ...t_feature.font_feature_locale_aware.0.dart | 0 ...ature.font_feature_notational_forms.0.dart | 0 ...ont_feature.font_feature_numerators.0.dart | 0 ...ature.font_feature_oldstyle_figures.0.dart | 0 ..._feature.font_feature_ordinal_forms.0.dart | 0 ...e.font_feature_proportional_figures.0.dart | 0 ...e.font_feature_scientific_inferiors.0.dart | 0 ...t_feature.font_feature_slashed_zero.0.dart | 0 ...e.font_feature_stylistic_alternates.0.dart | 0 ..._feature.font_feature_stylistic_set.0.dart | 0 ..._feature.font_feature_stylistic_set.1.dart | 0 ...ont_feature.font_feature_subscripts.0.dart | 0 ...t_feature.font_feature_superscripts.0.dart | 0 .../font_feature.font_feature_swash.0.dart | 0 ...eature.font_feature_tabular_figures.0.dart | 0 .../actions/action.action_overridable.0.dart | 0 .../widgets/actions/action_listener.0.dart | 0 .../api/lib/widgets/actions/actions.0.dart | 0 .../actions/focusable_action_detector.0.dart | 0 .../animated_grid/animated_grid.0.dart | 0 .../animated_grid/sliver_animated_grid.0.dart | 0 .../animated_list/animated_list.0.dart | 0 .../animated_list_separated.0.dart | 0 .../animated_list/sliver_animated_list.0.dart | 0 .../animated_size/animated_size.0.dart | 0 .../animated_switcher.0.dart | 0 .../app/widgets_app.widgets_app.0.dart | 0 .../app_lifecycle_listener.0.dart | 0 .../app_lifecycle_listener.1.dart | 0 .../lib/widgets/async/future_builder.0.dart | 0 .../lib/widgets/async/stream_builder.0.dart | 0 .../autocomplete/raw_autocomplete.0.dart | 0 .../autocomplete/raw_autocomplete.1.dart | 0 .../autocomplete/raw_autocomplete.2.dart | 0 .../raw_autocomplete.focus_node.0.dart | 0 .../widgets/autofill/autofill_group.0.dart | 0 .../lib/widgets/basic/absorb_pointer.0.dart | 0 .../api/lib/widgets/basic/aspect_ratio.0.dart | 0 .../api/lib/widgets/basic/aspect_ratio.1.dart | 0 .../api/lib/widgets/basic/aspect_ratio.2.dart | 0 .../api/lib/widgets/basic/clip_rrect.0.dart | 0 .../api/lib/widgets/basic/clip_rrect.1.dart | 0 .../basic/custom_multi_child_layout.0.dart | 0 .../api/lib/widgets/basic/expanded.0.dart | 0 .../api/lib/widgets/basic/expanded.1.dart | 0 .../api/lib/widgets/basic/fitted_box.0.dart | 0 .../api/lib/widgets/basic/flow.0.dart | 0 .../basic/fractionally_sized_box.0.dart | 0 .../lib/widgets/basic/ignore_pointer.0.dart | 0 .../lib/widgets/basic/indexed_stack.0.dart | 0 .../api/lib/widgets/basic/listener.0.dart | 0 .../api/lib/widgets/basic/mouse_region.0.dart | 0 .../widgets/basic/mouse_region.on_exit.0.dart | 0 .../widgets/basic/mouse_region.on_exit.1.dart | 0 .../api/lib/widgets/basic/offstage.0.dart | 0 .../api/lib/widgets/basic/overflowbox.0.dart | 0 .../lib/widgets/basic/physical_shape.0.dart | 0 .../binding/widget_binding_observer.0.dart | 0 .../color_filter/color_filtered.0.dart | 0 .../context_menu_controller.0.dart | 0 .../editable_text_toolbar_builder.0.dart | 0 .../editable_text_toolbar_builder.1.dart | 0 .../widgets/dismissible/dismissible.0.dart | 0 .../lib/widgets/drag_target/draggable.0.dart | 0 .../draggable_scrollable_sheet.0.dart | 0 .../editable_text.on_changed.0.dart | 0 .../editable_text.on_content_inserted.0.dart | 0 .../text_editing_controller.0.dart | 0 .../text_editing_controller.1.dart | 0 .../lib/widgets/expansible/expansible.0.dart | 0 .../widgets/focus_manager/focus_node.0.dart | 0 .../focus_manager/focus_node.unfocus.0.dart | 0 .../api/lib/widgets/focus_scope/focus.0.dart | 0 .../api/lib/widgets/focus_scope/focus.1.dart | 0 .../api/lib/widgets/focus_scope/focus.2.dart | 0 .../widgets/focus_scope/focus_scope.0.dart | 0 .../focus_traversal_group.0.dart | 0 .../ordered_traversal_policy.0.dart | 0 .../api/lib/widgets/form/form.0.dart | 0 .../api/lib/widgets/form/form.1.dart | 0 .../lib/widgets/framework/build_owner.0.dart | 0 .../lib/widgets/framework/error_widget.0.dart | 0 .../gesture_detector/gesture_detector.0.dart | 0 .../gesture_detector/gesture_detector.1.dart | 0 .../gesture_detector/gesture_detector.2.dart | 0 .../gesture_detector/gesture_detector.3.dart | 0 .../key_event_manager.0.dart | 0 .../api/lib/widgets/heroes/hero.0.dart | 0 .../api/lib/widgets/heroes/hero.1.dart | 0 .../widgets/image/image.error_builder.0.dart | 0 .../widgets/image/image.frame_builder.0.dart | 0 .../image/image.loading_builder.0.dart | 0 .../implicit_animations/animated_align.0.dart | 0 .../animated_container.0.dart | 0 .../animated_fractionally_sized_box.0.dart | 0 .../animated_padding.0.dart | 0 .../animated_positioned.0.dart | 0 .../implicit_animations/animated_slide.0.dart | 0 .../sliver_animated_opacity.0.dart | 0 .../inherited_model/inherited_model.0.dart | 0 .../inherited_notifier.0.dart | 0 .../inherited_theme/inherited_theme.0.dart | 0 .../interactive_viewer.0.dart | 0 .../interactive_viewer.builder.0.dart | 0 .../interactive_viewer.constrained.0.dart | 0 ...ve_viewer.transformation_controller.0.dart | 0 .../keep_alive/automatic_keep_alive.0.dart | 0 .../automatic_keep_alive_client_mixin.0.dart | 0 .../lib/widgets/keep_alive/keep_alive.0.dart | 0 .../layout_builder/layout_builder.0.dart | 0 .../lib/widgets/magnifier/magnifier.0.dart | 0 ...ia_query_data.system_gesture_insets.0.dart | 0 .../lib/widgets/navigator/navigator.0.dart | 0 .../navigator.restorable_push.0.dart | 0 ...or.restorable_push_and_remove_until.0.dart | 0 ...vigator.restorable_push_replacement.0.dart | 0 .../navigator_state.restorable_push.0.dart | 0 ...te.restorable_push_and_remove_until.0.dart | 0 ...r_state.restorable_push_replacement.0.dart | 0 .../navigator/restorable_route_future.0.dart | 0 .../navigator_pop_handler.0.dart | 0 .../navigator_pop_handler.1.dart | 0 .../nested_scroll_view.0.dart | 0 .../nested_scroll_view.1.dart | 0 .../nested_scroll_view.2.dart | 0 .../nested_scroll_view_state.0.dart | 0 .../notification_listener/notification.0.dart | 0 .../widgets/overflow_bar/overflow_bar.0.dart | 0 .../api/lib/widgets/overlay/overlay.0.dart | 0 .../lib/widgets/overlay/overlay_portal.0.dart | 0 .../lib/widgets/overlay/overlay_portal.1.dart | 0 .../glowing_overscroll_indicator.0.dart | 0 .../glowing_overscroll_indicator.1.dart | 0 .../api/lib/widgets/page/page_can_pop.0.dart | 0 .../widgets/page_storage/page_storage.0.dart | 0 .../page_transitions_builder.0.dart | 0 .../lib/widgets/page_view/page_view.0.dart | 0 .../lib/widgets/page_view/page_view.1.dart | 0 .../platform_menu_bar.0.dart | 0 .../lib/widgets/pop_scope/pop_scope.0.dart | 0 .../lib/widgets/pop_scope/pop_scope.1.dart | 0 .../preferred_size/preferred_size.0.dart | 0 .../widgets/radio_group/radio_group.0.dart | 0 .../raw_menu_anchor/raw_menu_anchor.0.dart | 0 .../raw_menu_anchor/raw_menu_anchor.1.dart | 0 .../raw_menu_anchor/raw_menu_anchor.2.dart | 0 .../raw_menu_anchor/raw_menu_anchor.3.dart | 0 .../widgets/raw_tooltip/raw_tooltip.0.dart | 0 .../repeating_animation_builder.0.dart | 0 .../restoration/restoration_mixin.0.dart | 0 .../restorable_value.0.dart | 0 .../routes/flexible_route_transitions.0.dart | 0 .../routes/flexible_route_transitions.1.dart | 0 .../widgets/routes/local_history_entry.0.dart | 0 .../api/lib/widgets/routes/popup_route.0.dart | 0 .../lib/widgets/routes/route_observer.0.dart | 0 .../widgets/routes/show_general_dialog.0.dart | 0 .../lib/widgets/safe_area/safe_area.0.dart | 0 .../scroll_end_notification.0.dart | 0 .../scroll_end_notification.1.dart | 0 .../scroll_notification_observer.0.dart | 0 .../is_scrolling_listener.0.dart | 0 .../scroll_controller_notification.0.dart | 0 .../scroll_controller_on_attach.0.dart | 0 .../scroll_metrics_notification.0.dart | 0 .../scroll_view/custom_scroll_view.1.dart | 0 .../lib/widgets/scroll_view/grid_view.0.dart | 0 .../lib/widgets/scroll_view/list_view.0.dart | 0 .../lib/widgets/scroll_view/list_view.1.dart | 0 .../widgets/scrollbar/raw_scrollbar.0.dart | 0 .../widgets/scrollbar/raw_scrollbar.1.dart | 0 .../widgets/scrollbar/raw_scrollbar.2.dart | 0 .../scrollbar/raw_scrollbar.desktop.0.dart | 0 .../scrollbar/raw_scrollbar.shape.0.dart | 0 .../selectable_region.0.dart | 0 .../selection_container.0.dart | 0 .../selection_container_disabled.0.dart | 0 .../sensitive_content.0.dart | 0 .../shared_app_data/shared_app_data.0.dart | 0 .../shared_app_data/shared_app_data.1.dart | 0 .../shortcuts/callback_shortcuts.0.dart | 0 .../shortcuts/character_activator.0.dart | 0 .../widgets/shortcuts/logical_key_set.0.dart | 0 .../lib/widgets/shortcuts/shortcuts.0.dart | 0 .../lib/widgets/shortcuts/shortcuts.1.dart | 0 .../widgets/shortcuts/single_activator.0.dart | 0 .../single_child_scroll_view.0.dart | 0 .../single_child_scroll_view.1.dart | 0 .../widgets/sliver/decorated_sliver.0.dart | 0 .../widgets/sliver/decorated_sliver.1.dart | 0 .../sliver/pinned_header_sliver.0.dart | 0 .../sliver/pinned_header_sliver.1.dart | 0 .../sliver_constrained_cross_axis.0.dart | 0 .../sliver/sliver_cross_axis_group.0.dart | 0 .../sliver/sliver_ensure_semantics.0.dart | 0 .../sliver/sliver_floating_header.0.dart | 0 .../api/lib/widgets/sliver/sliver_list.0.dart | 0 .../sliver/sliver_main_axis_group.0.dart | 0 .../lib/widgets/sliver/sliver_opacity.1.dart | 0 .../sliver/sliver_resizing_header.0.dart | 0 .../api/lib/widgets/sliver/sliver_tree.0.dart | 0 .../api/lib/widgets/sliver/sliver_tree.1.dart | 0 .../sliver_fill/sliver_fill_remaining.0.dart | 0 .../sliver_fill/sliver_fill_remaining.1.dart | 0 .../sliver_fill/sliver_fill_remaining.2.dart | 0 .../sliver_fill/sliver_fill_remaining.3.dart | 0 ...ti_child_render_object_widget_mixin.0.dart | 0 .../system_context_menu.0.dart | 0 .../system_context_menu.1.dart | 0 .../api/lib/widgets/table/table.0.dart | 0 .../lib/widgets/tap_region/tap_region.0.dart | 0 .../lib/widgets/tap_region/tap_region.1.dart | 0 .../tap_region/text_field_tap_region.0.dart | 0 .../api/lib/widgets/text/text.0.dart | 0 .../widgets/text/ui_testing_with_text.dart | 0 ...editable_text_tap_up_outside_intent.0.dart | 0 .../text_magnifier/text_magnifier.0.dart | 0 .../transitions/align_transition.0.dart | 0 .../transitions/animated_builder.0.dart | 0 .../transitions/animated_widget.0.dart | 0 .../decorated_box_transition.0.dart | 0 .../default_text_style_transition.0.dart | 0 .../transitions/fade_transition.0.dart | 0 .../transitions/listenable_builder.0.dart | 0 .../transitions/listenable_builder.1.dart | 0 .../transitions/listenable_builder.2.dart | 0 .../transitions/listenable_builder.3.dart | 0 .../transitions/matrix_transition.0.dart | 0 .../transitions/positioned_transition.0.dart | 0 .../relative_positioned_transition.0.dart | 0 .../transitions/rotation_transition.0.dart | 0 .../transitions/scale_transition.0.dart | 0 .../transitions/size_transition.0.dart | 0 .../transitions/slide_transition.0.dart | 0 .../transitions/sliver_fade_transition.0.dart | 0 .../tween_animation_builder.0.dart | 0 .../undo_history_controller.0.dart | 0 .../value_listenable_builder.0.dart | 0 .../widget_state_border_side.0.dart | 0 .../widget_state_mouse_cursor.0.dart | 0 .../widget_state_outlined_border.0.dart | 0 .../widget_state/widget_state_property.0.dart | 0 .../api/lib/widgets/windows/popup.0.dart | 0 .../api/lib/widgets/windows/satellite.0.dart | 0 .../api/lib/widgets/windows/tooltip.0.dart | 0 .../lib/widgets/windows/window_manager.0.dart | 0 .../flutter/examples}/api/linux/.gitignore | 0 .../examples}/api/linux/CMakeLists.txt | 0 .../api/linux/flutter/CMakeLists.txt | 0 .../examples}/api/linux/runner/CMakeLists.txt | 0 .../examples}/api/linux/runner/main.cc | 0 .../api/linux/runner/my_application.cc | 0 .../api/linux/runner/my_application.h | 0 .../flutter/examples}/api/macos/.gitignore | 0 .../api/macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../flutter/examples}/api/macos/Podfile | 0 .../macos/Runner.xcodeproj/project.pbxproj | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../api/macos/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../api/macos/Runner/Base.lproj/MainMenu.xib | 0 .../api/macos/Runner/Configs/AppInfo.xcconfig | 0 .../api/macos/Runner/Configs/Debug.xcconfig | 0 .../api/macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 .../examples}/api/macos/Runner/Info.plist | 0 .../api/macos/Runner/MainFlutterWindow.swift | 0 .../api/macos/Runner/Release.entitlements | 0 .../flutter/examples}/api/pubspec.yaml | 0 .../animated_digit.0_test.dart | 0 .../animation/curves/curve2_d.0_test.dart | 0 .../cupertino_activity_indicator.0_test.dart | 0 ...tino_linear_activity_indicator.0_test.dart | 0 .../cupertino_tab_bar.0_test.dart | 0 .../button/cupertino_button.0_test.dart | 0 .../checkbox/cupertino_checkbox.0_test.dart | 0 .../cupertino_context_menu.0_test.dart | 0 .../cupertino_context_menu.1_test.dart | 0 .../cupertino_date_picker.0_test.dart | 0 .../cupertino_timer_picker.0_test.dart | 0 .../dialog/cupertino_action_sheet.0_test.dart | 0 .../dialog/cupertino_alert_dialog.0_test.dart | 0 .../cupertino_popup_surface.0_test.dart | 0 .../cupertino_expansion_tile.0_test.dart | 0 .../form_row/cupertino_form_row.0_test.dart | 0 .../list_section_base.0_test.dart | 0 .../list_section_inset.0_test.dart | 0 .../list_tile/cupertino_list_tile.0_test.dart | 0 .../magnifier/cupertino_magnifier.0_test.dart | 0 .../cupertino_text_magnifier.0_test.dart | 0 .../menu_anchor/menu_anchor.0_test.dart | 0 .../menu_anchor/menu_anchor.1_test.dart | 0 .../cupertino_navigation_bar.0_test.dart | 0 .../cupertino_navigation_bar.1_test.dart | 0 .../cupertino_navigation_bar.2_test.dart | 0 .../cupertino_sliver_nav_bar.0_test.dart | 0 .../cupertino_sliver_nav_bar.1_test.dart | 0 .../cupertino_sliver_nav_bar.2_test.dart | 0 .../cupertino_page_scaffold.0_test.dart | 0 .../picker/cupertino_picker.0_test.dart | 0 .../radio/cupertino_radio.0_test.dart | 0 .../cupertino_radio.toggleable.0_test.dart | 0 ...pertino_sliver_refresh_control.0_test.dart | 0 .../route/show_cupertino_dialog.0_test.dart | 0 .../show_cupertino_modal_popup.0_test.dart | 0 .../scrollbar/cupertino_scrollbar.0_test.dart | 0 .../scrollbar/cupertino_scrollbar.1_test.dart | 0 .../cupertino_search_field.0_test.dart | 0 .../cupertino_search_field.1_test.dart | 0 .../cupertino_segmented_control.0_test.dart | 0 ...tino_sliding_segmented_control.0_test.dart | 0 .../sheet/cupertino_sheet.0_test.dart | 0 .../sheet/cupertino_sheet.1_test.dart | 0 .../sheet/cupertino_sheet.2_test.dart | 0 .../sheet/cupertino_sheet.3_test.dart | 0 .../slider/cupertino_slider.0_test.dart | 0 .../switch/cupertino_switch.0_test.dart | 0 .../cupertino_tab_controller.0_test.dart | 0 .../cupertino_tab_scaffold.0_test.dart | 0 .../cupertino_text_field.0_test.dart | 0 .../cupertino_text_form_field_row.1_test.dart | 0 .../api/test/flutter_test_config.dart | 0 .../test/foundation/key/value_key.0_test.dart | 0 .../pointer_signal_resolver.0_test.dart | 0 .../tap_and_drag/tap_and_drag.0_test.dart | 0 .../examples}/api/test/goldens_io.dart | 0 .../examples}/api/test/goldens_web.dart | 0 .../about/about_list_tile.0_test.dart | 0 .../action_icon_theme.0_test.dart | 0 .../action_chip/action_chip.0_test.dart | 0 .../animated_icon/animated_icon.0_test.dart | 0 .../animated_icons_data.0_test.dart | 0 .../api/test/material/app/app.0_test.dart | 0 .../test/material/app_bar/app_bar.0_test.dart | 0 .../test/material/app_bar/app_bar.1_test.dart | 0 .../test/material/app_bar/app_bar.2_test.dart | 0 .../test/material/app_bar/app_bar.3_test.dart | 0 .../test/material/app_bar/app_bar.4_test.dart | 0 .../app_bar/sliver_app_bar.1_test.dart | 0 .../app_bar/sliver_app_bar.2_test.dart | 0 .../app_bar/sliver_app_bar.3_test.dart | 0 .../app_bar/sliver_app_bar.4_test.dart | 0 .../autocomplete/autocomplete.0_test.dart | 0 .../autocomplete/autocomplete.1_test.dart | 0 .../autocomplete/autocomplete.2_test.dart | 0 .../autocomplete/autocomplete.3_test.dart | 0 .../autocomplete/autocomplete.4_test.dart | 0 .../api/test/material/badge/badge.0_test.dart | 0 .../banner/material_banner.0_test.dart | 0 .../banner/material_banner.1_test.dart | 0 .../bottom_app_bar/bottom_app_bar.1_test.dart | 0 .../bottom_app_bar/bottom_app_bar.2_test.dart | 0 .../bottom_navigation_bar.0_test.dart | 0 .../bottom_navigation_bar.1_test.dart | 0 .../bottom_navigation_bar.2_test.dart | 0 .../show_bottom_sheet.0_test.dart | 0 .../show_modal_bottom_sheet.0_test.dart | 0 .../show_modal_bottom_sheet.1_test.dart | 0 .../show_modal_bottom_sheet.2_test.dart | 0 .../button_style/button_style.0_test.dart | 0 .../api/test/material/card/card.0_test.dart | 0 .../api/test/material/card/card.1_test.dart | 0 .../api/test/material/card/card.2_test.dart | 0 .../material/carousel/carousel.0_test.dart | 0 .../material/carousel/carousel.1_test.dart | 0 .../material/checkbox/checkbox.0_test.dart | 0 .../material/checkbox/checkbox.1_test.dart | 0 .../checkbox_list_tile.0_test.dart | 0 .../checkbox_list_tile.1_test.dart | 0 .../custom_labeled_checkbox.0_test.dart | 0 .../custom_labeled_checkbox.1_test.dart | 0 ...ributes.avatar_box_constraints.0_test.dart | 0 ...ttributes.chip_animation_style.0_test.dart | 0 ...es.delete_icon_box_constraints.0_test.dart | 0 ...ble_chip_attributes.on_deleted.0_test.dart | 0 .../choice_chip/choice_chip.0_test.dart | 0 .../color_scheme/color_scheme.0_test.dart | 0 .../dynamic_content_color.0_test.dart | 0 .../editable_text_toolbar_builder.2_test.dart | 0 ...ectable_region_toolbar_builder.0_test.dart | 0 .../data_table/data_table.0_test.dart | 0 .../data_table/data_table.1_test.dart | 0 .../custom_calendar_date_picker.0_test.dart | 0 .../date_picker_theme_day_shape.0_test.dart | 0 .../date_picker/show_date_picker.0_test.dart | 0 .../date_picker/show_date_picker.1_test.dart | 0 .../show_date_range_picker.0_test.dart | 0 .../dialog/adaptive_alert_dialog.0_test.dart | 0 .../material/dialog/alert_dialog.0_test.dart | 0 .../material/dialog/alert_dialog.1_test.dart | 0 .../test/material/dialog/dialog.0_test.dart | 0 .../material/dialog/show_dialog.0_test.dart | 0 .../material/dialog/show_dialog.1_test.dart | 0 .../material/dialog/show_dialog.2_test.dart | 0 .../test/material/divider/divider.0_test.dart | 0 .../test/material/divider/divider.1_test.dart | 0 .../divider/vertical_divider.0_test.dart | 0 .../divider/vertical_divider.1_test.dart | 0 .../test/material/drawer/drawer.0_test.dart | 0 .../dropdown/dropdown_button.0_test.dart | 0 ...n_button.selected_item_builder.0_test.dart | 0 .../dropdown_button.style.0_test.dart | 0 .../dropdown_menu/dropdown_menu.0_test.dart | 0 .../dropdown_menu/dropdown_menu.1_test.dart | 0 .../dropdown_menu/dropdown_menu.2_test.dart | 0 ...opdown_menu_entry_label_widget.0_test.dart | 0 .../elevated_button.0_test.dart | 0 .../expansion_panel_list.0_test.dart | 0 ...ist.expansion_panel_list_radio.0_test.dart | 0 .../expansion_tile/expansion_tile.0_test.dart | 0 .../expansion_tile/expansion_tile.1_test.dart | 0 .../expansion_tile/expansion_tile.2_test.dart | 0 .../filled_button/filled_button.0_test.dart | 0 .../filter_chip/filter_chip.0_test.dart | 0 .../flexible_space_bar.0_test.dart | 0 .../floating_action_button.0_test.dart | 0 .../floating_action_button.1_test.dart | 0 .../floating_action_button.2_test.dart | 0 .../standard_fab_location.0_test.dart | 0 .../icon_alignment/icon_alignment.0_test.dart | 0 .../icon_button/icon_button.0_test.dart | 0 .../icon_button/icon_button.1_test.dart | 0 .../icon_button/icon_button.2_test.dart | 0 .../icon_button/icon_button.3_test.dart | 0 .../material/ink/ink.image_clip.0_test.dart | 0 .../material/ink/ink.image_clip.1_test.dart | 0 .../material/ink_well/ink_well.0_test.dart | 0 .../input_chip/input_chip.0_test.dart | 0 .../input_chip/input_chip.1_test.dart | 0 .../input_decoration.0_test.dart | 0 .../input_decoration.1_test.dart | 0 .../input_decoration.2_test.dart | 0 .../input_decoration.3_test.dart | 0 ...ion.floating_label_style_error.0_test.dart | 0 .../input_decoration.helper.0_test.dart | 0 .../input_decoration.label.0_test.dart | 0 ...t_decoration.label_style_error.0_test.dart | 0 .../input_decoration.prefix_icon.0_test.dart | 0 ...ration.prefix_icon_constraints.0_test.dart | 0 .../input_decoration.suffix_icon.0_test.dart | 0 ...ration.suffix_icon_constraints.0_test.dart | 0 .../input_decoration.widget_state.0_test.dart | 0 .../input_decoration.widget_state.1_test.dart | 0 .../list_tile/custom_list_item.0_test.dart | 0 .../list_tile/custom_list_item.1_test.dart | 0 .../material/list_tile/list_tile.0_test.dart | 0 .../material/list_tile/list_tile.1_test.dart | 0 .../material/list_tile/list_tile.2_test.dart | 0 .../material/list_tile/list_tile.3_test.dart | 0 .../material/list_tile/list_tile.4_test.dart | 0 .../list_tile/list_tile.selected.0_test.dart | 0 .../material_state_border_side.0_test.dart | 0 .../material_state_mouse_cursor.0_test.dart | 0 .../material_state_property.0_test.dart | 0 .../checkbox_menu_button.0_test.dart | 0 .../menu_accelerator_label.0_test.dart | 0 .../menu_anchor/menu_anchor.0_test.dart | 0 .../menu_anchor/menu_anchor.1_test.dart | 0 .../menu_anchor/menu_anchor.2_test.dart | 0 .../menu_anchor/menu_anchor.3_test.dart | 0 .../material/menu_anchor/menu_bar.0_test.dart | 0 .../menu_anchor/radio_menu_button.0_test.dart | 0 .../navigation_bar/navigation_bar.0_test.dart | 0 .../navigation_bar/navigation_bar.1_test.dart | 0 .../navigation_bar/navigation_bar.2_test.dart | 0 .../navigation_drawer.0_test.dart | 0 .../navigation_rail.0_test.dart | 0 ...gation_rail.extended_animation.0_test.dart | 0 .../outlined_button.0_test.dart | 0 .../page_transitions_theme.0_test.dart | 0 .../page_transitions_theme.1_test.dart | 0 .../page_transitions_theme.3_test.dart | 0 .../paginated_data_table.0_test.dart | 0 .../paginated_data_table.1_test.dart | 0 .../popup_menu/popup_menu.0_test.dart | 0 .../popup_menu/popup_menu.1_test.dart | 0 .../popup_menu/popup_menu.2_test.dart | 0 .../circular_progress_indicator.0_test.dart | 0 .../circular_progress_indicator.1_test.dart | 0 .../circular_progress_indicator.2_test.dart | 0 .../linear_progress_indicator.0_test.dart | 0 .../linear_progress_indicator.1_test.dart | 0 .../api/test/material/radio/radio.0_test.dart | 0 .../api/test/material/radio/radio.1_test.dart | 0 .../radio/radio.toggleable.0_test.dart | 0 .../custom_labeled_radio.0_test.dart | 0 .../custom_labeled_radio.1_test.dart | 0 .../radio_list_tile.0_test.dart | 0 .../radio_list_tile.1_test.dart | 0 .../radio_list_tile.toggleable.0_test.dart | 0 .../range_slider/range_slider.0_test.dart | 0 .../refresh_indicator.0_test.dart | 0 .../refresh_indicator.1_test.dart | 0 .../refresh_indicator.2_test.dart | 0 .../reorderable_list_view.0_test.dart | 0 .../reorderable_list_view.1_test.dart | 0 .../reorderable_list_view.2_test.dart | 0 ...iew.build_default_drag_handles.0_test.dart | 0 ....reorderable_list_view_builder.0_test.dart | 0 .../material/scaffold/scaffold.0_test.dart | 0 .../material/scaffold/scaffold.1_test.dart | 0 .../material/scaffold/scaffold.2_test.dart | 0 .../scaffold/scaffold.drawer.0_test.dart | 0 .../scaffold/scaffold.end_drawer.0_test.dart | 0 ...loating_action_button_animator.0_test.dart | 0 .../material/scaffold/scaffold.of.0_test.dart | 0 .../material/scaffold/scaffold.of.1_test.dart | 0 .../scaffold/scaffold_messenger.0_test.dart | 0 .../scaffold_messenger.of.0_test.dart | 0 .../scaffold_messenger.of.1_test.dart | 0 ...ger_state.show_material_banner.0_test.dart | 0 ...messenger_state.show_snack_bar.0_test.dart | 0 ...messenger_state.show_snack_bar.1_test.dart | 0 ...messenger_state.show_snack_bar.2_test.dart | 0 ...affold_state.show_bottom_sheet.0_test.dart | 0 ...affold_state.show_bottom_sheet.1_test.dart | 0 .../material/scrollbar/scrollbar.0_test.dart | 0 .../material/scrollbar/scrollbar.1_test.dart | 0 .../search_anchor/search_anchor.0_test.dart | 0 .../search_anchor/search_anchor.1_test.dart | 0 .../search_anchor/search_anchor.2_test.dart | 0 .../search_anchor/search_anchor.3_test.dart | 0 .../search_anchor/search_anchor.4_test.dart | 0 .../search_anchor/search_bar.0_test.dart | 0 .../segmented_button.0_test.dart | 0 .../segmented_button.1_test.dart | 0 .../selection_area/selection_area.0_test.dart | 0 .../selection_area/selection_area.1_test.dart | 0 .../selection_area/selection_area.2_test.dart | 0 .../shaped_input_border.0_test.dart | 0 .../test/material/slider/slider.0_test.dart | 0 .../test/material/slider/slider.1_test.dart | 0 .../material/snack_bar/snack_bar.0_test.dart | 0 .../material/snack_bar/snack_bar.1_test.dart | 0 .../material/snack_bar/snack_bar.2_test.dart | 0 .../material/stepper/step_style.0_test.dart | 0 .../test/material/stepper/stepper.0_test.dart | 0 .../stepper.controls_builder.0_test.dart | 0 .../test/material/switch/switch.0_test.dart | 0 .../test/material/switch/switch.1_test.dart | 0 .../test/material/switch/switch.2_test.dart | 0 .../test/material/switch/switch.3_test.dart | 0 .../test/material/switch/switch.4_test.dart | 0 .../custom_labeled_switch.0_test.dart | 0 .../custom_labeled_switch.1_test.dart | 0 .../switch_list_tile.0_test.dart | 0 .../switch_list_tile.1_test.dart | 0 .../tab_controller/tab_controller.1_test.dart | 0 .../test/material/tabs/tab_bar.0_test.dart | 0 .../test/material/tabs/tab_bar.1_test.dart | 0 .../test/material/tabs/tab_bar.2_test.dart | 0 .../test/material/tabs/tab_bar.3_test.dart | 0 .../tab_bar.indicator_animation.0_test.dart | 0 .../tabs/tab_bar.onFocusChange_test.dart | 0 .../material/tabs/tab_bar.onHover_test.dart | 0 .../text_button/text_button.0_test.dart | 0 .../text_button/text_button.1_test.dart | 0 .../text_field/text_field.0_test.dart | 0 .../text_field/text_field.1_test.dart | 0 .../text_field/text_field.2_test.dart | 0 .../text_field/text_field.3_test.dart | 0 .../text_form_field.1_test.dart | 0 .../text_form_field.2_test.dart | 0 .../theme/theme_extension.1_test.dart | 0 .../theme_data/theme_data.0_test.dart | 0 .../time_picker/show_time_picker.0_test.dart | 0 .../toggle_buttons/toggle_buttons.0_test.dart | 0 .../toggle_buttons/toggle_buttons.1_test.dart | 0 .../test/material/tooltip/tooltip.0_test.dart | 0 .../test/material/tooltip/tooltip.1_test.dart | 0 .../test/material/tooltip/tooltip.2_test.dart | 0 .../test/material/tooltip/tooltip.3_test.dart | 0 .../widget_state_input_border.0_test.dart | 0 .../axis_direction/axis_direction.0_test.dart | 0 .../border_side.stroke_align.0_test.dart | 0 .../gradient/linear_gradient.0_test.dart | 5 + .../image_provider/image_provider.0_test.dart | 0 .../linear_border/linear_border.0_test.dart | 0 .../rounded_superellipse_border.0_test.dart | 0 .../star_border/star_border.0_test.dart | 0 .../rendering/box/parent_data.0_test.dart | 0 .../growth_direction.0_test.dart | 0 .../scroll_direction.0_test.dart | 0 ...te_with_fixed_cross_axis_count.0_test.dart | 0 ...te_with_fixed_cross_axis_count.1_test.dart | 0 .../sample_templates/cupertino.0_test.dart | 0 .../sample_templates/material.0_test.dart | 0 .../test/sample_templates/widgets.0_test.dart | 0 .../handle_request_app_exit.0_test.dart | 0 .../logical_keyboard_key.0_test.dart | 0 .../physical_keyboard_key.0_test.dart | 0 .../mouse_cursor/mouse_cursor.0_test.dart | 0 ...e.set_system_u_i_overlay_style.0_test.dart | 0 ...e.set_system_u_i_overlay_style.1_test.dart | 0 .../text_input/text_input_control.0_test.dart | 0 .../api/test/ui/text/font_feature.0_test.dart | 0 ...ature.font_feature_alternative.0_test.dart | 0 ..._feature_alternative_fractions.0_test.dart | 0 ...t_feature_case_sensitive_forms.0_test.dart | 0 ...font_feature_character_variant.0_test.dart | 0 ..._feature_contextual_alternates.0_test.dart | 0 ...ature.font_feature_denominator.0_test.dart | 0 ...feature.font_feature_fractions.0_test.dart | 0 ....font_feature_historical_forms.0_test.dart | 0 ...t_feature_historical_ligatures.0_test.dart | 0 ...re.font_feature_lining_figures.0_test.dart | 0 ...ture.font_feature_locale_aware.0_test.dart | 0 ....font_feature_notational_forms.0_test.dart | 0 ...eature.font_feature_numerators.0_test.dart | 0 ....font_feature_oldstyle_figures.0_test.dart | 0 ...ure.font_feature_ordinal_forms.0_test.dart | 0 ...t_feature_proportional_figures.0_test.dart | 0 ...t_feature_scientific_inferiors.0_test.dart | 0 ...ture.font_feature_slashed_zero.0_test.dart | 0 ...t_feature_stylistic_alternates.0_test.dart | 0 ...ure.font_feature_stylistic_set.0_test.dart | 0 ...ure.font_feature_stylistic_set.1_test.dart | 0 ...eature.font_feature_subscripts.0_test.dart | 0 ...ture.font_feature_superscripts.0_test.dart | 0 ...ont_feature.font_feature_swash.0_test.dart | 0 ...e.font_feature_tabular_figures.0_test.dart | 0 .../action.action_overridable.0_test.dart | 0 .../actions/action_listener.0_test.dart | 0 .../test/widgets/actions/actions.0_test.dart | 0 .../focusable_action_detector.0_test.dart | 0 .../animated_grid/animated_grid.0_test.dart | 0 .../sliver_animated_grid.0_test.dart | 0 .../animated_list/animated_list.0_test.dart | 0 .../animated_list_separated.0_test.dart | 0 .../sliver_animated_list.0_test.dart | 0 .../animated_size/animated_size.0_test.dart | 0 .../animated_switcher.0_test.dart | 0 .../app/widgets_app.widgets_app.0_test.dart | 0 .../app_lifecycle_listener.0_test.dart | 0 .../app_lifecycle_listener.1_test.dart | 0 .../widgets/async/future_builder.0_test.dart | 0 .../widgets/async/stream_builder.0_test.dart | 0 .../autocomplete/raw_autocomplete.0_test.dart | 0 .../autocomplete/raw_autocomplete.1_test.dart | 0 .../autocomplete/raw_autocomplete.2_test.dart | 0 .../raw_autocomplete.focus_node.0_test.dart | 0 .../autofill/autofill_group.0_test.dart | 0 .../widgets/basic/absorb_pointer.0_test.dart | 0 .../widgets/basic/aspect_ratio.0_test.dart | 0 .../widgets/basic/aspect_ratio.1_test.dart | 0 .../widgets/basic/aspect_ratio.2_test.dart | 0 .../test/widgets/basic/clip_rrect.0_test.dart | 0 .../test/widgets/basic/clip_rrect.1_test.dart | 0 .../custom_multi_child_layout.0_test.dart | 0 .../test/widgets/basic/expanded.0_test.dart | 0 .../test/widgets/basic/expanded.1_test.dart | 0 .../test/widgets/basic/fitted_box.0_test.dart | 0 .../api/test/widgets/basic/flow.0_test.dart | 0 .../basic/fractionally_sized_box.0_test.dart | 0 .../widgets/basic/ignore_pointer.0_test.dart | 0 .../widgets/basic/indexed_stack.0_test.dart | 0 .../test/widgets/basic/listener.0_test.dart | 0 .../widgets/basic/mouse_region.0_test.dart | 0 .../basic/mouse_region.on_exit.0_test.dart | 0 .../basic/mouse_region.on_exit.1_test.dart | 0 .../test/widgets/basic/offstage.0_test.dart | 0 .../widgets/basic/overflowbox.0_test.dart | 0 .../widgets/basic/physical_shape.0_test.dart | 0 .../widget_binding_observer.0_test.dart | 0 .../color_filter/color_filtered.0_test.dart | 0 .../context_menu_controller.0_test.dart | 0 .../editable_text_toolbar_builder.0_test.dart | 0 .../editable_text_toolbar_builder.1_test.dart | 0 .../dismissible/dismissible.0_test.dart | 0 .../widgets/drag_target/draggable.0_test.dart | 0 .../draggable_scrollable_sheet.0_test.dart | 0 .../editable_text.on_changed.0_test.dart | 0 ...table_text.on_content_inserted.0_test.dart | 0 .../text_editing_controller.0_test.dart | 0 .../text_editing_controller.1_test.dart | 0 .../widgets/expansible/expansible.0_test.dart | 0 .../focus_manager/focus_node.0_test.dart | 0 .../focus_node.unfocus.0_test.dart | 0 .../widgets/focus_scope/focus.0_test.dart | 0 .../widgets/focus_scope/focus.1_test.dart | 0 .../widgets/focus_scope/focus.2_test.dart | 0 .../focus_scope/focus_scope.0_test.dart | 0 .../focus_traversal_group.0_test.dart | 0 .../ordered_traversal_policy.0_test.dart | 0 .../api/test/widgets/form/form.0_test.dart | 0 .../api/test/widgets/form/form.1_test.dart | 0 .../widgets/framework/build_owner.0_test.dart | 0 .../framework/error_widget.0_test.dart | 0 .../gesture_detector.0_test.dart | 0 .../gesture_detector.1_test.dart | 0 .../gesture_detector.2_test.dart | 0 .../gesture_detector.3_test.dart | 0 .../key_event_manager.0_test.dart | 0 .../api/test/widgets/heroes/hero.0_test.dart | 0 .../api/test/widgets/heroes/hero.1_test.dart | 0 .../image/image.error_builder.0_test.dart | 0 .../image/image.frame_builder.0_test.dart | 0 .../image/image.loading_builder.0_test.dart | 0 .../animated_align.0_test.dart | 0 .../animated_container.0_test.dart | 0 ...nimated_fractionally_sized_box.0_test.dart | 0 .../animated_padding.0_test.dart | 0 .../animated_positioned.0_test.dart | 0 .../animated_slide.0_test.dart | 0 .../sliver_animated_opacity.0_test.dart | 0 .../inherited_model.0_test.dart | 0 .../inherited_notifier.0_test.dart | 0 .../inherited_theme.0_test.dart | 0 .../interactive_viewer.0_test.dart | 0 .../interactive_viewer.builder.0_test.dart | 0 ...interactive_viewer.constrained.0_test.dart | 0 ...ewer.transformation_controller.0_test.dart | 0 .../automatic_keep_alive.0_test.dart | 0 ...omatic_keep_alive_client_mixin.0_test.dart | 0 .../widgets/keep_alive/keep_alive.0_test.dart | 0 .../layout_builder/layout_builder.0_test.dart | 0 .../widgets/magnifier/magnifier.0_test.dart | 5 + ...ery_data.system_gesture_insets.0_test.dart | 0 .../widgets/navigator/navigator.0_test.dart | 0 .../navigator.restorable_push.0_test.dart | 0 ...storable_push_and_remove_until.0_test.dart | 0 ...or.restorable_push_replacement.0_test.dart | 0 ...avigator_state.restorable_push.0_test.dart | 0 ...storable_push_and_remove_until.0_test.dart | 0 ...te.restorable_push_replacement.0_test.dart | 0 .../restorable_route_future.0_test.dart | 0 .../navigator_pop_handler.0_test.dart | 0 .../navigator_pop_handler.1_test.dart | 0 .../api/test/widgets/navigator_utils.dart | 0 .../nested_scroll_view.0_test.dart | 0 .../nested_scroll_view.1_test.dart | 0 .../nested_scroll_view.2_test.dart | 0 .../nested_scroll_view_state.0_test.dart | 0 .../notification.0_test.dart | 0 .../overflow_bar/overflow_bar.0_test.dart | 0 .../test/widgets/overlay/overlay.0_test.dart | 0 .../overlay/overlay_portal.0_test.dart | 0 .../overlay/overlay_portal.1_test.dart | 0 .../glowing_overscroll_indicator.0_test.dart | 0 .../glowing_overscroll_indicator.1_test.dart | 0 .../widgets/page/page_can_pop.0_test.dart | 0 .../page_storage/page_storage.0_test.dart | 0 .../page_transitions_builder.0_test.dart | 0 .../widgets/page_view/page_view.0_test.dart | 0 .../widgets/page_view/page_view.1_test.dart | 0 .../platform_menu_bar.0_test.dart | 0 .../widgets/pop_scope/pop_scope.0_test.dart | 0 .../widgets/pop_scope/pop_scope.1_test.dart | 0 .../preferred_size/preferred_size.0_test.dart | 0 .../radio_group/radio_group.0_test.dart | 0 .../raw_menu_anchor.0_test.dart | 0 .../raw_menu_anchor.1_test.dart | 0 .../raw_menu_anchor.2_test.dart | 0 .../raw_menu_anchor.3_test.dart | 0 .../raw_tooltip/raw_tooltip.0_test.dart | 0 .../repeating_animation_builder.0_test.dart | 0 .../restoration/restoration_mixin.0_test.dart | 0 .../restorable_value.0_test.dart | 0 .../flexible_route_transitions.0_test.dart | 0 .../flexible_route_transitions.1_test.dart | 0 .../routes/local_history_entry.0_test.dart | 0 .../widgets/routes/popup_route.0_test.dart | 0 .../widgets/routes/route_observer.0_test.dart | 0 .../routes/show_general_dialog.0_test.dart | 0 .../widgets/safe_area/safe_area.0_test.dart | 0 .../scroll_end_notification.0_test.dart | 0 .../scroll_end_notification.1_test.dart | 0 .../scroll_notification_observer.0_test.dart | 0 .../is_scrolling_listener.0_test.dart | 0 ...scroll_controller_notification.0_test.dart | 0 .../scroll_controller_on_attach.0_test.dart | 0 .../scroll_metrics_notification.0_test.dart | 0 .../custom_scroll_view.1_test.dart | 0 .../widgets/scroll_view/grid_view.0_test.dart | 0 .../widgets/scroll_view/list_view.0_test.dart | 0 .../widgets/scroll_view/list_view.1_test.dart | 0 .../scrollbar/raw_scrollbar.0_test.dart | 0 .../scrollbar/raw_scrollbar.1_test.dart | 0 .../scrollbar/raw_scrollbar.2_test.dart | 0 .../raw_scrollbar.desktop.0_test.dart | 0 .../scrollbar/raw_scrollbar.shape.0_test.dart | 0 .../selectable_region.0_test.dart | 0 .../selection_container.0_test.dart | 0 .../selection_container_disabled.0_test.dart | 0 .../sensitive_content.0_test.dart | 0 .../shared_app_data.0_test.dart | 0 .../shared_app_data.1_test.dart | 0 .../shortcuts/callback_shortcuts.0_test.dart | 0 .../shortcuts/character_activator.0_test.dart | 0 .../shortcuts/logical_key_set.0_test.dart | 0 .../widgets/shortcuts/shortcuts.0_test.dart | 0 .../widgets/shortcuts/shortcuts.1_test.dart | 0 .../shortcuts/single_activator.0_test.dart | 0 .../single_child_scroll_view.0_test.dart | 0 .../single_child_scroll_view.1_test.dart | 0 .../sliver/decorated_sliver.0_test.dart | 0 .../sliver/decorated_sliver.1_test.dart | 0 .../sliver/pinned_header_sliver.0_test.dart | 0 .../sliver/pinned_header_sliver.1_test.dart | 0 .../sliver_constrained_cross_axis.0_test.dart | 0 .../sliver_cross_axis_group.0_test.dart | 0 .../sliver_ensure_semantics.0_test.dart | 0 .../sliver/sliver_floating_header.0_test.dart | 0 .../widgets/sliver/sliver_list.0_test.dart | 0 .../sliver/sliver_main_axis_group.0_test.dart | 0 .../widgets/sliver/sliver_opacity.1_test.dart | 0 .../sliver/sliver_resizing_header.0_test.dart | 0 .../widgets/sliver/sliver_tree.0_test.dart | 0 .../widgets/sliver/sliver_tree.1_test.dart | 0 .../sliver_fill_remaining.0_test.dart | 0 .../sliver_fill_remaining.1_test.dart | 0 .../sliver_fill_remaining.2_test.dart | 0 .../sliver_fill_remaining.3_test.dart | 0 ...ild_render_object_widget_mixin.0_test.dart | 0 .../system_context_menu.0_test.dart | 0 .../system_context_menu.1_test.dart | 0 .../api/test/widgets/table/table.0_test.dart | 0 .../widgets/tap_region/tap_region.0_test.dart | 0 .../widgets/tap_region/tap_region.1_test.dart | 0 .../text_field_tap_region.0_test.dart | 0 .../api/test/widgets/text/text.0_test.dart | 0 ...ble_text_tap_up_outside_intent.0_test.dart | 0 .../text_magnifier/text_magnifier.0_test.dart | 5 + .../transitions/align_transition.0_test.dart | 0 .../transitions/animated_builder.0_test.dart | 0 .../transitions/animated_widget.0_test.dart | 0 .../decorated_box_transition.0_test.dart | 0 .../default_text_style_transition.0_test.dart | 0 .../transitions/fade_transition.0_test.dart | 0 .../listenable_builder.0_test.dart | 0 .../listenable_builder.1_test.dart | 0 .../listenable_builder.2_test.dart | 0 .../listenable_builder.3_test.dart | 0 .../transitions/matrix_transition.0_test.dart | 0 .../positioned_transition.0_test.dart | 0 ...relative_positioned_transition.0_test.dart | 0 .../rotation_transition.0_test.dart | 0 .../transitions/scale_transition.0_test.dart | 0 .../transitions/size_transition.0_test.dart | 0 .../transitions/slide_transition.0_test.dart | 0 .../sliver_fade_transition.0_test.dart | 0 .../tween_animation_builder.0_test.dart | 0 .../undo_history_controller.0_test.dart | 0 .../value_listenable_builder.0_test.dart | 0 .../widget_state_border_side.0_test.dart | 0 .../widget_state_mouse_cursor.0_test.dart | 0 .../widget_state_outlined_border.0_test.dart | 0 .../widget_state_property.0_test.dart | 0 .../test/widgets/windows/popup.0_test.dart | 0 .../widgets/windows/satellite.0_test.dart | 0 .../test/widgets/windows/tooltip.0_test.dart | 0 .../windows/window_manager.0_test.dart | 0 .../api/test_driver/integration_test.dart | 0 .../flutter/examples}/api/web/favicon.png | Bin .../examples}/api/web/icons/Icon-192.png | Bin .../examples}/api/web/icons/Icon-512.png | Bin .../flutter/examples}/api/web/index.html | 0 .../flutter/examples}/api/web/manifest.json | 0 .../flutter/examples}/api/windows/.gitignore | 0 .../examples}/api/windows/CMakeLists.txt | 0 .../api/windows/flutter/CMakeLists.txt | 0 .../api/windows/runner/CMakeLists.txt | 0 .../examples}/api/windows/runner/Runner.rc | 0 .../api/windows/runner/flutter_window.cpp | 0 .../api/windows/runner/flutter_window.h | 0 .../examples}/api/windows/runner/main.cpp | 0 .../examples}/api/windows/runner/resource.h | 0 .../api/windows/runner/runner.exe.manifest | 0 .../examples}/api/windows/runner/utils.cpp | 0 .../examples}/api/windows/runner/utils.h | 0 .../api/windows/runner/win32_window.cpp | 0 .../api/windows/runner/win32_window.h | 0 pubspec.yaml | 2 +- 1239 files changed, 761 insertions(+), 647 deletions(-) rename {examples => packages/flutter/examples}/api/.gitignore (100%) rename {examples => packages/flutter/examples}/api/.metadata (100%) rename {examples => packages/flutter/examples}/api/README.md (100%) rename {examples => packages/flutter/examples}/api/analysis_options.yaml (100%) rename {examples => packages/flutter/examples}/api/lib/animation/animation_controller/animated_digit.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/animation/curves/curve2_d.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/activity_indicator/cupertino_activity_indicator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/activity_indicator/cupertino_linear_activity_indicator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/bottom_tab_bar/cupertino_tab_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/button/cupertino_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/checkbox/cupertino_checkbox.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/context_menu/cupertino_context_menu.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/context_menu/cupertino_context_menu.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/date_picker/cupertino_date_picker.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/date_picker/cupertino_timer_picker.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/dialog/cupertino_action_sheet.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/dialog/cupertino_alert_dialog.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/dialog/cupertino_popup_surface.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/expansion_tile/cupertino_expansion_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/form_row/cupertino_form_row.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/list_section/list_section_base.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/list_section/list_section_inset.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/list_tile/cupertino_list_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/magnifier/cupertino_magnifier.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/magnifier/cupertino_text_magnifier.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/menu_anchor/menu_anchor.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/menu_anchor/menu_anchor.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/nav_bar/cupertino_navigation_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/picker/cupertino_picker.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/radio/cupertino_radio.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/radio/cupertino_radio.toggleable.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/refresh/cupertino_sliver_refresh_control.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/route/show_cupertino_dialog.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/scrollbar/cupertino_scrollbar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/scrollbar/cupertino_scrollbar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/search_field/cupertino_search_field.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/search_field/cupertino_search_field.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/segmented_control/cupertino_sliding_segmented_control.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/sheet/cupertino_sheet.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/sheet/cupertino_sheet.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/sheet/cupertino_sheet.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/sheet/cupertino_sheet.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/slider/cupertino_slider.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/switch/cupertino_switch.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/tab_scaffold/cupertino_tab_controller.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/tab_scaffold/cupertino_tab_scaffold.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/text_field/cupertino_text_field.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/cupertino/text_form_field_row/cupertino_text_form_field_row.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/foundation/key/value_key.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/gestures/pointer_signal_resolver/pointer_signal_resolver.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/about/about_list_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/action_buttons/action_icon_theme.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/action_chip/action_chip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/animated_icon/animated_icon.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/animated_icon/animated_icons_data.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app/app.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/app_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/app_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/app_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/app_bar.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/app_bar.4.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/sliver_app_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/sliver_app_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/sliver_app_bar.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/app_bar/sliver_app_bar.4.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/autocomplete/autocomplete.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/autocomplete/autocomplete.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/autocomplete/autocomplete.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/autocomplete/autocomplete.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/autocomplete/autocomplete.4.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/badge/badge.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/banner/material_banner.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/banner/material_banner.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_app_bar/bottom_app_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/button_style/button_style.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/card/card.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/card/card.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/card/card.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/carousel/carousel.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/carousel/carousel.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/checkbox/checkbox.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/checkbox/checkbox.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/checkbox_list_tile/checkbox_list_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/choice_chip/choice_chip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/color_scheme/color_scheme.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/color_scheme/dynamic_content_color.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/data_table/data_table.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/data_table/data_table.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/date_picker/custom_calendar_date_picker.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/date_picker/show_date_picker.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/date_picker/show_date_picker.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/date_picker/show_date_range_picker.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dialog/adaptive_alert_dialog.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dialog/alert_dialog.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dialog/alert_dialog.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dialog/dialog.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dialog/show_dialog.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dialog/show_dialog.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dialog/show_dialog.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/divider/divider.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/divider/divider.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/divider/vertical_divider.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/divider/vertical_divider.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/drawer/drawer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dropdown/dropdown_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dropdown/dropdown_button.style.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dropdown_menu/dropdown_menu.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dropdown_menu/dropdown_menu.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dropdown_menu/dropdown_menu.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/elevated_button/elevated_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/expansion_panel/expansion_panel_list.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/expansion_tile/expansion_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/expansion_tile/expansion_tile.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/expansion_tile/expansion_tile.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/filled_button/filled_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/filter_chip/filter_chip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/flexible_space_bar/flexible_space_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/floating_action_button/floating_action_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/floating_action_button/floating_action_button.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/floating_action_button/floating_action_button.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/floating_action_button_location/standard_fab_location.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/icon_alignment/icon_alignment.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/icon_button/icon_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/icon_button/icon_button.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/icon_button/icon_button.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/icon_button/icon_button.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/ink/ink.image_clip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/ink/ink.image_clip.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/ink_well/ink_well.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_chip/input_chip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_chip/input_chip.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.floating_label_style_error.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.helper.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.label.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.label_style_error.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.prefix_icon.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.prefix_icon_constraints.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.suffix_icon.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.suffix_icon_constraints.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.widget_state.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/input_decorator/input_decoration.widget_state.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/custom_list_item.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/custom_list_item.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/list_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/list_tile.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/list_tile.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/list_tile.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/list_tile.4.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/list_tile/list_tile.selected.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/material_state/material_state_border_side.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/material_state/material_state_mouse_cursor.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/material_state/material_state_property.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/checkbox_menu_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/menu_accelerator_label.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/menu_anchor.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/menu_anchor.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/menu_anchor.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/menu_anchor.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/menu_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/menu_anchor/radio_menu_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/navigation_bar/navigation_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/navigation_bar/navigation_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/navigation_bar/navigation_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/navigation_drawer/navigation_drawer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/navigation_rail/navigation_rail.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/outlined_button/outlined_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/page_transitions_theme/page_transitions_theme.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/paginated_data_table/paginated_data_table.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/paginated_data_table/paginated_data_table.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/popup_menu/popup_menu.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/popup_menu/popup_menu.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/popup_menu/popup_menu.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/progress_indicator/circular_progress_indicator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/progress_indicator/circular_progress_indicator.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/progress_indicator/circular_progress_indicator.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/progress_indicator/linear_progress_indicator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/progress_indicator/linear_progress_indicator.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio/radio.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio/radio.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio/radio.toggleable.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio_list_tile/custom_labeled_radio.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio_list_tile/custom_labeled_radio.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio_list_tile/radio_list_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio_list_tile/radio_list_tile.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/radio_list_tile/radio_list_tile.toggleable.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/range_slider/range_slider.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/refresh_indicator/refresh_indicator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/refresh_indicator/refresh_indicator.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/refresh_indicator/refresh_indicator.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/reorderable_list/reorderable_list_view.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/reorderable_list/reorderable_list_view.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/reorderable_list/reorderable_list_view.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.drawer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.end_drawer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.floating_action_button_animator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.of.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold.of.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_messenger.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_messenger.of.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_messenger.of.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_messenger_state.show_material_banner.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scrollbar/scrollbar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/scrollbar/scrollbar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/search_anchor/search_anchor.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/search_anchor/search_anchor.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/search_anchor/search_anchor.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/search_anchor/search_anchor.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/search_anchor/search_anchor.4.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/search_anchor/search_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/segmented_button/segmented_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/segmented_button/segmented_button.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/selection_area/selection_area.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/selection_area/selection_area.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/selection_area/selection_area.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/shaped_input_border/shaped_input_border.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/slider/slider.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/slider/slider.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/snack_bar/snack_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/snack_bar/snack_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/snack_bar/snack_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/stepper/step_style.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/stepper/stepper.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/stepper/stepper.controls_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch/switch.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch/switch.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch/switch.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch/switch.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch/switch.4.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch_list_tile/custom_labeled_switch.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch_list_tile/custom_labeled_switch.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch_list_tile/switch_list_tile.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/switch_list_tile/switch_list_tile.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tab_controller/tab_controller.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tabs/tab_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tabs/tab_bar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tabs/tab_bar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tabs/tab_bar.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tabs/tab_bar.indicator_animation.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tabs/tab_bar.onFocusChange.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tabs/tab_bar.onHover.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_button/text_button.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_button/text_button.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_field/text_field.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_field/text_field.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_field/text_field.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_field/text_field.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_form_field/text_form_field.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/text_form_field/text_form_field.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/theme/theme_extension.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/theme_data/theme_data.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/time_picker/show_time_picker.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/toggle_buttons/toggle_buttons.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/toggle_buttons/toggle_buttons.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tooltip/tooltip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tooltip/tooltip.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tooltip/tooltip.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/tooltip/tooltip.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/material/widget_state_input_border/widget_state_input_border.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/painting/axis_direction/axis_direction.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/painting/borders/border_side.stroke_align.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/painting/gradient/linear_gradient.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/painting/image_provider/image_provider.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/painting/linear_border/linear_border.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/painting/rounded_superellipse_border/rounded_superellipse_border.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/painting/star_border/star_border.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/rendering/box/parent_data.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/rendering/growth_direction/growth_direction.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/rendering/scroll_direction/scroll_direction.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/sample_templates/cupertino.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/sample_templates/material.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/sample_templates/widgets.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/services/binding/handle_request_app_exit.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/services/keyboard_key/logical_keyboard_key.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/services/keyboard_key/physical_keyboard_key.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/services/mouse_cursor/mouse_cursor.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/services/text_input/text_input_control.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_alternative.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_alternative_fractions.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_case_sensitive_forms.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_character_variant.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_contextual_alternates.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_denominator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_fractions.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_historical_forms.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_historical_ligatures.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_lining_figures.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_locale_aware.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_notational_forms.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_numerators.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_oldstyle_figures.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_ordinal_forms.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_proportional_figures.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_scientific_inferiors.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_slashed_zero.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_stylistic_alternates.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_stylistic_set.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_stylistic_set.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_subscripts.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_superscripts.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_swash.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/ui/text/font_feature.font_feature_tabular_figures.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/actions/action.action_overridable.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/actions/action_listener.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/actions/actions.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/actions/focusable_action_detector.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/animated_grid/animated_grid.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/animated_list/animated_list.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/animated_list/animated_list_separated.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/animated_list/sliver_animated_list.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/animated_size/animated_size.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/animated_switcher/animated_switcher.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/app/widgets_app.widgets_app.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/async/future_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/async/stream_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/autocomplete/raw_autocomplete.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/autocomplete/raw_autocomplete.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/autocomplete/raw_autocomplete.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/autofill/autofill_group.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/absorb_pointer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/aspect_ratio.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/aspect_ratio.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/aspect_ratio.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/clip_rrect.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/clip_rrect.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/custom_multi_child_layout.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/expanded.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/expanded.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/fitted_box.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/flow.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/fractionally_sized_box.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/ignore_pointer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/indexed_stack.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/listener.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/mouse_region.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/mouse_region.on_exit.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/mouse_region.on_exit.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/offstage.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/overflowbox.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/basic/physical_shape.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/binding/widget_binding_observer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/color_filter/color_filtered.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/context_menu/context_menu_controller.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/dismissible/dismissible.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/drag_target/draggable.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/editable_text/editable_text.on_changed.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/editable_text/text_editing_controller.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/editable_text/text_editing_controller.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/expansible/expansible.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_manager/focus_node.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_scope/focus.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_scope/focus.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_scope/focus.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_scope/focus_scope.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/form/form.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/form/form.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/framework/build_owner.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/framework/error_widget.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/gesture_detector/gesture_detector.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/gesture_detector/gesture_detector.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/gesture_detector/gesture_detector.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/gesture_detector/gesture_detector.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/heroes/hero.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/heroes/hero.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/image/image.error_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/image/image.frame_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/image/image.loading_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/implicit_animations/animated_align.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/implicit_animations/animated_container.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/implicit_animations/animated_padding.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/implicit_animations/animated_positioned.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/implicit_animations/animated_slide.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/implicit_animations/sliver_animated_opacity.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/inherited_model/inherited_model.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/inherited_theme/inherited_theme.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/keep_alive/keep_alive.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/layout_builder/layout_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/magnifier/magnifier.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/media_query/media_query_data.system_gesture_insets.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/navigator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/navigator.restorable_push.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator/restorable_route_future.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/notification_listener/notification.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/overflow_bar/overflow_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/overlay/overlay.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/overlay/overlay_portal.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/overlay/overlay_portal.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/page/page_can_pop.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/page_storage/page_storage.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/page_transitions_builder/page_transitions_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/page_view/page_view.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/page_view/page_view.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/pop_scope/pop_scope.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/pop_scope/pop_scope.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/preferred_size/preferred_size.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/radio_group/radio_group.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/raw_tooltip/raw_tooltip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/repeating_animation_builder/repeating_animation_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/restoration/restoration_mixin.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/restoration_properties/restorable_value.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/routes/flexible_route_transitions.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/routes/flexible_route_transitions.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/routes/local_history_entry.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/routes/popup_route.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/routes/route_observer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/routes/show_general_dialog.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/safe_area/safe_area.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_view/custom_scroll_view.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_view/grid_view.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_view/list_view.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scroll_view/list_view.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scrollbar/raw_scrollbar.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scrollbar/raw_scrollbar.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scrollbar/raw_scrollbar.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/selectable_region/selectable_region.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/selection_container/selection_container.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/selection_container/selection_container_disabled.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sensitive_content/sensitive_content.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shared_app_data/shared_app_data.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shared_app_data/shared_app_data.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shortcuts/callback_shortcuts.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shortcuts/character_activator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shortcuts/logical_key_set.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shortcuts/shortcuts.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shortcuts/shortcuts.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/shortcuts/single_activator.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/decorated_sliver.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/decorated_sliver.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/pinned_header_sliver.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/pinned_header_sliver.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_floating_header.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_list.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_main_axis_group.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_opacity.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_resizing_header.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_tree.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver/sliver_tree.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/system_context_menu/system_context_menu.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/system_context_menu/system_context_menu.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/table/table.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/tap_region/tap_region.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/tap_region/tap_region.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/tap_region/text_field_tap_region.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/text/text.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/text/ui_testing_with_text.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/text_magnifier/text_magnifier.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/align_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/animated_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/animated_widget.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/decorated_box_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/default_text_style_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/fade_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/listenable_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/listenable_builder.1.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/listenable_builder.2.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/listenable_builder.3.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/matrix_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/positioned_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/relative_positioned_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/rotation_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/scale_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/size_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/slide_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/transitions/sliver_fade_transition.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/tween_animation_builder/tween_animation_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/undo_history/undo_history_controller.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/value_listenable_builder/value_listenable_builder.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/widget_state/widget_state_border_side.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/widget_state/widget_state_mouse_cursor.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/widget_state/widget_state_outlined_border.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/widget_state/widget_state_property.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/windows/popup.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/windows/satellite.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/windows/tooltip.0.dart (100%) rename {examples => packages/flutter/examples}/api/lib/widgets/windows/window_manager.0.dart (100%) rename {examples => packages/flutter/examples}/api/linux/.gitignore (100%) rename {examples => packages/flutter/examples}/api/linux/CMakeLists.txt (100%) rename {examples => packages/flutter/examples}/api/linux/flutter/CMakeLists.txt (100%) rename {examples => packages/flutter/examples}/api/linux/runner/CMakeLists.txt (100%) rename {examples => packages/flutter/examples}/api/linux/runner/main.cc (100%) rename {examples => packages/flutter/examples}/api/linux/runner/my_application.cc (100%) rename {examples => packages/flutter/examples}/api/linux/runner/my_application.h (100%) rename {examples => packages/flutter/examples}/api/macos/.gitignore (100%) rename {examples => packages/flutter/examples}/api/macos/Flutter/Flutter-Debug.xcconfig (100%) rename {examples => packages/flutter/examples}/api/macos/Flutter/Flutter-Release.xcconfig (100%) rename {examples => packages/flutter/examples}/api/macos/Podfile (100%) rename {examples => packages/flutter/examples}/api/macos/Runner.xcodeproj/project.pbxproj (100%) rename {examples => packages/flutter/examples}/api/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {examples => packages/flutter/examples}/api/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {examples => packages/flutter/examples}/api/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {examples => packages/flutter/examples}/api/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/AppDelegate.swift (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Base.lproj/MainMenu.xib (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Configs/AppInfo.xcconfig (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Configs/Debug.xcconfig (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Configs/Release.xcconfig (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Configs/Warnings.xcconfig (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/DebugProfile.entitlements (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Info.plist (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/MainFlutterWindow.swift (100%) rename {examples => packages/flutter/examples}/api/macos/Runner/Release.entitlements (100%) rename {examples => packages/flutter/examples}/api/pubspec.yaml (100%) rename {examples => packages/flutter/examples}/api/test/animation/animation_controller/animated_digit.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/animation/curves/curve2_d.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/activity_indicator/cupertino_activity_indicator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/activity_indicator/cupertino_linear_activity_indicator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/bottom_tab_bar/cupertino_tab_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/button/cupertino_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/checkbox/cupertino_checkbox.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/context_menu/cupertino_context_menu.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/context_menu/cupertino_context_menu.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/date_picker/cupertino_date_picker.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/date_picker/cupertino_timer_picker.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/dialog/cupertino_action_sheet.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/dialog/cupertino_alert_dialog.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/dialog/cupertino_popup_surface.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/expansion_tile/cupertino_expansion_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/form_row/cupertino_form_row.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/list_section/list_section_base.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/list_section/list_section_inset.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/list_tile/cupertino_list_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/magnifier/cupertino_magnifier.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/magnifier/cupertino_text_magnifier.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/menu_anchor/menu_anchor.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/menu_anchor/menu_anchor.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/nav_bar/cupertino_navigation_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/nav_bar/cupertino_navigation_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/page_scaffold/cupertino_page_scaffold.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/picker/cupertino_picker.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/radio/cupertino_radio.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/radio/cupertino_radio.toggleable.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/refresh/cupertino_sliver_refresh_control.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/route/show_cupertino_dialog.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/route/show_cupertino_modal_popup.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/scrollbar/cupertino_scrollbar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/scrollbar/cupertino_scrollbar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/search_field/cupertino_search_field.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/search_field/cupertino_search_field.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/segmented_control/cupertino_sliding_segmented_control.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/sheet/cupertino_sheet.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/sheet/cupertino_sheet.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/sheet/cupertino_sheet.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/sheet/cupertino_sheet.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/slider/cupertino_slider.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/switch/cupertino_switch.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/tab_scaffold/cupertino_tab_controller.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/tab_scaffold/cupertino_tab_scaffold.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/text_field/cupertino_text_field.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/cupertino/text_form_field_row/cupertino_text_form_field_row.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/flutter_test_config.dart (100%) rename {examples => packages/flutter/examples}/api/test/foundation/key/value_key.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/goldens_io.dart (100%) rename {examples => packages/flutter/examples}/api/test/goldens_web.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/about/about_list_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/action_buttons/action_icon_theme.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/action_chip/action_chip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/animated_icon/animated_icon.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/animated_icon/animated_icons_data.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app/app.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/app_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/app_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/app_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/app_bar.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/app_bar.4_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/sliver_app_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/sliver_app_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/sliver_app_bar.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/app_bar/sliver_app_bar.4_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/autocomplete/autocomplete.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/autocomplete/autocomplete.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/autocomplete/autocomplete.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/autocomplete/autocomplete.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/autocomplete/autocomplete.4_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/badge/badge.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/banner/material_banner.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/banner/material_banner.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_navigation_bar/bottom_navigation_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_sheet/show_modal_bottom_sheet.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_sheet/show_modal_bottom_sheet.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/button_style/button_style.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/card/card.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/card/card.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/card/card.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/carousel/carousel.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/carousel/carousel.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/checkbox/checkbox.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/checkbox/checkbox.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/checkbox_list_tile/checkbox_list_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/checkbox_list_tile/checkbox_list_tile.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/checkbox_list_tile/custom_labeled_checkbox.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/checkbox_list_tile/custom_labeled_checkbox.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/choice_chip/choice_chip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/color_scheme/color_scheme.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/color_scheme/dynamic_content_color.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/context_menu/editable_text_toolbar_builder.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/data_table/data_table.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/data_table/data_table.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/date_picker/custom_calendar_date_picker.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/date_picker/show_date_picker.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/date_picker/show_date_picker.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/date_picker/show_date_range_picker.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dialog/adaptive_alert_dialog.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dialog/alert_dialog.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dialog/alert_dialog.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dialog/dialog.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dialog/show_dialog.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dialog/show_dialog.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dialog/show_dialog.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/divider/divider.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/divider/divider.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/divider/vertical_divider.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/divider/vertical_divider.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/drawer/drawer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dropdown/dropdown_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dropdown/dropdown_button.selected_item_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dropdown/dropdown_button.style.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dropdown_menu/dropdown_menu.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dropdown_menu/dropdown_menu.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dropdown_menu/dropdown_menu.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/elevated_button/elevated_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/expansion_panel/expansion_panel_list.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/expansion_tile/expansion_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/expansion_tile/expansion_tile.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/expansion_tile/expansion_tile.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/filled_button/filled_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/filter_chip/filter_chip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/floating_action_button/floating_action_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/floating_action_button/floating_action_button.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/floating_action_button/floating_action_button.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/icon_alignment/icon_alignment.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/icon_button/icon_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/icon_button/icon_button.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/icon_button/icon_button.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/icon_button/icon_button.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/ink/ink.image_clip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/ink/ink.image_clip.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/ink_well/ink_well.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_chip/input_chip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_chip/input_chip.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.floating_label_style_error.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.helper.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.label.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.label_style_error.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.prefix_icon.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.suffix_icon.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.widget_state.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/input_decorator/input_decoration.widget_state.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/custom_list_item.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/custom_list_item.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/list_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/list_tile.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/list_tile.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/list_tile.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/list_tile.4_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/list_tile/list_tile.selected.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/material_state/material_state_border_side.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/material_state/material_state_mouse_cursor.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/material_state/material_state_property.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/menu_anchor.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/menu_anchor.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/menu_anchor.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/menu_anchor.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/menu_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/menu_anchor/radio_menu_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/navigation_bar/navigation_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/navigation_bar/navigation_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/navigation_bar/navigation_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/navigation_drawer/navigation_drawer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/navigation_rail/navigation_rail.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/outlined_button/outlined_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/page_transitions_theme/page_transitions_theme.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/page_transitions_theme/page_transitions_theme.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/paginated_data_table/paginated_data_table.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/paginated_data_table/paginated_data_table.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/popup_menu/popup_menu.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/popup_menu/popup_menu.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/popup_menu/popup_menu.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/progress_indicator/circular_progress_indicator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/progress_indicator/circular_progress_indicator.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/progress_indicator/circular_progress_indicator.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio/radio.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio/radio.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio/radio.toggleable.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio_list_tile/custom_labeled_radio.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio_list_tile/custom_labeled_radio.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio_list_tile/radio_list_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio_list_tile/radio_list_tile.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/radio_list_tile/radio_list_tile.toggleable.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/range_slider/range_slider.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/refresh_indicator/refresh_indicator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/refresh_indicator/refresh_indicator.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/refresh_indicator/refresh_indicator.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/reorderable_list/reorderable_list_view.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/reorderable_list/reorderable_list_view.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/reorderable_list/reorderable_list_view.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.drawer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.end_drawer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.floating_action_button_animator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.of.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold.of.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_messenger.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_messenger.of.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_messenger.of.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scrollbar/scrollbar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/scrollbar/scrollbar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/search_anchor/search_anchor.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/search_anchor/search_anchor.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/search_anchor/search_anchor.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/search_anchor/search_anchor.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/search_anchor/search_anchor.4_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/search_anchor/search_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/segmented_button/segmented_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/segmented_button/segmented_button.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/selection_area/selection_area.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/selection_area/selection_area.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/selection_area/selection_area.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/shaped_input_border/shaped_input_border.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/slider/slider.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/slider/slider.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/snack_bar/snack_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/snack_bar/snack_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/snack_bar/snack_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/stepper/step_style.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/stepper/stepper.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/stepper/stepper.controls_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch/switch.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch/switch.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch/switch.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch/switch.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch/switch.4_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch_list_tile/custom_labeled_switch.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch_list_tile/custom_labeled_switch.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch_list_tile/switch_list_tile.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/switch_list_tile/switch_list_tile.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tab_controller/tab_controller.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tabs/tab_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tabs/tab_bar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tabs/tab_bar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tabs/tab_bar.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tabs/tab_bar.indicator_animation.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tabs/tab_bar.onFocusChange_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tabs/tab_bar.onHover_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_button/text_button.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_button/text_button.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_field/text_field.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_field/text_field.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_field/text_field.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_field/text_field.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_form_field/text_form_field.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/text_form_field/text_form_field.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/theme/theme_extension.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/theme_data/theme_data.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/time_picker/show_time_picker.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/toggle_buttons/toggle_buttons.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/toggle_buttons/toggle_buttons.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tooltip/tooltip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tooltip/tooltip.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tooltip/tooltip.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/tooltip/tooltip.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/material/widget_state_input_border/widget_state_input_border.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/painting/axis_direction/axis_direction.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/painting/borders/border_side.stroke_align.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/painting/gradient/linear_gradient.0_test.dart (88%) rename {examples => packages/flutter/examples}/api/test/painting/image_provider/image_provider.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/painting/linear_border/linear_border.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/painting/rounded_superellipse_border/rounded_superellipse_border.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/painting/star_border/star_border.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/rendering/box/parent_data.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/rendering/growth_direction/growth_direction.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/rendering/scroll_direction/scroll_direction.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/sample_templates/cupertino.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/sample_templates/material.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/sample_templates/widgets.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/services/binding/handle_request_app_exit.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/services/mouse_cursor/mouse_cursor.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/services/text_input/text_input_control.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_alternative.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_alternative_fractions.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_case_sensitive_forms.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_character_variant.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_contextual_alternates.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_denominator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_fractions.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_historical_forms.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_historical_ligatures.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_lining_figures.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_locale_aware.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_notational_forms.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_numerators.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_oldstyle_figures.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_ordinal_forms.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_proportional_figures.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_scientific_inferiors.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_slashed_zero.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_stylistic_alternates.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_stylistic_set.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_stylistic_set.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_subscripts.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_superscripts.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_swash.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/ui/text/font_feature.font_feature_tabular_figures.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/actions/action.action_overridable.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/actions/action_listener.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/actions/actions.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/actions/focusable_action_detector.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/animated_grid/animated_grid.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/animated_list/animated_list.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/animated_list/animated_list_separated.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/animated_list/sliver_animated_list.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/animated_size/animated_size.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/animated_switcher/animated_switcher.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/app/widgets_app.widgets_app.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/async/future_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/async/stream_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/autofill/autofill_group.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/absorb_pointer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/aspect_ratio.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/aspect_ratio.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/aspect_ratio.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/clip_rrect.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/clip_rrect.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/custom_multi_child_layout.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/expanded.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/expanded.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/fitted_box.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/flow.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/fractionally_sized_box.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/ignore_pointer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/indexed_stack.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/listener.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/mouse_region.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/mouse_region.on_exit.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/mouse_region.on_exit.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/offstage.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/overflowbox.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/basic/physical_shape.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/binding/widget_binding_observer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/color_filter/color_filtered.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/context_menu/context_menu_controller.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/dismissible/dismissible.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/drag_target/draggable.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/editable_text/text_editing_controller.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/editable_text/text_editing_controller.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/expansible/expansible.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_manager/focus_node.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_scope/focus.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_scope/focus.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_scope/focus.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_scope/focus_scope.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/form/form.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/form/form.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/framework/build_owner.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/framework/error_widget.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/gesture_detector/gesture_detector.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/gesture_detector/gesture_detector.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/gesture_detector/gesture_detector.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/gesture_detector/gesture_detector.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/hardware_keyboard/key_event_manager.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/heroes/hero.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/heroes/hero.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/image/image.error_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/image/image.frame_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/image/image.loading_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/implicit_animations/animated_align.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/implicit_animations/animated_container.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/implicit_animations/animated_padding.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/implicit_animations/animated_positioned.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/implicit_animations/animated_slide.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/inherited_model/inherited_model.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/inherited_theme/inherited_theme.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/interactive_viewer/interactive_viewer.builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/keep_alive/keep_alive.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/layout_builder/layout_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/magnifier/magnifier.0_test.dart (94%) rename {examples => packages/flutter/examples}/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/navigator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/navigator.restorable_push.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator/restorable_route_future.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/navigator_utils.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/notification_listener/notification.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/overflow_bar/overflow_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/overlay/overlay.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/overlay/overlay_portal.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/overlay/overlay_portal.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/page/page_can_pop.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/page_storage/page_storage.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/page_transitions_builder/page_transitions_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/page_view/page_view.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/page_view/page_view.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/pop_scope/pop_scope.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/pop_scope/pop_scope.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/preferred_size/preferred_size.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/radio_group/radio_group.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/raw_menu_anchor/raw_menu_anchor.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/raw_menu_anchor/raw_menu_anchor.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/raw_menu_anchor/raw_menu_anchor.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/raw_tooltip/raw_tooltip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/repeating_animation_builder/repeating_animation_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/restoration/restoration_mixin.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/restoration_properties/restorable_value.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/routes/flexible_route_transitions.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/routes/flexible_route_transitions.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/routes/local_history_entry.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/routes/popup_route.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/routes/route_observer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/routes/show_general_dialog.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/safe_area/safe_area.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_end_notification/scroll_end_notification.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_end_notification/scroll_end_notification.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_position/is_scrolling_listener.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_position/scroll_controller_notification.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_position/scroll_controller_on_attach.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_view/grid_view.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_view/list_view.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scroll_view/list_view.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/selectable_region/selectable_region.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/selection_container/selection_container.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/selection_container/selection_container_disabled.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sensitive_content/sensitive_content.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shared_app_data/shared_app_data.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shared_app_data/shared_app_data.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shortcuts/character_activator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shortcuts/logical_key_set.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shortcuts/shortcuts.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shortcuts/shortcuts.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/shortcuts/single_activator.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/decorated_sliver.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/decorated_sliver.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/pinned_header_sliver.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/pinned_header_sliver.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_constrained_cross_axis.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_cross_axis_group.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_floating_header.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_list.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_main_axis_group.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_opacity.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_resizing_header.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_tree.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver/sliver_tree.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/system_context_menu/system_context_menu.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/system_context_menu/system_context_menu.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/table/table.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/tap_region/tap_region.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/tap_region/tap_region.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/tap_region/text_field_tap_region.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/text/text.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/text_magnifier/text_magnifier.0_test.dart (96%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/align_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/animated_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/animated_widget.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/decorated_box_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/default_text_style_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/fade_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/listenable_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/listenable_builder.1_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/listenable_builder.2_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/listenable_builder.3_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/matrix_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/positioned_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/relative_positioned_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/rotation_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/scale_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/size_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/slide_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/transitions/sliver_fade_transition.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/undo_history/undo_history_controller.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/value_listenable_builder/value_listenable_builder.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/widget_state/widget_state_border_side.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/widget_state/widget_state_mouse_cursor.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/widget_state/widget_state_outlined_border.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/widget_state/widget_state_property.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/windows/popup.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/windows/satellite.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/windows/tooltip.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test/widgets/windows/window_manager.0_test.dart (100%) rename {examples => packages/flutter/examples}/api/test_driver/integration_test.dart (100%) rename {examples => packages/flutter/examples}/api/web/favicon.png (100%) rename {examples => packages/flutter/examples}/api/web/icons/Icon-192.png (100%) rename {examples => packages/flutter/examples}/api/web/icons/Icon-512.png (100%) rename {examples => packages/flutter/examples}/api/web/index.html (100%) rename {examples => packages/flutter/examples}/api/web/manifest.json (100%) rename {examples => packages/flutter/examples}/api/windows/.gitignore (100%) rename {examples => packages/flutter/examples}/api/windows/CMakeLists.txt (100%) rename {examples => packages/flutter/examples}/api/windows/flutter/CMakeLists.txt (100%) rename {examples => packages/flutter/examples}/api/windows/runner/CMakeLists.txt (100%) rename {examples => packages/flutter/examples}/api/windows/runner/Runner.rc (100%) rename {examples => packages/flutter/examples}/api/windows/runner/flutter_window.cpp (100%) rename {examples => packages/flutter/examples}/api/windows/runner/flutter_window.h (100%) rename {examples => packages/flutter/examples}/api/windows/runner/main.cpp (100%) rename {examples => packages/flutter/examples}/api/windows/runner/resource.h (100%) rename {examples => packages/flutter/examples}/api/windows/runner/runner.exe.manifest (100%) rename {examples => packages/flutter/examples}/api/windows/runner/utils.cpp (100%) rename {examples => packages/flutter/examples}/api/windows/runner/utils.h (100%) rename {examples => packages/flutter/examples}/api/windows/runner/win32_window.cpp (100%) rename {examples => packages/flutter/examples}/api/windows/runner/win32_window.h (100%) diff --git a/.github/labeler.yml b/.github/labeler.yml index 0353541bf2961..ba01674f4c24a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -50,7 +50,7 @@ 'd: api docs': - changed-files: - any-glob-to-any-file: - - examples/api/**/* + - packages/flutter/examples/api/**/* 'd: docs/': - changed-files: @@ -61,6 +61,7 @@ - changed-files: - any-glob-to-any-file: - examples/**/* + - packages/flutter/examples/**/* 'e: embedder': - changed-files: @@ -138,7 +139,6 @@ framework: - packages/flutter_localizations/**/* - packages/flutter_test/**/* - packages/integration_test/**/* - - examples/api/**/* - docs/about/**/* - docs/contributing/**/* - docs/libraries/**/* diff --git a/.github/workflows/freeze.yml b/.github/workflows/freeze.yml index 7927f4d07e500..cad386d43b33c 100644 --- a/.github/workflows/freeze.yml +++ b/.github/workflows/freeze.yml @@ -32,14 +32,14 @@ jobs: token: ${{ github.token }} filters: | frozen: + - 'packages/flutter/examples/api/lib/material/**' + - 'packages/flutter/examples/api/lib/cupertino/**' + - 'packages/flutter/examples/api/test/material/**' + - 'packages/flutter/examples/api/test/cupertino/**' - 'packages/flutter/lib/src/material/**' - 'packages/flutter/lib/src/cupertino/**' - 'packages/flutter/test/material/**' - 'packages/flutter/test/cupertino/**' - - 'examples/api/lib/material/**' - - 'examples/api/lib/cupertino/**' - - 'examples/api/test/material/**' - - 'examples/api/test/cupertino/**' - 'packages/flutter/lib/fix_data/fix_cupertino.yaml' - 'packages/flutter/lib/fix_data/fix_material/**' - 'packages/flutter/test_fixes/material/**' diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index dad6843d9ab11..f272a87ae33ed 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -225,7 +225,7 @@ List _getValidations({ ), Validation('no-sync-star-async-star', 'No sync*/async*', () async { await verifyNoSyncAsyncStar(flutterPackages); - await verifyNoSyncAsyncStar(flutterExamples, minimumMatches: 200); + await verifyNoSyncAsyncStar(flutterExamples, minimumMatches: 80); }), Validation( 'no-runtime-type', diff --git a/dev/bots/analyze_snippet_code.dart b/dev/bots/analyze_snippet_code.dart index 5bcd78a98cef0..823180f42fb11 100644 --- a/dev/bots/analyze_snippet_code.dart +++ b/dev/bots/analyze_snippet_code.dart @@ -7,8 +7,9 @@ // In general, please prefer using full linked examples in API docs. // -// For documentation on creating sample code, see ../../examples/api/README.md -// See also our style guide's discussion on documentation and sample code: +// For documentation on creating sample code, see +// ../../packages/flutter/examples/api/README.md See also our style guide's +// discussion on documentation and sample code: // https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md // // This tool is used to analyze smaller snippets of code in the API docs. @@ -1008,13 +1009,13 @@ class _SnippetChecker { _copyPubspec( path.join(_contentDirectory.path, _pubspecName), - path.join(_flutterRoot, 'examples', 'api', _pubspecName), + path.join(_flutterRoot, 'packages', 'flutter', 'examples', 'api', _pubspecName), ); final targetAnalysisOptions = File(path.join(_contentDirectory.path, 'analysis_options.yaml')); if (!targetAnalysisOptions.existsSync()) { - // Use the same analysis_options.yaml configuration that's used for examples/api. + // Use the same analysis_options.yaml configuration that's used for packages/flutter/examples/api. final sourceAnalysisOptions = File( - path.join(_flutterRoot, 'examples', 'api', 'analysis_options.yaml'), + path.join(_flutterRoot, 'packages', 'flutter', 'examples', 'api', 'analysis_options.yaml'), ); if (!sourceAnalysisOptions.existsSync()) { throw 'Cannot find analysis_options.yaml at ${sourceAnalysisOptions.path}, which is also used to analyze code snippets.'; @@ -1028,7 +1029,7 @@ class _SnippetChecker { void _copyPubspec(String targetPath, String sourcePath) { final targetPubSpec = File(targetPath); if (!targetPubSpec.existsSync()) { - // Copying pubspec.yaml from examples/api into temp directory. + // Copying pubspec.yaml from packages/flutter/examples/api into temp directory. final sourcePubSpec = File(sourcePath); if (!sourcePubSpec.existsSync()) { throw 'Cannot find pubspec.yaml at ${sourcePubSpec.path}, which is also used to analyze code snippets.'; diff --git a/dev/bots/check_code_samples.dart b/dev/bots/check_code_samples.dart index b8921f5008b5f..89d714b3fee0d 100644 --- a/dev/bots/check_code_samples.dart +++ b/dev/bots/check_code_samples.dart @@ -16,7 +16,13 @@ import 'utils.dart'; final String _scriptLocation = path.fromUri(Platform.script); final String _flutterRoot = path.dirname(path.dirname(path.dirname(_scriptLocation))); -final String _exampleDirectoryPath = path.join(_flutterRoot, 'examples', 'api'); +final String _exampleDirectoryPath = path.join( + _flutterRoot, + 'packages', + 'flutter', + 'examples', + 'api', +); final String _packageDirectoryPath = path.join(_flutterRoot, 'packages'); final String _dartUIDirectoryPath = path.join( _flutterRoot, @@ -128,6 +134,10 @@ class SampleChecker { final Directory flutterRoot; final FileSystem filesystem; + // The `exampleBase` is where the paths in "See code in" are relative to. + // Defaults to /packages/flutter. + Directory get exampleBase => examples.parent.parent; + bool checkCodeSamples() { filesystem.currentDirectory = flutterRoot; @@ -258,7 +268,7 @@ class SampleChecker { List checkForMissingLinks(List exampleFilenames, Set searchStrings) { final missingFilenames = []; for (final example in exampleFilenames) { - final String relativePath = getRelativePath(example); + final String relativePath = getRelativePath(example, exampleBase); if (!searchStrings.contains(relativePath)) { missingFilenames.add(relativePath); } diff --git a/dev/bots/check_examples_cross_imports.dart b/dev/bots/check_examples_cross_imports.dart index 728aacf3e77ac..ac8d8fdb94c7d 100644 --- a/dev/bots/check_examples_cross_imports.dart +++ b/dev/bots/check_examples_cross_imports.dart @@ -18,6 +18,13 @@ import 'utils.dart'; final String _scriptLocation = path.fromUri(Platform.script); final String _flutterRoot = path.dirname(path.dirname(path.dirname(_scriptLocation))); final String _examplesDirectoryPath = path.join(_flutterRoot, 'examples'); +final String _apiDocDirectoryPath = path.join( + _flutterRoot, + 'packages', + 'flutter', + 'examples', + 'api', +); void main(List args) { final argParser = ArgParser(); @@ -26,7 +33,13 @@ void main(List args) { 'examples', valueHelp: 'path', defaultsTo: _examplesDirectoryPath, - help: 'A location where the examples are found.', + help: 'A location where the non-API examples are found.', + ); + argParser.addOption( + 'api-doc', + valueHelp: 'path', + defaultsTo: _apiDocDirectoryPath, + help: 'A location where the API documents are found.', ); argParser.addOption( 'flutter-root', @@ -56,10 +69,12 @@ void main(List args) { const FileSystem filesystem = LocalFileSystem(); final Directory examplesDirectory = filesystem.directory(parsedArgs['examples']! as String); + final Directory apiDocDirectory = filesystem.directory(parsedArgs['api-doc']! as String); final Directory flutterRoot = filesystem.directory(parsedArgs['flutter-root']! as String); final checker = ExamplesCrossImportChecker( examplesDirectory: examplesDirectory, + apiDocDirectory: apiDocDirectory, flutterRoot: flutterRoot, ); @@ -81,11 +96,13 @@ void main(List args) { class ExamplesCrossImportChecker { ExamplesCrossImportChecker({ required this.examplesDirectory, + required this.apiDocDirectory, required this.flutterRoot, this.filesystem = const LocalFileSystem(), }); final Directory examplesDirectory; + final Directory apiDocDirectory; final Directory flutterRoot; final FileSystem filesystem; @@ -98,504 +115,492 @@ class ExamplesCrossImportChecker { // TODO(justinmc): Fix all of these tests so there are no cross imports. // See https://github.com/flutter/flutter/issues/187645. static final Set knownExamplesCrossImports = { - 'examples/api/lib/animation/animation_controller/animated_digit.0.dart', - 'examples/api/lib/animation/curves/curve2_d.0.dart', - 'examples/api/test/animation/animation_controller/animated_digit.0_test.dart', - 'examples/api/test/animation/curves/curve2_d.0_test.dart', - 'examples/api/lib/foundation/key/value_key.0.dart', - 'examples/api/test/foundation/key/value_key.0_test.dart', - 'examples/api/lib/gestures/pointer_signal_resolver/pointer_signal_resolver.0.dart', - 'examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart', - 'examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart', - 'examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart', - 'examples/api/lib/painting/gradient/linear_gradient.0.dart', - 'examples/api/lib/painting/star_border/star_border.0.dart', - 'examples/api/lib/painting/axis_direction/axis_direction.0.dart', - 'examples/api/lib/painting/borders/border_side.stroke_align.0.dart', - 'examples/api/lib/painting/linear_border/linear_border.0.dart', - 'examples/api/lib/painting/image_provider/image_provider.0.dart', - 'examples/api/lib/painting/rounded_superellipse_border/rounded_superellipse_border.0.dart', - 'examples/api/test/painting/gradient/linear_gradient.0_test.dart', - 'examples/api/test/painting/star_border/star_border.0_test.dart', - 'examples/api/test/painting/axis_direction/axis_direction.0_test.dart', - 'examples/api/test/painting/borders/border_side.stroke_align.0_test.dart', - 'examples/api/test/painting/linear_border/linear_border.0_test.dart', - 'examples/api/test/painting/rounded_superellipse_border/rounded_superellipse_border.0_test.dart', - 'examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart', - 'examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart', - 'examples/api/lib/rendering/growth_direction/growth_direction.0.dart', - 'examples/api/lib/rendering/box/parent_data.0.dart', - 'examples/api/lib/rendering/scroll_direction/scroll_direction.0.dart', - 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart', - 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart', - 'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart', - 'examples/api/test/rendering/box/parent_data.0_test.dart', - 'examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart', - 'examples/api/lib/services/mouse_cursor/mouse_cursor.0.dart', - 'examples/api/lib/services/binding/handle_request_app_exit.0.dart', - 'examples/api/lib/services/text_input/text_input_control.0.dart', - 'examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart', - 'examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart', - 'examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0.dart', - 'examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart', - 'examples/api/test/services/text_input/text_input_control.0_test.dart', - 'examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0_test.dart', - 'examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_ordinal_forms.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_contextual_alternates.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_alternative_fractions.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_oldstyle_figures.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_stylistic_alternates.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_historical_forms.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_stylistic_set.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_slashed_zero.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_superscripts.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_case_sensitive_forms.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_proportional_figures.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_historical_ligatures.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_tabular_figures.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_notational_forms.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_stylistic_set.1_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_alternative.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_locale_aware.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_fractions.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_denominator.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_lining_figures.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_numerators.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_swash.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_scientific_inferiors.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_subscripts.0_test.dart', - 'examples/api/test/ui/text/font_feature.font_feature_character_variant.0_test.dart', - 'examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart', - 'examples/api/lib/widgets/animated_grid/animated_grid.0.dart', - 'examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart', - 'examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart', - 'examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart', - 'examples/api/lib/widgets/editable_text/text_editing_controller.0.dart', - 'examples/api/lib/widgets/editable_text/text_editing_controller.1.dart', - 'examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart', - 'examples/api/lib/widgets/page/page_can_pop.0.dart', - 'examples/api/lib/widgets/undo_history/undo_history_controller.0.dart', - 'examples/api/lib/widgets/raw_tooltip/raw_tooltip.0.dart', - 'examples/api/lib/widgets/form/form.1.dart', - 'examples/api/lib/widgets/form/form.0.dart', - 'examples/api/lib/widgets/layout_builder/layout_builder.0.dart', - 'examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart', - 'examples/api/lib/widgets/restoration/restoration_mixin.0.dart', - 'examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart', - 'examples/api/lib/widgets/drag_target/draggable.0.dart', - 'examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart', - 'examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart', - 'examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart', - 'examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart', - 'examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart', - 'examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart', - 'examples/api/lib/widgets/keep_alive/keep_alive.0.dart', - 'examples/api/lib/widgets/safe_area/safe_area.0.dart', - 'examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart', - 'examples/api/lib/widgets/animated_size/animated_size.0.dart', - 'examples/api/lib/widgets/framework/error_widget.0.dart', - 'examples/api/lib/widgets/framework/build_owner.0.dart', - 'examples/api/lib/widgets/sliver/pinned_header_sliver.1.dart', - 'examples/api/lib/widgets/sliver/sliver_list.0.dart', - 'examples/api/lib/widgets/sliver/pinned_header_sliver.0.dart', - 'examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart', - 'examples/api/lib/widgets/sliver/sliver_tree.1.dart', - 'examples/api/lib/widgets/sliver/sliver_tree.0.dart', - 'examples/api/lib/widgets/sliver/sliver_floating_header.0.dart', - 'examples/api/lib/widgets/sliver/decorated_sliver.1.dart', - 'examples/api/lib/widgets/sliver/sliver_opacity.1.dart', - 'examples/api/lib/widgets/sliver/decorated_sliver.0.dart', - 'examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart', - 'examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart', - 'examples/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart', - 'examples/api/lib/widgets/sliver/sliver_main_axis_group.0.dart', - 'examples/api/lib/widgets/heroes/hero.0.dart', - 'examples/api/lib/widgets/heroes/hero.1.dart', - 'examples/api/lib/widgets/dismissible/dismissible.0.dart', - 'examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart', - 'examples/api/lib/widgets/preferred_size/preferred_size.0.dart', - 'examples/api/lib/widgets/media_query/media_query_data.system_gesture_insets.0.dart', - 'examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart', - 'examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart', - 'examples/api/lib/widgets/value_listenable_builder/value_listenable_builder.0.dart', - 'examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart', - 'examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart', - 'examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart', - 'examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart', - 'examples/api/lib/widgets/async/stream_builder.0.dart', - 'examples/api/lib/widgets/async/future_builder.0.dart', - 'examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart', - 'examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart', - 'examples/api/lib/widgets/animated_list/animated_list_separated.0.dart', - 'examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart', - 'examples/api/lib/widgets/animated_list/animated_list.0.dart', - 'examples/api/lib/widgets/basic/fractionally_sized_box.0.dart', - 'examples/api/lib/widgets/basic/physical_shape.0.dart', - 'examples/api/lib/widgets/basic/aspect_ratio.2.dart', - 'examples/api/lib/widgets/basic/flow.0.dart', - 'examples/api/lib/widgets/basic/aspect_ratio.0.dart', - 'examples/api/lib/widgets/basic/clip_rrect.0.dart', - 'examples/api/lib/widgets/basic/ignore_pointer.0.dart', - 'examples/api/lib/widgets/basic/fitted_box.0.dart', - 'examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart', - 'examples/api/lib/widgets/basic/listener.0.dart', - 'examples/api/lib/widgets/basic/clip_rrect.1.dart', - 'examples/api/lib/widgets/basic/offstage.0.dart', - 'examples/api/lib/widgets/basic/aspect_ratio.1.dart', - 'examples/api/lib/widgets/basic/overflowbox.0.dart', - 'examples/api/lib/widgets/basic/indexed_stack.0.dart', - 'examples/api/lib/widgets/basic/mouse_region.on_exit.1.dart', - 'examples/api/lib/widgets/basic/expanded.1.dart', - 'examples/api/lib/widgets/basic/mouse_region.0.dart', - 'examples/api/lib/widgets/basic/expanded.0.dart', - 'examples/api/lib/widgets/basic/mouse_region.on_exit.0.dart', - 'examples/api/lib/widgets/basic/absorb_pointer.0.dart', - 'examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart', - 'examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart', - 'examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart', - 'examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart', - 'examples/api/lib/widgets/autofill/autofill_group.0.dart', - 'examples/api/lib/widgets/scroll_view/list_view.1.dart', - 'examples/api/lib/widgets/scroll_view/list_view.0.dart', - 'examples/api/lib/widgets/scroll_view/grid_view.0.dart', - 'examples/api/lib/widgets/scroll_view/custom_scroll_view.1.dart', - 'examples/api/lib/widgets/binding/widget_binding_observer.0.dart', - 'examples/api/lib/widgets/image/image.loading_builder.0.dart', - 'examples/api/lib/widgets/image/image.error_builder.0.dart', - 'examples/api/lib/widgets/image/image.frame_builder.0.dart', - 'examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart', - 'examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart', - 'examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart', - 'examples/api/lib/widgets/color_filter/color_filtered.0.dart', - 'examples/api/lib/widgets/implicit_animations/animated_padding.0.dart', - 'examples/api/lib/widgets/implicit_animations/animated_positioned.0.dart', - 'examples/api/lib/widgets/implicit_animations/animated_align.0.dart', - 'examples/api/lib/widgets/implicit_animations/animated_slide.0.dart', - 'examples/api/lib/widgets/implicit_animations/animated_container.0.dart', - 'examples/api/lib/widgets/implicit_animations/sliver_animated_opacity.0.dart', - 'examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart', - 'examples/api/lib/widgets/radio_group/radio_group.0.dart', - 'examples/api/lib/widgets/inherited_theme/inherited_theme.0.dart', - 'examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart', - 'examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart', - 'examples/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart', - 'examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart', - 'examples/api/lib/widgets/page_transitions_builder/page_transitions_builder.0.dart', - 'examples/api/lib/widgets/page_storage/page_storage.0.dart', - 'examples/api/lib/widgets/table/table.0.dart', - 'examples/api/lib/widgets/notification_listener/notification.0.dart', - 'examples/api/lib/widgets/inherited_model/inherited_model.0.dart', - 'examples/api/lib/widgets/focus_scope/focus.2.dart', - 'examples/api/lib/widgets/focus_scope/focus_scope.0.dart', - 'examples/api/lib/widgets/focus_scope/focus.1.dart', - 'examples/api/lib/widgets/focus_scope/focus.0.dart', - 'examples/api/lib/widgets/pop_scope/pop_scope.1.dart', - 'examples/api/lib/widgets/pop_scope/pop_scope.0.dart', - 'examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart', - 'examples/api/lib/widgets/shortcuts/shortcuts.0.dart', - 'examples/api/lib/widgets/shortcuts/character_activator.0.dart', - 'examples/api/lib/widgets/shortcuts/shortcuts.1.dart', - 'examples/api/lib/widgets/shortcuts/callback_shortcuts.0.dart', - 'examples/api/lib/widgets/shortcuts/single_activator.0.dart', - 'examples/api/lib/widgets/shortcuts/logical_key_set.0.dart', - 'examples/api/lib/widgets/actions/action_listener.0.dart', - 'examples/api/lib/widgets/actions/action.action_overridable.0.dart', - 'examples/api/lib/widgets/actions/focusable_action_detector.0.dart', - 'examples/api/lib/widgets/actions/actions.0.dart', - 'examples/api/lib/widgets/widget_state/widget_state_property.0.dart', - 'examples/api/lib/widgets/widget_state/widget_state_border_side.0.dart', - 'examples/api/lib/widgets/widget_state/widget_state_outlined_border.0.dart', - 'examples/api/lib/widgets/widget_state/widget_state_mouse_cursor.0.dart', - 'examples/api/lib/widgets/magnifier/magnifier.0.dart', - 'examples/api/lib/widgets/navigator/restorable_route_future.0.dart', - 'examples/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart', - 'examples/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart', - 'examples/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart', - 'examples/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart', - 'examples/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart', - 'examples/api/lib/widgets/navigator/navigator.0.dart', - 'examples/api/lib/widgets/navigator/navigator.restorable_push.0.dart', - 'examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart', - 'examples/api/lib/widgets/system_context_menu/system_context_menu.1.dart', - 'examples/api/lib/widgets/tween_animation_builder/tween_animation_builder.0.dart', - 'examples/api/lib/widgets/page_view/page_view.0.dart', - 'examples/api/lib/widgets/page_view/page_view.1.dart', - 'examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart', - 'examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart', - 'examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart', - 'examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart', - 'examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart', - 'examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart', - 'examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart', - 'examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart', - 'examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart', - 'examples/api/lib/widgets/text/ui_testing_with_text.dart', - 'examples/api/lib/widgets/text/text.0.dart', - 'examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart', - 'examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart', - 'examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart', - 'examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart', - 'examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.1.dart', - 'examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.0.dart', - 'examples/api/lib/widgets/transitions/positioned_transition.0.dart', - 'examples/api/lib/widgets/transitions/listenable_builder.3.dart', - 'examples/api/lib/widgets/transitions/matrix_transition.0.dart', - 'examples/api/lib/widgets/transitions/listenable_builder.2.dart', - 'examples/api/lib/widgets/transitions/size_transition.0.dart', - 'examples/api/lib/widgets/transitions/relative_positioned_transition.0.dart', - 'examples/api/lib/widgets/transitions/animated_builder.0.dart', - 'examples/api/lib/widgets/transitions/decorated_box_transition.0.dart', - 'examples/api/lib/widgets/transitions/rotation_transition.0.dart', - 'examples/api/lib/widgets/transitions/fade_transition.0.dart', - 'examples/api/lib/widgets/transitions/animated_widget.0.dart', - 'examples/api/lib/widgets/transitions/align_transition.0.dart', - 'examples/api/lib/widgets/transitions/listenable_builder.1.dart', - 'examples/api/lib/widgets/transitions/listenable_builder.0.dart', - 'examples/api/lib/widgets/transitions/default_text_style_transition.0.dart', - 'examples/api/lib/widgets/transitions/slide_transition.0.dart', - 'examples/api/lib/widgets/transitions/scale_transition.0.dart', - 'examples/api/lib/widgets/transitions/sliver_fade_transition.0.dart', - 'examples/api/lib/widgets/windows/popup.0.dart', - 'examples/api/lib/widgets/windows/tooltip.0.dart', - 'examples/api/lib/widgets/windows/satellite.0.dart', - 'examples/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart', - 'examples/api/lib/widgets/overlay/overlay_portal.0.dart', - 'examples/api/lib/widgets/overlay/overlay.0.dart', - 'examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart', - 'examples/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart', - 'examples/api/lib/widgets/focus_manager/focus_node.0.dart', - 'examples/api/lib/widgets/restoration_properties/restorable_value.0.dart', - 'examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart', - 'examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart', - 'examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart', - 'examples/api/lib/widgets/gesture_detector/gesture_detector.0.dart', - 'examples/api/lib/widgets/routes/show_general_dialog.0.dart', - 'examples/api/lib/widgets/routes/local_history_entry.0.dart', - 'examples/api/lib/widgets/routes/route_observer.0.dart', - 'examples/api/lib/widgets/routes/flexible_route_transitions.1.dart', - 'examples/api/lib/widgets/routes/flexible_route_transitions.0.dart', - 'examples/api/lib/widgets/routes/popup_route.0.dart', - 'examples/api/lib/widgets/repeating_animation_builder/repeating_animation_builder.0.dart', - 'examples/api/lib/widgets/scrollbar/raw_scrollbar.1.dart', - 'examples/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart', - 'examples/api/lib/widgets/scrollbar/raw_scrollbar.0.dart', - 'examples/api/lib/widgets/scrollbar/raw_scrollbar.2.dart', - 'examples/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart', - 'examples/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart', - 'examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart', - 'examples/api/test/widgets/animated_grid/animated_grid.0_test.dart', - 'examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart', - 'examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart', - 'examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart', - 'examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart', - 'examples/api/test/widgets/editable_text/text_editing_controller.1_test.dart', - 'examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart', - 'examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart', - 'examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart', - 'examples/api/test/widgets/raw_tooltip/raw_tooltip.0_test.dart', - 'examples/api/test/widgets/form/form.0_test.dart', - 'examples/api/test/widgets/form/form.1_test.dart', - 'examples/api/test/widgets/tap_region/text_field_tap_region.0_test.dart', - 'examples/api/test/widgets/restoration/restoration_mixin.0_test.dart', - 'examples/api/test/widgets/drag_target/draggable.0_test.dart', - 'examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart', - 'examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart', - 'examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart', - 'examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart', - 'examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart', - 'examples/api/test/widgets/keep_alive/keep_alive.0_test.dart', - 'examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart', - 'examples/api/test/widgets/safe_area/safe_area.0_test.dart', - 'examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart', - 'examples/api/test/widgets/animated_size/animated_size.0_test.dart', - 'examples/api/test/widgets/framework/build_owner.0_test.dart', - 'examples/api/test/widgets/framework/error_widget.0_test.dart', - 'examples/api/test/widgets/sliver/pinned_header_sliver.1_test.dart', - 'examples/api/test/widgets/sliver/sliver_floating_header.0_test.dart', - 'examples/api/test/widgets/sliver/pinned_header_sliver.0_test.dart', - 'examples/api/test/widgets/sliver/sliver_opacity.1_test.dart', - 'examples/api/test/widgets/sliver/decorated_sliver.0_test.dart', - 'examples/api/test/widgets/sliver/sliver_cross_axis_group.0_test.dart', - 'examples/api/test/widgets/sliver/decorated_sliver.1_test.dart', - 'examples/api/test/widgets/sliver/sliver_main_axis_group.0_test.dart', - 'examples/api/test/widgets/sliver/sliver_constrained_cross_axis.0_test.dart', - 'examples/api/test/widgets/sliver/sliver_resizing_header.0_test.dart', - 'examples/api/test/widgets/heroes/hero.0_test.dart', - 'examples/api/test/widgets/heroes/hero.1_test.dart', - 'examples/api/test/widgets/dismissible/dismissible.0_test.dart', - 'examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart', - 'examples/api/test/widgets/preferred_size/preferred_size.0_test.dart', - 'examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart', - 'examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart', - 'examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart', - 'examples/api/test/widgets/value_listenable_builder/value_listenable_builder.0_test.dart', - 'examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.3_test.dart', - 'examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.2_test.dart', - 'examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart', - 'examples/api/test/widgets/async/stream_builder.0_test.dart', - 'examples/api/test/widgets/async/future_builder.0_test.dart', - 'examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart', - 'examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart', - 'examples/api/test/widgets/animated_list/animated_list_separated.0_test.dart', - 'examples/api/test/widgets/animated_list/sliver_animated_list.0_test.dart', - 'examples/api/test/widgets/animated_list/animated_list.0_test.dart', - 'examples/api/test/widgets/basic/physical_shape.0_test.dart', - 'examples/api/test/widgets/basic/aspect_ratio.2_test.dart', - 'examples/api/test/widgets/basic/indexed_stack.0_test.dart', - 'examples/api/test/widgets/basic/clip_rrect.1_test.dart', - 'examples/api/test/widgets/basic/absorb_pointer.0_test.dart', - 'examples/api/test/widgets/basic/listener.0_test.dart', - 'examples/api/test/widgets/basic/clip_rrect.0_test.dart', - 'examples/api/test/widgets/basic/mouse_region.0_test.dart', - 'examples/api/test/widgets/basic/expanded.0_test.dart', - 'examples/api/test/widgets/basic/fitted_box.0_test.dart', - 'examples/api/test/widgets/basic/mouse_region.on_exit.0_test.dart', - 'examples/api/test/widgets/basic/aspect_ratio.0_test.dart', - 'examples/api/test/widgets/basic/fractionally_sized_box.0_test.dart', - 'examples/api/test/widgets/basic/expanded.1_test.dart', - 'examples/api/test/widgets/basic/mouse_region.on_exit.1_test.dart', - 'examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart', - 'examples/api/test/widgets/basic/aspect_ratio.1_test.dart', - 'examples/api/test/widgets/basic/flow.0_test.dart', - 'examples/api/test/widgets/basic/overflowbox.0_test.dart', - 'examples/api/test/widgets/scroll_end_notification/scroll_end_notification.0_test.dart', - 'examples/api/test/widgets/scroll_end_notification/scroll_end_notification.1_test.dart', - 'examples/api/test/widgets/autofill/autofill_group.0_test.dart', - 'examples/api/test/widgets/scroll_view/list_view.1_test.dart', - 'examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart', - 'examples/api/test/widgets/scroll_view/list_view.0_test.dart', - 'examples/api/test/widgets/image/image.loading_builder.0_test.dart', - 'examples/api/test/widgets/image/image.error_builder.0_test.dart', - 'examples/api/test/widgets/image/image.frame_builder.0_test.dart', - 'examples/api/test/widgets/color_filter/color_filtered.0_test.dart', - 'examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart', - 'examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart', - 'examples/api/test/widgets/implicit_animations/animated_slide.0_test.dart', - 'examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart', - 'examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart', - 'examples/api/test/widgets/implicit_animations/animated_align.0_test.dart', - 'examples/api/test/widgets/implicit_animations/animated_container.0_test.dart', - 'examples/api/test/widgets/radio_group/radio_group.0_test.dart', - 'examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart', - 'examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart', - 'examples/api/test/widgets/scroll_position/scroll_controller_on_attach.0_test.dart', - 'examples/api/test/widgets/scroll_position/is_scrolling_listener.0_test.dart', - 'examples/api/test/widgets/scroll_position/scroll_controller_notification.0_test.dart', - 'examples/api/test/widgets/page_transitions_builder/page_transitions_builder.0_test.dart', - 'examples/api/test/widgets/page_storage/page_storage.0_test.dart', - 'examples/api/test/widgets/table/table.0_test.dart', - 'examples/api/test/widgets/notification_listener/notification.0_test.dart', - 'examples/api/test/widgets/inherited_model/inherited_model.0_test.dart', - 'examples/api/test/widgets/focus_scope/focus.0_test.dart', - 'examples/api/test/widgets/focus_scope/focus.1_test.dart', - 'examples/api/test/widgets/focus_scope/focus.2_test.dart', - 'examples/api/test/widgets/focus_scope/focus_scope.0_test.dart', - 'examples/api/test/widgets/pop_scope/pop_scope.1_test.dart', - 'examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart', - 'examples/api/test/widgets/shortcuts/character_activator.0_test.dart', - 'examples/api/test/widgets/actions/action_listener.0_test.dart', - 'examples/api/test/widgets/actions/focusable_action_detector.0_test.dart', - 'examples/api/test/widgets/actions/actions.0_test.dart', - 'examples/api/test/widgets/actions/action.action_overridable.0_test.dart', - 'examples/api/test/widgets/widget_state/widget_state_property.0_test.dart', - 'examples/api/test/widgets/widget_state/widget_state_border_side.0_test.dart', - 'examples/api/test/widgets/widget_state/widget_state_outlined_border.0_test.dart', - 'examples/api/test/widgets/widget_state/widget_state_mouse_cursor.0_test.dart', - 'examples/api/test/widgets/magnifier/magnifier.0_test.dart', - 'examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart', - 'examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart', - 'examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart', - 'examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart', - 'examples/api/test/widgets/navigator/restorable_route_future.0_test.dart', - 'examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart', - 'examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart', - 'examples/api/test/widgets/system_context_menu/system_context_menu.1_test.dart', - 'examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart', - 'examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart', - 'examples/api/test/widgets/page_view/page_view.0_test.dart', - 'examples/api/test/widgets/page_view/page_view.1_test.dart', - 'examples/api/test/widgets/hardware_keyboard/key_event_manager.0_test.dart', - 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart', - 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart', - 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart', - 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart', - 'examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart', - 'examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart', - 'examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart', - 'examples/api/test/widgets/text/text.0_test.dart', - 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart', - 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart', - 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart', - 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart', - 'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart', - 'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart', - 'examples/api/test/widgets/transitions/listenable_builder.3_test.dart', - 'examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart', - 'examples/api/test/widgets/transitions/matrix_transition.0_test.dart', - 'examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart', - 'examples/api/test/widgets/transitions/listenable_builder.2_test.dart', - 'examples/api/test/widgets/transitions/align_transition.0_test.dart', - 'examples/api/test/widgets/transitions/size_transition.0_test.dart', - 'examples/api/test/widgets/transitions/fade_transition.0_test.dart', - 'examples/api/test/widgets/transitions/listenable_builder.1_test.dart', - 'examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart', - 'examples/api/test/widgets/transitions/slide_transition.0_test.dart', - 'examples/api/test/widgets/transitions/positioned_transition.0_test.dart', - 'examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart', - 'examples/api/test/widgets/transitions/animated_builder.0_test.dart', - 'examples/api/test/widgets/transitions/listenable_builder.0_test.dart', - 'examples/api/test/widgets/transitions/scale_transition.0_test.dart', - 'examples/api/test/widgets/transitions/animated_widget.0_test.dart', - 'examples/api/test/widgets/transitions/rotation_transition.0_test.dart', - 'examples/api/test/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0_test.dart', - 'examples/api/test/widgets/overlay/overlay.0_test.dart', - 'examples/api/test/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0_test.dart', - 'examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart', - 'examples/api/test/widgets/focus_manager/focus_node.0_test.dart', - 'examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart', - 'examples/api/test/widgets/gesture_detector/gesture_detector.3_test.dart', - 'examples/api/test/widgets/gesture_detector/gesture_detector.2_test.dart', - 'examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart', - 'examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart', - 'examples/api/test/widgets/routes/popup_route.0_test.dart', - 'examples/api/test/widgets/routes/show_general_dialog.0_test.dart', - 'examples/api/test/widgets/repeating_animation_builder/repeating_animation_builder.0_test.dart', - 'examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart', - 'examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart', - 'examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart', - 'examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart', - 'examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart', - 'examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart', - 'examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart', + 'packages/flutter/examples/api/lib/animation/animation_controller/animated_digit.0.dart', + 'packages/flutter/examples/api/lib/animation/curves/curve2_d.0.dart', + 'packages/flutter/examples/api/test/animation/animation_controller/animated_digit.0_test.dart', + 'packages/flutter/examples/api/test/animation/curves/curve2_d.0_test.dart', + 'packages/flutter/examples/api/lib/foundation/key/value_key.0.dart', + 'packages/flutter/examples/api/test/foundation/key/value_key.0_test.dart', + 'packages/flutter/examples/api/lib/gestures/pointer_signal_resolver/pointer_signal_resolver.0.dart', + 'packages/flutter/examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart', + 'packages/flutter/examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart', + 'packages/flutter/examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart', + 'packages/flutter/examples/api/lib/painting/gradient/linear_gradient.0.dart', + 'packages/flutter/examples/api/lib/painting/star_border/star_border.0.dart', + 'packages/flutter/examples/api/lib/painting/axis_direction/axis_direction.0.dart', + 'packages/flutter/examples/api/lib/painting/borders/border_side.stroke_align.0.dart', + 'packages/flutter/examples/api/lib/painting/linear_border/linear_border.0.dart', + 'packages/flutter/examples/api/lib/painting/image_provider/image_provider.0.dart', + 'packages/flutter/examples/api/lib/painting/rounded_superellipse_border/rounded_superellipse_border.0.dart', + 'packages/flutter/examples/api/test/painting/gradient/linear_gradient.0_test.dart', + 'packages/flutter/examples/api/test/painting/star_border/star_border.0_test.dart', + 'packages/flutter/examples/api/test/painting/axis_direction/axis_direction.0_test.dart', + 'packages/flutter/examples/api/test/painting/borders/border_side.stroke_align.0_test.dart', + 'packages/flutter/examples/api/test/painting/linear_border/linear_border.0_test.dart', + 'packages/flutter/examples/api/test/painting/rounded_superellipse_border/rounded_superellipse_border.0_test.dart', + 'packages/flutter/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart', + 'packages/flutter/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart', + 'packages/flutter/examples/api/lib/rendering/growth_direction/growth_direction.0.dart', + 'packages/flutter/examples/api/lib/rendering/box/parent_data.0.dart', + 'packages/flutter/examples/api/lib/rendering/scroll_direction/scroll_direction.0.dart', + 'packages/flutter/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart', + 'packages/flutter/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart', + 'packages/flutter/examples/api/test/rendering/growth_direction/growth_direction.0_test.dart', + 'packages/flutter/examples/api/test/rendering/box/parent_data.0_test.dart', + 'packages/flutter/examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart', + 'packages/flutter/examples/api/lib/services/mouse_cursor/mouse_cursor.0.dart', + 'packages/flutter/examples/api/lib/services/binding/handle_request_app_exit.0.dart', + 'packages/flutter/examples/api/lib/services/text_input/text_input_control.0.dart', + 'packages/flutter/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart', + 'packages/flutter/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart', + 'packages/flutter/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0.dart', + 'packages/flutter/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart', + 'packages/flutter/examples/api/test/services/text_input/text_input_control.0_test.dart', + 'packages/flutter/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0_test.dart', + 'packages/flutter/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_ordinal_forms.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_contextual_alternates.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_alternative_fractions.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_oldstyle_figures.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_alternates.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_historical_forms.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_slashed_zero.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_superscripts.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_case_sensitive_forms.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_proportional_figures.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_historical_ligatures.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_tabular_figures.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_notational_forms.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.1_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_alternative.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_locale_aware.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_fractions.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_denominator.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_lining_figures.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_numerators.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_swash.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_scientific_inferiors.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_subscripts.0_test.dart', + 'packages/flutter/examples/api/test/ui/text/font_feature.font_feature_character_variant.0_test.dart', + 'packages/flutter/examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart', + 'packages/flutter/examples/api/lib/widgets/animated_grid/animated_grid.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart', + 'packages/flutter/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart', + 'packages/flutter/examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart', + 'packages/flutter/examples/api/lib/widgets/editable_text/text_editing_controller.0.dart', + 'packages/flutter/examples/api/lib/widgets/editable_text/text_editing_controller.1.dart', + 'packages/flutter/examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart', + 'packages/flutter/examples/api/lib/widgets/page/page_can_pop.0.dart', + 'packages/flutter/examples/api/lib/widgets/undo_history/undo_history_controller.0.dart', + 'packages/flutter/examples/api/lib/widgets/raw_tooltip/raw_tooltip.0.dart', + 'packages/flutter/examples/api/lib/widgets/form/form.1.dart', + 'packages/flutter/examples/api/lib/widgets/form/form.0.dart', + 'packages/flutter/examples/api/lib/widgets/layout_builder/layout_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart', + 'packages/flutter/examples/api/lib/widgets/restoration/restoration_mixin.0.dart', + 'packages/flutter/examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart', + 'packages/flutter/examples/api/lib/widgets/drag_target/draggable.0.dart', + 'packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart', + 'packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart', + 'packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart', + 'packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart', + 'packages/flutter/examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart', + 'packages/flutter/examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart', + 'packages/flutter/examples/api/lib/widgets/keep_alive/keep_alive.0.dart', + 'packages/flutter/examples/api/lib/widgets/safe_area/safe_area.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart', + 'packages/flutter/examples/api/lib/widgets/animated_size/animated_size.0.dart', + 'packages/flutter/examples/api/lib/widgets/framework/error_widget.0.dart', + 'packages/flutter/examples/api/lib/widgets/framework/build_owner.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/pinned_header_sliver.1.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_list.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/pinned_header_sliver.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_tree.1.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_tree.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_floating_header.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/decorated_sliver.1.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_opacity.1.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/decorated_sliver.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver/sliver_main_axis_group.0.dart', + 'packages/flutter/examples/api/lib/widgets/heroes/hero.0.dart', + 'packages/flutter/examples/api/lib/widgets/heroes/hero.1.dart', + 'packages/flutter/examples/api/lib/widgets/dismissible/dismissible.0.dart', + 'packages/flutter/examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart', + 'packages/flutter/examples/api/lib/widgets/preferred_size/preferred_size.0.dart', + 'packages/flutter/examples/api/lib/widgets/media_query/media_query_data.system_gesture_insets.0.dart', + 'packages/flutter/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart', + 'packages/flutter/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart', + 'packages/flutter/examples/api/lib/widgets/value_listenable_builder/value_listenable_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart', + 'packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart', + 'packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart', + 'packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart', + 'packages/flutter/examples/api/lib/widgets/async/stream_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/async/future_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart', + 'packages/flutter/examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart', + 'packages/flutter/examples/api/lib/widgets/animated_list/animated_list_separated.0.dart', + 'packages/flutter/examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart', + 'packages/flutter/examples/api/lib/widgets/animated_list/animated_list.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/fractionally_sized_box.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/physical_shape.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.2.dart', + 'packages/flutter/examples/api/lib/widgets/basic/flow.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/clip_rrect.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/ignore_pointer.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/fitted_box.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/listener.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/clip_rrect.1.dart', + 'packages/flutter/examples/api/lib/widgets/basic/offstage.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.1.dart', + 'packages/flutter/examples/api/lib/widgets/basic/overflowbox.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/indexed_stack.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/mouse_region.on_exit.1.dart', + 'packages/flutter/examples/api/lib/widgets/basic/expanded.1.dart', + 'packages/flutter/examples/api/lib/widgets/basic/mouse_region.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/expanded.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/mouse_region.on_exit.0.dart', + 'packages/flutter/examples/api/lib/widgets/basic/absorb_pointer.0.dart', + 'packages/flutter/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart', + 'packages/flutter/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart', + 'packages/flutter/examples/api/lib/widgets/autofill/autofill_group.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_view/list_view.1.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_view/list_view.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_view/grid_view.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_view/custom_scroll_view.1.dart', + 'packages/flutter/examples/api/lib/widgets/binding/widget_binding_observer.0.dart', + 'packages/flutter/examples/api/lib/widgets/image/image.loading_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/image/image.error_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/image/image.frame_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart', + 'packages/flutter/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart', + 'packages/flutter/examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart', + 'packages/flutter/examples/api/lib/widgets/color_filter/color_filtered.0.dart', + 'packages/flutter/examples/api/lib/widgets/implicit_animations/animated_padding.0.dart', + 'packages/flutter/examples/api/lib/widgets/implicit_animations/animated_positioned.0.dart', + 'packages/flutter/examples/api/lib/widgets/implicit_animations/animated_align.0.dart', + 'packages/flutter/examples/api/lib/widgets/implicit_animations/animated_slide.0.dart', + 'packages/flutter/examples/api/lib/widgets/implicit_animations/animated_container.0.dart', + 'packages/flutter/examples/api/lib/widgets/implicit_animations/sliver_animated_opacity.0.dart', + 'packages/flutter/examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart', + 'packages/flutter/examples/api/lib/widgets/radio_group/radio_group.0.dart', + 'packages/flutter/examples/api/lib/widgets/inherited_theme/inherited_theme.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart', + 'packages/flutter/examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart', + 'packages/flutter/examples/api/lib/widgets/page_transitions_builder/page_transitions_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/page_storage/page_storage.0.dart', + 'packages/flutter/examples/api/lib/widgets/table/table.0.dart', + 'packages/flutter/examples/api/lib/widgets/notification_listener/notification.0.dart', + 'packages/flutter/examples/api/lib/widgets/inherited_model/inherited_model.0.dart', + 'packages/flutter/examples/api/lib/widgets/focus_scope/focus.2.dart', + 'packages/flutter/examples/api/lib/widgets/focus_scope/focus_scope.0.dart', + 'packages/flutter/examples/api/lib/widgets/focus_scope/focus.1.dart', + 'packages/flutter/examples/api/lib/widgets/focus_scope/focus.0.dart', + 'packages/flutter/examples/api/lib/widgets/pop_scope/pop_scope.1.dart', + 'packages/flutter/examples/api/lib/widgets/pop_scope/pop_scope.0.dart', + 'packages/flutter/examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart', + 'packages/flutter/examples/api/lib/widgets/shortcuts/shortcuts.0.dart', + 'packages/flutter/examples/api/lib/widgets/shortcuts/character_activator.0.dart', + 'packages/flutter/examples/api/lib/widgets/shortcuts/shortcuts.1.dart', + 'packages/flutter/examples/api/lib/widgets/shortcuts/callback_shortcuts.0.dart', + 'packages/flutter/examples/api/lib/widgets/shortcuts/single_activator.0.dart', + 'packages/flutter/examples/api/lib/widgets/shortcuts/logical_key_set.0.dart', + 'packages/flutter/examples/api/lib/widgets/actions/action_listener.0.dart', + 'packages/flutter/examples/api/lib/widgets/actions/action.action_overridable.0.dart', + 'packages/flutter/examples/api/lib/widgets/actions/focusable_action_detector.0.dart', + 'packages/flutter/examples/api/lib/widgets/actions/actions.0.dart', + 'packages/flutter/examples/api/lib/widgets/widget_state/widget_state_property.0.dart', + 'packages/flutter/examples/api/lib/widgets/widget_state/widget_state_border_side.0.dart', + 'packages/flutter/examples/api/lib/widgets/widget_state/widget_state_outlined_border.0.dart', + 'packages/flutter/examples/api/lib/widgets/widget_state/widget_state_mouse_cursor.0.dart', + 'packages/flutter/examples/api/lib/widgets/magnifier/magnifier.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/restorable_route_future.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/navigator.0.dart', + 'packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push.0.dart', + 'packages/flutter/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart', + 'packages/flutter/examples/api/lib/widgets/system_context_menu/system_context_menu.1.dart', + 'packages/flutter/examples/api/lib/widgets/tween_animation_builder/tween_animation_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/page_view/page_view.0.dart', + 'packages/flutter/examples/api/lib/widgets/page_view/page_view.1.dart', + 'packages/flutter/examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart', + 'packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart', + 'packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart', + 'packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart', + 'packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart', + 'packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart', + 'packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart', + 'packages/flutter/examples/api/lib/widgets/text/ui_testing_with_text.dart', + 'packages/flutter/examples/api/lib/widgets/text/text.0.dart', + 'packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart', + 'packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart', + 'packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart', + 'packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart', + 'packages/flutter/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.1.dart', + 'packages/flutter/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/positioned_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.3.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/matrix_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.2.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/size_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/relative_positioned_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/animated_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/decorated_box_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/rotation_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/fade_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/animated_widget.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/align_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.1.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/default_text_style_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/slide_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/scale_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/transitions/sliver_fade_transition.0.dart', + 'packages/flutter/examples/api/lib/widgets/windows/popup.0.dart', + 'packages/flutter/examples/api/lib/widgets/windows/tooltip.0.dart', + 'packages/flutter/examples/api/lib/widgets/windows/satellite.0.dart', + 'packages/flutter/examples/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart', + 'packages/flutter/examples/api/lib/widgets/overlay/overlay_portal.0.dart', + 'packages/flutter/examples/api/lib/widgets/overlay/overlay.0.dart', + 'packages/flutter/examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart', + 'packages/flutter/examples/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart', + 'packages/flutter/examples/api/lib/widgets/focus_manager/focus_node.0.dart', + 'packages/flutter/examples/api/lib/widgets/restoration_properties/restorable_value.0.dart', + 'packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart', + 'packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart', + 'packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart', + 'packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.0.dart', + 'packages/flutter/examples/api/lib/widgets/routes/show_general_dialog.0.dart', + 'packages/flutter/examples/api/lib/widgets/routes/local_history_entry.0.dart', + 'packages/flutter/examples/api/lib/widgets/routes/route_observer.0.dart', + 'packages/flutter/examples/api/lib/widgets/routes/flexible_route_transitions.1.dart', + 'packages/flutter/examples/api/lib/widgets/routes/flexible_route_transitions.0.dart', + 'packages/flutter/examples/api/lib/widgets/routes/popup_route.0.dart', + 'packages/flutter/examples/api/lib/widgets/repeating_animation_builder/repeating_animation_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.1.dart', + 'packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart', + 'packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.0.dart', + 'packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.2.dart', + 'packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart', + 'packages/flutter/examples/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart', + 'packages/flutter/examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart', + 'packages/flutter/examples/api/test/widgets/animated_grid/animated_grid.0_test.dart', + 'packages/flutter/examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart', + 'packages/flutter/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart', + 'packages/flutter/examples/api/test/widgets/editable_text/text_editing_controller.1_test.dart', + 'packages/flutter/examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart', + 'packages/flutter/examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart', + 'packages/flutter/examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart', + 'packages/flutter/examples/api/test/widgets/raw_tooltip/raw_tooltip.0_test.dart', + 'packages/flutter/examples/api/test/widgets/form/form.0_test.dart', + 'packages/flutter/examples/api/test/widgets/form/form.1_test.dart', + 'packages/flutter/examples/api/test/widgets/tap_region/text_field_tap_region.0_test.dart', + 'packages/flutter/examples/api/test/widgets/restoration/restoration_mixin.0_test.dart', + 'packages/flutter/examples/api/test/widgets/drag_target/draggable.0_test.dart', + 'packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart', + 'packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart', + 'packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart', + 'packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart', + 'packages/flutter/examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart', + 'packages/flutter/examples/api/test/widgets/keep_alive/keep_alive.0_test.dart', + 'packages/flutter/examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart', + 'packages/flutter/examples/api/test/widgets/safe_area/safe_area.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart', + 'packages/flutter/examples/api/test/widgets/animated_size/animated_size.0_test.dart', + 'packages/flutter/examples/api/test/widgets/framework/build_owner.0_test.dart', + 'packages/flutter/examples/api/test/widgets/framework/error_widget.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/pinned_header_sliver.1_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/sliver_floating_header.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/pinned_header_sliver.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/sliver_opacity.1_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/decorated_sliver.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/sliver_cross_axis_group.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/decorated_sliver.1_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/sliver_main_axis_group.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/sliver_constrained_cross_axis.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver/sliver_resizing_header.0_test.dart', + 'packages/flutter/examples/api/test/widgets/heroes/hero.0_test.dart', + 'packages/flutter/examples/api/test/widgets/heroes/hero.1_test.dart', + 'packages/flutter/examples/api/test/widgets/dismissible/dismissible.0_test.dart', + 'packages/flutter/examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart', + 'packages/flutter/examples/api/test/widgets/preferred_size/preferred_size.0_test.dart', + 'packages/flutter/examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart', + 'packages/flutter/examples/api/test/widgets/value_listenable_builder/value_listenable_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.3_test.dart', + 'packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.2_test.dart', + 'packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart', + 'packages/flutter/examples/api/test/widgets/async/stream_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/async/future_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart', + 'packages/flutter/examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart', + 'packages/flutter/examples/api/test/widgets/animated_list/animated_list_separated.0_test.dart', + 'packages/flutter/examples/api/test/widgets/animated_list/sliver_animated_list.0_test.dart', + 'packages/flutter/examples/api/test/widgets/animated_list/animated_list.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/physical_shape.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/aspect_ratio.2_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/indexed_stack.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/clip_rrect.1_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/absorb_pointer.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/listener.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/clip_rrect.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/mouse_region.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/expanded.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/fitted_box.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/mouse_region.on_exit.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/aspect_ratio.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/fractionally_sized_box.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/expanded.1_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/mouse_region.on_exit.1_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/aspect_ratio.1_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/flow.0_test.dart', + 'packages/flutter/examples/api/test/widgets/basic/overflowbox.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.1_test.dart', + 'packages/flutter/examples/api/test/widgets/autofill/autofill_group.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_view/list_view.1_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_view/list_view.0_test.dart', + 'packages/flutter/examples/api/test/widgets/image/image.loading_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/image/image.error_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/image/image.frame_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/color_filter/color_filtered.0_test.dart', + 'packages/flutter/examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart', + 'packages/flutter/examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart', + 'packages/flutter/examples/api/test/widgets/implicit_animations/animated_slide.0_test.dart', + 'packages/flutter/examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart', + 'packages/flutter/examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart', + 'packages/flutter/examples/api/test/widgets/implicit_animations/animated_align.0_test.dart', + 'packages/flutter/examples/api/test/widgets/implicit_animations/animated_container.0_test.dart', + 'packages/flutter/examples/api/test/widgets/radio_group/radio_group.0_test.dart', + 'packages/flutter/examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_position/scroll_controller_on_attach.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_position/is_scrolling_listener.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scroll_position/scroll_controller_notification.0_test.dart', + 'packages/flutter/examples/api/test/widgets/page_transitions_builder/page_transitions_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/page_storage/page_storage.0_test.dart', + 'packages/flutter/examples/api/test/widgets/table/table.0_test.dart', + 'packages/flutter/examples/api/test/widgets/notification_listener/notification.0_test.dart', + 'packages/flutter/examples/api/test/widgets/inherited_model/inherited_model.0_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_scope/focus.0_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_scope/focus.1_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_scope/focus.2_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_scope/focus_scope.0_test.dart', + 'packages/flutter/examples/api/test/widgets/pop_scope/pop_scope.1_test.dart', + 'packages/flutter/examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart', + 'packages/flutter/examples/api/test/widgets/shortcuts/character_activator.0_test.dart', + 'packages/flutter/examples/api/test/widgets/actions/action_listener.0_test.dart', + 'packages/flutter/examples/api/test/widgets/actions/focusable_action_detector.0_test.dart', + 'packages/flutter/examples/api/test/widgets/actions/actions.0_test.dart', + 'packages/flutter/examples/api/test/widgets/actions/action.action_overridable.0_test.dart', + 'packages/flutter/examples/api/test/widgets/widget_state/widget_state_property.0_test.dart', + 'packages/flutter/examples/api/test/widgets/widget_state/widget_state_border_side.0_test.dart', + 'packages/flutter/examples/api/test/widgets/widget_state/widget_state_outlined_border.0_test.dart', + 'packages/flutter/examples/api/test/widgets/widget_state/widget_state_mouse_cursor.0_test.dart', + 'packages/flutter/examples/api/test/widgets/magnifier/magnifier.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator/restorable_route_future.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart', + 'packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart', + 'packages/flutter/examples/api/test/widgets/system_context_menu/system_context_menu.1_test.dart', + 'packages/flutter/examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart', + 'packages/flutter/examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/page_view/page_view.0_test.dart', + 'packages/flutter/examples/api/test/widgets/page_view/page_view.1_test.dart', + 'packages/flutter/examples/api/test/widgets/hardware_keyboard/key_event_manager.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart', + 'packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart', + 'packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart', + 'packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart', + 'packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart', + 'packages/flutter/examples/api/test/widgets/text/text.0_test.dart', + 'packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart', + 'packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart', + 'packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart', + 'packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart', + 'packages/flutter/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart', + 'packages/flutter/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/listenable_builder.3_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/matrix_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/listenable_builder.2_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/align_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/size_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/fade_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/listenable_builder.1_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/slide_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/positioned_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/animated_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/listenable_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/scale_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/animated_widget.0_test.dart', + 'packages/flutter/examples/api/test/widgets/transitions/rotation_transition.0_test.dart', + 'packages/flutter/examples/api/test/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0_test.dart', + 'packages/flutter/examples/api/test/widgets/overlay/overlay.0_test.dart', + 'packages/flutter/examples/api/test/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart', + 'packages/flutter/examples/api/test/widgets/focus_manager/focus_node.0_test.dart', + 'packages/flutter/examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart', + 'packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.3_test.dart', + 'packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.2_test.dart', + 'packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart', + 'packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart', + 'packages/flutter/examples/api/test/widgets/routes/popup_route.0_test.dart', + 'packages/flutter/examples/api/test/widgets/routes/show_general_dialog.0_test.dart', + 'packages/flutter/examples/api/test/widgets/repeating_animation_builder/repeating_animation_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart', + 'packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart', + 'packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart', + 'packages/flutter/examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart', + 'packages/flutter/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart', 'examples/flutter_view/lib/main.dart', - 'examples/multiple_windows/lib/app/main_window.dart', - 'examples/multiple_windows/lib/app/tooltip_button.dart', - 'examples/multiple_windows/lib/app/tooltip_window_edit_dialog.dart', - 'examples/multiple_windows/lib/app/dialog_window_content.dart', - 'examples/multiple_windows/lib/app/dialog_window_edit_dialog.dart', - 'examples/multiple_windows/lib/app/popup_window_content.dart', - 'examples/multiple_windows/lib/app/window_content.dart', - 'examples/multiple_windows/lib/app/window_edit_dialog.dart', - 'examples/multiple_windows/lib/app/rotated_wire_cube.dart', - 'examples/multiple_windows/lib/app/popup_window_edit_dialog.dart', - 'examples/multiple_windows/lib/app/tooltip_window_content.dart', - 'examples/multiple_windows/lib/app/popup_button.dart', - 'examples/multiple_windows/lib/app/window_settings_dialog.dart', - 'examples/multiple_windows/lib/main.dart', - 'examples/multiple_windows/test/multiple_windows_test.dart', 'examples/texture/lib/main.dart', }; - static final RegExp _examplesPrefix = RegExp(r'examples'); + static final RegExp _examplesPrefix = RegExp(r'packages[/\\]flutter[/\\]examples[/\\]api|examples'); - /// Find the `examples/api/lib` and `examples/api/test` directories - /// which contain the API examples and relevant tests. + /// Find the `packages/flutter/examples/api/lib` and + /// `packages/flutter/examples/api/test` directories which contain the API + /// examples and relevant tests. /// - /// For the cross imports checker, only the `examples/api/lib` and `examples/api/test` directories are relevant. - /// The other directories in `examples/api` are either generated (e.g. build or .dart_tool), - /// platform directories for the samples (e.g. windows or linux), - /// or a shim for the integration test driver. + /// For the cross imports checker, only the + /// `packages/flutter/examples/api/lib` and + /// `packages/flutter/examples/api/test` directories are relevant. The other + /// directories in `packages/flutter/examples/api` are either generated (e.g. + /// build or .dart_tool), platform directories for the samples (e.g. windows + /// or linux), or a shim for the integration test driver. ({Directory libDirectory, Directory testDirectory}) _findExamplesSlashApiDirectories( Directory examplesSlashApiDirectory, ) { @@ -613,11 +618,11 @@ class ExamplesCrossImportChecker { } if (examplesSlashApiLibDirectory == null) { - throw StateError('Could not find lib directory in examples/api.'); + throw StateError('Could not find lib directory in packages/flutter/examples/api.'); } if (examplesSlashApiTestDirectory == null) { - throw StateError('Could not find test directory in examples/api.'); + throw StateError('Could not find test directory in packages/flutter/examples/api.'); } return ( @@ -632,9 +637,14 @@ class ExamplesCrossImportChecker { const _ExamplesLibrary examplesRoot = _GenericExampleLibrary('examples'); final Map<_ExamplesLibrary, Set> mapping = {examplesRoot: {}}; + final List allExampleFiles = [ + ...examplesDirectory.listSync(), + if (!apiDocDirectory.absolute.path.startsWith(examplesDirectory.absolute.path)) + apiDocDirectory, + ]; // List the files directly under `examples` and then walk the subdirectories. - for (final FileSystemEntity fileSystemEntity in examplesDirectory.listSync()) { + for (final fileSystemEntity in allExampleFiles) { if (fileSystemEntity is File && fileSystemEntity.absolute.path.contains(dartFilePattern)) { mapping[examplesRoot]?.add(fileSystemEntity); @@ -651,7 +661,7 @@ class ExamplesCrossImportChecker { continue; } - // The examples/api folder contains examples in a single Flutter project, + // The packages/flutter/examples/api folder contains examples in a single Flutter project, // grouped in subfolders in lib/ and test/, so these need to be handled separately. if (directoryName == 'api') { final examplesSlashApiLibrary = _ExamplesLibrary.fromDirectory( @@ -659,7 +669,7 @@ class ExamplesCrossImportChecker { flutterRoot: flutterRoot, ); - // First list the files directly under examples/api. + // First list the files directly under packages/flutter/examples/api. mapping[examplesSlashApiLibrary] = { for (final File file in fileSystemEntity.listSync().whereType()) if (file.absolute.path.contains(dartFilePattern)) file, @@ -668,7 +678,9 @@ class ExamplesCrossImportChecker { final (:Directory libDirectory, :Directory testDirectory) = _findExamplesSlashApiDirectories(fileSystemEntity); - // Handle the files under examples/api/lib/sample_templates and examples/api/test/sample_templates, + // Handle the files under + // `packages/flutter/examples/api/lib/sample_templates` and + // `packages/flutter/examples/api/test/sample_templates`, // which list individual files with a specific file pattern. mapping.addAll( _getExamplesSlashApiSampleTemplatesFiles( @@ -703,12 +715,21 @@ class ExamplesCrossImportChecker { /// Get a list of all the filenames that end in ".dart" for the given examples directory. /// - /// The [directory] must not be a subdirectory of `examples/api`. + /// The [directory] must not be a subdirectory of `packages/flutter/examples/api`. Set _getExampleFilesForDirectory(Directory directory, {required Pattern dartFilePattern}) { - final String examplesSlashApiPath = path.join(flutterRoot.absolute.path, 'examples', 'api'); + final String examplesSlashApiPath = path.join( + flutterRoot.absolute.path, + 'packages', + 'flutter', + 'examples', + 'api', + ); if (directory.absolute.path.startsWith(examplesSlashApiPath)) { - throw ArgumentError('Directory must not be an examples/api subdirectory.', 'directory'); + throw ArgumentError( + 'Directory must not be an packages/flutter/examples/api subdirectory.', + 'directory', + ); } final files = {}; @@ -742,7 +763,9 @@ class ExamplesCrossImportChecker { } /// Get a list of all the filenames that end in ".dart", grouped by library, - /// for the subdrectories of `examples/api/lib/sample_templates` and `examples/api/test/sample_templates`. + /// for the subdrectories of + /// `packages/flutter/examples/api/lib/sample_templates` and + /// `packages/flutter/examples/api/test/sample_templates`. Map<_SampleTemplatesLibraryFile, Set> _getExamplesSlashApiSampleTemplatesFiles({ required Directory libDirectory, required Directory testDirectory, @@ -775,8 +798,9 @@ class ExamplesCrossImportChecker { } /// Get a list of all the filenames that end in ".dart", grouped by library, - /// for the subdirectories of `examples/api`, - /// except `examples/api/lib/sample_templates` and `examples/api/test/sample_templates`. + /// for the subdirectories of `packages/flutter/examples/api`, except + /// `packages/flutter/examples/api/lib/sample_templates` and + /// `packages/flutter/examples/api/test/sample_templates`. Map<_ExamplesLibrary, Set> _getExamplesSlashApiExamples({ required Directory libDirectory, required Directory testDirectory, @@ -878,7 +902,8 @@ class ExamplesCrossImportChecker { // Find any known cross imports that weren't found, and are therefore fixed. // Pre-compute all library prefixes so that root libraries (e.g. `examples`, - // `examples/api`) don't claim entries that belong to a more-specific sub-library. + // `packages/flutter/examples/api`) don't claim entries that belong to a + // more-specific sub-library. // TODO(justinmc): Remove this after all known cross imports have been // fixed. // See https://github.com/flutter/flutter/issues/187645. @@ -926,6 +951,9 @@ class ExamplesCrossImportChecker { } /// The examples that we are concerned with cross importing. +/// +/// Include the API docs in `packages/flutter/examples/api` and non-API docs in +/// `examples`. sealed class _ExamplesLibrary implements CrossImportCheckedLibrary { const _ExamplesLibrary(this._name); @@ -943,33 +971,33 @@ sealed class _ExamplesLibrary implements CrossImportCheckedLibrary { return switch (relativePath) { _ - when relativePath.startsWith('examples/api/lib/cupertino') || - relativePath.startsWith('examples/api/test/cupertino') => + when relativePath.startsWith('packages/flutter/examples/api/lib/cupertino') || + relativePath.startsWith('packages/flutter/examples/api/test/cupertino') => _CupertinoApiExampleLibrary(relativePath), _ - when relativePath.startsWith('examples/api/lib/material') || - relativePath.startsWith('examples/api/test/material') => + when relativePath.startsWith('packages/flutter/examples/api/lib/material') || + relativePath.startsWith('packages/flutter/examples/api/test/material') => _MaterialApiExampleLibrary(relativePath), _ - when relativePath.startsWith('examples/api/lib/animation') || - relativePath.startsWith('examples/api/lib/foundation') || - relativePath.startsWith('examples/api/lib/gestures') || - relativePath.startsWith('examples/api/lib/painting') || - relativePath.startsWith('examples/api/lib/rendering') || - relativePath.startsWith('examples/api/lib/services') || - relativePath.startsWith('examples/api/lib/ui') || - relativePath.startsWith('examples/api/lib/widgets') => + when relativePath.startsWith('packages/flutter/examples/api/lib/animation') || + relativePath.startsWith('packages/flutter/examples/api/lib/foundation') || + relativePath.startsWith('packages/flutter/examples/api/lib/gestures') || + relativePath.startsWith('packages/flutter/examples/api/lib/painting') || + relativePath.startsWith('packages/flutter/examples/api/lib/rendering') || + relativePath.startsWith('packages/flutter/examples/api/lib/services') || + relativePath.startsWith('packages/flutter/examples/api/lib/ui') || + relativePath.startsWith('packages/flutter/examples/api/lib/widgets') => _ApiExampleLibrary(relativePath), _ - when relativePath.startsWith('examples/api/test/animation') || - relativePath.startsWith('examples/api/test/foundation') || - relativePath.startsWith('examples/api/test/gestures') || - relativePath.startsWith('examples/api/test/painting') || - relativePath.startsWith('examples/api/test/rendering') || - relativePath.startsWith('examples/api/test/services') || - relativePath.startsWith('examples/api/test/ui') || - relativePath.startsWith('examples/api/test/widgets') => + when relativePath.startsWith('packages/flutter/examples/api/test/animation') || + relativePath.startsWith('packages/flutter/examples/api/test/foundation') || + relativePath.startsWith('packages/flutter/examples/api/test/gestures') || + relativePath.startsWith('packages/flutter/examples/api/test/painting') || + relativePath.startsWith('packages/flutter/examples/api/test/rendering') || + relativePath.startsWith('packages/flutter/examples/api/test/services') || + relativePath.startsWith('packages/flutter/examples/api/test/ui') || + relativePath.startsWith('packages/flutter/examples/api/test/widgets') => _ApiExampleLibrary(relativePath), _ when relativePath.startsWith('examples/flutter_view') || @@ -983,7 +1011,9 @@ sealed class _ExamplesLibrary implements CrossImportCheckedLibrary { relativePath.startsWith('examples/splash') || relativePath.startsWith('examples/texture') => _ApiExampleLibrary(relativePath), - _ when relativePath.startsWith('examples/api') || relativePath.startsWith('examples') => + _ + when relativePath.startsWith('packages/flutter/examples/api') || + relativePath.startsWith('examples') => _ApiExampleLibrary(relativePath), _ => throw UnimplementedError('Unknown library: $relativePath'), }; @@ -1022,61 +1052,65 @@ sealed class _ExamplesLibrary implements CrossImportCheckedLibrary { } } -/// Any API example - not related to Material or Cupertino - inside `examples/api`, and its tests. +/// Any API example - not related to Material or Cupertino - inside +/// `packages/flutter/examples/api`, and its tests. /// -/// For example `examples/api/lib/foundation` and `examples/api/test/foundation`. +/// For example +/// `packages/flutter/examples/api/lib/foundation` and +/// `packages/flutter/examples/api/test/foundation`. final class _ApiExampleLibrary extends _ExamplesLibrary { const _ApiExampleLibrary(super.name); } -/// The examples in `examples/api/lib/cupertino` -/// and their tests in `examples/api/test/cupertino`. +/// The examples in `packages/flutter/examples/api/lib/cupertino` +/// and their tests in `packages/flutter/examples/api/test/cupertino`. final class _CupertinoApiExampleLibrary extends _ExamplesLibrary { const _CupertinoApiExampleLibrary(super.name); @override bool canImport(LibraryCrossImportStatementType import) { - // While the Cupertino examples under `examples/api` are not allowed to import Material, - // the actual samples have been relocated to `packages/cupertino_ui`, - // where their cross imports will be addressed separately. - // The existing samples in `examples/api` are the now defunct orginal samples. - // For the purpose of the checker, allow all imports. + // While the Cupertino examples under `packages/flutter/examples/api` are + // not allowed to import Material, the actual samples have been relocated to + // `packages/cupertino_ui`, where their cross imports will be addressed + // separately. The existing samples in `packages/flutter/examples/api` are + // the now defunct orginal samples. For the purpose of the checker, allow + // all imports. return true; } } -/// Any non-API example, not in `examples/api`, +/// Any non-API example, not in `packages/flutter/examples/api`, /// such as `examples/flutter_view` or `examples/hello_world`. final class _GenericExampleLibrary extends _ExamplesLibrary { const _GenericExampleLibrary(super.name); } -/// The examples in `examples/api/lib/material` -/// and their tests in `examples/api/test/material`. +/// The examples in `packages/flutter/examples/api/lib/material` +/// and their tests in `packages/flutter/examples/api/test/material`. final class _MaterialApiExampleLibrary extends _ExamplesLibrary { const _MaterialApiExampleLibrary(super.name); @override bool canImport(LibraryCrossImportStatementType import) { - // While the Material examples under `examples/api` are not allowed to import Cupertino, + // While the Material examples under `packages/flutter/examples/api` are not allowed to import Cupertino, // the actual samples have been relocated to `packages/material_ui`, // where their cross imports will be addressed separately. - // The existing samples in `examples/api` are the now defunct orginal samples. + // The existing samples in `packages/flutter/examples/api` are the now defunct orginal samples. // For the purpose of the checker, allow all imports. return true; } } -/// The examples in `examples/api/lib/sample_templates` -/// and their tests in `examples/api/test/sample_templates`. +/// The examples in `packages/flutter/examples/api/lib/sample_templates` +/// and their tests in `packages/flutter/examples/api/test/sample_templates`. /// /// The sample templates are individual files, rather than directories. final class _SampleTemplatesLibraryFile extends _ExamplesLibrary { const _SampleTemplatesLibraryFile._(super.name, this._filePath); factory _SampleTemplatesLibraryFile.fromFile(File file) { - const examplesLibPrefix = 'examples/api/lib/sample_templates'; - const examplesTestPrefix = 'examples/api/test/sample_templates'; + const examplesLibPrefix = 'packages/flutter/examples/api/lib/sample_templates'; + const examplesTestPrefix = 'packages/flutter/examples/api/test/sample_templates'; final String filePath = file.absolute.path.replaceAll(Platform.pathSeparator, '/'); final int libIndex = filePath.indexOf(examplesLibPrefix); diff --git a/dev/bots/cross_imports_checker_utils.dart b/dev/bots/cross_imports_checker_utils.dart index da55972cc1296..467c2b908d1b3 100644 --- a/dev/bots/cross_imports_checker_utils.dart +++ b/dev/bots/cross_imports_checker_utils.dart @@ -26,7 +26,8 @@ abstract interface class CrossImportCheckedLibrary { /// The short name of the library. /// - /// For example `packages/flutter/test/widgets` or `examples/api/foo`. + /// For example `packages/flutter/test/widgets` or + /// `packages/flutter/examples/api/foo`. String get libraryName; /// The message that instructs how to remove now-fixed cross imports for this library, diff --git a/dev/bots/suite_runners/run_framework_tests.dart b/dev/bots/suite_runners/run_framework_tests.dart index 0b32d1ac18e5e..1151980a6599f 100644 --- a/dev/bots/suite_runners/run_framework_tests.dart +++ b/dev/bots/suite_runners/run_framework_tests.dart @@ -87,10 +87,13 @@ Future frameworkTestsRunner() async { ], workingDirectory: flutterRoot); await runCommand(dart, [ path.join(flutterRoot, 'dev', 'tools', 'examples_smoke_test.dart'), - ], workingDirectory: path.join(flutterRoot, 'examples', 'api')); - for (final FileSystemEntity entity in Directory( - path.join(flutterRoot, 'examples'), - ).listSync()) { + ], workingDirectory: path.join(flutterRoot, 'packages', 'flutter', 'examples', 'api')); + final List allExamplesDir = [ + ...Directory(path.join(flutterRoot, 'examples')).listSync(), + ...Directory(path.join(flutterRoot, 'packages', 'flutter', 'examples')).listSync(), + ]; + + for (final entity in allExamplesDir) { if (entity is! Directory || !Directory(path.join(entity.path, 'test')).existsSync()) { continue; } diff --git a/dev/bots/test/analyze-test-input/ktlint-baseline.xml b/dev/bots/test/analyze-test-input/ktlint-baseline.xml index da312850b537a..d9fd2a0b8503b 100644 --- a/dev/bots/test/analyze-test-input/ktlint-baseline.xml +++ b/dev/bots/test/analyze-test-input/ktlint-baseline.xml @@ -33,7 +33,7 @@ - + diff --git a/dev/bots/test/check_code_samples_test.dart b/dev/bots/test/check_code_samples_test.dart index 0a78cd47e7b4d..8de914b0b3286 100644 --- a/dev/bots/test/check_code_samples_test.dart +++ b/dev/bots/test/check_code_samples_test.dart @@ -20,13 +20,16 @@ void main() { late Directory dartUIPath; late Directory flutterRoot; + Directory exampleBase() => examples.parent.parent; + String getRelativePath(File file, [Directory? from]) { from ??= flutterRoot; - return path.relative(file.absolute.path, from: flutterRoot.absolute.path); + return path.relative(file.absolute.path, from: from.absolute.path); } void writeLink({required File source, required File example, String? alternateLink}) { - final String link = alternateLink ?? ' ** See code in ${getRelativePath(example)} **'; + final String relativePath = getRelativePath(example, exampleBase()); + final String link = alternateLink ?? ' ** See code in $relativePath **'; source ..createSync(recursive: true) ..writeAsStringSync(''' @@ -45,8 +48,14 @@ void main() { bool missingTests = false, bool malformedLinks = false, }) { - final Directory examplesLib = examples.childDirectory('lib').childDirectory('layer') - ..createSync(recursive: true); + final Directory examplesLib = + packages + .childDirectory('flutter') + .childDirectory('examples') + .childDirectory('api') + .childDirectory('lib') + .childDirectory('layer') + ..createSync(recursive: true); final File fooExample = examplesLib.childFile('foo_example.0.dart') ..createSync(recursive: true) ..writeAsStringSync('// Example for foo'); @@ -58,8 +67,14 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync('// Example that is not linked'); } - final Directory examplesTests = examples.childDirectory('test').childDirectory('layer') - ..createSync(recursive: true); + final Directory examplesTests = + packages + .childDirectory('flutter') + .childDirectory('examples') + .childDirectory('api') + .childDirectory('test') + .childDirectory('layer') + ..createSync(recursive: true); examplesTests.childFile('foo_example.0_test.dart') ..createSync(recursive: true) ..writeAsStringSync('// test for foo example'); @@ -109,8 +124,13 @@ void main() { path.join(path.rootPrefix(fs.currentDirectory.absolute.path), 'flutter sdk'), )..createSync(recursive: true); fs.currentDirectory = flutterRoot; - examples = flutterRoot.childDirectory('examples').childDirectory('api') - ..createSync(recursive: true); + examples = + flutterRoot + .childDirectory('packages') + .childDirectory('flutter') + .childDirectory('examples') + .childDirectory('api') + ..createSync(recursive: true); packages = flutterRoot.childDirectory('packages')..createSync(recursive: true); dartUIPath = flutterRoot @@ -194,7 +214,7 @@ void main() { [ '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════', '║ The following example test files are missing:', - '║ examples/api/test/layer/bar_example.0_test.dart', + '║ packages/flutter/examples/api/test/layer/bar_example.0_test.dart', '╚═══════════════════════════════════════════════════════════════════════════════', ] .map((String line) { diff --git a/dev/bots/test/check_examples_cross_imports_test.dart b/dev/bots/test/check_examples_cross_imports_test.dart index 809cd6b4be40d..3e2620658f203 100644 --- a/dev/bots/test/check_examples_cross_imports_test.dart +++ b/dev/bots/test/check_examples_cross_imports_test.dart @@ -13,8 +13,11 @@ import '../cross_imports_checker_utils.dart'; import 'common.dart'; import 'cross_imports_checker_test_utils.dart'; -// A pattern that matches `examples/api/lib/**` and `examples/api/test/**`, for use in tests. -final _kExamplesSlashApiLibraryPattern = RegExp(r'^examples/api/(lib|test)/[a-z_]+'); +// A pattern that matches `packages/flutter/examples/api/lib/**` and +// `packages/flutter/examples/api/test/**`, for use in tests. +final _kExamplesSlashApiLibraryPattern = RegExp( + r'^packages/flutter/examples/api/(lib|test)/[a-z_]+', +); void main() { late ExamplesCrossImportChecker checker; @@ -58,9 +61,17 @@ void main() { fs.currentDirectory = flutterRoot; final Directory examplesDirectory = flutterRoot.childDirectory('examples')..createSync(); + final Directory apiDocDirectory = + flutterRoot + .childDirectory('packages') + .childDirectory('flutter') + .childDirectory('examples') + .childDirectory('api') + ..createSync(recursive: true); checker = ExamplesCrossImportChecker( examplesDirectory: examplesDirectory, + apiDocDirectory: apiDocDirectory, flutterRoot: flutterRoot, filesystem: fs, ); @@ -125,8 +136,7 @@ void main() { }); test('examples/api/lib/sample_templates templates produce no violations when valid', () async { - final Directory sampleTemplatesDirectory = checker.examplesDirectory - .childDirectory('api') + final Directory sampleTemplatesDirectory = checker.apiDocDirectory .childDirectory('lib') .childDirectory('sample_templates'); @@ -152,8 +162,7 @@ void main() { }); test('examples/api/lib/sample_templates templates produce violations when invalid', () async { - final Directory sampleTemplatesDirectory = checker.examplesDirectory - .childDirectory('api') + final Directory sampleTemplatesDirectory = checker.apiDocDirectory .childDirectory('lib') .childDirectory('sample_templates'); @@ -175,16 +184,16 @@ void main() { final String lines = [ '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════', - '║ The following file in examples/api/lib/sample_templates has a disallowed import of Material. Refactor it or move it to the Material examples.', - '║ examples/api/lib/sample_templates/cupertino.0.dart', + '║ The following file in packages/flutter/examples/api/lib/sample_templates has a disallowed import of Material. Refactor it or move it to the Material examples.', + '║ packages/flutter/examples/api/lib/sample_templates/cupertino.0.dart', '╚═══════════════════════════════════════════════════════════════════════════════', '╔═╡ERROR #2╞════════════════════════════════════════════════════════════════════', - '║ The following file in examples/api/lib/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', - '║ examples/api/lib/sample_templates/material.0.dart', + '║ The following file in packages/flutter/examples/api/lib/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', + '║ packages/flutter/examples/api/lib/sample_templates/material.0.dart', '╚═══════════════════════════════════════════════════════════════════════════════', '╔═╡ERROR #3╞════════════════════════════════════════════════════════════════════', - '║ The following file in examples/api/lib/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', - '║ examples/api/lib/sample_templates/widgets.0.dart', + '║ The following file in packages/flutter/examples/api/lib/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', + '║ packages/flutter/examples/api/lib/sample_templates/widgets.0.dart', '╚═══════════════════════════════════════════════════════════════════════════════', ].join('\n'); expect(result, equals('$lines\n')); @@ -192,8 +201,7 @@ void main() { }); test('examples/api/test/sample_templates templates produce no violations when valid', () async { - final Directory sampleTemplatesDirectory = checker.examplesDirectory - .childDirectory('api') + final Directory sampleTemplatesDirectory = checker.apiDocDirectory .childDirectory('test') .childDirectory('sample_templates'); @@ -219,8 +227,7 @@ void main() { }); test('examples/api/test/sample_templates templates produce violations when invalid', () async { - final Directory sampleTemplatesDirectory = checker.examplesDirectory - .childDirectory('api') + final Directory sampleTemplatesDirectory = checker.apiDocDirectory .childDirectory('test') .childDirectory('sample_templates'); @@ -242,16 +249,16 @@ void main() { final String lines = [ '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════', - '║ The following file in examples/api/test/sample_templates has a disallowed import of Material. Refactor it or move it to the Material examples.', - '║ examples/api/test/sample_templates/cupertino.0_test.dart', + '║ The following file in packages/flutter/examples/api/test/sample_templates has a disallowed import of Material. Refactor it or move it to the Material examples.', + '║ packages/flutter/examples/api/test/sample_templates/cupertino.0_test.dart', '╚═══════════════════════════════════════════════════════════════════════════════', '╔═╡ERROR #2╞════════════════════════════════════════════════════════════════════', - '║ The following file in examples/api/test/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', - '║ examples/api/test/sample_templates/material.0_test.dart', + '║ The following file in packages/flutter/examples/api/test/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', + '║ packages/flutter/examples/api/test/sample_templates/material.0_test.dart', '╚═══════════════════════════════════════════════════════════════════════════════', '╔═╡ERROR #3╞════════════════════════════════════════════════════════════════════', - '║ The following file in examples/api/test/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', - '║ examples/api/test/sample_templates/widgets.0_test.dart', + '║ The following file in packages/flutter/examples/api/test/sample_templates has a disallowed import of Cupertino. Refactor it or move it to the Cupertino examples.', + '║ packages/flutter/examples/api/test/sample_templates/widgets.0_test.dart', '╚═══════════════════════════════════════════════════════════════════════════════', ].join('\n'); expect(result, equals('$lines\n')); @@ -802,8 +809,8 @@ void main() { buildKnownCrossImportExamplesFiles(); - const examplesApiMaterialLibraryName = 'examples/api/lib/material'; - const examplesApiMaterialTestLibraryName = 'examples/api/test/material'; + const examplesApiMaterialLibraryName = 'packages/flutter/examples/api/lib/material'; + const examplesApiMaterialTestLibraryName = 'packages/flutter/examples/api/test/material'; const LibraryCrossImportStatementType importStatement = .cupertino; final Directory examplesMaterialLibFilesDirectory = getDirectoryForExamplesSlashApiLibrary( @@ -817,13 +824,19 @@ void main() { ); writeImportInFiles( - {'examples/api/lib/material/qux.dart', 'examples/api/lib/material/baz/foo.dart'}, + { + 'packages/flutter/examples/api/lib/material/qux.dart', + 'packages/flutter/examples/api/lib/material/baz/foo.dart', + }, inDirectory: examplesMaterialLibFilesDirectory, importString: importStatement.importString, ); writeImportInFiles( - {'examples/api/test/material/qux_test.dart', 'examples/api/test/material/baz/foo_test.dart'}, + { + 'packages/flutter/examples/api/test/material/qux_test.dart', + 'packages/flutter/examples/api/test/material/baz/foo_test.dart', + }, inDirectory: examplesMaterialTestFilesDirectory, importString: importStatement.importString, ); @@ -844,8 +857,8 @@ void main() { buildKnownCrossImportExamplesFiles(); - const examplesApiCupertinoLibraryName = 'examples/api/lib/cupertino'; - const examplesApiCupertinoTestLibraryName = 'examples/api/test/cupertino'; + const examplesApiCupertinoLibraryName = 'packages/flutter/examples/api/lib/cupertino'; + const examplesApiCupertinoTestLibraryName = 'packages/flutter/examples/api/test/cupertino'; const LibraryCrossImportStatementType importStatement = .material; final Directory examplesCupertinoLibFilesDirectory = getDirectoryForExamplesSlashApiLibrary( @@ -859,15 +872,18 @@ void main() { ); writeImportInFiles( - {'examples/api/lib/cupertino/qux.dart', 'examples/api/lib/cupertino/baz/foo.dart'}, + { + 'packages/flutter/examples/api/lib/cupertino/qux.dart', + 'packages/flutter/examples/api/lib/cupertino/baz/foo.dart', + }, inDirectory: examplesCupertinoLibFilesDirectory, importString: importStatement.importString, ); writeImportInFiles( { - 'examples/api/test/cupertino/qux_test.dart', - 'examples/api/test/cupertino/baz/foo_test.dart', + 'packages/flutter/examples/api/test/cupertino/qux_test.dart', + 'packages/flutter/examples/api/test/cupertino/baz/foo_test.dart', }, inDirectory: examplesCupertinoTestFilesDirectory, importString: importStatement.importString, @@ -891,13 +907,13 @@ void main() { /// Returns [LibraryCrossImportStatementType.material] for any other library, /// such as `examples/layers/rendering/spinning_square.dart`. LibraryCrossImportStatementType getCrossImportStatementForExamplesLibraryFile(String filePath) { - if (filePath.startsWith('examples/api/lib/material/') || - filePath.startsWith('examples/api/test/material/')) { + if (filePath.startsWith('packages/flutter/examples/api/lib/material/') || + filePath.startsWith('packages/flutter/examples/api/test/material/')) { return LibraryCrossImportStatementType.cupertino; } - if (filePath.startsWith('examples/api/lib/cupertino/') || - filePath.startsWith('examples/api/test/cupertino/')) { + if (filePath.startsWith('packages/flutter/examples/api/lib/cupertino/') || + filePath.startsWith('packages/flutter/examples/api/test/cupertino/')) { return LibraryCrossImportStatementType.material; } @@ -941,19 +957,20 @@ bool hasNoKnownCrossImports(String libraryName) { /// Returns whether the given [libraryName] matches the Material examples under `examples/api`. bool isMaterialExample(String libraryName) { - return libraryName == 'examples/api/lib/material' || libraryName == 'examples/api/test/material'; + return libraryName == 'packages/flutter/examples/api/lib/material' || + libraryName == 'packages/flutter/examples/api/test/material'; } /// Returns whether the given [libraryName] matches the Cupertino examples under `examples/api`. bool isCupertinoExample(String libraryName) { - return libraryName == 'examples/api/lib/cupertino' || - libraryName == 'examples/api/test/cupertino'; + return libraryName == 'packages/flutter/examples/api/lib/cupertino' || + libraryName == 'packages/flutter/examples/api/test/cupertino'; } /// Returns whether the given [libraryName] matches the root `examples` or `examples/api` directories, /// which contain subdirectories with examples, but should themselves be void of examples. bool isExamplesRoot(String libraryName) { - return libraryName == 'examples' || libraryName == 'examples/api'; + return libraryName == 'examples' || libraryName == 'packages/flutter/examples/api'; } // A utility that keeps track of the directories under test, @@ -961,7 +978,11 @@ bool isExamplesRoot(String libraryName) { class _CrossImportsExamplesDirectories { factory _CrossImportsExamplesDirectories(Directory examplesDirectory) { return _CrossImportsExamplesDirectories._( - examplesSlashApiDirectory: examplesDirectory.childDirectory('api'), + examplesSlashApiDirectory: examplesDirectory.parent + .childDirectory('packages') + .childDirectory('flutter') + .childDirectory('examples') + .childDirectory('api'), examplesFlutterViewDirectory: examplesDirectory.childDirectory('flutter_view'), examplesHelloWorldDirectory: examplesDirectory.childDirectory('hello_world'), examplesImageListDirectory: examplesDirectory.childDirectory('image_list'), @@ -1064,7 +1085,7 @@ class _CrossImportsExamplesDirectories { /// Get the examples directory for the given [libraryName]. Directory examplesFilesDirectoryFor(String libraryName, Directory examplesDirectory) { - const unsupportedPrefix = 'examples/api'; + const unsupportedPrefix = 'packages/flutter/examples/api'; if (libraryName.startsWith(unsupportedPrefix) && libraryName.length > unsupportedPrefix.length) { @@ -1076,7 +1097,7 @@ class _CrossImportsExamplesDirectories { return switch (libraryName) { 'examples' => examplesDirectory, - 'examples/api' => examplesSlashApiDirectory, + 'packages/flutter/examples/api' => examplesSlashApiDirectory, 'examples/flutter_view' => examplesFlutterViewDirectory, 'examples/hello_world' => examplesHelloWorldDirectory, 'examples/image_list' => examplesImageListDirectory, @@ -1095,7 +1116,7 @@ class _CrossImportsExamplesDirectories { // A mapping of `examples/**` test cases for the cross imports checker, excluding `examples/api/**`. const crossImportsGenericExamplesTestCases = [ 'examples', - 'examples/api', + 'packages/flutter/examples/api', 'examples/flutter_view', 'examples/hello_world', 'examples/image_list', @@ -1111,20 +1132,20 @@ const crossImportsGenericExamplesTestCases = [ // A mapping of `examples/api/lib/**` and `examples/api/test/**` test cases for the cross imports checker, // excluding `examples/api/lib/sample_templates` and `examples/api/test/sample_templates`. const crossImportsExamplesApiTestCases = [ - 'examples/api/lib/animation', - 'examples/api/lib/foundation', - 'examples/api/lib/gestures', - 'examples/api/lib/painting', - 'examples/api/lib/rendering', - 'examples/api/lib/services', - 'examples/api/lib/ui', - 'examples/api/lib/widgets', - 'examples/api/test/animation', - 'examples/api/test/foundation', - 'examples/api/test/gestures', - 'examples/api/test/painting', - 'examples/api/test/rendering', - 'examples/api/test/services', - 'examples/api/test/ui', - 'examples/api/test/widgets', + 'packages/flutter/examples/api/lib/animation', + 'packages/flutter/examples/api/lib/foundation', + 'packages/flutter/examples/api/lib/gestures', + 'packages/flutter/examples/api/lib/painting', + 'packages/flutter/examples/api/lib/rendering', + 'packages/flutter/examples/api/lib/services', + 'packages/flutter/examples/api/lib/ui', + 'packages/flutter/examples/api/lib/widgets', + 'packages/flutter/examples/api/test/animation', + 'packages/flutter/examples/api/test/foundation', + 'packages/flutter/examples/api/test/gestures', + 'packages/flutter/examples/api/test/painting', + 'packages/flutter/examples/api/test/rendering', + 'packages/flutter/examples/api/test/services', + 'packages/flutter/examples/api/test/ui', + 'packages/flutter/examples/api/test/widgets', ]; diff --git a/dev/snippets/lib/src/snippet_parser.dart b/dev/snippets/lib/src/snippet_parser.dart index c542f1eaffe3c..145c040027e1b 100644 --- a/dev/snippets/lib/src/snippet_parser.dart +++ b/dev/snippets/lib/src/snippet_parser.dart @@ -286,7 +286,7 @@ class SnippetDartdocParser { } final RegExpMatch match = _filePointerRegex.firstMatch(trimmedLine)!; linkedFile = filesystem.file( - path.join(flutterRoot.absolute.path, match.namedGroup('file')), + path.join(flutterRoot.absolute.path, 'packages', 'flutter', match.namedGroup('file')), ); } else { block.add(line.copyWith(text: line.text.replaceFirst(RegExp(r'\s*/// ?'), ''))); diff --git a/dev/snippets/test/snippet_parser_test.dart b/dev/snippets/test/snippet_parser_test.dart index 287777fea32c1..d9beacd60bb86 100644 --- a/dev/snippets/test/snippet_parser_test.dart +++ b/dev/snippets/test/snippet_parser_test.dart @@ -264,7 +264,12 @@ File _createDartpadSourceFile( Directory flutterRoot, { bool linked = false, }) { - final File linkedFile = filesystem.file(path.join(flutterRoot.absolute.path, 'linked_file.dart')) + final Directory flutterPackageDir = filesystem.directory( + path.join(flutterRoot.absolute.path, 'packages', 'flutter'), + ); + final File linkedFile = filesystem.file( + path.join(flutterPackageDir.absolute.path, 'linked_file.dart'), + ) ..createSync(recursive: true) ..writeAsStringSync(''' // Copyright @@ -280,7 +285,7 @@ void DocumentedClassSample() { final source = linked ? ''' -/// ** See code in ${path.relative(linkedFile.path, from: flutterRoot.absolute.path)} **''' +/// ** See code in ${path.relative(linkedFile.path, from: flutterPackageDir.absolute.path)} **''' : ''' /// ```dart /// void DocumentedClassSample() { diff --git a/dev/snippets/test/snippets_test.dart b/dev/snippets/test/snippets_test.dart index 4d0a214e72b88..e1c56dcc80bd1 100644 --- a/dev/snippets/test/snippets_test.dart +++ b/dev/snippets/test/snippets_test.dart @@ -93,7 +93,7 @@ On several lines. '''); final String examplePath = path.join( configuration.flutterRoot.path, - 'examples/api/widgets/foo/foo_example.0.dart', + 'packages/flutter/examples/api/widgets/foo/foo_example.0.dart', ); final File exampleFile = memoryFileSystem.file(examplePath); await exampleFile.create(recursive: true); @@ -199,7 +199,7 @@ On several lines. '''); final String examplePath = path.join( configuration.flutterRoot.path, - 'examples/api/widgets/foo/foo_example.0.dart', + 'packages/flutter/examples/api/widgets/foo/foo_example.0.dart', ); final File exampleFile = memoryFileSystem.file(examplePath); await exampleFile.create(recursive: true); diff --git a/dev/tools/examples_smoke_test.dart b/dev/tools/examples_smoke_test.dart index b17c360b66f9f..2ef80106b25b8 100644 --- a/dev/tools/examples_smoke_test.dart +++ b/dev/tools/examples_smoke_test.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. // This test builds an integration test from the list of samples in the -// examples/api/lib directory, and then runs it. The tests are just smoke tests, -// designed to start up each example and run it for a couple of frames to make -// sure it doesn't throw an exception or fail to compile. +// packages/flutter/examples/api/lib directory, and then runs it. The tests are +// just smoke tests, designed to start up each example and run it for a couple +// of frames to make sure it doesn't throw an exception or fail to compile. import 'dart:async'; import 'dart:convert'; @@ -30,7 +30,11 @@ FutureOr main() async { final Directory flutterDir = _kFilesystem.directory( path.absolute(path.dirname(path.dirname(path.dirname(_kPlatform.script.toFilePath())))), ); - final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api'); + final Directory apiDir = flutterDir + .childDirectory('packages') + .childDirectory('flutter') + .childDirectory('examples') + .childDirectory('api'); final File integrationTest = await generateTest(apiDir); try { await runSmokeTests(flutterDir: flutterDir, integrationTest: integrationTest, apiDir: apiDir); @@ -131,7 +135,7 @@ Future generateTest(Directory apiDir) async { buffer.writeln(r''' -import '../../../dev/manual_tests/test/mock_image_http.dart'; +import '../../../../../dev/manual_tests/test/mock_image_http.dart'; void main() { IntegrationTestWidgetsFlutterBinding? binding; diff --git a/docs/contributing/Style-guide-for-Flutter-repo.md b/docs/contributing/Style-guide-for-Flutter-repo.md index 3b68d0aa59bd8..443ad613f1cde 100644 --- a/docs/contributing/Style-guide-for-Flutter-repo.md +++ b/docs/contributing/Style-guide-for-Flutter-repo.md @@ -596,13 +596,13 @@ By definition, if they are looking at the documentation, they are not finding it Sample code helps developers learn your API quickly. Writing sample code also helps you think through how your API is going to be used by app developers. -Sample code should go in a documentation comment that typically begins with `/// {@tool dartpad}`, and ends with `/// {@end-tool}`, with the example source and corresponding tests placed in a file under [the API examples directory](https://github.com/flutter/flutter/blob/main/examples/api). This will then be checked by automated tools, and formatted for display on the API documentation web site [api.flutter.dev](https://api.flutter.dev). For details on how to write sample code, see [the API example documentation](https://github.com/flutter/flutter/blob/main/examples/api/README.md#authoring). +Sample code should go in a documentation comment that typically begins with `/// {@tool dartpad}`, and ends with `/// {@end-tool}`, with the example source and corresponding tests placed in a file under [the API examples directory](https://github.com/flutter/flutter/blob/main/packages/flutter/examples/api). This will then be checked by automated tools, and formatted for display on the API documentation web site [api.flutter.dev](https://api.flutter.dev). For details on how to write sample code, see [the API example documentation](https://github.com/flutter/flutter/blob/main/packages/flutter/examples/api/README.md#authoring). #### Provide full application samples. Our UX research has shown that developers prefer to see examples that are in the context of an entire app. So, whenever it makes sense, provide an example that can be presented as part of an entire application instead of just a snippet that uses the `{@tool snippet}` or ```dart ... ``` indicators. -An application sample can be created using the `{@tool dartpad}` ... `{@end-tool}` or `{@tool sample}` ... `{@end-tool}` dartdoc indicators. See [here](https://github.com/flutter/flutter/blob/main/examples/api/README.md#authoring) for more details about writing these kinds of examples. +An application sample can be created using the `{@tool dartpad}` ... `{@end-tool}` or `{@tool sample}` ... `{@end-tool}` dartdoc indicators. See [here](https://github.com/flutter/flutter/blob/main/packages/flutter/examples/api/README.md#authoring) for more details about writing these kinds of examples. Dartpad examples (those using the dartdoc `{@tool dartpad}` indicator) will be presented on the [API documentation website](https://api.flutter.dev) as an in-page executable and editable example. This allows developers to interact with the example right there on the page, and is the preferred form of example. Here is [one such example](https://api.flutter.dev/flutter/widgets/AnimatedSwitcher-class.html#widgets.AnimatedSwitcher.1). diff --git a/examples/api/.gitignore b/packages/flutter/examples/api/.gitignore similarity index 100% rename from examples/api/.gitignore rename to packages/flutter/examples/api/.gitignore diff --git a/examples/api/.metadata b/packages/flutter/examples/api/.metadata similarity index 100% rename from examples/api/.metadata rename to packages/flutter/examples/api/.metadata diff --git a/examples/api/README.md b/packages/flutter/examples/api/README.md similarity index 100% rename from examples/api/README.md rename to packages/flutter/examples/api/README.md diff --git a/examples/api/analysis_options.yaml b/packages/flutter/examples/api/analysis_options.yaml similarity index 100% rename from examples/api/analysis_options.yaml rename to packages/flutter/examples/api/analysis_options.yaml diff --git a/examples/api/lib/animation/animation_controller/animated_digit.0.dart b/packages/flutter/examples/api/lib/animation/animation_controller/animated_digit.0.dart similarity index 100% rename from examples/api/lib/animation/animation_controller/animated_digit.0.dart rename to packages/flutter/examples/api/lib/animation/animation_controller/animated_digit.0.dart diff --git a/examples/api/lib/animation/curves/curve2_d.0.dart b/packages/flutter/examples/api/lib/animation/curves/curve2_d.0.dart similarity index 100% rename from examples/api/lib/animation/curves/curve2_d.0.dart rename to packages/flutter/examples/api/lib/animation/curves/curve2_d.0.dart diff --git a/examples/api/lib/cupertino/activity_indicator/cupertino_activity_indicator.0.dart b/packages/flutter/examples/api/lib/cupertino/activity_indicator/cupertino_activity_indicator.0.dart similarity index 100% rename from examples/api/lib/cupertino/activity_indicator/cupertino_activity_indicator.0.dart rename to packages/flutter/examples/api/lib/cupertino/activity_indicator/cupertino_activity_indicator.0.dart diff --git a/examples/api/lib/cupertino/activity_indicator/cupertino_linear_activity_indicator.0.dart b/packages/flutter/examples/api/lib/cupertino/activity_indicator/cupertino_linear_activity_indicator.0.dart similarity index 100% rename from examples/api/lib/cupertino/activity_indicator/cupertino_linear_activity_indicator.0.dart rename to packages/flutter/examples/api/lib/cupertino/activity_indicator/cupertino_linear_activity_indicator.0.dart diff --git a/examples/api/lib/cupertino/bottom_tab_bar/cupertino_tab_bar.0.dart b/packages/flutter/examples/api/lib/cupertino/bottom_tab_bar/cupertino_tab_bar.0.dart similarity index 100% rename from examples/api/lib/cupertino/bottom_tab_bar/cupertino_tab_bar.0.dart rename to packages/flutter/examples/api/lib/cupertino/bottom_tab_bar/cupertino_tab_bar.0.dart diff --git a/examples/api/lib/cupertino/button/cupertino_button.0.dart b/packages/flutter/examples/api/lib/cupertino/button/cupertino_button.0.dart similarity index 100% rename from examples/api/lib/cupertino/button/cupertino_button.0.dart rename to packages/flutter/examples/api/lib/cupertino/button/cupertino_button.0.dart diff --git a/examples/api/lib/cupertino/checkbox/cupertino_checkbox.0.dart b/packages/flutter/examples/api/lib/cupertino/checkbox/cupertino_checkbox.0.dart similarity index 100% rename from examples/api/lib/cupertino/checkbox/cupertino_checkbox.0.dart rename to packages/flutter/examples/api/lib/cupertino/checkbox/cupertino_checkbox.0.dart diff --git a/examples/api/lib/cupertino/context_menu/cupertino_context_menu.0.dart b/packages/flutter/examples/api/lib/cupertino/context_menu/cupertino_context_menu.0.dart similarity index 100% rename from examples/api/lib/cupertino/context_menu/cupertino_context_menu.0.dart rename to packages/flutter/examples/api/lib/cupertino/context_menu/cupertino_context_menu.0.dart diff --git a/examples/api/lib/cupertino/context_menu/cupertino_context_menu.1.dart b/packages/flutter/examples/api/lib/cupertino/context_menu/cupertino_context_menu.1.dart similarity index 100% rename from examples/api/lib/cupertino/context_menu/cupertino_context_menu.1.dart rename to packages/flutter/examples/api/lib/cupertino/context_menu/cupertino_context_menu.1.dart diff --git a/examples/api/lib/cupertino/date_picker/cupertino_date_picker.0.dart b/packages/flutter/examples/api/lib/cupertino/date_picker/cupertino_date_picker.0.dart similarity index 100% rename from examples/api/lib/cupertino/date_picker/cupertino_date_picker.0.dart rename to packages/flutter/examples/api/lib/cupertino/date_picker/cupertino_date_picker.0.dart diff --git a/examples/api/lib/cupertino/date_picker/cupertino_timer_picker.0.dart b/packages/flutter/examples/api/lib/cupertino/date_picker/cupertino_timer_picker.0.dart similarity index 100% rename from examples/api/lib/cupertino/date_picker/cupertino_timer_picker.0.dart rename to packages/flutter/examples/api/lib/cupertino/date_picker/cupertino_timer_picker.0.dart diff --git a/examples/api/lib/cupertino/dialog/cupertino_action_sheet.0.dart b/packages/flutter/examples/api/lib/cupertino/dialog/cupertino_action_sheet.0.dart similarity index 100% rename from examples/api/lib/cupertino/dialog/cupertino_action_sheet.0.dart rename to packages/flutter/examples/api/lib/cupertino/dialog/cupertino_action_sheet.0.dart diff --git a/examples/api/lib/cupertino/dialog/cupertino_alert_dialog.0.dart b/packages/flutter/examples/api/lib/cupertino/dialog/cupertino_alert_dialog.0.dart similarity index 100% rename from examples/api/lib/cupertino/dialog/cupertino_alert_dialog.0.dart rename to packages/flutter/examples/api/lib/cupertino/dialog/cupertino_alert_dialog.0.dart diff --git a/examples/api/lib/cupertino/dialog/cupertino_popup_surface.0.dart b/packages/flutter/examples/api/lib/cupertino/dialog/cupertino_popup_surface.0.dart similarity index 100% rename from examples/api/lib/cupertino/dialog/cupertino_popup_surface.0.dart rename to packages/flutter/examples/api/lib/cupertino/dialog/cupertino_popup_surface.0.dart diff --git a/examples/api/lib/cupertino/expansion_tile/cupertino_expansion_tile.0.dart b/packages/flutter/examples/api/lib/cupertino/expansion_tile/cupertino_expansion_tile.0.dart similarity index 100% rename from examples/api/lib/cupertino/expansion_tile/cupertino_expansion_tile.0.dart rename to packages/flutter/examples/api/lib/cupertino/expansion_tile/cupertino_expansion_tile.0.dart diff --git a/examples/api/lib/cupertino/form_row/cupertino_form_row.0.dart b/packages/flutter/examples/api/lib/cupertino/form_row/cupertino_form_row.0.dart similarity index 100% rename from examples/api/lib/cupertino/form_row/cupertino_form_row.0.dart rename to packages/flutter/examples/api/lib/cupertino/form_row/cupertino_form_row.0.dart diff --git a/examples/api/lib/cupertino/list_section/list_section_base.0.dart b/packages/flutter/examples/api/lib/cupertino/list_section/list_section_base.0.dart similarity index 100% rename from examples/api/lib/cupertino/list_section/list_section_base.0.dart rename to packages/flutter/examples/api/lib/cupertino/list_section/list_section_base.0.dart diff --git a/examples/api/lib/cupertino/list_section/list_section_inset.0.dart b/packages/flutter/examples/api/lib/cupertino/list_section/list_section_inset.0.dart similarity index 100% rename from examples/api/lib/cupertino/list_section/list_section_inset.0.dart rename to packages/flutter/examples/api/lib/cupertino/list_section/list_section_inset.0.dart diff --git a/examples/api/lib/cupertino/list_tile/cupertino_list_tile.0.dart b/packages/flutter/examples/api/lib/cupertino/list_tile/cupertino_list_tile.0.dart similarity index 100% rename from examples/api/lib/cupertino/list_tile/cupertino_list_tile.0.dart rename to packages/flutter/examples/api/lib/cupertino/list_tile/cupertino_list_tile.0.dart diff --git a/examples/api/lib/cupertino/magnifier/cupertino_magnifier.0.dart b/packages/flutter/examples/api/lib/cupertino/magnifier/cupertino_magnifier.0.dart similarity index 100% rename from examples/api/lib/cupertino/magnifier/cupertino_magnifier.0.dart rename to packages/flutter/examples/api/lib/cupertino/magnifier/cupertino_magnifier.0.dart diff --git a/examples/api/lib/cupertino/magnifier/cupertino_text_magnifier.0.dart b/packages/flutter/examples/api/lib/cupertino/magnifier/cupertino_text_magnifier.0.dart similarity index 100% rename from examples/api/lib/cupertino/magnifier/cupertino_text_magnifier.0.dart rename to packages/flutter/examples/api/lib/cupertino/magnifier/cupertino_text_magnifier.0.dart diff --git a/examples/api/lib/cupertino/menu_anchor/menu_anchor.0.dart b/packages/flutter/examples/api/lib/cupertino/menu_anchor/menu_anchor.0.dart similarity index 100% rename from examples/api/lib/cupertino/menu_anchor/menu_anchor.0.dart rename to packages/flutter/examples/api/lib/cupertino/menu_anchor/menu_anchor.0.dart diff --git a/examples/api/lib/cupertino/menu_anchor/menu_anchor.1.dart b/packages/flutter/examples/api/lib/cupertino/menu_anchor/menu_anchor.1.dart similarity index 100% rename from examples/api/lib/cupertino/menu_anchor/menu_anchor.1.dart rename to packages/flutter/examples/api/lib/cupertino/menu_anchor/menu_anchor.1.dart diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart b/packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart similarity index 100% rename from examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart rename to packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart b/packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart similarity index 100% rename from examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart rename to packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.2.dart b/packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.2.dart similarity index 100% rename from examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.2.dart rename to packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.2.dart diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.0.dart b/packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.0.dart similarity index 100% rename from examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.0.dart rename to packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.0.dart diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart b/packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart similarity index 100% rename from examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart rename to packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.2.dart b/packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.2.dart similarity index 100% rename from examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.2.dart rename to packages/flutter/examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.2.dart diff --git a/examples/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart b/packages/flutter/examples/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart similarity index 100% rename from examples/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart rename to packages/flutter/examples/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart diff --git a/examples/api/lib/cupertino/picker/cupertino_picker.0.dart b/packages/flutter/examples/api/lib/cupertino/picker/cupertino_picker.0.dart similarity index 100% rename from examples/api/lib/cupertino/picker/cupertino_picker.0.dart rename to packages/flutter/examples/api/lib/cupertino/picker/cupertino_picker.0.dart diff --git a/examples/api/lib/cupertino/radio/cupertino_radio.0.dart b/packages/flutter/examples/api/lib/cupertino/radio/cupertino_radio.0.dart similarity index 100% rename from examples/api/lib/cupertino/radio/cupertino_radio.0.dart rename to packages/flutter/examples/api/lib/cupertino/radio/cupertino_radio.0.dart diff --git a/examples/api/lib/cupertino/radio/cupertino_radio.toggleable.0.dart b/packages/flutter/examples/api/lib/cupertino/radio/cupertino_radio.toggleable.0.dart similarity index 100% rename from examples/api/lib/cupertino/radio/cupertino_radio.toggleable.0.dart rename to packages/flutter/examples/api/lib/cupertino/radio/cupertino_radio.toggleable.0.dart diff --git a/examples/api/lib/cupertino/refresh/cupertino_sliver_refresh_control.0.dart b/packages/flutter/examples/api/lib/cupertino/refresh/cupertino_sliver_refresh_control.0.dart similarity index 100% rename from examples/api/lib/cupertino/refresh/cupertino_sliver_refresh_control.0.dart rename to packages/flutter/examples/api/lib/cupertino/refresh/cupertino_sliver_refresh_control.0.dart diff --git a/examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart b/packages/flutter/examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart similarity index 100% rename from examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart rename to packages/flutter/examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart diff --git a/examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart b/packages/flutter/examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart similarity index 100% rename from examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart rename to packages/flutter/examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart diff --git a/examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.0.dart b/packages/flutter/examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.0.dart similarity index 100% rename from examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.0.dart rename to packages/flutter/examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.0.dart diff --git a/examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.1.dart b/packages/flutter/examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.1.dart similarity index 100% rename from examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.1.dart rename to packages/flutter/examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.1.dart diff --git a/examples/api/lib/cupertino/search_field/cupertino_search_field.0.dart b/packages/flutter/examples/api/lib/cupertino/search_field/cupertino_search_field.0.dart similarity index 100% rename from examples/api/lib/cupertino/search_field/cupertino_search_field.0.dart rename to packages/flutter/examples/api/lib/cupertino/search_field/cupertino_search_field.0.dart diff --git a/examples/api/lib/cupertino/search_field/cupertino_search_field.1.dart b/packages/flutter/examples/api/lib/cupertino/search_field/cupertino_search_field.1.dart similarity index 100% rename from examples/api/lib/cupertino/search_field/cupertino_search_field.1.dart rename to packages/flutter/examples/api/lib/cupertino/search_field/cupertino_search_field.1.dart diff --git a/examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart b/packages/flutter/examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart similarity index 100% rename from examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart rename to packages/flutter/examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart diff --git a/examples/api/lib/cupertino/segmented_control/cupertino_sliding_segmented_control.0.dart b/packages/flutter/examples/api/lib/cupertino/segmented_control/cupertino_sliding_segmented_control.0.dart similarity index 100% rename from examples/api/lib/cupertino/segmented_control/cupertino_sliding_segmented_control.0.dart rename to packages/flutter/examples/api/lib/cupertino/segmented_control/cupertino_sliding_segmented_control.0.dart diff --git a/examples/api/lib/cupertino/sheet/cupertino_sheet.0.dart b/packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.0.dart similarity index 100% rename from examples/api/lib/cupertino/sheet/cupertino_sheet.0.dart rename to packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.0.dart diff --git a/examples/api/lib/cupertino/sheet/cupertino_sheet.1.dart b/packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.1.dart similarity index 100% rename from examples/api/lib/cupertino/sheet/cupertino_sheet.1.dart rename to packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.1.dart diff --git a/examples/api/lib/cupertino/sheet/cupertino_sheet.2.dart b/packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.2.dart similarity index 100% rename from examples/api/lib/cupertino/sheet/cupertino_sheet.2.dart rename to packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.2.dart diff --git a/examples/api/lib/cupertino/sheet/cupertino_sheet.3.dart b/packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.3.dart similarity index 100% rename from examples/api/lib/cupertino/sheet/cupertino_sheet.3.dart rename to packages/flutter/examples/api/lib/cupertino/sheet/cupertino_sheet.3.dart diff --git a/examples/api/lib/cupertino/slider/cupertino_slider.0.dart b/packages/flutter/examples/api/lib/cupertino/slider/cupertino_slider.0.dart similarity index 100% rename from examples/api/lib/cupertino/slider/cupertino_slider.0.dart rename to packages/flutter/examples/api/lib/cupertino/slider/cupertino_slider.0.dart diff --git a/examples/api/lib/cupertino/switch/cupertino_switch.0.dart b/packages/flutter/examples/api/lib/cupertino/switch/cupertino_switch.0.dart similarity index 100% rename from examples/api/lib/cupertino/switch/cupertino_switch.0.dart rename to packages/flutter/examples/api/lib/cupertino/switch/cupertino_switch.0.dart diff --git a/examples/api/lib/cupertino/tab_scaffold/cupertino_tab_controller.0.dart b/packages/flutter/examples/api/lib/cupertino/tab_scaffold/cupertino_tab_controller.0.dart similarity index 100% rename from examples/api/lib/cupertino/tab_scaffold/cupertino_tab_controller.0.dart rename to packages/flutter/examples/api/lib/cupertino/tab_scaffold/cupertino_tab_controller.0.dart diff --git a/examples/api/lib/cupertino/tab_scaffold/cupertino_tab_scaffold.0.dart b/packages/flutter/examples/api/lib/cupertino/tab_scaffold/cupertino_tab_scaffold.0.dart similarity index 100% rename from examples/api/lib/cupertino/tab_scaffold/cupertino_tab_scaffold.0.dart rename to packages/flutter/examples/api/lib/cupertino/tab_scaffold/cupertino_tab_scaffold.0.dart diff --git a/examples/api/lib/cupertino/text_field/cupertino_text_field.0.dart b/packages/flutter/examples/api/lib/cupertino/text_field/cupertino_text_field.0.dart similarity index 100% rename from examples/api/lib/cupertino/text_field/cupertino_text_field.0.dart rename to packages/flutter/examples/api/lib/cupertino/text_field/cupertino_text_field.0.dart diff --git a/examples/api/lib/cupertino/text_form_field_row/cupertino_text_form_field_row.1.dart b/packages/flutter/examples/api/lib/cupertino/text_form_field_row/cupertino_text_form_field_row.1.dart similarity index 100% rename from examples/api/lib/cupertino/text_form_field_row/cupertino_text_form_field_row.1.dart rename to packages/flutter/examples/api/lib/cupertino/text_form_field_row/cupertino_text_form_field_row.1.dart diff --git a/examples/api/lib/foundation/key/value_key.0.dart b/packages/flutter/examples/api/lib/foundation/key/value_key.0.dart similarity index 100% rename from examples/api/lib/foundation/key/value_key.0.dart rename to packages/flutter/examples/api/lib/foundation/key/value_key.0.dart diff --git a/examples/api/lib/gestures/pointer_signal_resolver/pointer_signal_resolver.0.dart b/packages/flutter/examples/api/lib/gestures/pointer_signal_resolver/pointer_signal_resolver.0.dart similarity index 100% rename from examples/api/lib/gestures/pointer_signal_resolver/pointer_signal_resolver.0.dart rename to packages/flutter/examples/api/lib/gestures/pointer_signal_resolver/pointer_signal_resolver.0.dart diff --git a/examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart b/packages/flutter/examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart similarity index 100% rename from examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart rename to packages/flutter/examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart diff --git a/examples/api/lib/material/about/about_list_tile.0.dart b/packages/flutter/examples/api/lib/material/about/about_list_tile.0.dart similarity index 100% rename from examples/api/lib/material/about/about_list_tile.0.dart rename to packages/flutter/examples/api/lib/material/about/about_list_tile.0.dart diff --git a/examples/api/lib/material/action_buttons/action_icon_theme.0.dart b/packages/flutter/examples/api/lib/material/action_buttons/action_icon_theme.0.dart similarity index 100% rename from examples/api/lib/material/action_buttons/action_icon_theme.0.dart rename to packages/flutter/examples/api/lib/material/action_buttons/action_icon_theme.0.dart diff --git a/examples/api/lib/material/action_chip/action_chip.0.dart b/packages/flutter/examples/api/lib/material/action_chip/action_chip.0.dart similarity index 100% rename from examples/api/lib/material/action_chip/action_chip.0.dart rename to packages/flutter/examples/api/lib/material/action_chip/action_chip.0.dart diff --git a/examples/api/lib/material/animated_icon/animated_icon.0.dart b/packages/flutter/examples/api/lib/material/animated_icon/animated_icon.0.dart similarity index 100% rename from examples/api/lib/material/animated_icon/animated_icon.0.dart rename to packages/flutter/examples/api/lib/material/animated_icon/animated_icon.0.dart diff --git a/examples/api/lib/material/animated_icon/animated_icons_data.0.dart b/packages/flutter/examples/api/lib/material/animated_icon/animated_icons_data.0.dart similarity index 100% rename from examples/api/lib/material/animated_icon/animated_icons_data.0.dart rename to packages/flutter/examples/api/lib/material/animated_icon/animated_icons_data.0.dart diff --git a/examples/api/lib/material/app/app.0.dart b/packages/flutter/examples/api/lib/material/app/app.0.dart similarity index 100% rename from examples/api/lib/material/app/app.0.dart rename to packages/flutter/examples/api/lib/material/app/app.0.dart diff --git a/examples/api/lib/material/app_bar/app_bar.0.dart b/packages/flutter/examples/api/lib/material/app_bar/app_bar.0.dart similarity index 100% rename from examples/api/lib/material/app_bar/app_bar.0.dart rename to packages/flutter/examples/api/lib/material/app_bar/app_bar.0.dart diff --git a/examples/api/lib/material/app_bar/app_bar.1.dart b/packages/flutter/examples/api/lib/material/app_bar/app_bar.1.dart similarity index 100% rename from examples/api/lib/material/app_bar/app_bar.1.dart rename to packages/flutter/examples/api/lib/material/app_bar/app_bar.1.dart diff --git a/examples/api/lib/material/app_bar/app_bar.2.dart b/packages/flutter/examples/api/lib/material/app_bar/app_bar.2.dart similarity index 100% rename from examples/api/lib/material/app_bar/app_bar.2.dart rename to packages/flutter/examples/api/lib/material/app_bar/app_bar.2.dart diff --git a/examples/api/lib/material/app_bar/app_bar.3.dart b/packages/flutter/examples/api/lib/material/app_bar/app_bar.3.dart similarity index 100% rename from examples/api/lib/material/app_bar/app_bar.3.dart rename to packages/flutter/examples/api/lib/material/app_bar/app_bar.3.dart diff --git a/examples/api/lib/material/app_bar/app_bar.4.dart b/packages/flutter/examples/api/lib/material/app_bar/app_bar.4.dart similarity index 100% rename from examples/api/lib/material/app_bar/app_bar.4.dart rename to packages/flutter/examples/api/lib/material/app_bar/app_bar.4.dart diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.1.dart b/packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.1.dart similarity index 100% rename from examples/api/lib/material/app_bar/sliver_app_bar.1.dart rename to packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.1.dart diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.2.dart b/packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.2.dart similarity index 100% rename from examples/api/lib/material/app_bar/sliver_app_bar.2.dart rename to packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.2.dart diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.3.dart b/packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.3.dart similarity index 100% rename from examples/api/lib/material/app_bar/sliver_app_bar.3.dart rename to packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.3.dart diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.4.dart b/packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.4.dart similarity index 100% rename from examples/api/lib/material/app_bar/sliver_app_bar.4.dart rename to packages/flutter/examples/api/lib/material/app_bar/sliver_app_bar.4.dart diff --git a/examples/api/lib/material/autocomplete/autocomplete.0.dart b/packages/flutter/examples/api/lib/material/autocomplete/autocomplete.0.dart similarity index 100% rename from examples/api/lib/material/autocomplete/autocomplete.0.dart rename to packages/flutter/examples/api/lib/material/autocomplete/autocomplete.0.dart diff --git a/examples/api/lib/material/autocomplete/autocomplete.1.dart b/packages/flutter/examples/api/lib/material/autocomplete/autocomplete.1.dart similarity index 100% rename from examples/api/lib/material/autocomplete/autocomplete.1.dart rename to packages/flutter/examples/api/lib/material/autocomplete/autocomplete.1.dart diff --git a/examples/api/lib/material/autocomplete/autocomplete.2.dart b/packages/flutter/examples/api/lib/material/autocomplete/autocomplete.2.dart similarity index 100% rename from examples/api/lib/material/autocomplete/autocomplete.2.dart rename to packages/flutter/examples/api/lib/material/autocomplete/autocomplete.2.dart diff --git a/examples/api/lib/material/autocomplete/autocomplete.3.dart b/packages/flutter/examples/api/lib/material/autocomplete/autocomplete.3.dart similarity index 100% rename from examples/api/lib/material/autocomplete/autocomplete.3.dart rename to packages/flutter/examples/api/lib/material/autocomplete/autocomplete.3.dart diff --git a/examples/api/lib/material/autocomplete/autocomplete.4.dart b/packages/flutter/examples/api/lib/material/autocomplete/autocomplete.4.dart similarity index 100% rename from examples/api/lib/material/autocomplete/autocomplete.4.dart rename to packages/flutter/examples/api/lib/material/autocomplete/autocomplete.4.dart diff --git a/examples/api/lib/material/badge/badge.0.dart b/packages/flutter/examples/api/lib/material/badge/badge.0.dart similarity index 100% rename from examples/api/lib/material/badge/badge.0.dart rename to packages/flutter/examples/api/lib/material/badge/badge.0.dart diff --git a/examples/api/lib/material/banner/material_banner.0.dart b/packages/flutter/examples/api/lib/material/banner/material_banner.0.dart similarity index 100% rename from examples/api/lib/material/banner/material_banner.0.dart rename to packages/flutter/examples/api/lib/material/banner/material_banner.0.dart diff --git a/examples/api/lib/material/banner/material_banner.1.dart b/packages/flutter/examples/api/lib/material/banner/material_banner.1.dart similarity index 100% rename from examples/api/lib/material/banner/material_banner.1.dart rename to packages/flutter/examples/api/lib/material/banner/material_banner.1.dart diff --git a/examples/api/lib/material/bottom_app_bar/bottom_app_bar.1.dart b/packages/flutter/examples/api/lib/material/bottom_app_bar/bottom_app_bar.1.dart similarity index 100% rename from examples/api/lib/material/bottom_app_bar/bottom_app_bar.1.dart rename to packages/flutter/examples/api/lib/material/bottom_app_bar/bottom_app_bar.1.dart diff --git a/examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart b/packages/flutter/examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart similarity index 100% rename from examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart rename to packages/flutter/examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart diff --git a/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.0.dart b/packages/flutter/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.0.dart similarity index 100% rename from examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.0.dart rename to packages/flutter/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.0.dart diff --git a/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.1.dart b/packages/flutter/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.1.dart similarity index 100% rename from examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.1.dart rename to packages/flutter/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.1.dart diff --git a/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart b/packages/flutter/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart similarity index 100% rename from examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart rename to packages/flutter/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart diff --git a/examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart b/packages/flutter/examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart similarity index 100% rename from examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart rename to packages/flutter/examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart diff --git a/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart b/packages/flutter/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart similarity index 100% rename from examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart rename to packages/flutter/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart diff --git a/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart b/packages/flutter/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart similarity index 100% rename from examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart rename to packages/flutter/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart diff --git a/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart b/packages/flutter/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart similarity index 100% rename from examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart rename to packages/flutter/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart diff --git a/examples/api/lib/material/button_style/button_style.0.dart b/packages/flutter/examples/api/lib/material/button_style/button_style.0.dart similarity index 100% rename from examples/api/lib/material/button_style/button_style.0.dart rename to packages/flutter/examples/api/lib/material/button_style/button_style.0.dart diff --git a/examples/api/lib/material/card/card.0.dart b/packages/flutter/examples/api/lib/material/card/card.0.dart similarity index 100% rename from examples/api/lib/material/card/card.0.dart rename to packages/flutter/examples/api/lib/material/card/card.0.dart diff --git a/examples/api/lib/material/card/card.1.dart b/packages/flutter/examples/api/lib/material/card/card.1.dart similarity index 100% rename from examples/api/lib/material/card/card.1.dart rename to packages/flutter/examples/api/lib/material/card/card.1.dart diff --git a/examples/api/lib/material/card/card.2.dart b/packages/flutter/examples/api/lib/material/card/card.2.dart similarity index 100% rename from examples/api/lib/material/card/card.2.dart rename to packages/flutter/examples/api/lib/material/card/card.2.dart diff --git a/examples/api/lib/material/carousel/carousel.0.dart b/packages/flutter/examples/api/lib/material/carousel/carousel.0.dart similarity index 100% rename from examples/api/lib/material/carousel/carousel.0.dart rename to packages/flutter/examples/api/lib/material/carousel/carousel.0.dart diff --git a/examples/api/lib/material/carousel/carousel.1.dart b/packages/flutter/examples/api/lib/material/carousel/carousel.1.dart similarity index 100% rename from examples/api/lib/material/carousel/carousel.1.dart rename to packages/flutter/examples/api/lib/material/carousel/carousel.1.dart diff --git a/examples/api/lib/material/checkbox/checkbox.0.dart b/packages/flutter/examples/api/lib/material/checkbox/checkbox.0.dart similarity index 100% rename from examples/api/lib/material/checkbox/checkbox.0.dart rename to packages/flutter/examples/api/lib/material/checkbox/checkbox.0.dart diff --git a/examples/api/lib/material/checkbox/checkbox.1.dart b/packages/flutter/examples/api/lib/material/checkbox/checkbox.1.dart similarity index 100% rename from examples/api/lib/material/checkbox/checkbox.1.dart rename to packages/flutter/examples/api/lib/material/checkbox/checkbox.1.dart diff --git a/examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.0.dart b/packages/flutter/examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.0.dart similarity index 100% rename from examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.0.dart rename to packages/flutter/examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.0.dart diff --git a/examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart b/packages/flutter/examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart similarity index 100% rename from examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart rename to packages/flutter/examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart diff --git a/examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.0.dart b/packages/flutter/examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.0.dart similarity index 100% rename from examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.0.dart rename to packages/flutter/examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.0.dart diff --git a/examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.1.dart b/packages/flutter/examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.1.dart similarity index 100% rename from examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.1.dart rename to packages/flutter/examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.1.dart diff --git a/examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart b/packages/flutter/examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart similarity index 100% rename from examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart rename to packages/flutter/examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart diff --git a/examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart b/packages/flutter/examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart similarity index 100% rename from examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart rename to packages/flutter/examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart diff --git a/examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart b/packages/flutter/examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart similarity index 100% rename from examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart rename to packages/flutter/examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart diff --git a/examples/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart b/packages/flutter/examples/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart similarity index 100% rename from examples/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart rename to packages/flutter/examples/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart diff --git a/examples/api/lib/material/choice_chip/choice_chip.0.dart b/packages/flutter/examples/api/lib/material/choice_chip/choice_chip.0.dart similarity index 100% rename from examples/api/lib/material/choice_chip/choice_chip.0.dart rename to packages/flutter/examples/api/lib/material/choice_chip/choice_chip.0.dart diff --git a/examples/api/lib/material/color_scheme/color_scheme.0.dart b/packages/flutter/examples/api/lib/material/color_scheme/color_scheme.0.dart similarity index 100% rename from examples/api/lib/material/color_scheme/color_scheme.0.dart rename to packages/flutter/examples/api/lib/material/color_scheme/color_scheme.0.dart diff --git a/examples/api/lib/material/color_scheme/dynamic_content_color.0.dart b/packages/flutter/examples/api/lib/material/color_scheme/dynamic_content_color.0.dart similarity index 100% rename from examples/api/lib/material/color_scheme/dynamic_content_color.0.dart rename to packages/flutter/examples/api/lib/material/color_scheme/dynamic_content_color.0.dart diff --git a/examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart b/packages/flutter/examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart similarity index 100% rename from examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart rename to packages/flutter/examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart diff --git a/examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart b/packages/flutter/examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart similarity index 100% rename from examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart rename to packages/flutter/examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart diff --git a/examples/api/lib/material/data_table/data_table.0.dart b/packages/flutter/examples/api/lib/material/data_table/data_table.0.dart similarity index 100% rename from examples/api/lib/material/data_table/data_table.0.dart rename to packages/flutter/examples/api/lib/material/data_table/data_table.0.dart diff --git a/examples/api/lib/material/data_table/data_table.1.dart b/packages/flutter/examples/api/lib/material/data_table/data_table.1.dart similarity index 100% rename from examples/api/lib/material/data_table/data_table.1.dart rename to packages/flutter/examples/api/lib/material/data_table/data_table.1.dart diff --git a/examples/api/lib/material/date_picker/custom_calendar_date_picker.0.dart b/packages/flutter/examples/api/lib/material/date_picker/custom_calendar_date_picker.0.dart similarity index 100% rename from examples/api/lib/material/date_picker/custom_calendar_date_picker.0.dart rename to packages/flutter/examples/api/lib/material/date_picker/custom_calendar_date_picker.0.dart diff --git a/examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart b/packages/flutter/examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart similarity index 100% rename from examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart rename to packages/flutter/examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart diff --git a/examples/api/lib/material/date_picker/show_date_picker.0.dart b/packages/flutter/examples/api/lib/material/date_picker/show_date_picker.0.dart similarity index 100% rename from examples/api/lib/material/date_picker/show_date_picker.0.dart rename to packages/flutter/examples/api/lib/material/date_picker/show_date_picker.0.dart diff --git a/examples/api/lib/material/date_picker/show_date_picker.1.dart b/packages/flutter/examples/api/lib/material/date_picker/show_date_picker.1.dart similarity index 100% rename from examples/api/lib/material/date_picker/show_date_picker.1.dart rename to packages/flutter/examples/api/lib/material/date_picker/show_date_picker.1.dart diff --git a/examples/api/lib/material/date_picker/show_date_range_picker.0.dart b/packages/flutter/examples/api/lib/material/date_picker/show_date_range_picker.0.dart similarity index 100% rename from examples/api/lib/material/date_picker/show_date_range_picker.0.dart rename to packages/flutter/examples/api/lib/material/date_picker/show_date_range_picker.0.dart diff --git a/examples/api/lib/material/dialog/adaptive_alert_dialog.0.dart b/packages/flutter/examples/api/lib/material/dialog/adaptive_alert_dialog.0.dart similarity index 100% rename from examples/api/lib/material/dialog/adaptive_alert_dialog.0.dart rename to packages/flutter/examples/api/lib/material/dialog/adaptive_alert_dialog.0.dart diff --git a/examples/api/lib/material/dialog/alert_dialog.0.dart b/packages/flutter/examples/api/lib/material/dialog/alert_dialog.0.dart similarity index 100% rename from examples/api/lib/material/dialog/alert_dialog.0.dart rename to packages/flutter/examples/api/lib/material/dialog/alert_dialog.0.dart diff --git a/examples/api/lib/material/dialog/alert_dialog.1.dart b/packages/flutter/examples/api/lib/material/dialog/alert_dialog.1.dart similarity index 100% rename from examples/api/lib/material/dialog/alert_dialog.1.dart rename to packages/flutter/examples/api/lib/material/dialog/alert_dialog.1.dart diff --git a/examples/api/lib/material/dialog/dialog.0.dart b/packages/flutter/examples/api/lib/material/dialog/dialog.0.dart similarity index 100% rename from examples/api/lib/material/dialog/dialog.0.dart rename to packages/flutter/examples/api/lib/material/dialog/dialog.0.dart diff --git a/examples/api/lib/material/dialog/show_dialog.0.dart b/packages/flutter/examples/api/lib/material/dialog/show_dialog.0.dart similarity index 100% rename from examples/api/lib/material/dialog/show_dialog.0.dart rename to packages/flutter/examples/api/lib/material/dialog/show_dialog.0.dart diff --git a/examples/api/lib/material/dialog/show_dialog.1.dart b/packages/flutter/examples/api/lib/material/dialog/show_dialog.1.dart similarity index 100% rename from examples/api/lib/material/dialog/show_dialog.1.dart rename to packages/flutter/examples/api/lib/material/dialog/show_dialog.1.dart diff --git a/examples/api/lib/material/dialog/show_dialog.2.dart b/packages/flutter/examples/api/lib/material/dialog/show_dialog.2.dart similarity index 100% rename from examples/api/lib/material/dialog/show_dialog.2.dart rename to packages/flutter/examples/api/lib/material/dialog/show_dialog.2.dart diff --git a/examples/api/lib/material/divider/divider.0.dart b/packages/flutter/examples/api/lib/material/divider/divider.0.dart similarity index 100% rename from examples/api/lib/material/divider/divider.0.dart rename to packages/flutter/examples/api/lib/material/divider/divider.0.dart diff --git a/examples/api/lib/material/divider/divider.1.dart b/packages/flutter/examples/api/lib/material/divider/divider.1.dart similarity index 100% rename from examples/api/lib/material/divider/divider.1.dart rename to packages/flutter/examples/api/lib/material/divider/divider.1.dart diff --git a/examples/api/lib/material/divider/vertical_divider.0.dart b/packages/flutter/examples/api/lib/material/divider/vertical_divider.0.dart similarity index 100% rename from examples/api/lib/material/divider/vertical_divider.0.dart rename to packages/flutter/examples/api/lib/material/divider/vertical_divider.0.dart diff --git a/examples/api/lib/material/divider/vertical_divider.1.dart b/packages/flutter/examples/api/lib/material/divider/vertical_divider.1.dart similarity index 100% rename from examples/api/lib/material/divider/vertical_divider.1.dart rename to packages/flutter/examples/api/lib/material/divider/vertical_divider.1.dart diff --git a/examples/api/lib/material/drawer/drawer.0.dart b/packages/flutter/examples/api/lib/material/drawer/drawer.0.dart similarity index 100% rename from examples/api/lib/material/drawer/drawer.0.dart rename to packages/flutter/examples/api/lib/material/drawer/drawer.0.dart diff --git a/examples/api/lib/material/dropdown/dropdown_button.0.dart b/packages/flutter/examples/api/lib/material/dropdown/dropdown_button.0.dart similarity index 100% rename from examples/api/lib/material/dropdown/dropdown_button.0.dart rename to packages/flutter/examples/api/lib/material/dropdown/dropdown_button.0.dart diff --git a/examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart b/packages/flutter/examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart similarity index 100% rename from examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart rename to packages/flutter/examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart diff --git a/examples/api/lib/material/dropdown/dropdown_button.style.0.dart b/packages/flutter/examples/api/lib/material/dropdown/dropdown_button.style.0.dart similarity index 100% rename from examples/api/lib/material/dropdown/dropdown_button.style.0.dart rename to packages/flutter/examples/api/lib/material/dropdown/dropdown_button.style.0.dart diff --git a/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart b/packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart similarity index 100% rename from examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart rename to packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart diff --git a/examples/api/lib/material/dropdown_menu/dropdown_menu.1.dart b/packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu.1.dart similarity index 100% rename from examples/api/lib/material/dropdown_menu/dropdown_menu.1.dart rename to packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu.1.dart diff --git a/examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart b/packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart similarity index 100% rename from examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart rename to packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart diff --git a/examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart b/packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart similarity index 100% rename from examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart rename to packages/flutter/examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart diff --git a/examples/api/lib/material/elevated_button/elevated_button.0.dart b/packages/flutter/examples/api/lib/material/elevated_button/elevated_button.0.dart similarity index 100% rename from examples/api/lib/material/elevated_button/elevated_button.0.dart rename to packages/flutter/examples/api/lib/material/elevated_button/elevated_button.0.dart diff --git a/examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart b/packages/flutter/examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart similarity index 100% rename from examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart rename to packages/flutter/examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart diff --git a/examples/api/lib/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0.dart b/packages/flutter/examples/api/lib/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0.dart similarity index 100% rename from examples/api/lib/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0.dart rename to packages/flutter/examples/api/lib/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0.dart diff --git a/examples/api/lib/material/expansion_tile/expansion_tile.0.dart b/packages/flutter/examples/api/lib/material/expansion_tile/expansion_tile.0.dart similarity index 100% rename from examples/api/lib/material/expansion_tile/expansion_tile.0.dart rename to packages/flutter/examples/api/lib/material/expansion_tile/expansion_tile.0.dart diff --git a/examples/api/lib/material/expansion_tile/expansion_tile.1.dart b/packages/flutter/examples/api/lib/material/expansion_tile/expansion_tile.1.dart similarity index 100% rename from examples/api/lib/material/expansion_tile/expansion_tile.1.dart rename to packages/flutter/examples/api/lib/material/expansion_tile/expansion_tile.1.dart diff --git a/examples/api/lib/material/expansion_tile/expansion_tile.2.dart b/packages/flutter/examples/api/lib/material/expansion_tile/expansion_tile.2.dart similarity index 100% rename from examples/api/lib/material/expansion_tile/expansion_tile.2.dart rename to packages/flutter/examples/api/lib/material/expansion_tile/expansion_tile.2.dart diff --git a/examples/api/lib/material/filled_button/filled_button.0.dart b/packages/flutter/examples/api/lib/material/filled_button/filled_button.0.dart similarity index 100% rename from examples/api/lib/material/filled_button/filled_button.0.dart rename to packages/flutter/examples/api/lib/material/filled_button/filled_button.0.dart diff --git a/examples/api/lib/material/filter_chip/filter_chip.0.dart b/packages/flutter/examples/api/lib/material/filter_chip/filter_chip.0.dart similarity index 100% rename from examples/api/lib/material/filter_chip/filter_chip.0.dart rename to packages/flutter/examples/api/lib/material/filter_chip/filter_chip.0.dart diff --git a/examples/api/lib/material/flexible_space_bar/flexible_space_bar.0.dart b/packages/flutter/examples/api/lib/material/flexible_space_bar/flexible_space_bar.0.dart similarity index 100% rename from examples/api/lib/material/flexible_space_bar/flexible_space_bar.0.dart rename to packages/flutter/examples/api/lib/material/flexible_space_bar/flexible_space_bar.0.dart diff --git a/examples/api/lib/material/floating_action_button/floating_action_button.0.dart b/packages/flutter/examples/api/lib/material/floating_action_button/floating_action_button.0.dart similarity index 100% rename from examples/api/lib/material/floating_action_button/floating_action_button.0.dart rename to packages/flutter/examples/api/lib/material/floating_action_button/floating_action_button.0.dart diff --git a/examples/api/lib/material/floating_action_button/floating_action_button.1.dart b/packages/flutter/examples/api/lib/material/floating_action_button/floating_action_button.1.dart similarity index 100% rename from examples/api/lib/material/floating_action_button/floating_action_button.1.dart rename to packages/flutter/examples/api/lib/material/floating_action_button/floating_action_button.1.dart diff --git a/examples/api/lib/material/floating_action_button/floating_action_button.2.dart b/packages/flutter/examples/api/lib/material/floating_action_button/floating_action_button.2.dart similarity index 100% rename from examples/api/lib/material/floating_action_button/floating_action_button.2.dart rename to packages/flutter/examples/api/lib/material/floating_action_button/floating_action_button.2.dart diff --git a/examples/api/lib/material/floating_action_button_location/standard_fab_location.0.dart b/packages/flutter/examples/api/lib/material/floating_action_button_location/standard_fab_location.0.dart similarity index 100% rename from examples/api/lib/material/floating_action_button_location/standard_fab_location.0.dart rename to packages/flutter/examples/api/lib/material/floating_action_button_location/standard_fab_location.0.dart diff --git a/examples/api/lib/material/icon_alignment/icon_alignment.0.dart b/packages/flutter/examples/api/lib/material/icon_alignment/icon_alignment.0.dart similarity index 100% rename from examples/api/lib/material/icon_alignment/icon_alignment.0.dart rename to packages/flutter/examples/api/lib/material/icon_alignment/icon_alignment.0.dart diff --git a/examples/api/lib/material/icon_button/icon_button.0.dart b/packages/flutter/examples/api/lib/material/icon_button/icon_button.0.dart similarity index 100% rename from examples/api/lib/material/icon_button/icon_button.0.dart rename to packages/flutter/examples/api/lib/material/icon_button/icon_button.0.dart diff --git a/examples/api/lib/material/icon_button/icon_button.1.dart b/packages/flutter/examples/api/lib/material/icon_button/icon_button.1.dart similarity index 100% rename from examples/api/lib/material/icon_button/icon_button.1.dart rename to packages/flutter/examples/api/lib/material/icon_button/icon_button.1.dart diff --git a/examples/api/lib/material/icon_button/icon_button.2.dart b/packages/flutter/examples/api/lib/material/icon_button/icon_button.2.dart similarity index 100% rename from examples/api/lib/material/icon_button/icon_button.2.dart rename to packages/flutter/examples/api/lib/material/icon_button/icon_button.2.dart diff --git a/examples/api/lib/material/icon_button/icon_button.3.dart b/packages/flutter/examples/api/lib/material/icon_button/icon_button.3.dart similarity index 100% rename from examples/api/lib/material/icon_button/icon_button.3.dart rename to packages/flutter/examples/api/lib/material/icon_button/icon_button.3.dart diff --git a/examples/api/lib/material/ink/ink.image_clip.0.dart b/packages/flutter/examples/api/lib/material/ink/ink.image_clip.0.dart similarity index 100% rename from examples/api/lib/material/ink/ink.image_clip.0.dart rename to packages/flutter/examples/api/lib/material/ink/ink.image_clip.0.dart diff --git a/examples/api/lib/material/ink/ink.image_clip.1.dart b/packages/flutter/examples/api/lib/material/ink/ink.image_clip.1.dart similarity index 100% rename from examples/api/lib/material/ink/ink.image_clip.1.dart rename to packages/flutter/examples/api/lib/material/ink/ink.image_clip.1.dart diff --git a/examples/api/lib/material/ink_well/ink_well.0.dart b/packages/flutter/examples/api/lib/material/ink_well/ink_well.0.dart similarity index 100% rename from examples/api/lib/material/ink_well/ink_well.0.dart rename to packages/flutter/examples/api/lib/material/ink_well/ink_well.0.dart diff --git a/examples/api/lib/material/input_chip/input_chip.0.dart b/packages/flutter/examples/api/lib/material/input_chip/input_chip.0.dart similarity index 100% rename from examples/api/lib/material/input_chip/input_chip.0.dart rename to packages/flutter/examples/api/lib/material/input_chip/input_chip.0.dart diff --git a/examples/api/lib/material/input_chip/input_chip.1.dart b/packages/flutter/examples/api/lib/material/input_chip/input_chip.1.dart similarity index 100% rename from examples/api/lib/material/input_chip/input_chip.1.dart rename to packages/flutter/examples/api/lib/material/input_chip/input_chip.1.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.1.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.1.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.1.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.1.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.2.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.2.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.2.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.2.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.3.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.3.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.3.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.3.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.floating_label_style_error.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.floating_label_style_error.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.floating_label_style_error.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.floating_label_style_error.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.helper.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.helper.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.helper.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.helper.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.label.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.label.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.label.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.label.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.label_style_error.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.label_style_error.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.label_style_error.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.label_style_error.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.prefix_icon.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.prefix_icon.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.prefix_icon.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.prefix_icon.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.prefix_icon_constraints.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.prefix_icon_constraints.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.prefix_icon_constraints.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.prefix_icon_constraints.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.suffix_icon.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.suffix_icon.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.suffix_icon.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.suffix_icon.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.suffix_icon_constraints.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.suffix_icon_constraints.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.suffix_icon_constraints.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.suffix_icon_constraints.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.widget_state.0.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.widget_state.0.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.widget_state.0.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.widget_state.0.dart diff --git a/examples/api/lib/material/input_decorator/input_decoration.widget_state.1.dart b/packages/flutter/examples/api/lib/material/input_decorator/input_decoration.widget_state.1.dart similarity index 100% rename from examples/api/lib/material/input_decorator/input_decoration.widget_state.1.dart rename to packages/flutter/examples/api/lib/material/input_decorator/input_decoration.widget_state.1.dart diff --git a/examples/api/lib/material/list_tile/custom_list_item.0.dart b/packages/flutter/examples/api/lib/material/list_tile/custom_list_item.0.dart similarity index 100% rename from examples/api/lib/material/list_tile/custom_list_item.0.dart rename to packages/flutter/examples/api/lib/material/list_tile/custom_list_item.0.dart diff --git a/examples/api/lib/material/list_tile/custom_list_item.1.dart b/packages/flutter/examples/api/lib/material/list_tile/custom_list_item.1.dart similarity index 100% rename from examples/api/lib/material/list_tile/custom_list_item.1.dart rename to packages/flutter/examples/api/lib/material/list_tile/custom_list_item.1.dart diff --git a/examples/api/lib/material/list_tile/list_tile.0.dart b/packages/flutter/examples/api/lib/material/list_tile/list_tile.0.dart similarity index 100% rename from examples/api/lib/material/list_tile/list_tile.0.dart rename to packages/flutter/examples/api/lib/material/list_tile/list_tile.0.dart diff --git a/examples/api/lib/material/list_tile/list_tile.1.dart b/packages/flutter/examples/api/lib/material/list_tile/list_tile.1.dart similarity index 100% rename from examples/api/lib/material/list_tile/list_tile.1.dart rename to packages/flutter/examples/api/lib/material/list_tile/list_tile.1.dart diff --git a/examples/api/lib/material/list_tile/list_tile.2.dart b/packages/flutter/examples/api/lib/material/list_tile/list_tile.2.dart similarity index 100% rename from examples/api/lib/material/list_tile/list_tile.2.dart rename to packages/flutter/examples/api/lib/material/list_tile/list_tile.2.dart diff --git a/examples/api/lib/material/list_tile/list_tile.3.dart b/packages/flutter/examples/api/lib/material/list_tile/list_tile.3.dart similarity index 100% rename from examples/api/lib/material/list_tile/list_tile.3.dart rename to packages/flutter/examples/api/lib/material/list_tile/list_tile.3.dart diff --git a/examples/api/lib/material/list_tile/list_tile.4.dart b/packages/flutter/examples/api/lib/material/list_tile/list_tile.4.dart similarity index 100% rename from examples/api/lib/material/list_tile/list_tile.4.dart rename to packages/flutter/examples/api/lib/material/list_tile/list_tile.4.dart diff --git a/examples/api/lib/material/list_tile/list_tile.selected.0.dart b/packages/flutter/examples/api/lib/material/list_tile/list_tile.selected.0.dart similarity index 100% rename from examples/api/lib/material/list_tile/list_tile.selected.0.dart rename to packages/flutter/examples/api/lib/material/list_tile/list_tile.selected.0.dart diff --git a/examples/api/lib/material/material_state/material_state_border_side.0.dart b/packages/flutter/examples/api/lib/material/material_state/material_state_border_side.0.dart similarity index 100% rename from examples/api/lib/material/material_state/material_state_border_side.0.dart rename to packages/flutter/examples/api/lib/material/material_state/material_state_border_side.0.dart diff --git a/examples/api/lib/material/material_state/material_state_mouse_cursor.0.dart b/packages/flutter/examples/api/lib/material/material_state/material_state_mouse_cursor.0.dart similarity index 100% rename from examples/api/lib/material/material_state/material_state_mouse_cursor.0.dart rename to packages/flutter/examples/api/lib/material/material_state/material_state_mouse_cursor.0.dart diff --git a/examples/api/lib/material/material_state/material_state_property.0.dart b/packages/flutter/examples/api/lib/material/material_state/material_state_property.0.dart similarity index 100% rename from examples/api/lib/material/material_state/material_state_property.0.dart rename to packages/flutter/examples/api/lib/material/material_state/material_state_property.0.dart diff --git a/examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart b/packages/flutter/examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart diff --git a/examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart b/packages/flutter/examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart diff --git a/examples/api/lib/material/menu_anchor/menu_anchor.0.dart b/packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.0.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/menu_anchor.0.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.0.dart diff --git a/examples/api/lib/material/menu_anchor/menu_anchor.1.dart b/packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.1.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/menu_anchor.1.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.1.dart diff --git a/examples/api/lib/material/menu_anchor/menu_anchor.2.dart b/packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.2.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/menu_anchor.2.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.2.dart diff --git a/examples/api/lib/material/menu_anchor/menu_anchor.3.dart b/packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.3.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/menu_anchor.3.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/menu_anchor.3.dart diff --git a/examples/api/lib/material/menu_anchor/menu_bar.0.dart b/packages/flutter/examples/api/lib/material/menu_anchor/menu_bar.0.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/menu_bar.0.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/menu_bar.0.dart diff --git a/examples/api/lib/material/menu_anchor/radio_menu_button.0.dart b/packages/flutter/examples/api/lib/material/menu_anchor/radio_menu_button.0.dart similarity index 100% rename from examples/api/lib/material/menu_anchor/radio_menu_button.0.dart rename to packages/flutter/examples/api/lib/material/menu_anchor/radio_menu_button.0.dart diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.0.dart b/packages/flutter/examples/api/lib/material/navigation_bar/navigation_bar.0.dart similarity index 100% rename from examples/api/lib/material/navigation_bar/navigation_bar.0.dart rename to packages/flutter/examples/api/lib/material/navigation_bar/navigation_bar.0.dart diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.1.dart b/packages/flutter/examples/api/lib/material/navigation_bar/navigation_bar.1.dart similarity index 100% rename from examples/api/lib/material/navigation_bar/navigation_bar.1.dart rename to packages/flutter/examples/api/lib/material/navigation_bar/navigation_bar.1.dart diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.2.dart b/packages/flutter/examples/api/lib/material/navigation_bar/navigation_bar.2.dart similarity index 100% rename from examples/api/lib/material/navigation_bar/navigation_bar.2.dart rename to packages/flutter/examples/api/lib/material/navigation_bar/navigation_bar.2.dart diff --git a/examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart b/packages/flutter/examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart similarity index 100% rename from examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart rename to packages/flutter/examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart diff --git a/examples/api/lib/material/navigation_rail/navigation_rail.0.dart b/packages/flutter/examples/api/lib/material/navigation_rail/navigation_rail.0.dart similarity index 100% rename from examples/api/lib/material/navigation_rail/navigation_rail.0.dart rename to packages/flutter/examples/api/lib/material/navigation_rail/navigation_rail.0.dart diff --git a/examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart b/packages/flutter/examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart similarity index 100% rename from examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart rename to packages/flutter/examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart diff --git a/examples/api/lib/material/outlined_button/outlined_button.0.dart b/packages/flutter/examples/api/lib/material/outlined_button/outlined_button.0.dart similarity index 100% rename from examples/api/lib/material/outlined_button/outlined_button.0.dart rename to packages/flutter/examples/api/lib/material/outlined_button/outlined_button.0.dart diff --git a/examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart b/packages/flutter/examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart similarity index 100% rename from examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart rename to packages/flutter/examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart diff --git a/examples/api/lib/material/page_transitions_theme/page_transitions_theme.1.dart b/packages/flutter/examples/api/lib/material/page_transitions_theme/page_transitions_theme.1.dart similarity index 100% rename from examples/api/lib/material/page_transitions_theme/page_transitions_theme.1.dart rename to packages/flutter/examples/api/lib/material/page_transitions_theme/page_transitions_theme.1.dart diff --git a/examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart b/packages/flutter/examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart similarity index 100% rename from examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart rename to packages/flutter/examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart diff --git a/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart b/packages/flutter/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart similarity index 100% rename from examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart rename to packages/flutter/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart diff --git a/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart b/packages/flutter/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart similarity index 100% rename from examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart rename to packages/flutter/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart diff --git a/examples/api/lib/material/popup_menu/popup_menu.0.dart b/packages/flutter/examples/api/lib/material/popup_menu/popup_menu.0.dart similarity index 100% rename from examples/api/lib/material/popup_menu/popup_menu.0.dart rename to packages/flutter/examples/api/lib/material/popup_menu/popup_menu.0.dart diff --git a/examples/api/lib/material/popup_menu/popup_menu.1.dart b/packages/flutter/examples/api/lib/material/popup_menu/popup_menu.1.dart similarity index 100% rename from examples/api/lib/material/popup_menu/popup_menu.1.dart rename to packages/flutter/examples/api/lib/material/popup_menu/popup_menu.1.dart diff --git a/examples/api/lib/material/popup_menu/popup_menu.2.dart b/packages/flutter/examples/api/lib/material/popup_menu/popup_menu.2.dart similarity index 100% rename from examples/api/lib/material/popup_menu/popup_menu.2.dart rename to packages/flutter/examples/api/lib/material/popup_menu/popup_menu.2.dart diff --git a/examples/api/lib/material/progress_indicator/circular_progress_indicator.0.dart b/packages/flutter/examples/api/lib/material/progress_indicator/circular_progress_indicator.0.dart similarity index 100% rename from examples/api/lib/material/progress_indicator/circular_progress_indicator.0.dart rename to packages/flutter/examples/api/lib/material/progress_indicator/circular_progress_indicator.0.dart diff --git a/examples/api/lib/material/progress_indicator/circular_progress_indicator.1.dart b/packages/flutter/examples/api/lib/material/progress_indicator/circular_progress_indicator.1.dart similarity index 100% rename from examples/api/lib/material/progress_indicator/circular_progress_indicator.1.dart rename to packages/flutter/examples/api/lib/material/progress_indicator/circular_progress_indicator.1.dart diff --git a/examples/api/lib/material/progress_indicator/circular_progress_indicator.2.dart b/packages/flutter/examples/api/lib/material/progress_indicator/circular_progress_indicator.2.dart similarity index 100% rename from examples/api/lib/material/progress_indicator/circular_progress_indicator.2.dart rename to packages/flutter/examples/api/lib/material/progress_indicator/circular_progress_indicator.2.dart diff --git a/examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart b/packages/flutter/examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart similarity index 100% rename from examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart rename to packages/flutter/examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart diff --git a/examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart b/packages/flutter/examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart similarity index 100% rename from examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart rename to packages/flutter/examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart diff --git a/examples/api/lib/material/radio/radio.0.dart b/packages/flutter/examples/api/lib/material/radio/radio.0.dart similarity index 100% rename from examples/api/lib/material/radio/radio.0.dart rename to packages/flutter/examples/api/lib/material/radio/radio.0.dart diff --git a/examples/api/lib/material/radio/radio.1.dart b/packages/flutter/examples/api/lib/material/radio/radio.1.dart similarity index 100% rename from examples/api/lib/material/radio/radio.1.dart rename to packages/flutter/examples/api/lib/material/radio/radio.1.dart diff --git a/examples/api/lib/material/radio/radio.toggleable.0.dart b/packages/flutter/examples/api/lib/material/radio/radio.toggleable.0.dart similarity index 100% rename from examples/api/lib/material/radio/radio.toggleable.0.dart rename to packages/flutter/examples/api/lib/material/radio/radio.toggleable.0.dart diff --git a/examples/api/lib/material/radio_list_tile/custom_labeled_radio.0.dart b/packages/flutter/examples/api/lib/material/radio_list_tile/custom_labeled_radio.0.dart similarity index 100% rename from examples/api/lib/material/radio_list_tile/custom_labeled_radio.0.dart rename to packages/flutter/examples/api/lib/material/radio_list_tile/custom_labeled_radio.0.dart diff --git a/examples/api/lib/material/radio_list_tile/custom_labeled_radio.1.dart b/packages/flutter/examples/api/lib/material/radio_list_tile/custom_labeled_radio.1.dart similarity index 100% rename from examples/api/lib/material/radio_list_tile/custom_labeled_radio.1.dart rename to packages/flutter/examples/api/lib/material/radio_list_tile/custom_labeled_radio.1.dart diff --git a/examples/api/lib/material/radio_list_tile/radio_list_tile.0.dart b/packages/flutter/examples/api/lib/material/radio_list_tile/radio_list_tile.0.dart similarity index 100% rename from examples/api/lib/material/radio_list_tile/radio_list_tile.0.dart rename to packages/flutter/examples/api/lib/material/radio_list_tile/radio_list_tile.0.dart diff --git a/examples/api/lib/material/radio_list_tile/radio_list_tile.1.dart b/packages/flutter/examples/api/lib/material/radio_list_tile/radio_list_tile.1.dart similarity index 100% rename from examples/api/lib/material/radio_list_tile/radio_list_tile.1.dart rename to packages/flutter/examples/api/lib/material/radio_list_tile/radio_list_tile.1.dart diff --git a/examples/api/lib/material/radio_list_tile/radio_list_tile.toggleable.0.dart b/packages/flutter/examples/api/lib/material/radio_list_tile/radio_list_tile.toggleable.0.dart similarity index 100% rename from examples/api/lib/material/radio_list_tile/radio_list_tile.toggleable.0.dart rename to packages/flutter/examples/api/lib/material/radio_list_tile/radio_list_tile.toggleable.0.dart diff --git a/examples/api/lib/material/range_slider/range_slider.0.dart b/packages/flutter/examples/api/lib/material/range_slider/range_slider.0.dart similarity index 100% rename from examples/api/lib/material/range_slider/range_slider.0.dart rename to packages/flutter/examples/api/lib/material/range_slider/range_slider.0.dart diff --git a/examples/api/lib/material/refresh_indicator/refresh_indicator.0.dart b/packages/flutter/examples/api/lib/material/refresh_indicator/refresh_indicator.0.dart similarity index 100% rename from examples/api/lib/material/refresh_indicator/refresh_indicator.0.dart rename to packages/flutter/examples/api/lib/material/refresh_indicator/refresh_indicator.0.dart diff --git a/examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart b/packages/flutter/examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart similarity index 100% rename from examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart rename to packages/flutter/examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart diff --git a/examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart b/packages/flutter/examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart similarity index 100% rename from examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart rename to packages/flutter/examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart diff --git a/examples/api/lib/material/reorderable_list/reorderable_list_view.0.dart b/packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.0.dart similarity index 100% rename from examples/api/lib/material/reorderable_list/reorderable_list_view.0.dart rename to packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.0.dart diff --git a/examples/api/lib/material/reorderable_list/reorderable_list_view.1.dart b/packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.1.dart similarity index 100% rename from examples/api/lib/material/reorderable_list/reorderable_list_view.1.dart rename to packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.1.dart diff --git a/examples/api/lib/material/reorderable_list/reorderable_list_view.2.dart b/packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.2.dart similarity index 100% rename from examples/api/lib/material/reorderable_list/reorderable_list_view.2.dart rename to packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.2.dart diff --git a/examples/api/lib/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0.dart b/packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0.dart similarity index 100% rename from examples/api/lib/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0.dart rename to packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0.dart diff --git a/examples/api/lib/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0.dart b/packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0.dart similarity index 100% rename from examples/api/lib/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0.dart rename to packages/flutter/examples/api/lib/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold.1.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.1.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.1.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.1.dart diff --git a/examples/api/lib/material/scaffold/scaffold.2.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.2.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.2.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.2.dart diff --git a/examples/api/lib/material/scaffold/scaffold.drawer.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.drawer.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.drawer.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.drawer.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold.end_drawer.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.end_drawer.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.end_drawer.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.end_drawer.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold.floating_action_button_animator.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.floating_action_button_animator.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.floating_action_button_animator.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.floating_action_button_animator.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold.of.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.of.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.of.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.of.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold.of.1.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold.of.1.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold.of.1.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold.of.1.dart diff --git a/examples/api/lib/material/scaffold/scaffold_messenger.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_messenger.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold_messenger.of.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger.of.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_messenger.of.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger.of.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold_messenger.of.1.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger.of.1.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_messenger.of.1.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger.of.1.dart diff --git a/examples/api/lib/material/scaffold/scaffold_messenger_state.show_material_banner.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_material_banner.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_messenger_state.show_material_banner.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_material_banner.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart diff --git a/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart diff --git a/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.0.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.0.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.0.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.0.dart diff --git a/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart b/packages/flutter/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart similarity index 100% rename from examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart rename to packages/flutter/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart diff --git a/examples/api/lib/material/scrollbar/scrollbar.0.dart b/packages/flutter/examples/api/lib/material/scrollbar/scrollbar.0.dart similarity index 100% rename from examples/api/lib/material/scrollbar/scrollbar.0.dart rename to packages/flutter/examples/api/lib/material/scrollbar/scrollbar.0.dart diff --git a/examples/api/lib/material/scrollbar/scrollbar.1.dart b/packages/flutter/examples/api/lib/material/scrollbar/scrollbar.1.dart similarity index 100% rename from examples/api/lib/material/scrollbar/scrollbar.1.dart rename to packages/flutter/examples/api/lib/material/scrollbar/scrollbar.1.dart diff --git a/examples/api/lib/material/search_anchor/search_anchor.0.dart b/packages/flutter/examples/api/lib/material/search_anchor/search_anchor.0.dart similarity index 100% rename from examples/api/lib/material/search_anchor/search_anchor.0.dart rename to packages/flutter/examples/api/lib/material/search_anchor/search_anchor.0.dart diff --git a/examples/api/lib/material/search_anchor/search_anchor.1.dart b/packages/flutter/examples/api/lib/material/search_anchor/search_anchor.1.dart similarity index 100% rename from examples/api/lib/material/search_anchor/search_anchor.1.dart rename to packages/flutter/examples/api/lib/material/search_anchor/search_anchor.1.dart diff --git a/examples/api/lib/material/search_anchor/search_anchor.2.dart b/packages/flutter/examples/api/lib/material/search_anchor/search_anchor.2.dart similarity index 100% rename from examples/api/lib/material/search_anchor/search_anchor.2.dart rename to packages/flutter/examples/api/lib/material/search_anchor/search_anchor.2.dart diff --git a/examples/api/lib/material/search_anchor/search_anchor.3.dart b/packages/flutter/examples/api/lib/material/search_anchor/search_anchor.3.dart similarity index 100% rename from examples/api/lib/material/search_anchor/search_anchor.3.dart rename to packages/flutter/examples/api/lib/material/search_anchor/search_anchor.3.dart diff --git a/examples/api/lib/material/search_anchor/search_anchor.4.dart b/packages/flutter/examples/api/lib/material/search_anchor/search_anchor.4.dart similarity index 100% rename from examples/api/lib/material/search_anchor/search_anchor.4.dart rename to packages/flutter/examples/api/lib/material/search_anchor/search_anchor.4.dart diff --git a/examples/api/lib/material/search_anchor/search_bar.0.dart b/packages/flutter/examples/api/lib/material/search_anchor/search_bar.0.dart similarity index 100% rename from examples/api/lib/material/search_anchor/search_bar.0.dart rename to packages/flutter/examples/api/lib/material/search_anchor/search_bar.0.dart diff --git a/examples/api/lib/material/segmented_button/segmented_button.0.dart b/packages/flutter/examples/api/lib/material/segmented_button/segmented_button.0.dart similarity index 100% rename from examples/api/lib/material/segmented_button/segmented_button.0.dart rename to packages/flutter/examples/api/lib/material/segmented_button/segmented_button.0.dart diff --git a/examples/api/lib/material/segmented_button/segmented_button.1.dart b/packages/flutter/examples/api/lib/material/segmented_button/segmented_button.1.dart similarity index 100% rename from examples/api/lib/material/segmented_button/segmented_button.1.dart rename to packages/flutter/examples/api/lib/material/segmented_button/segmented_button.1.dart diff --git a/examples/api/lib/material/selection_area/selection_area.0.dart b/packages/flutter/examples/api/lib/material/selection_area/selection_area.0.dart similarity index 100% rename from examples/api/lib/material/selection_area/selection_area.0.dart rename to packages/flutter/examples/api/lib/material/selection_area/selection_area.0.dart diff --git a/examples/api/lib/material/selection_area/selection_area.1.dart b/packages/flutter/examples/api/lib/material/selection_area/selection_area.1.dart similarity index 100% rename from examples/api/lib/material/selection_area/selection_area.1.dart rename to packages/flutter/examples/api/lib/material/selection_area/selection_area.1.dart diff --git a/examples/api/lib/material/selection_area/selection_area.2.dart b/packages/flutter/examples/api/lib/material/selection_area/selection_area.2.dart similarity index 100% rename from examples/api/lib/material/selection_area/selection_area.2.dart rename to packages/flutter/examples/api/lib/material/selection_area/selection_area.2.dart diff --git a/examples/api/lib/material/shaped_input_border/shaped_input_border.0.dart b/packages/flutter/examples/api/lib/material/shaped_input_border/shaped_input_border.0.dart similarity index 100% rename from examples/api/lib/material/shaped_input_border/shaped_input_border.0.dart rename to packages/flutter/examples/api/lib/material/shaped_input_border/shaped_input_border.0.dart diff --git a/examples/api/lib/material/slider/slider.0.dart b/packages/flutter/examples/api/lib/material/slider/slider.0.dart similarity index 100% rename from examples/api/lib/material/slider/slider.0.dart rename to packages/flutter/examples/api/lib/material/slider/slider.0.dart diff --git a/examples/api/lib/material/slider/slider.1.dart b/packages/flutter/examples/api/lib/material/slider/slider.1.dart similarity index 100% rename from examples/api/lib/material/slider/slider.1.dart rename to packages/flutter/examples/api/lib/material/slider/slider.1.dart diff --git a/examples/api/lib/material/snack_bar/snack_bar.0.dart b/packages/flutter/examples/api/lib/material/snack_bar/snack_bar.0.dart similarity index 100% rename from examples/api/lib/material/snack_bar/snack_bar.0.dart rename to packages/flutter/examples/api/lib/material/snack_bar/snack_bar.0.dart diff --git a/examples/api/lib/material/snack_bar/snack_bar.1.dart b/packages/flutter/examples/api/lib/material/snack_bar/snack_bar.1.dart similarity index 100% rename from examples/api/lib/material/snack_bar/snack_bar.1.dart rename to packages/flutter/examples/api/lib/material/snack_bar/snack_bar.1.dart diff --git a/examples/api/lib/material/snack_bar/snack_bar.2.dart b/packages/flutter/examples/api/lib/material/snack_bar/snack_bar.2.dart similarity index 100% rename from examples/api/lib/material/snack_bar/snack_bar.2.dart rename to packages/flutter/examples/api/lib/material/snack_bar/snack_bar.2.dart diff --git a/examples/api/lib/material/stepper/step_style.0.dart b/packages/flutter/examples/api/lib/material/stepper/step_style.0.dart similarity index 100% rename from examples/api/lib/material/stepper/step_style.0.dart rename to packages/flutter/examples/api/lib/material/stepper/step_style.0.dart diff --git a/examples/api/lib/material/stepper/stepper.0.dart b/packages/flutter/examples/api/lib/material/stepper/stepper.0.dart similarity index 100% rename from examples/api/lib/material/stepper/stepper.0.dart rename to packages/flutter/examples/api/lib/material/stepper/stepper.0.dart diff --git a/examples/api/lib/material/stepper/stepper.controls_builder.0.dart b/packages/flutter/examples/api/lib/material/stepper/stepper.controls_builder.0.dart similarity index 100% rename from examples/api/lib/material/stepper/stepper.controls_builder.0.dart rename to packages/flutter/examples/api/lib/material/stepper/stepper.controls_builder.0.dart diff --git a/examples/api/lib/material/switch/switch.0.dart b/packages/flutter/examples/api/lib/material/switch/switch.0.dart similarity index 100% rename from examples/api/lib/material/switch/switch.0.dart rename to packages/flutter/examples/api/lib/material/switch/switch.0.dart diff --git a/examples/api/lib/material/switch/switch.1.dart b/packages/flutter/examples/api/lib/material/switch/switch.1.dart similarity index 100% rename from examples/api/lib/material/switch/switch.1.dart rename to packages/flutter/examples/api/lib/material/switch/switch.1.dart diff --git a/examples/api/lib/material/switch/switch.2.dart b/packages/flutter/examples/api/lib/material/switch/switch.2.dart similarity index 100% rename from examples/api/lib/material/switch/switch.2.dart rename to packages/flutter/examples/api/lib/material/switch/switch.2.dart diff --git a/examples/api/lib/material/switch/switch.3.dart b/packages/flutter/examples/api/lib/material/switch/switch.3.dart similarity index 100% rename from examples/api/lib/material/switch/switch.3.dart rename to packages/flutter/examples/api/lib/material/switch/switch.3.dart diff --git a/examples/api/lib/material/switch/switch.4.dart b/packages/flutter/examples/api/lib/material/switch/switch.4.dart similarity index 100% rename from examples/api/lib/material/switch/switch.4.dart rename to packages/flutter/examples/api/lib/material/switch/switch.4.dart diff --git a/examples/api/lib/material/switch_list_tile/custom_labeled_switch.0.dart b/packages/flutter/examples/api/lib/material/switch_list_tile/custom_labeled_switch.0.dart similarity index 100% rename from examples/api/lib/material/switch_list_tile/custom_labeled_switch.0.dart rename to packages/flutter/examples/api/lib/material/switch_list_tile/custom_labeled_switch.0.dart diff --git a/examples/api/lib/material/switch_list_tile/custom_labeled_switch.1.dart b/packages/flutter/examples/api/lib/material/switch_list_tile/custom_labeled_switch.1.dart similarity index 100% rename from examples/api/lib/material/switch_list_tile/custom_labeled_switch.1.dart rename to packages/flutter/examples/api/lib/material/switch_list_tile/custom_labeled_switch.1.dart diff --git a/examples/api/lib/material/switch_list_tile/switch_list_tile.0.dart b/packages/flutter/examples/api/lib/material/switch_list_tile/switch_list_tile.0.dart similarity index 100% rename from examples/api/lib/material/switch_list_tile/switch_list_tile.0.dart rename to packages/flutter/examples/api/lib/material/switch_list_tile/switch_list_tile.0.dart diff --git a/examples/api/lib/material/switch_list_tile/switch_list_tile.1.dart b/packages/flutter/examples/api/lib/material/switch_list_tile/switch_list_tile.1.dart similarity index 100% rename from examples/api/lib/material/switch_list_tile/switch_list_tile.1.dart rename to packages/flutter/examples/api/lib/material/switch_list_tile/switch_list_tile.1.dart diff --git a/examples/api/lib/material/tab_controller/tab_controller.1.dart b/packages/flutter/examples/api/lib/material/tab_controller/tab_controller.1.dart similarity index 100% rename from examples/api/lib/material/tab_controller/tab_controller.1.dart rename to packages/flutter/examples/api/lib/material/tab_controller/tab_controller.1.dart diff --git a/examples/api/lib/material/tabs/tab_bar.0.dart b/packages/flutter/examples/api/lib/material/tabs/tab_bar.0.dart similarity index 100% rename from examples/api/lib/material/tabs/tab_bar.0.dart rename to packages/flutter/examples/api/lib/material/tabs/tab_bar.0.dart diff --git a/examples/api/lib/material/tabs/tab_bar.1.dart b/packages/flutter/examples/api/lib/material/tabs/tab_bar.1.dart similarity index 100% rename from examples/api/lib/material/tabs/tab_bar.1.dart rename to packages/flutter/examples/api/lib/material/tabs/tab_bar.1.dart diff --git a/examples/api/lib/material/tabs/tab_bar.2.dart b/packages/flutter/examples/api/lib/material/tabs/tab_bar.2.dart similarity index 100% rename from examples/api/lib/material/tabs/tab_bar.2.dart rename to packages/flutter/examples/api/lib/material/tabs/tab_bar.2.dart diff --git a/examples/api/lib/material/tabs/tab_bar.3.dart b/packages/flutter/examples/api/lib/material/tabs/tab_bar.3.dart similarity index 100% rename from examples/api/lib/material/tabs/tab_bar.3.dart rename to packages/flutter/examples/api/lib/material/tabs/tab_bar.3.dart diff --git a/examples/api/lib/material/tabs/tab_bar.indicator_animation.0.dart b/packages/flutter/examples/api/lib/material/tabs/tab_bar.indicator_animation.0.dart similarity index 100% rename from examples/api/lib/material/tabs/tab_bar.indicator_animation.0.dart rename to packages/flutter/examples/api/lib/material/tabs/tab_bar.indicator_animation.0.dart diff --git a/examples/api/lib/material/tabs/tab_bar.onFocusChange.dart b/packages/flutter/examples/api/lib/material/tabs/tab_bar.onFocusChange.dart similarity index 100% rename from examples/api/lib/material/tabs/tab_bar.onFocusChange.dart rename to packages/flutter/examples/api/lib/material/tabs/tab_bar.onFocusChange.dart diff --git a/examples/api/lib/material/tabs/tab_bar.onHover.dart b/packages/flutter/examples/api/lib/material/tabs/tab_bar.onHover.dart similarity index 100% rename from examples/api/lib/material/tabs/tab_bar.onHover.dart rename to packages/flutter/examples/api/lib/material/tabs/tab_bar.onHover.dart diff --git a/examples/api/lib/material/text_button/text_button.0.dart b/packages/flutter/examples/api/lib/material/text_button/text_button.0.dart similarity index 100% rename from examples/api/lib/material/text_button/text_button.0.dart rename to packages/flutter/examples/api/lib/material/text_button/text_button.0.dart diff --git a/examples/api/lib/material/text_button/text_button.1.dart b/packages/flutter/examples/api/lib/material/text_button/text_button.1.dart similarity index 100% rename from examples/api/lib/material/text_button/text_button.1.dart rename to packages/flutter/examples/api/lib/material/text_button/text_button.1.dart diff --git a/examples/api/lib/material/text_field/text_field.0.dart b/packages/flutter/examples/api/lib/material/text_field/text_field.0.dart similarity index 100% rename from examples/api/lib/material/text_field/text_field.0.dart rename to packages/flutter/examples/api/lib/material/text_field/text_field.0.dart diff --git a/examples/api/lib/material/text_field/text_field.1.dart b/packages/flutter/examples/api/lib/material/text_field/text_field.1.dart similarity index 100% rename from examples/api/lib/material/text_field/text_field.1.dart rename to packages/flutter/examples/api/lib/material/text_field/text_field.1.dart diff --git a/examples/api/lib/material/text_field/text_field.2.dart b/packages/flutter/examples/api/lib/material/text_field/text_field.2.dart similarity index 100% rename from examples/api/lib/material/text_field/text_field.2.dart rename to packages/flutter/examples/api/lib/material/text_field/text_field.2.dart diff --git a/examples/api/lib/material/text_field/text_field.3.dart b/packages/flutter/examples/api/lib/material/text_field/text_field.3.dart similarity index 100% rename from examples/api/lib/material/text_field/text_field.3.dart rename to packages/flutter/examples/api/lib/material/text_field/text_field.3.dart diff --git a/examples/api/lib/material/text_form_field/text_form_field.1.dart b/packages/flutter/examples/api/lib/material/text_form_field/text_form_field.1.dart similarity index 100% rename from examples/api/lib/material/text_form_field/text_form_field.1.dart rename to packages/flutter/examples/api/lib/material/text_form_field/text_form_field.1.dart diff --git a/examples/api/lib/material/text_form_field/text_form_field.2.dart b/packages/flutter/examples/api/lib/material/text_form_field/text_form_field.2.dart similarity index 100% rename from examples/api/lib/material/text_form_field/text_form_field.2.dart rename to packages/flutter/examples/api/lib/material/text_form_field/text_form_field.2.dart diff --git a/examples/api/lib/material/theme/theme_extension.1.dart b/packages/flutter/examples/api/lib/material/theme/theme_extension.1.dart similarity index 100% rename from examples/api/lib/material/theme/theme_extension.1.dart rename to packages/flutter/examples/api/lib/material/theme/theme_extension.1.dart diff --git a/examples/api/lib/material/theme_data/theme_data.0.dart b/packages/flutter/examples/api/lib/material/theme_data/theme_data.0.dart similarity index 100% rename from examples/api/lib/material/theme_data/theme_data.0.dart rename to packages/flutter/examples/api/lib/material/theme_data/theme_data.0.dart diff --git a/examples/api/lib/material/time_picker/show_time_picker.0.dart b/packages/flutter/examples/api/lib/material/time_picker/show_time_picker.0.dart similarity index 100% rename from examples/api/lib/material/time_picker/show_time_picker.0.dart rename to packages/flutter/examples/api/lib/material/time_picker/show_time_picker.0.dart diff --git a/examples/api/lib/material/toggle_buttons/toggle_buttons.0.dart b/packages/flutter/examples/api/lib/material/toggle_buttons/toggle_buttons.0.dart similarity index 100% rename from examples/api/lib/material/toggle_buttons/toggle_buttons.0.dart rename to packages/flutter/examples/api/lib/material/toggle_buttons/toggle_buttons.0.dart diff --git a/examples/api/lib/material/toggle_buttons/toggle_buttons.1.dart b/packages/flutter/examples/api/lib/material/toggle_buttons/toggle_buttons.1.dart similarity index 100% rename from examples/api/lib/material/toggle_buttons/toggle_buttons.1.dart rename to packages/flutter/examples/api/lib/material/toggle_buttons/toggle_buttons.1.dart diff --git a/examples/api/lib/material/tooltip/tooltip.0.dart b/packages/flutter/examples/api/lib/material/tooltip/tooltip.0.dart similarity index 100% rename from examples/api/lib/material/tooltip/tooltip.0.dart rename to packages/flutter/examples/api/lib/material/tooltip/tooltip.0.dart diff --git a/examples/api/lib/material/tooltip/tooltip.1.dart b/packages/flutter/examples/api/lib/material/tooltip/tooltip.1.dart similarity index 100% rename from examples/api/lib/material/tooltip/tooltip.1.dart rename to packages/flutter/examples/api/lib/material/tooltip/tooltip.1.dart diff --git a/examples/api/lib/material/tooltip/tooltip.2.dart b/packages/flutter/examples/api/lib/material/tooltip/tooltip.2.dart similarity index 100% rename from examples/api/lib/material/tooltip/tooltip.2.dart rename to packages/flutter/examples/api/lib/material/tooltip/tooltip.2.dart diff --git a/examples/api/lib/material/tooltip/tooltip.3.dart b/packages/flutter/examples/api/lib/material/tooltip/tooltip.3.dart similarity index 100% rename from examples/api/lib/material/tooltip/tooltip.3.dart rename to packages/flutter/examples/api/lib/material/tooltip/tooltip.3.dart diff --git a/examples/api/lib/material/widget_state_input_border/widget_state_input_border.0.dart b/packages/flutter/examples/api/lib/material/widget_state_input_border/widget_state_input_border.0.dart similarity index 100% rename from examples/api/lib/material/widget_state_input_border/widget_state_input_border.0.dart rename to packages/flutter/examples/api/lib/material/widget_state_input_border/widget_state_input_border.0.dart diff --git a/examples/api/lib/painting/axis_direction/axis_direction.0.dart b/packages/flutter/examples/api/lib/painting/axis_direction/axis_direction.0.dart similarity index 100% rename from examples/api/lib/painting/axis_direction/axis_direction.0.dart rename to packages/flutter/examples/api/lib/painting/axis_direction/axis_direction.0.dart diff --git a/examples/api/lib/painting/borders/border_side.stroke_align.0.dart b/packages/flutter/examples/api/lib/painting/borders/border_side.stroke_align.0.dart similarity index 100% rename from examples/api/lib/painting/borders/border_side.stroke_align.0.dart rename to packages/flutter/examples/api/lib/painting/borders/border_side.stroke_align.0.dart diff --git a/examples/api/lib/painting/gradient/linear_gradient.0.dart b/packages/flutter/examples/api/lib/painting/gradient/linear_gradient.0.dart similarity index 100% rename from examples/api/lib/painting/gradient/linear_gradient.0.dart rename to packages/flutter/examples/api/lib/painting/gradient/linear_gradient.0.dart diff --git a/examples/api/lib/painting/image_provider/image_provider.0.dart b/packages/flutter/examples/api/lib/painting/image_provider/image_provider.0.dart similarity index 100% rename from examples/api/lib/painting/image_provider/image_provider.0.dart rename to packages/flutter/examples/api/lib/painting/image_provider/image_provider.0.dart diff --git a/examples/api/lib/painting/linear_border/linear_border.0.dart b/packages/flutter/examples/api/lib/painting/linear_border/linear_border.0.dart similarity index 100% rename from examples/api/lib/painting/linear_border/linear_border.0.dart rename to packages/flutter/examples/api/lib/painting/linear_border/linear_border.0.dart diff --git a/examples/api/lib/painting/rounded_superellipse_border/rounded_superellipse_border.0.dart b/packages/flutter/examples/api/lib/painting/rounded_superellipse_border/rounded_superellipse_border.0.dart similarity index 100% rename from examples/api/lib/painting/rounded_superellipse_border/rounded_superellipse_border.0.dart rename to packages/flutter/examples/api/lib/painting/rounded_superellipse_border/rounded_superellipse_border.0.dart diff --git a/examples/api/lib/painting/star_border/star_border.0.dart b/packages/flutter/examples/api/lib/painting/star_border/star_border.0.dart similarity index 100% rename from examples/api/lib/painting/star_border/star_border.0.dart rename to packages/flutter/examples/api/lib/painting/star_border/star_border.0.dart diff --git a/examples/api/lib/rendering/box/parent_data.0.dart b/packages/flutter/examples/api/lib/rendering/box/parent_data.0.dart similarity index 100% rename from examples/api/lib/rendering/box/parent_data.0.dart rename to packages/flutter/examples/api/lib/rendering/box/parent_data.0.dart diff --git a/examples/api/lib/rendering/growth_direction/growth_direction.0.dart b/packages/flutter/examples/api/lib/rendering/growth_direction/growth_direction.0.dart similarity index 100% rename from examples/api/lib/rendering/growth_direction/growth_direction.0.dart rename to packages/flutter/examples/api/lib/rendering/growth_direction/growth_direction.0.dart diff --git a/examples/api/lib/rendering/scroll_direction/scroll_direction.0.dart b/packages/flutter/examples/api/lib/rendering/scroll_direction/scroll_direction.0.dart similarity index 100% rename from examples/api/lib/rendering/scroll_direction/scroll_direction.0.dart rename to packages/flutter/examples/api/lib/rendering/scroll_direction/scroll_direction.0.dart diff --git a/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart b/packages/flutter/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart similarity index 100% rename from examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart rename to packages/flutter/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart diff --git a/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart b/packages/flutter/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart similarity index 100% rename from examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart rename to packages/flutter/examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart diff --git a/examples/api/lib/sample_templates/cupertino.0.dart b/packages/flutter/examples/api/lib/sample_templates/cupertino.0.dart similarity index 100% rename from examples/api/lib/sample_templates/cupertino.0.dart rename to packages/flutter/examples/api/lib/sample_templates/cupertino.0.dart diff --git a/examples/api/lib/sample_templates/material.0.dart b/packages/flutter/examples/api/lib/sample_templates/material.0.dart similarity index 100% rename from examples/api/lib/sample_templates/material.0.dart rename to packages/flutter/examples/api/lib/sample_templates/material.0.dart diff --git a/examples/api/lib/sample_templates/widgets.0.dart b/packages/flutter/examples/api/lib/sample_templates/widgets.0.dart similarity index 100% rename from examples/api/lib/sample_templates/widgets.0.dart rename to packages/flutter/examples/api/lib/sample_templates/widgets.0.dart diff --git a/examples/api/lib/services/binding/handle_request_app_exit.0.dart b/packages/flutter/examples/api/lib/services/binding/handle_request_app_exit.0.dart similarity index 100% rename from examples/api/lib/services/binding/handle_request_app_exit.0.dart rename to packages/flutter/examples/api/lib/services/binding/handle_request_app_exit.0.dart diff --git a/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart b/packages/flutter/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart similarity index 100% rename from examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart rename to packages/flutter/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart diff --git a/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart b/packages/flutter/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart similarity index 100% rename from examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart rename to packages/flutter/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart diff --git a/examples/api/lib/services/mouse_cursor/mouse_cursor.0.dart b/packages/flutter/examples/api/lib/services/mouse_cursor/mouse_cursor.0.dart similarity index 100% rename from examples/api/lib/services/mouse_cursor/mouse_cursor.0.dart rename to packages/flutter/examples/api/lib/services/mouse_cursor/mouse_cursor.0.dart diff --git a/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0.dart b/packages/flutter/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0.dart similarity index 100% rename from examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0.dart rename to packages/flutter/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0.dart diff --git a/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart b/packages/flutter/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart similarity index 100% rename from examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart rename to packages/flutter/examples/api/lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart diff --git a/examples/api/lib/services/text_input/text_input_control.0.dart b/packages/flutter/examples/api/lib/services/text_input/text_input_control.0.dart similarity index 100% rename from examples/api/lib/services/text_input/text_input_control.0.dart rename to packages/flutter/examples/api/lib/services/text_input/text_input_control.0.dart diff --git a/examples/api/lib/ui/text/font_feature.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_alternative.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_alternative.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_alternative.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_alternative.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_alternative_fractions.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_alternative_fractions.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_alternative_fractions.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_alternative_fractions.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_case_sensitive_forms.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_case_sensitive_forms.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_case_sensitive_forms.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_case_sensitive_forms.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_character_variant.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_character_variant.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_character_variant.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_character_variant.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_contextual_alternates.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_contextual_alternates.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_contextual_alternates.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_contextual_alternates.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_denominator.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_denominator.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_denominator.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_denominator.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_fractions.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_fractions.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_fractions.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_fractions.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_historical_forms.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_historical_forms.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_historical_forms.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_historical_forms.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_historical_ligatures.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_historical_ligatures.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_historical_ligatures.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_historical_ligatures.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_lining_figures.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_lining_figures.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_lining_figures.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_lining_figures.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_locale_aware.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_locale_aware.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_locale_aware.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_locale_aware.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_notational_forms.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_notational_forms.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_notational_forms.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_notational_forms.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_numerators.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_numerators.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_numerators.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_numerators.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_oldstyle_figures.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_oldstyle_figures.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_oldstyle_figures.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_oldstyle_figures.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_ordinal_forms.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_ordinal_forms.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_ordinal_forms.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_ordinal_forms.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_proportional_figures.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_proportional_figures.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_proportional_figures.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_proportional_figures.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_scientific_inferiors.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_scientific_inferiors.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_scientific_inferiors.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_scientific_inferiors.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_slashed_zero.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_slashed_zero.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_slashed_zero.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_slashed_zero.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_stylistic_alternates.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_stylistic_alternates.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_stylistic_alternates.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_stylistic_alternates.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.1.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.1.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.1.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_stylistic_set.1.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_subscripts.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_subscripts.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_subscripts.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_subscripts.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_superscripts.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_superscripts.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_superscripts.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_superscripts.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_swash.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_swash.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_swash.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_swash.0.dart diff --git a/examples/api/lib/ui/text/font_feature.font_feature_tabular_figures.0.dart b/packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_tabular_figures.0.dart similarity index 100% rename from examples/api/lib/ui/text/font_feature.font_feature_tabular_figures.0.dart rename to packages/flutter/examples/api/lib/ui/text/font_feature.font_feature_tabular_figures.0.dart diff --git a/examples/api/lib/widgets/actions/action.action_overridable.0.dart b/packages/flutter/examples/api/lib/widgets/actions/action.action_overridable.0.dart similarity index 100% rename from examples/api/lib/widgets/actions/action.action_overridable.0.dart rename to packages/flutter/examples/api/lib/widgets/actions/action.action_overridable.0.dart diff --git a/examples/api/lib/widgets/actions/action_listener.0.dart b/packages/flutter/examples/api/lib/widgets/actions/action_listener.0.dart similarity index 100% rename from examples/api/lib/widgets/actions/action_listener.0.dart rename to packages/flutter/examples/api/lib/widgets/actions/action_listener.0.dart diff --git a/examples/api/lib/widgets/actions/actions.0.dart b/packages/flutter/examples/api/lib/widgets/actions/actions.0.dart similarity index 100% rename from examples/api/lib/widgets/actions/actions.0.dart rename to packages/flutter/examples/api/lib/widgets/actions/actions.0.dart diff --git a/examples/api/lib/widgets/actions/focusable_action_detector.0.dart b/packages/flutter/examples/api/lib/widgets/actions/focusable_action_detector.0.dart similarity index 100% rename from examples/api/lib/widgets/actions/focusable_action_detector.0.dart rename to packages/flutter/examples/api/lib/widgets/actions/focusable_action_detector.0.dart diff --git a/examples/api/lib/widgets/animated_grid/animated_grid.0.dart b/packages/flutter/examples/api/lib/widgets/animated_grid/animated_grid.0.dart similarity index 100% rename from examples/api/lib/widgets/animated_grid/animated_grid.0.dart rename to packages/flutter/examples/api/lib/widgets/animated_grid/animated_grid.0.dart diff --git a/examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart b/packages/flutter/examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart similarity index 100% rename from examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart rename to packages/flutter/examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart diff --git a/examples/api/lib/widgets/animated_list/animated_list.0.dart b/packages/flutter/examples/api/lib/widgets/animated_list/animated_list.0.dart similarity index 100% rename from examples/api/lib/widgets/animated_list/animated_list.0.dart rename to packages/flutter/examples/api/lib/widgets/animated_list/animated_list.0.dart diff --git a/examples/api/lib/widgets/animated_list/animated_list_separated.0.dart b/packages/flutter/examples/api/lib/widgets/animated_list/animated_list_separated.0.dart similarity index 100% rename from examples/api/lib/widgets/animated_list/animated_list_separated.0.dart rename to packages/flutter/examples/api/lib/widgets/animated_list/animated_list_separated.0.dart diff --git a/examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart b/packages/flutter/examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart similarity index 100% rename from examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart rename to packages/flutter/examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart diff --git a/examples/api/lib/widgets/animated_size/animated_size.0.dart b/packages/flutter/examples/api/lib/widgets/animated_size/animated_size.0.dart similarity index 100% rename from examples/api/lib/widgets/animated_size/animated_size.0.dart rename to packages/flutter/examples/api/lib/widgets/animated_size/animated_size.0.dart diff --git a/examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart b/packages/flutter/examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart similarity index 100% rename from examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart rename to packages/flutter/examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart diff --git a/examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart b/packages/flutter/examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart similarity index 100% rename from examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart rename to packages/flutter/examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart diff --git a/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart b/packages/flutter/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart similarity index 100% rename from examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart rename to packages/flutter/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart diff --git a/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart b/packages/flutter/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart similarity index 100% rename from examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart rename to packages/flutter/examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart diff --git a/examples/api/lib/widgets/async/future_builder.0.dart b/packages/flutter/examples/api/lib/widgets/async/future_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/async/future_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/async/future_builder.0.dart diff --git a/examples/api/lib/widgets/async/stream_builder.0.dart b/packages/flutter/examples/api/lib/widgets/async/stream_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/async/stream_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/async/stream_builder.0.dart diff --git a/examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart b/packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart similarity index 100% rename from examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart rename to packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart diff --git a/examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart b/packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart similarity index 100% rename from examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart rename to packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart diff --git a/examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart b/packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart similarity index 100% rename from examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart rename to packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart diff --git a/examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart b/packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart similarity index 100% rename from examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart rename to packages/flutter/examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart diff --git a/examples/api/lib/widgets/autofill/autofill_group.0.dart b/packages/flutter/examples/api/lib/widgets/autofill/autofill_group.0.dart similarity index 100% rename from examples/api/lib/widgets/autofill/autofill_group.0.dart rename to packages/flutter/examples/api/lib/widgets/autofill/autofill_group.0.dart diff --git a/examples/api/lib/widgets/basic/absorb_pointer.0.dart b/packages/flutter/examples/api/lib/widgets/basic/absorb_pointer.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/absorb_pointer.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/absorb_pointer.0.dart diff --git a/examples/api/lib/widgets/basic/aspect_ratio.0.dart b/packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/aspect_ratio.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.0.dart diff --git a/examples/api/lib/widgets/basic/aspect_ratio.1.dart b/packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.1.dart similarity index 100% rename from examples/api/lib/widgets/basic/aspect_ratio.1.dart rename to packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.1.dart diff --git a/examples/api/lib/widgets/basic/aspect_ratio.2.dart b/packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.2.dart similarity index 100% rename from examples/api/lib/widgets/basic/aspect_ratio.2.dart rename to packages/flutter/examples/api/lib/widgets/basic/aspect_ratio.2.dart diff --git a/examples/api/lib/widgets/basic/clip_rrect.0.dart b/packages/flutter/examples/api/lib/widgets/basic/clip_rrect.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/clip_rrect.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/clip_rrect.0.dart diff --git a/examples/api/lib/widgets/basic/clip_rrect.1.dart b/packages/flutter/examples/api/lib/widgets/basic/clip_rrect.1.dart similarity index 100% rename from examples/api/lib/widgets/basic/clip_rrect.1.dart rename to packages/flutter/examples/api/lib/widgets/basic/clip_rrect.1.dart diff --git a/examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart b/packages/flutter/examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart diff --git a/examples/api/lib/widgets/basic/expanded.0.dart b/packages/flutter/examples/api/lib/widgets/basic/expanded.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/expanded.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/expanded.0.dart diff --git a/examples/api/lib/widgets/basic/expanded.1.dart b/packages/flutter/examples/api/lib/widgets/basic/expanded.1.dart similarity index 100% rename from examples/api/lib/widgets/basic/expanded.1.dart rename to packages/flutter/examples/api/lib/widgets/basic/expanded.1.dart diff --git a/examples/api/lib/widgets/basic/fitted_box.0.dart b/packages/flutter/examples/api/lib/widgets/basic/fitted_box.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/fitted_box.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/fitted_box.0.dart diff --git a/examples/api/lib/widgets/basic/flow.0.dart b/packages/flutter/examples/api/lib/widgets/basic/flow.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/flow.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/flow.0.dart diff --git a/examples/api/lib/widgets/basic/fractionally_sized_box.0.dart b/packages/flutter/examples/api/lib/widgets/basic/fractionally_sized_box.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/fractionally_sized_box.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/fractionally_sized_box.0.dart diff --git a/examples/api/lib/widgets/basic/ignore_pointer.0.dart b/packages/flutter/examples/api/lib/widgets/basic/ignore_pointer.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/ignore_pointer.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/ignore_pointer.0.dart diff --git a/examples/api/lib/widgets/basic/indexed_stack.0.dart b/packages/flutter/examples/api/lib/widgets/basic/indexed_stack.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/indexed_stack.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/indexed_stack.0.dart diff --git a/examples/api/lib/widgets/basic/listener.0.dart b/packages/flutter/examples/api/lib/widgets/basic/listener.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/listener.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/listener.0.dart diff --git a/examples/api/lib/widgets/basic/mouse_region.0.dart b/packages/flutter/examples/api/lib/widgets/basic/mouse_region.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/mouse_region.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/mouse_region.0.dart diff --git a/examples/api/lib/widgets/basic/mouse_region.on_exit.0.dart b/packages/flutter/examples/api/lib/widgets/basic/mouse_region.on_exit.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/mouse_region.on_exit.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/mouse_region.on_exit.0.dart diff --git a/examples/api/lib/widgets/basic/mouse_region.on_exit.1.dart b/packages/flutter/examples/api/lib/widgets/basic/mouse_region.on_exit.1.dart similarity index 100% rename from examples/api/lib/widgets/basic/mouse_region.on_exit.1.dart rename to packages/flutter/examples/api/lib/widgets/basic/mouse_region.on_exit.1.dart diff --git a/examples/api/lib/widgets/basic/offstage.0.dart b/packages/flutter/examples/api/lib/widgets/basic/offstage.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/offstage.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/offstage.0.dart diff --git a/examples/api/lib/widgets/basic/overflowbox.0.dart b/packages/flutter/examples/api/lib/widgets/basic/overflowbox.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/overflowbox.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/overflowbox.0.dart diff --git a/examples/api/lib/widgets/basic/physical_shape.0.dart b/packages/flutter/examples/api/lib/widgets/basic/physical_shape.0.dart similarity index 100% rename from examples/api/lib/widgets/basic/physical_shape.0.dart rename to packages/flutter/examples/api/lib/widgets/basic/physical_shape.0.dart diff --git a/examples/api/lib/widgets/binding/widget_binding_observer.0.dart b/packages/flutter/examples/api/lib/widgets/binding/widget_binding_observer.0.dart similarity index 100% rename from examples/api/lib/widgets/binding/widget_binding_observer.0.dart rename to packages/flutter/examples/api/lib/widgets/binding/widget_binding_observer.0.dart diff --git a/examples/api/lib/widgets/color_filter/color_filtered.0.dart b/packages/flutter/examples/api/lib/widgets/color_filter/color_filtered.0.dart similarity index 100% rename from examples/api/lib/widgets/color_filter/color_filtered.0.dart rename to packages/flutter/examples/api/lib/widgets/color_filter/color_filtered.0.dart diff --git a/examples/api/lib/widgets/context_menu/context_menu_controller.0.dart b/packages/flutter/examples/api/lib/widgets/context_menu/context_menu_controller.0.dart similarity index 100% rename from examples/api/lib/widgets/context_menu/context_menu_controller.0.dart rename to packages/flutter/examples/api/lib/widgets/context_menu/context_menu_controller.0.dart diff --git a/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart b/packages/flutter/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart diff --git a/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart b/packages/flutter/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart similarity index 100% rename from examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart rename to packages/flutter/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart diff --git a/examples/api/lib/widgets/dismissible/dismissible.0.dart b/packages/flutter/examples/api/lib/widgets/dismissible/dismissible.0.dart similarity index 100% rename from examples/api/lib/widgets/dismissible/dismissible.0.dart rename to packages/flutter/examples/api/lib/widgets/dismissible/dismissible.0.dart diff --git a/examples/api/lib/widgets/drag_target/draggable.0.dart b/packages/flutter/examples/api/lib/widgets/drag_target/draggable.0.dart similarity index 100% rename from examples/api/lib/widgets/drag_target/draggable.0.dart rename to packages/flutter/examples/api/lib/widgets/drag_target/draggable.0.dart diff --git a/examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart b/packages/flutter/examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart similarity index 100% rename from examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart rename to packages/flutter/examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart diff --git a/examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart b/packages/flutter/examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart similarity index 100% rename from examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart rename to packages/flutter/examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart diff --git a/examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart b/packages/flutter/examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart similarity index 100% rename from examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart rename to packages/flutter/examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart diff --git a/examples/api/lib/widgets/editable_text/text_editing_controller.0.dart b/packages/flutter/examples/api/lib/widgets/editable_text/text_editing_controller.0.dart similarity index 100% rename from examples/api/lib/widgets/editable_text/text_editing_controller.0.dart rename to packages/flutter/examples/api/lib/widgets/editable_text/text_editing_controller.0.dart diff --git a/examples/api/lib/widgets/editable_text/text_editing_controller.1.dart b/packages/flutter/examples/api/lib/widgets/editable_text/text_editing_controller.1.dart similarity index 100% rename from examples/api/lib/widgets/editable_text/text_editing_controller.1.dart rename to packages/flutter/examples/api/lib/widgets/editable_text/text_editing_controller.1.dart diff --git a/examples/api/lib/widgets/expansible/expansible.0.dart b/packages/flutter/examples/api/lib/widgets/expansible/expansible.0.dart similarity index 100% rename from examples/api/lib/widgets/expansible/expansible.0.dart rename to packages/flutter/examples/api/lib/widgets/expansible/expansible.0.dart diff --git a/examples/api/lib/widgets/focus_manager/focus_node.0.dart b/packages/flutter/examples/api/lib/widgets/focus_manager/focus_node.0.dart similarity index 100% rename from examples/api/lib/widgets/focus_manager/focus_node.0.dart rename to packages/flutter/examples/api/lib/widgets/focus_manager/focus_node.0.dart diff --git a/examples/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart b/packages/flutter/examples/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart similarity index 100% rename from examples/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart rename to packages/flutter/examples/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart diff --git a/examples/api/lib/widgets/focus_scope/focus.0.dart b/packages/flutter/examples/api/lib/widgets/focus_scope/focus.0.dart similarity index 100% rename from examples/api/lib/widgets/focus_scope/focus.0.dart rename to packages/flutter/examples/api/lib/widgets/focus_scope/focus.0.dart diff --git a/examples/api/lib/widgets/focus_scope/focus.1.dart b/packages/flutter/examples/api/lib/widgets/focus_scope/focus.1.dart similarity index 100% rename from examples/api/lib/widgets/focus_scope/focus.1.dart rename to packages/flutter/examples/api/lib/widgets/focus_scope/focus.1.dart diff --git a/examples/api/lib/widgets/focus_scope/focus.2.dart b/packages/flutter/examples/api/lib/widgets/focus_scope/focus.2.dart similarity index 100% rename from examples/api/lib/widgets/focus_scope/focus.2.dart rename to packages/flutter/examples/api/lib/widgets/focus_scope/focus.2.dart diff --git a/examples/api/lib/widgets/focus_scope/focus_scope.0.dart b/packages/flutter/examples/api/lib/widgets/focus_scope/focus_scope.0.dart similarity index 100% rename from examples/api/lib/widgets/focus_scope/focus_scope.0.dart rename to packages/flutter/examples/api/lib/widgets/focus_scope/focus_scope.0.dart diff --git a/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart b/packages/flutter/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart similarity index 100% rename from examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart rename to packages/flutter/examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart diff --git a/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart b/packages/flutter/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart similarity index 100% rename from examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart rename to packages/flutter/examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart diff --git a/examples/api/lib/widgets/form/form.0.dart b/packages/flutter/examples/api/lib/widgets/form/form.0.dart similarity index 100% rename from examples/api/lib/widgets/form/form.0.dart rename to packages/flutter/examples/api/lib/widgets/form/form.0.dart diff --git a/examples/api/lib/widgets/form/form.1.dart b/packages/flutter/examples/api/lib/widgets/form/form.1.dart similarity index 100% rename from examples/api/lib/widgets/form/form.1.dart rename to packages/flutter/examples/api/lib/widgets/form/form.1.dart diff --git a/examples/api/lib/widgets/framework/build_owner.0.dart b/packages/flutter/examples/api/lib/widgets/framework/build_owner.0.dart similarity index 100% rename from examples/api/lib/widgets/framework/build_owner.0.dart rename to packages/flutter/examples/api/lib/widgets/framework/build_owner.0.dart diff --git a/examples/api/lib/widgets/framework/error_widget.0.dart b/packages/flutter/examples/api/lib/widgets/framework/error_widget.0.dart similarity index 100% rename from examples/api/lib/widgets/framework/error_widget.0.dart rename to packages/flutter/examples/api/lib/widgets/framework/error_widget.0.dart diff --git a/examples/api/lib/widgets/gesture_detector/gesture_detector.0.dart b/packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.0.dart similarity index 100% rename from examples/api/lib/widgets/gesture_detector/gesture_detector.0.dart rename to packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.0.dart diff --git a/examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart b/packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart similarity index 100% rename from examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart rename to packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart diff --git a/examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart b/packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart similarity index 100% rename from examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart rename to packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart diff --git a/examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart b/packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart similarity index 100% rename from examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart rename to packages/flutter/examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart diff --git a/examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart b/packages/flutter/examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart similarity index 100% rename from examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart rename to packages/flutter/examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart diff --git a/examples/api/lib/widgets/heroes/hero.0.dart b/packages/flutter/examples/api/lib/widgets/heroes/hero.0.dart similarity index 100% rename from examples/api/lib/widgets/heroes/hero.0.dart rename to packages/flutter/examples/api/lib/widgets/heroes/hero.0.dart diff --git a/examples/api/lib/widgets/heroes/hero.1.dart b/packages/flutter/examples/api/lib/widgets/heroes/hero.1.dart similarity index 100% rename from examples/api/lib/widgets/heroes/hero.1.dart rename to packages/flutter/examples/api/lib/widgets/heroes/hero.1.dart diff --git a/examples/api/lib/widgets/image/image.error_builder.0.dart b/packages/flutter/examples/api/lib/widgets/image/image.error_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/image/image.error_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/image/image.error_builder.0.dart diff --git a/examples/api/lib/widgets/image/image.frame_builder.0.dart b/packages/flutter/examples/api/lib/widgets/image/image.frame_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/image/image.frame_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/image/image.frame_builder.0.dart diff --git a/examples/api/lib/widgets/image/image.loading_builder.0.dart b/packages/flutter/examples/api/lib/widgets/image/image.loading_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/image/image.loading_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/image/image.loading_builder.0.dart diff --git a/examples/api/lib/widgets/implicit_animations/animated_align.0.dart b/packages/flutter/examples/api/lib/widgets/implicit_animations/animated_align.0.dart similarity index 100% rename from examples/api/lib/widgets/implicit_animations/animated_align.0.dart rename to packages/flutter/examples/api/lib/widgets/implicit_animations/animated_align.0.dart diff --git a/examples/api/lib/widgets/implicit_animations/animated_container.0.dart b/packages/flutter/examples/api/lib/widgets/implicit_animations/animated_container.0.dart similarity index 100% rename from examples/api/lib/widgets/implicit_animations/animated_container.0.dart rename to packages/flutter/examples/api/lib/widgets/implicit_animations/animated_container.0.dart diff --git a/examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart b/packages/flutter/examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart similarity index 100% rename from examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart rename to packages/flutter/examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart diff --git a/examples/api/lib/widgets/implicit_animations/animated_padding.0.dart b/packages/flutter/examples/api/lib/widgets/implicit_animations/animated_padding.0.dart similarity index 100% rename from examples/api/lib/widgets/implicit_animations/animated_padding.0.dart rename to packages/flutter/examples/api/lib/widgets/implicit_animations/animated_padding.0.dart diff --git a/examples/api/lib/widgets/implicit_animations/animated_positioned.0.dart b/packages/flutter/examples/api/lib/widgets/implicit_animations/animated_positioned.0.dart similarity index 100% rename from examples/api/lib/widgets/implicit_animations/animated_positioned.0.dart rename to packages/flutter/examples/api/lib/widgets/implicit_animations/animated_positioned.0.dart diff --git a/examples/api/lib/widgets/implicit_animations/animated_slide.0.dart b/packages/flutter/examples/api/lib/widgets/implicit_animations/animated_slide.0.dart similarity index 100% rename from examples/api/lib/widgets/implicit_animations/animated_slide.0.dart rename to packages/flutter/examples/api/lib/widgets/implicit_animations/animated_slide.0.dart diff --git a/examples/api/lib/widgets/implicit_animations/sliver_animated_opacity.0.dart b/packages/flutter/examples/api/lib/widgets/implicit_animations/sliver_animated_opacity.0.dart similarity index 100% rename from examples/api/lib/widgets/implicit_animations/sliver_animated_opacity.0.dart rename to packages/flutter/examples/api/lib/widgets/implicit_animations/sliver_animated_opacity.0.dart diff --git a/examples/api/lib/widgets/inherited_model/inherited_model.0.dart b/packages/flutter/examples/api/lib/widgets/inherited_model/inherited_model.0.dart similarity index 100% rename from examples/api/lib/widgets/inherited_model/inherited_model.0.dart rename to packages/flutter/examples/api/lib/widgets/inherited_model/inherited_model.0.dart diff --git a/examples/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart b/packages/flutter/examples/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart similarity index 100% rename from examples/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart rename to packages/flutter/examples/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart diff --git a/examples/api/lib/widgets/inherited_theme/inherited_theme.0.dart b/packages/flutter/examples/api/lib/widgets/inherited_theme/inherited_theme.0.dart similarity index 100% rename from examples/api/lib/widgets/inherited_theme/inherited_theme.0.dart rename to packages/flutter/examples/api/lib/widgets/inherited_theme/inherited_theme.0.dart diff --git a/examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart b/packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart similarity index 100% rename from examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart rename to packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart diff --git a/examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart b/packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart similarity index 100% rename from examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart rename to packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart diff --git a/examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart b/packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart similarity index 100% rename from examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart rename to packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart diff --git a/examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart b/packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart similarity index 100% rename from examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart rename to packages/flutter/examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart diff --git a/examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart b/packages/flutter/examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart similarity index 100% rename from examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart rename to packages/flutter/examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart diff --git a/examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart b/packages/flutter/examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart similarity index 100% rename from examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart rename to packages/flutter/examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart diff --git a/examples/api/lib/widgets/keep_alive/keep_alive.0.dart b/packages/flutter/examples/api/lib/widgets/keep_alive/keep_alive.0.dart similarity index 100% rename from examples/api/lib/widgets/keep_alive/keep_alive.0.dart rename to packages/flutter/examples/api/lib/widgets/keep_alive/keep_alive.0.dart diff --git a/examples/api/lib/widgets/layout_builder/layout_builder.0.dart b/packages/flutter/examples/api/lib/widgets/layout_builder/layout_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/layout_builder/layout_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/layout_builder/layout_builder.0.dart diff --git a/examples/api/lib/widgets/magnifier/magnifier.0.dart b/packages/flutter/examples/api/lib/widgets/magnifier/magnifier.0.dart similarity index 100% rename from examples/api/lib/widgets/magnifier/magnifier.0.dart rename to packages/flutter/examples/api/lib/widgets/magnifier/magnifier.0.dart diff --git a/examples/api/lib/widgets/media_query/media_query_data.system_gesture_insets.0.dart b/packages/flutter/examples/api/lib/widgets/media_query/media_query_data.system_gesture_insets.0.dart similarity index 100% rename from examples/api/lib/widgets/media_query/media_query_data.system_gesture_insets.0.dart rename to packages/flutter/examples/api/lib/widgets/media_query/media_query_data.system_gesture_insets.0.dart diff --git a/examples/api/lib/widgets/navigator/navigator.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/navigator.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/navigator.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/navigator.0.dart diff --git a/examples/api/lib/widgets/navigator/navigator.restorable_push.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/navigator.restorable_push.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push.0.dart diff --git a/examples/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart diff --git a/examples/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart diff --git a/examples/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart diff --git a/examples/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart diff --git a/examples/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart diff --git a/examples/api/lib/widgets/navigator/restorable_route_future.0.dart b/packages/flutter/examples/api/lib/widgets/navigator/restorable_route_future.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator/restorable_route_future.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator/restorable_route_future.0.dart diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart b/packages/flutter/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart similarity index 100% rename from examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart rename to packages/flutter/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart b/packages/flutter/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart similarity index 100% rename from examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart rename to packages/flutter/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart diff --git a/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart b/packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart similarity index 100% rename from examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart rename to packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart diff --git a/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart b/packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart similarity index 100% rename from examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart rename to packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart diff --git a/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart b/packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart similarity index 100% rename from examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart rename to packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart diff --git a/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart b/packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart similarity index 100% rename from examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart rename to packages/flutter/examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart diff --git a/examples/api/lib/widgets/notification_listener/notification.0.dart b/packages/flutter/examples/api/lib/widgets/notification_listener/notification.0.dart similarity index 100% rename from examples/api/lib/widgets/notification_listener/notification.0.dart rename to packages/flutter/examples/api/lib/widgets/notification_listener/notification.0.dart diff --git a/examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart b/packages/flutter/examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart similarity index 100% rename from examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart rename to packages/flutter/examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart diff --git a/examples/api/lib/widgets/overlay/overlay.0.dart b/packages/flutter/examples/api/lib/widgets/overlay/overlay.0.dart similarity index 100% rename from examples/api/lib/widgets/overlay/overlay.0.dart rename to packages/flutter/examples/api/lib/widgets/overlay/overlay.0.dart diff --git a/examples/api/lib/widgets/overlay/overlay_portal.0.dart b/packages/flutter/examples/api/lib/widgets/overlay/overlay_portal.0.dart similarity index 100% rename from examples/api/lib/widgets/overlay/overlay_portal.0.dart rename to packages/flutter/examples/api/lib/widgets/overlay/overlay_portal.0.dart diff --git a/examples/api/lib/widgets/overlay/overlay_portal.1.dart b/packages/flutter/examples/api/lib/widgets/overlay/overlay_portal.1.dart similarity index 100% rename from examples/api/lib/widgets/overlay/overlay_portal.1.dart rename to packages/flutter/examples/api/lib/widgets/overlay/overlay_portal.1.dart diff --git a/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.0.dart b/packages/flutter/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.0.dart similarity index 100% rename from examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.0.dart rename to packages/flutter/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.0.dart diff --git a/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.1.dart b/packages/flutter/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.1.dart similarity index 100% rename from examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.1.dart rename to packages/flutter/examples/api/lib/widgets/overscroll_indicator/glowing_overscroll_indicator.1.dart diff --git a/examples/api/lib/widgets/page/page_can_pop.0.dart b/packages/flutter/examples/api/lib/widgets/page/page_can_pop.0.dart similarity index 100% rename from examples/api/lib/widgets/page/page_can_pop.0.dart rename to packages/flutter/examples/api/lib/widgets/page/page_can_pop.0.dart diff --git a/examples/api/lib/widgets/page_storage/page_storage.0.dart b/packages/flutter/examples/api/lib/widgets/page_storage/page_storage.0.dart similarity index 100% rename from examples/api/lib/widgets/page_storage/page_storage.0.dart rename to packages/flutter/examples/api/lib/widgets/page_storage/page_storage.0.dart diff --git a/examples/api/lib/widgets/page_transitions_builder/page_transitions_builder.0.dart b/packages/flutter/examples/api/lib/widgets/page_transitions_builder/page_transitions_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/page_transitions_builder/page_transitions_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/page_transitions_builder/page_transitions_builder.0.dart diff --git a/examples/api/lib/widgets/page_view/page_view.0.dart b/packages/flutter/examples/api/lib/widgets/page_view/page_view.0.dart similarity index 100% rename from examples/api/lib/widgets/page_view/page_view.0.dart rename to packages/flutter/examples/api/lib/widgets/page_view/page_view.0.dart diff --git a/examples/api/lib/widgets/page_view/page_view.1.dart b/packages/flutter/examples/api/lib/widgets/page_view/page_view.1.dart similarity index 100% rename from examples/api/lib/widgets/page_view/page_view.1.dart rename to packages/flutter/examples/api/lib/widgets/page_view/page_view.1.dart diff --git a/examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart b/packages/flutter/examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart similarity index 100% rename from examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart rename to packages/flutter/examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart diff --git a/examples/api/lib/widgets/pop_scope/pop_scope.0.dart b/packages/flutter/examples/api/lib/widgets/pop_scope/pop_scope.0.dart similarity index 100% rename from examples/api/lib/widgets/pop_scope/pop_scope.0.dart rename to packages/flutter/examples/api/lib/widgets/pop_scope/pop_scope.0.dart diff --git a/examples/api/lib/widgets/pop_scope/pop_scope.1.dart b/packages/flutter/examples/api/lib/widgets/pop_scope/pop_scope.1.dart similarity index 100% rename from examples/api/lib/widgets/pop_scope/pop_scope.1.dart rename to packages/flutter/examples/api/lib/widgets/pop_scope/pop_scope.1.dart diff --git a/examples/api/lib/widgets/preferred_size/preferred_size.0.dart b/packages/flutter/examples/api/lib/widgets/preferred_size/preferred_size.0.dart similarity index 100% rename from examples/api/lib/widgets/preferred_size/preferred_size.0.dart rename to packages/flutter/examples/api/lib/widgets/preferred_size/preferred_size.0.dart diff --git a/examples/api/lib/widgets/radio_group/radio_group.0.dart b/packages/flutter/examples/api/lib/widgets/radio_group/radio_group.0.dart similarity index 100% rename from examples/api/lib/widgets/radio_group/radio_group.0.dart rename to packages/flutter/examples/api/lib/widgets/radio_group/radio_group.0.dart diff --git a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart b/packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart similarity index 100% rename from examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart rename to packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart diff --git a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart b/packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart similarity index 100% rename from examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart rename to packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart diff --git a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart b/packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart similarity index 100% rename from examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart rename to packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart diff --git a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart b/packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart similarity index 100% rename from examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart rename to packages/flutter/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart diff --git a/examples/api/lib/widgets/raw_tooltip/raw_tooltip.0.dart b/packages/flutter/examples/api/lib/widgets/raw_tooltip/raw_tooltip.0.dart similarity index 100% rename from examples/api/lib/widgets/raw_tooltip/raw_tooltip.0.dart rename to packages/flutter/examples/api/lib/widgets/raw_tooltip/raw_tooltip.0.dart diff --git a/examples/api/lib/widgets/repeating_animation_builder/repeating_animation_builder.0.dart b/packages/flutter/examples/api/lib/widgets/repeating_animation_builder/repeating_animation_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/repeating_animation_builder/repeating_animation_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/repeating_animation_builder/repeating_animation_builder.0.dart diff --git a/examples/api/lib/widgets/restoration/restoration_mixin.0.dart b/packages/flutter/examples/api/lib/widgets/restoration/restoration_mixin.0.dart similarity index 100% rename from examples/api/lib/widgets/restoration/restoration_mixin.0.dart rename to packages/flutter/examples/api/lib/widgets/restoration/restoration_mixin.0.dart diff --git a/examples/api/lib/widgets/restoration_properties/restorable_value.0.dart b/packages/flutter/examples/api/lib/widgets/restoration_properties/restorable_value.0.dart similarity index 100% rename from examples/api/lib/widgets/restoration_properties/restorable_value.0.dart rename to packages/flutter/examples/api/lib/widgets/restoration_properties/restorable_value.0.dart diff --git a/examples/api/lib/widgets/routes/flexible_route_transitions.0.dart b/packages/flutter/examples/api/lib/widgets/routes/flexible_route_transitions.0.dart similarity index 100% rename from examples/api/lib/widgets/routes/flexible_route_transitions.0.dart rename to packages/flutter/examples/api/lib/widgets/routes/flexible_route_transitions.0.dart diff --git a/examples/api/lib/widgets/routes/flexible_route_transitions.1.dart b/packages/flutter/examples/api/lib/widgets/routes/flexible_route_transitions.1.dart similarity index 100% rename from examples/api/lib/widgets/routes/flexible_route_transitions.1.dart rename to packages/flutter/examples/api/lib/widgets/routes/flexible_route_transitions.1.dart diff --git a/examples/api/lib/widgets/routes/local_history_entry.0.dart b/packages/flutter/examples/api/lib/widgets/routes/local_history_entry.0.dart similarity index 100% rename from examples/api/lib/widgets/routes/local_history_entry.0.dart rename to packages/flutter/examples/api/lib/widgets/routes/local_history_entry.0.dart diff --git a/examples/api/lib/widgets/routes/popup_route.0.dart b/packages/flutter/examples/api/lib/widgets/routes/popup_route.0.dart similarity index 100% rename from examples/api/lib/widgets/routes/popup_route.0.dart rename to packages/flutter/examples/api/lib/widgets/routes/popup_route.0.dart diff --git a/examples/api/lib/widgets/routes/route_observer.0.dart b/packages/flutter/examples/api/lib/widgets/routes/route_observer.0.dart similarity index 100% rename from examples/api/lib/widgets/routes/route_observer.0.dart rename to packages/flutter/examples/api/lib/widgets/routes/route_observer.0.dart diff --git a/examples/api/lib/widgets/routes/show_general_dialog.0.dart b/packages/flutter/examples/api/lib/widgets/routes/show_general_dialog.0.dart similarity index 100% rename from examples/api/lib/widgets/routes/show_general_dialog.0.dart rename to packages/flutter/examples/api/lib/widgets/routes/show_general_dialog.0.dart diff --git a/examples/api/lib/widgets/safe_area/safe_area.0.dart b/packages/flutter/examples/api/lib/widgets/safe_area/safe_area.0.dart similarity index 100% rename from examples/api/lib/widgets/safe_area/safe_area.0.dart rename to packages/flutter/examples/api/lib/widgets/safe_area/safe_area.0.dart diff --git a/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart diff --git a/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart b/packages/flutter/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart similarity index 100% rename from examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart rename to packages/flutter/examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart diff --git a/examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart diff --git a/examples/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart diff --git a/examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart diff --git a/examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart diff --git a/examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart diff --git a/examples/api/lib/widgets/scroll_view/custom_scroll_view.1.dart b/packages/flutter/examples/api/lib/widgets/scroll_view/custom_scroll_view.1.dart similarity index 100% rename from examples/api/lib/widgets/scroll_view/custom_scroll_view.1.dart rename to packages/flutter/examples/api/lib/widgets/scroll_view/custom_scroll_view.1.dart diff --git a/examples/api/lib/widgets/scroll_view/grid_view.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_view/grid_view.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_view/grid_view.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_view/grid_view.0.dart diff --git a/examples/api/lib/widgets/scroll_view/list_view.0.dart b/packages/flutter/examples/api/lib/widgets/scroll_view/list_view.0.dart similarity index 100% rename from examples/api/lib/widgets/scroll_view/list_view.0.dart rename to packages/flutter/examples/api/lib/widgets/scroll_view/list_view.0.dart diff --git a/examples/api/lib/widgets/scroll_view/list_view.1.dart b/packages/flutter/examples/api/lib/widgets/scroll_view/list_view.1.dart similarity index 100% rename from examples/api/lib/widgets/scroll_view/list_view.1.dart rename to packages/flutter/examples/api/lib/widgets/scroll_view/list_view.1.dart diff --git a/examples/api/lib/widgets/scrollbar/raw_scrollbar.0.dart b/packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.0.dart similarity index 100% rename from examples/api/lib/widgets/scrollbar/raw_scrollbar.0.dart rename to packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.0.dart diff --git a/examples/api/lib/widgets/scrollbar/raw_scrollbar.1.dart b/packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.1.dart similarity index 100% rename from examples/api/lib/widgets/scrollbar/raw_scrollbar.1.dart rename to packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.1.dart diff --git a/examples/api/lib/widgets/scrollbar/raw_scrollbar.2.dart b/packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.2.dart similarity index 100% rename from examples/api/lib/widgets/scrollbar/raw_scrollbar.2.dart rename to packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.2.dart diff --git a/examples/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart b/packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart similarity index 100% rename from examples/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart rename to packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart diff --git a/examples/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart b/packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart similarity index 100% rename from examples/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart rename to packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart diff --git a/examples/api/lib/widgets/selectable_region/selectable_region.0.dart b/packages/flutter/examples/api/lib/widgets/selectable_region/selectable_region.0.dart similarity index 100% rename from examples/api/lib/widgets/selectable_region/selectable_region.0.dart rename to packages/flutter/examples/api/lib/widgets/selectable_region/selectable_region.0.dart diff --git a/examples/api/lib/widgets/selection_container/selection_container.0.dart b/packages/flutter/examples/api/lib/widgets/selection_container/selection_container.0.dart similarity index 100% rename from examples/api/lib/widgets/selection_container/selection_container.0.dart rename to packages/flutter/examples/api/lib/widgets/selection_container/selection_container.0.dart diff --git a/examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart b/packages/flutter/examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart similarity index 100% rename from examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart rename to packages/flutter/examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart diff --git a/examples/api/lib/widgets/sensitive_content/sensitive_content.0.dart b/packages/flutter/examples/api/lib/widgets/sensitive_content/sensitive_content.0.dart similarity index 100% rename from examples/api/lib/widgets/sensitive_content/sensitive_content.0.dart rename to packages/flutter/examples/api/lib/widgets/sensitive_content/sensitive_content.0.dart diff --git a/examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart b/packages/flutter/examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart similarity index 100% rename from examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart rename to packages/flutter/examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart diff --git a/examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart b/packages/flutter/examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart similarity index 100% rename from examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart rename to packages/flutter/examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart diff --git a/examples/api/lib/widgets/shortcuts/callback_shortcuts.0.dart b/packages/flutter/examples/api/lib/widgets/shortcuts/callback_shortcuts.0.dart similarity index 100% rename from examples/api/lib/widgets/shortcuts/callback_shortcuts.0.dart rename to packages/flutter/examples/api/lib/widgets/shortcuts/callback_shortcuts.0.dart diff --git a/examples/api/lib/widgets/shortcuts/character_activator.0.dart b/packages/flutter/examples/api/lib/widgets/shortcuts/character_activator.0.dart similarity index 100% rename from examples/api/lib/widgets/shortcuts/character_activator.0.dart rename to packages/flutter/examples/api/lib/widgets/shortcuts/character_activator.0.dart diff --git a/examples/api/lib/widgets/shortcuts/logical_key_set.0.dart b/packages/flutter/examples/api/lib/widgets/shortcuts/logical_key_set.0.dart similarity index 100% rename from examples/api/lib/widgets/shortcuts/logical_key_set.0.dart rename to packages/flutter/examples/api/lib/widgets/shortcuts/logical_key_set.0.dart diff --git a/examples/api/lib/widgets/shortcuts/shortcuts.0.dart b/packages/flutter/examples/api/lib/widgets/shortcuts/shortcuts.0.dart similarity index 100% rename from examples/api/lib/widgets/shortcuts/shortcuts.0.dart rename to packages/flutter/examples/api/lib/widgets/shortcuts/shortcuts.0.dart diff --git a/examples/api/lib/widgets/shortcuts/shortcuts.1.dart b/packages/flutter/examples/api/lib/widgets/shortcuts/shortcuts.1.dart similarity index 100% rename from examples/api/lib/widgets/shortcuts/shortcuts.1.dart rename to packages/flutter/examples/api/lib/widgets/shortcuts/shortcuts.1.dart diff --git a/examples/api/lib/widgets/shortcuts/single_activator.0.dart b/packages/flutter/examples/api/lib/widgets/shortcuts/single_activator.0.dart similarity index 100% rename from examples/api/lib/widgets/shortcuts/single_activator.0.dart rename to packages/flutter/examples/api/lib/widgets/shortcuts/single_activator.0.dart diff --git a/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart b/packages/flutter/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart similarity index 100% rename from examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart rename to packages/flutter/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart diff --git a/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart b/packages/flutter/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart similarity index 100% rename from examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart rename to packages/flutter/examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart diff --git a/examples/api/lib/widgets/sliver/decorated_sliver.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/decorated_sliver.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/decorated_sliver.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/decorated_sliver.0.dart diff --git a/examples/api/lib/widgets/sliver/decorated_sliver.1.dart b/packages/flutter/examples/api/lib/widgets/sliver/decorated_sliver.1.dart similarity index 100% rename from examples/api/lib/widgets/sliver/decorated_sliver.1.dart rename to packages/flutter/examples/api/lib/widgets/sliver/decorated_sliver.1.dart diff --git a/examples/api/lib/widgets/sliver/pinned_header_sliver.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/pinned_header_sliver.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/pinned_header_sliver.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/pinned_header_sliver.0.dart diff --git a/examples/api/lib/widgets/sliver/pinned_header_sliver.1.dart b/packages/flutter/examples/api/lib/widgets/sliver/pinned_header_sliver.1.dart similarity index 100% rename from examples/api/lib/widgets/sliver/pinned_header_sliver.1.dart rename to packages/flutter/examples/api/lib/widgets/sliver/pinned_header_sliver.1.dart diff --git a/examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_floating_header.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_floating_header.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_floating_header.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_floating_header.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_list.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_list.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_list.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_list.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_main_axis_group.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_main_axis_group.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_main_axis_group.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_main_axis_group.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_opacity.1.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_opacity.1.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_opacity.1.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_opacity.1.dart diff --git a/examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_tree.0.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_tree.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_tree.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_tree.0.dart diff --git a/examples/api/lib/widgets/sliver/sliver_tree.1.dart b/packages/flutter/examples/api/lib/widgets/sliver/sliver_tree.1.dart similarity index 100% rename from examples/api/lib/widgets/sliver/sliver_tree.1.dart rename to packages/flutter/examples/api/lib/widgets/sliver/sliver_tree.1.dart diff --git a/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart b/packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart similarity index 100% rename from examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart rename to packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart diff --git a/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart b/packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart similarity index 100% rename from examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart rename to packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart diff --git a/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart b/packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart similarity index 100% rename from examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart rename to packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart diff --git a/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart b/packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart similarity index 100% rename from examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart rename to packages/flutter/examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart diff --git a/examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart b/packages/flutter/examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart similarity index 100% rename from examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart rename to packages/flutter/examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart diff --git a/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart b/packages/flutter/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart similarity index 100% rename from examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart rename to packages/flutter/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart diff --git a/examples/api/lib/widgets/system_context_menu/system_context_menu.1.dart b/packages/flutter/examples/api/lib/widgets/system_context_menu/system_context_menu.1.dart similarity index 100% rename from examples/api/lib/widgets/system_context_menu/system_context_menu.1.dart rename to packages/flutter/examples/api/lib/widgets/system_context_menu/system_context_menu.1.dart diff --git a/examples/api/lib/widgets/table/table.0.dart b/packages/flutter/examples/api/lib/widgets/table/table.0.dart similarity index 100% rename from examples/api/lib/widgets/table/table.0.dart rename to packages/flutter/examples/api/lib/widgets/table/table.0.dart diff --git a/examples/api/lib/widgets/tap_region/tap_region.0.dart b/packages/flutter/examples/api/lib/widgets/tap_region/tap_region.0.dart similarity index 100% rename from examples/api/lib/widgets/tap_region/tap_region.0.dart rename to packages/flutter/examples/api/lib/widgets/tap_region/tap_region.0.dart diff --git a/examples/api/lib/widgets/tap_region/tap_region.1.dart b/packages/flutter/examples/api/lib/widgets/tap_region/tap_region.1.dart similarity index 100% rename from examples/api/lib/widgets/tap_region/tap_region.1.dart rename to packages/flutter/examples/api/lib/widgets/tap_region/tap_region.1.dart diff --git a/examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart b/packages/flutter/examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart similarity index 100% rename from examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart rename to packages/flutter/examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart diff --git a/examples/api/lib/widgets/text/text.0.dart b/packages/flutter/examples/api/lib/widgets/text/text.0.dart similarity index 100% rename from examples/api/lib/widgets/text/text.0.dart rename to packages/flutter/examples/api/lib/widgets/text/text.0.dart diff --git a/examples/api/lib/widgets/text/ui_testing_with_text.dart b/packages/flutter/examples/api/lib/widgets/text/ui_testing_with_text.dart similarity index 100% rename from examples/api/lib/widgets/text/ui_testing_with_text.dart rename to packages/flutter/examples/api/lib/widgets/text/ui_testing_with_text.dart diff --git a/examples/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart b/packages/flutter/examples/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart similarity index 100% rename from examples/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart rename to packages/flutter/examples/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart diff --git a/examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart b/packages/flutter/examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart similarity index 100% rename from examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart rename to packages/flutter/examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart diff --git a/examples/api/lib/widgets/transitions/align_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/align_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/align_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/align_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/animated_builder.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/animated_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/animated_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/animated_builder.0.dart diff --git a/examples/api/lib/widgets/transitions/animated_widget.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/animated_widget.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/animated_widget.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/animated_widget.0.dart diff --git a/examples/api/lib/widgets/transitions/decorated_box_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/decorated_box_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/decorated_box_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/decorated_box_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/default_text_style_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/default_text_style_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/default_text_style_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/default_text_style_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/fade_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/fade_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/fade_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/fade_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/listenable_builder.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/listenable_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.0.dart diff --git a/examples/api/lib/widgets/transitions/listenable_builder.1.dart b/packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.1.dart similarity index 100% rename from examples/api/lib/widgets/transitions/listenable_builder.1.dart rename to packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.1.dart diff --git a/examples/api/lib/widgets/transitions/listenable_builder.2.dart b/packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.2.dart similarity index 100% rename from examples/api/lib/widgets/transitions/listenable_builder.2.dart rename to packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.2.dart diff --git a/examples/api/lib/widgets/transitions/listenable_builder.3.dart b/packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.3.dart similarity index 100% rename from examples/api/lib/widgets/transitions/listenable_builder.3.dart rename to packages/flutter/examples/api/lib/widgets/transitions/listenable_builder.3.dart diff --git a/examples/api/lib/widgets/transitions/matrix_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/matrix_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/matrix_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/matrix_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/positioned_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/positioned_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/positioned_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/positioned_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/relative_positioned_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/relative_positioned_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/relative_positioned_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/relative_positioned_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/rotation_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/rotation_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/rotation_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/rotation_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/scale_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/scale_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/scale_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/scale_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/size_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/size_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/size_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/size_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/slide_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/slide_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/slide_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/slide_transition.0.dart diff --git a/examples/api/lib/widgets/transitions/sliver_fade_transition.0.dart b/packages/flutter/examples/api/lib/widgets/transitions/sliver_fade_transition.0.dart similarity index 100% rename from examples/api/lib/widgets/transitions/sliver_fade_transition.0.dart rename to packages/flutter/examples/api/lib/widgets/transitions/sliver_fade_transition.0.dart diff --git a/examples/api/lib/widgets/tween_animation_builder/tween_animation_builder.0.dart b/packages/flutter/examples/api/lib/widgets/tween_animation_builder/tween_animation_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/tween_animation_builder/tween_animation_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/tween_animation_builder/tween_animation_builder.0.dart diff --git a/examples/api/lib/widgets/undo_history/undo_history_controller.0.dart b/packages/flutter/examples/api/lib/widgets/undo_history/undo_history_controller.0.dart similarity index 100% rename from examples/api/lib/widgets/undo_history/undo_history_controller.0.dart rename to packages/flutter/examples/api/lib/widgets/undo_history/undo_history_controller.0.dart diff --git a/examples/api/lib/widgets/value_listenable_builder/value_listenable_builder.0.dart b/packages/flutter/examples/api/lib/widgets/value_listenable_builder/value_listenable_builder.0.dart similarity index 100% rename from examples/api/lib/widgets/value_listenable_builder/value_listenable_builder.0.dart rename to packages/flutter/examples/api/lib/widgets/value_listenable_builder/value_listenable_builder.0.dart diff --git a/examples/api/lib/widgets/widget_state/widget_state_border_side.0.dart b/packages/flutter/examples/api/lib/widgets/widget_state/widget_state_border_side.0.dart similarity index 100% rename from examples/api/lib/widgets/widget_state/widget_state_border_side.0.dart rename to packages/flutter/examples/api/lib/widgets/widget_state/widget_state_border_side.0.dart diff --git a/examples/api/lib/widgets/widget_state/widget_state_mouse_cursor.0.dart b/packages/flutter/examples/api/lib/widgets/widget_state/widget_state_mouse_cursor.0.dart similarity index 100% rename from examples/api/lib/widgets/widget_state/widget_state_mouse_cursor.0.dart rename to packages/flutter/examples/api/lib/widgets/widget_state/widget_state_mouse_cursor.0.dart diff --git a/examples/api/lib/widgets/widget_state/widget_state_outlined_border.0.dart b/packages/flutter/examples/api/lib/widgets/widget_state/widget_state_outlined_border.0.dart similarity index 100% rename from examples/api/lib/widgets/widget_state/widget_state_outlined_border.0.dart rename to packages/flutter/examples/api/lib/widgets/widget_state/widget_state_outlined_border.0.dart diff --git a/examples/api/lib/widgets/widget_state/widget_state_property.0.dart b/packages/flutter/examples/api/lib/widgets/widget_state/widget_state_property.0.dart similarity index 100% rename from examples/api/lib/widgets/widget_state/widget_state_property.0.dart rename to packages/flutter/examples/api/lib/widgets/widget_state/widget_state_property.0.dart diff --git a/examples/api/lib/widgets/windows/popup.0.dart b/packages/flutter/examples/api/lib/widgets/windows/popup.0.dart similarity index 100% rename from examples/api/lib/widgets/windows/popup.0.dart rename to packages/flutter/examples/api/lib/widgets/windows/popup.0.dart diff --git a/examples/api/lib/widgets/windows/satellite.0.dart b/packages/flutter/examples/api/lib/widgets/windows/satellite.0.dart similarity index 100% rename from examples/api/lib/widgets/windows/satellite.0.dart rename to packages/flutter/examples/api/lib/widgets/windows/satellite.0.dart diff --git a/examples/api/lib/widgets/windows/tooltip.0.dart b/packages/flutter/examples/api/lib/widgets/windows/tooltip.0.dart similarity index 100% rename from examples/api/lib/widgets/windows/tooltip.0.dart rename to packages/flutter/examples/api/lib/widgets/windows/tooltip.0.dart diff --git a/examples/api/lib/widgets/windows/window_manager.0.dart b/packages/flutter/examples/api/lib/widgets/windows/window_manager.0.dart similarity index 100% rename from examples/api/lib/widgets/windows/window_manager.0.dart rename to packages/flutter/examples/api/lib/widgets/windows/window_manager.0.dart diff --git a/examples/api/linux/.gitignore b/packages/flutter/examples/api/linux/.gitignore similarity index 100% rename from examples/api/linux/.gitignore rename to packages/flutter/examples/api/linux/.gitignore diff --git a/examples/api/linux/CMakeLists.txt b/packages/flutter/examples/api/linux/CMakeLists.txt similarity index 100% rename from examples/api/linux/CMakeLists.txt rename to packages/flutter/examples/api/linux/CMakeLists.txt diff --git a/examples/api/linux/flutter/CMakeLists.txt b/packages/flutter/examples/api/linux/flutter/CMakeLists.txt similarity index 100% rename from examples/api/linux/flutter/CMakeLists.txt rename to packages/flutter/examples/api/linux/flutter/CMakeLists.txt diff --git a/examples/api/linux/runner/CMakeLists.txt b/packages/flutter/examples/api/linux/runner/CMakeLists.txt similarity index 100% rename from examples/api/linux/runner/CMakeLists.txt rename to packages/flutter/examples/api/linux/runner/CMakeLists.txt diff --git a/examples/api/linux/runner/main.cc b/packages/flutter/examples/api/linux/runner/main.cc similarity index 100% rename from examples/api/linux/runner/main.cc rename to packages/flutter/examples/api/linux/runner/main.cc diff --git a/examples/api/linux/runner/my_application.cc b/packages/flutter/examples/api/linux/runner/my_application.cc similarity index 100% rename from examples/api/linux/runner/my_application.cc rename to packages/flutter/examples/api/linux/runner/my_application.cc diff --git a/examples/api/linux/runner/my_application.h b/packages/flutter/examples/api/linux/runner/my_application.h similarity index 100% rename from examples/api/linux/runner/my_application.h rename to packages/flutter/examples/api/linux/runner/my_application.h diff --git a/examples/api/macos/.gitignore b/packages/flutter/examples/api/macos/.gitignore similarity index 100% rename from examples/api/macos/.gitignore rename to packages/flutter/examples/api/macos/.gitignore diff --git a/examples/api/macos/Flutter/Flutter-Debug.xcconfig b/packages/flutter/examples/api/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from examples/api/macos/Flutter/Flutter-Debug.xcconfig rename to packages/flutter/examples/api/macos/Flutter/Flutter-Debug.xcconfig diff --git a/examples/api/macos/Flutter/Flutter-Release.xcconfig b/packages/flutter/examples/api/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from examples/api/macos/Flutter/Flutter-Release.xcconfig rename to packages/flutter/examples/api/macos/Flutter/Flutter-Release.xcconfig diff --git a/examples/api/macos/Podfile b/packages/flutter/examples/api/macos/Podfile similarity index 100% rename from examples/api/macos/Podfile rename to packages/flutter/examples/api/macos/Podfile diff --git a/examples/api/macos/Runner.xcodeproj/project.pbxproj b/packages/flutter/examples/api/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from examples/api/macos/Runner.xcodeproj/project.pbxproj rename to packages/flutter/examples/api/macos/Runner.xcodeproj/project.pbxproj diff --git a/examples/api/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter/examples/api/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from examples/api/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/flutter/examples/api/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/examples/api/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter/examples/api/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from examples/api/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/flutter/examples/api/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/examples/api/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/flutter/examples/api/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from examples/api/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/flutter/examples/api/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/examples/api/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter/examples/api/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from examples/api/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/flutter/examples/api/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/examples/api/macos/Runner/AppDelegate.swift b/packages/flutter/examples/api/macos/Runner/AppDelegate.swift similarity index 100% rename from examples/api/macos/Runner/AppDelegate.swift rename to packages/flutter/examples/api/macos/Runner/AppDelegate.swift diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to packages/flutter/examples/api/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/examples/api/macos/Runner/Base.lproj/MainMenu.xib b/packages/flutter/examples/api/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from examples/api/macos/Runner/Base.lproj/MainMenu.xib rename to packages/flutter/examples/api/macos/Runner/Base.lproj/MainMenu.xib diff --git a/examples/api/macos/Runner/Configs/AppInfo.xcconfig b/packages/flutter/examples/api/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from examples/api/macos/Runner/Configs/AppInfo.xcconfig rename to packages/flutter/examples/api/macos/Runner/Configs/AppInfo.xcconfig diff --git a/examples/api/macos/Runner/Configs/Debug.xcconfig b/packages/flutter/examples/api/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from examples/api/macos/Runner/Configs/Debug.xcconfig rename to packages/flutter/examples/api/macos/Runner/Configs/Debug.xcconfig diff --git a/examples/api/macos/Runner/Configs/Release.xcconfig b/packages/flutter/examples/api/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from examples/api/macos/Runner/Configs/Release.xcconfig rename to packages/flutter/examples/api/macos/Runner/Configs/Release.xcconfig diff --git a/examples/api/macos/Runner/Configs/Warnings.xcconfig b/packages/flutter/examples/api/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from examples/api/macos/Runner/Configs/Warnings.xcconfig rename to packages/flutter/examples/api/macos/Runner/Configs/Warnings.xcconfig diff --git a/examples/api/macos/Runner/DebugProfile.entitlements b/packages/flutter/examples/api/macos/Runner/DebugProfile.entitlements similarity index 100% rename from examples/api/macos/Runner/DebugProfile.entitlements rename to packages/flutter/examples/api/macos/Runner/DebugProfile.entitlements diff --git a/examples/api/macos/Runner/Info.plist b/packages/flutter/examples/api/macos/Runner/Info.plist similarity index 100% rename from examples/api/macos/Runner/Info.plist rename to packages/flutter/examples/api/macos/Runner/Info.plist diff --git a/examples/api/macos/Runner/MainFlutterWindow.swift b/packages/flutter/examples/api/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from examples/api/macos/Runner/MainFlutterWindow.swift rename to packages/flutter/examples/api/macos/Runner/MainFlutterWindow.swift diff --git a/examples/api/macos/Runner/Release.entitlements b/packages/flutter/examples/api/macos/Runner/Release.entitlements similarity index 100% rename from examples/api/macos/Runner/Release.entitlements rename to packages/flutter/examples/api/macos/Runner/Release.entitlements diff --git a/examples/api/pubspec.yaml b/packages/flutter/examples/api/pubspec.yaml similarity index 100% rename from examples/api/pubspec.yaml rename to packages/flutter/examples/api/pubspec.yaml diff --git a/examples/api/test/animation/animation_controller/animated_digit.0_test.dart b/packages/flutter/examples/api/test/animation/animation_controller/animated_digit.0_test.dart similarity index 100% rename from examples/api/test/animation/animation_controller/animated_digit.0_test.dart rename to packages/flutter/examples/api/test/animation/animation_controller/animated_digit.0_test.dart diff --git a/examples/api/test/animation/curves/curve2_d.0_test.dart b/packages/flutter/examples/api/test/animation/curves/curve2_d.0_test.dart similarity index 100% rename from examples/api/test/animation/curves/curve2_d.0_test.dart rename to packages/flutter/examples/api/test/animation/curves/curve2_d.0_test.dart diff --git a/examples/api/test/cupertino/activity_indicator/cupertino_activity_indicator.0_test.dart b/packages/flutter/examples/api/test/cupertino/activity_indicator/cupertino_activity_indicator.0_test.dart similarity index 100% rename from examples/api/test/cupertino/activity_indicator/cupertino_activity_indicator.0_test.dart rename to packages/flutter/examples/api/test/cupertino/activity_indicator/cupertino_activity_indicator.0_test.dart diff --git a/examples/api/test/cupertino/activity_indicator/cupertino_linear_activity_indicator.0_test.dart b/packages/flutter/examples/api/test/cupertino/activity_indicator/cupertino_linear_activity_indicator.0_test.dart similarity index 100% rename from examples/api/test/cupertino/activity_indicator/cupertino_linear_activity_indicator.0_test.dart rename to packages/flutter/examples/api/test/cupertino/activity_indicator/cupertino_linear_activity_indicator.0_test.dart diff --git a/examples/api/test/cupertino/bottom_tab_bar/cupertino_tab_bar.0_test.dart b/packages/flutter/examples/api/test/cupertino/bottom_tab_bar/cupertino_tab_bar.0_test.dart similarity index 100% rename from examples/api/test/cupertino/bottom_tab_bar/cupertino_tab_bar.0_test.dart rename to packages/flutter/examples/api/test/cupertino/bottom_tab_bar/cupertino_tab_bar.0_test.dart diff --git a/examples/api/test/cupertino/button/cupertino_button.0_test.dart b/packages/flutter/examples/api/test/cupertino/button/cupertino_button.0_test.dart similarity index 100% rename from examples/api/test/cupertino/button/cupertino_button.0_test.dart rename to packages/flutter/examples/api/test/cupertino/button/cupertino_button.0_test.dart diff --git a/examples/api/test/cupertino/checkbox/cupertino_checkbox.0_test.dart b/packages/flutter/examples/api/test/cupertino/checkbox/cupertino_checkbox.0_test.dart similarity index 100% rename from examples/api/test/cupertino/checkbox/cupertino_checkbox.0_test.dart rename to packages/flutter/examples/api/test/cupertino/checkbox/cupertino_checkbox.0_test.dart diff --git a/examples/api/test/cupertino/context_menu/cupertino_context_menu.0_test.dart b/packages/flutter/examples/api/test/cupertino/context_menu/cupertino_context_menu.0_test.dart similarity index 100% rename from examples/api/test/cupertino/context_menu/cupertino_context_menu.0_test.dart rename to packages/flutter/examples/api/test/cupertino/context_menu/cupertino_context_menu.0_test.dart diff --git a/examples/api/test/cupertino/context_menu/cupertino_context_menu.1_test.dart b/packages/flutter/examples/api/test/cupertino/context_menu/cupertino_context_menu.1_test.dart similarity index 100% rename from examples/api/test/cupertino/context_menu/cupertino_context_menu.1_test.dart rename to packages/flutter/examples/api/test/cupertino/context_menu/cupertino_context_menu.1_test.dart diff --git a/examples/api/test/cupertino/date_picker/cupertino_date_picker.0_test.dart b/packages/flutter/examples/api/test/cupertino/date_picker/cupertino_date_picker.0_test.dart similarity index 100% rename from examples/api/test/cupertino/date_picker/cupertino_date_picker.0_test.dart rename to packages/flutter/examples/api/test/cupertino/date_picker/cupertino_date_picker.0_test.dart diff --git a/examples/api/test/cupertino/date_picker/cupertino_timer_picker.0_test.dart b/packages/flutter/examples/api/test/cupertino/date_picker/cupertino_timer_picker.0_test.dart similarity index 100% rename from examples/api/test/cupertino/date_picker/cupertino_timer_picker.0_test.dart rename to packages/flutter/examples/api/test/cupertino/date_picker/cupertino_timer_picker.0_test.dart diff --git a/examples/api/test/cupertino/dialog/cupertino_action_sheet.0_test.dart b/packages/flutter/examples/api/test/cupertino/dialog/cupertino_action_sheet.0_test.dart similarity index 100% rename from examples/api/test/cupertino/dialog/cupertino_action_sheet.0_test.dart rename to packages/flutter/examples/api/test/cupertino/dialog/cupertino_action_sheet.0_test.dart diff --git a/examples/api/test/cupertino/dialog/cupertino_alert_dialog.0_test.dart b/packages/flutter/examples/api/test/cupertino/dialog/cupertino_alert_dialog.0_test.dart similarity index 100% rename from examples/api/test/cupertino/dialog/cupertino_alert_dialog.0_test.dart rename to packages/flutter/examples/api/test/cupertino/dialog/cupertino_alert_dialog.0_test.dart diff --git a/examples/api/test/cupertino/dialog/cupertino_popup_surface.0_test.dart b/packages/flutter/examples/api/test/cupertino/dialog/cupertino_popup_surface.0_test.dart similarity index 100% rename from examples/api/test/cupertino/dialog/cupertino_popup_surface.0_test.dart rename to packages/flutter/examples/api/test/cupertino/dialog/cupertino_popup_surface.0_test.dart diff --git a/examples/api/test/cupertino/expansion_tile/cupertino_expansion_tile.0_test.dart b/packages/flutter/examples/api/test/cupertino/expansion_tile/cupertino_expansion_tile.0_test.dart similarity index 100% rename from examples/api/test/cupertino/expansion_tile/cupertino_expansion_tile.0_test.dart rename to packages/flutter/examples/api/test/cupertino/expansion_tile/cupertino_expansion_tile.0_test.dart diff --git a/examples/api/test/cupertino/form_row/cupertino_form_row.0_test.dart b/packages/flutter/examples/api/test/cupertino/form_row/cupertino_form_row.0_test.dart similarity index 100% rename from examples/api/test/cupertino/form_row/cupertino_form_row.0_test.dart rename to packages/flutter/examples/api/test/cupertino/form_row/cupertino_form_row.0_test.dart diff --git a/examples/api/test/cupertino/list_section/list_section_base.0_test.dart b/packages/flutter/examples/api/test/cupertino/list_section/list_section_base.0_test.dart similarity index 100% rename from examples/api/test/cupertino/list_section/list_section_base.0_test.dart rename to packages/flutter/examples/api/test/cupertino/list_section/list_section_base.0_test.dart diff --git a/examples/api/test/cupertino/list_section/list_section_inset.0_test.dart b/packages/flutter/examples/api/test/cupertino/list_section/list_section_inset.0_test.dart similarity index 100% rename from examples/api/test/cupertino/list_section/list_section_inset.0_test.dart rename to packages/flutter/examples/api/test/cupertino/list_section/list_section_inset.0_test.dart diff --git a/examples/api/test/cupertino/list_tile/cupertino_list_tile.0_test.dart b/packages/flutter/examples/api/test/cupertino/list_tile/cupertino_list_tile.0_test.dart similarity index 100% rename from examples/api/test/cupertino/list_tile/cupertino_list_tile.0_test.dart rename to packages/flutter/examples/api/test/cupertino/list_tile/cupertino_list_tile.0_test.dart diff --git a/examples/api/test/cupertino/magnifier/cupertino_magnifier.0_test.dart b/packages/flutter/examples/api/test/cupertino/magnifier/cupertino_magnifier.0_test.dart similarity index 100% rename from examples/api/test/cupertino/magnifier/cupertino_magnifier.0_test.dart rename to packages/flutter/examples/api/test/cupertino/magnifier/cupertino_magnifier.0_test.dart diff --git a/examples/api/test/cupertino/magnifier/cupertino_text_magnifier.0_test.dart b/packages/flutter/examples/api/test/cupertino/magnifier/cupertino_text_magnifier.0_test.dart similarity index 100% rename from examples/api/test/cupertino/magnifier/cupertino_text_magnifier.0_test.dart rename to packages/flutter/examples/api/test/cupertino/magnifier/cupertino_text_magnifier.0_test.dart diff --git a/examples/api/test/cupertino/menu_anchor/menu_anchor.0_test.dart b/packages/flutter/examples/api/test/cupertino/menu_anchor/menu_anchor.0_test.dart similarity index 100% rename from examples/api/test/cupertino/menu_anchor/menu_anchor.0_test.dart rename to packages/flutter/examples/api/test/cupertino/menu_anchor/menu_anchor.0_test.dart diff --git a/examples/api/test/cupertino/menu_anchor/menu_anchor.1_test.dart b/packages/flutter/examples/api/test/cupertino/menu_anchor/menu_anchor.1_test.dart similarity index 100% rename from examples/api/test/cupertino/menu_anchor/menu_anchor.1_test.dart rename to packages/flutter/examples/api/test/cupertino/menu_anchor/menu_anchor.1_test.dart diff --git a/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.0_test.dart b/packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.0_test.dart similarity index 100% rename from examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.0_test.dart rename to packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.0_test.dart diff --git a/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart b/packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart similarity index 100% rename from examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart rename to packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart diff --git a/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.2_test.dart b/packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.2_test.dart similarity index 100% rename from examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.2_test.dart rename to packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.2_test.dart diff --git a/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart b/packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart similarity index 100% rename from examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart rename to packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart diff --git a/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart b/packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart similarity index 100% rename from examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart rename to packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart diff --git a/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.2_test.dart b/packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.2_test.dart similarity index 100% rename from examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.2_test.dart rename to packages/flutter/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.2_test.dart diff --git a/examples/api/test/cupertino/page_scaffold/cupertino_page_scaffold.0_test.dart b/packages/flutter/examples/api/test/cupertino/page_scaffold/cupertino_page_scaffold.0_test.dart similarity index 100% rename from examples/api/test/cupertino/page_scaffold/cupertino_page_scaffold.0_test.dart rename to packages/flutter/examples/api/test/cupertino/page_scaffold/cupertino_page_scaffold.0_test.dart diff --git a/examples/api/test/cupertino/picker/cupertino_picker.0_test.dart b/packages/flutter/examples/api/test/cupertino/picker/cupertino_picker.0_test.dart similarity index 100% rename from examples/api/test/cupertino/picker/cupertino_picker.0_test.dart rename to packages/flutter/examples/api/test/cupertino/picker/cupertino_picker.0_test.dart diff --git a/examples/api/test/cupertino/radio/cupertino_radio.0_test.dart b/packages/flutter/examples/api/test/cupertino/radio/cupertino_radio.0_test.dart similarity index 100% rename from examples/api/test/cupertino/radio/cupertino_radio.0_test.dart rename to packages/flutter/examples/api/test/cupertino/radio/cupertino_radio.0_test.dart diff --git a/examples/api/test/cupertino/radio/cupertino_radio.toggleable.0_test.dart b/packages/flutter/examples/api/test/cupertino/radio/cupertino_radio.toggleable.0_test.dart similarity index 100% rename from examples/api/test/cupertino/radio/cupertino_radio.toggleable.0_test.dart rename to packages/flutter/examples/api/test/cupertino/radio/cupertino_radio.toggleable.0_test.dart diff --git a/examples/api/test/cupertino/refresh/cupertino_sliver_refresh_control.0_test.dart b/packages/flutter/examples/api/test/cupertino/refresh/cupertino_sliver_refresh_control.0_test.dart similarity index 100% rename from examples/api/test/cupertino/refresh/cupertino_sliver_refresh_control.0_test.dart rename to packages/flutter/examples/api/test/cupertino/refresh/cupertino_sliver_refresh_control.0_test.dart diff --git a/examples/api/test/cupertino/route/show_cupertino_dialog.0_test.dart b/packages/flutter/examples/api/test/cupertino/route/show_cupertino_dialog.0_test.dart similarity index 100% rename from examples/api/test/cupertino/route/show_cupertino_dialog.0_test.dart rename to packages/flutter/examples/api/test/cupertino/route/show_cupertino_dialog.0_test.dart diff --git a/examples/api/test/cupertino/route/show_cupertino_modal_popup.0_test.dart b/packages/flutter/examples/api/test/cupertino/route/show_cupertino_modal_popup.0_test.dart similarity index 100% rename from examples/api/test/cupertino/route/show_cupertino_modal_popup.0_test.dart rename to packages/flutter/examples/api/test/cupertino/route/show_cupertino_modal_popup.0_test.dart diff --git a/examples/api/test/cupertino/scrollbar/cupertino_scrollbar.0_test.dart b/packages/flutter/examples/api/test/cupertino/scrollbar/cupertino_scrollbar.0_test.dart similarity index 100% rename from examples/api/test/cupertino/scrollbar/cupertino_scrollbar.0_test.dart rename to packages/flutter/examples/api/test/cupertino/scrollbar/cupertino_scrollbar.0_test.dart diff --git a/examples/api/test/cupertino/scrollbar/cupertino_scrollbar.1_test.dart b/packages/flutter/examples/api/test/cupertino/scrollbar/cupertino_scrollbar.1_test.dart similarity index 100% rename from examples/api/test/cupertino/scrollbar/cupertino_scrollbar.1_test.dart rename to packages/flutter/examples/api/test/cupertino/scrollbar/cupertino_scrollbar.1_test.dart diff --git a/examples/api/test/cupertino/search_field/cupertino_search_field.0_test.dart b/packages/flutter/examples/api/test/cupertino/search_field/cupertino_search_field.0_test.dart similarity index 100% rename from examples/api/test/cupertino/search_field/cupertino_search_field.0_test.dart rename to packages/flutter/examples/api/test/cupertino/search_field/cupertino_search_field.0_test.dart diff --git a/examples/api/test/cupertino/search_field/cupertino_search_field.1_test.dart b/packages/flutter/examples/api/test/cupertino/search_field/cupertino_search_field.1_test.dart similarity index 100% rename from examples/api/test/cupertino/search_field/cupertino_search_field.1_test.dart rename to packages/flutter/examples/api/test/cupertino/search_field/cupertino_search_field.1_test.dart diff --git a/examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart b/packages/flutter/examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart similarity index 100% rename from examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart rename to packages/flutter/examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart diff --git a/examples/api/test/cupertino/segmented_control/cupertino_sliding_segmented_control.0_test.dart b/packages/flutter/examples/api/test/cupertino/segmented_control/cupertino_sliding_segmented_control.0_test.dart similarity index 100% rename from examples/api/test/cupertino/segmented_control/cupertino_sliding_segmented_control.0_test.dart rename to packages/flutter/examples/api/test/cupertino/segmented_control/cupertino_sliding_segmented_control.0_test.dart diff --git a/examples/api/test/cupertino/sheet/cupertino_sheet.0_test.dart b/packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.0_test.dart similarity index 100% rename from examples/api/test/cupertino/sheet/cupertino_sheet.0_test.dart rename to packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.0_test.dart diff --git a/examples/api/test/cupertino/sheet/cupertino_sheet.1_test.dart b/packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.1_test.dart similarity index 100% rename from examples/api/test/cupertino/sheet/cupertino_sheet.1_test.dart rename to packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.1_test.dart diff --git a/examples/api/test/cupertino/sheet/cupertino_sheet.2_test.dart b/packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.2_test.dart similarity index 100% rename from examples/api/test/cupertino/sheet/cupertino_sheet.2_test.dart rename to packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.2_test.dart diff --git a/examples/api/test/cupertino/sheet/cupertino_sheet.3_test.dart b/packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.3_test.dart similarity index 100% rename from examples/api/test/cupertino/sheet/cupertino_sheet.3_test.dart rename to packages/flutter/examples/api/test/cupertino/sheet/cupertino_sheet.3_test.dart diff --git a/examples/api/test/cupertino/slider/cupertino_slider.0_test.dart b/packages/flutter/examples/api/test/cupertino/slider/cupertino_slider.0_test.dart similarity index 100% rename from examples/api/test/cupertino/slider/cupertino_slider.0_test.dart rename to packages/flutter/examples/api/test/cupertino/slider/cupertino_slider.0_test.dart diff --git a/examples/api/test/cupertino/switch/cupertino_switch.0_test.dart b/packages/flutter/examples/api/test/cupertino/switch/cupertino_switch.0_test.dart similarity index 100% rename from examples/api/test/cupertino/switch/cupertino_switch.0_test.dart rename to packages/flutter/examples/api/test/cupertino/switch/cupertino_switch.0_test.dart diff --git a/examples/api/test/cupertino/tab_scaffold/cupertino_tab_controller.0_test.dart b/packages/flutter/examples/api/test/cupertino/tab_scaffold/cupertino_tab_controller.0_test.dart similarity index 100% rename from examples/api/test/cupertino/tab_scaffold/cupertino_tab_controller.0_test.dart rename to packages/flutter/examples/api/test/cupertino/tab_scaffold/cupertino_tab_controller.0_test.dart diff --git a/examples/api/test/cupertino/tab_scaffold/cupertino_tab_scaffold.0_test.dart b/packages/flutter/examples/api/test/cupertino/tab_scaffold/cupertino_tab_scaffold.0_test.dart similarity index 100% rename from examples/api/test/cupertino/tab_scaffold/cupertino_tab_scaffold.0_test.dart rename to packages/flutter/examples/api/test/cupertino/tab_scaffold/cupertino_tab_scaffold.0_test.dart diff --git a/examples/api/test/cupertino/text_field/cupertino_text_field.0_test.dart b/packages/flutter/examples/api/test/cupertino/text_field/cupertino_text_field.0_test.dart similarity index 100% rename from examples/api/test/cupertino/text_field/cupertino_text_field.0_test.dart rename to packages/flutter/examples/api/test/cupertino/text_field/cupertino_text_field.0_test.dart diff --git a/examples/api/test/cupertino/text_form_field_row/cupertino_text_form_field_row.1_test.dart b/packages/flutter/examples/api/test/cupertino/text_form_field_row/cupertino_text_form_field_row.1_test.dart similarity index 100% rename from examples/api/test/cupertino/text_form_field_row/cupertino_text_form_field_row.1_test.dart rename to packages/flutter/examples/api/test/cupertino/text_form_field_row/cupertino_text_form_field_row.1_test.dart diff --git a/examples/api/test/flutter_test_config.dart b/packages/flutter/examples/api/test/flutter_test_config.dart similarity index 100% rename from examples/api/test/flutter_test_config.dart rename to packages/flutter/examples/api/test/flutter_test_config.dart diff --git a/examples/api/test/foundation/key/value_key.0_test.dart b/packages/flutter/examples/api/test/foundation/key/value_key.0_test.dart similarity index 100% rename from examples/api/test/foundation/key/value_key.0_test.dart rename to packages/flutter/examples/api/test/foundation/key/value_key.0_test.dart diff --git a/examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart b/packages/flutter/examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart similarity index 100% rename from examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart rename to packages/flutter/examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart diff --git a/examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart b/packages/flutter/examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart similarity index 100% rename from examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart rename to packages/flutter/examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart diff --git a/examples/api/test/goldens_io.dart b/packages/flutter/examples/api/test/goldens_io.dart similarity index 100% rename from examples/api/test/goldens_io.dart rename to packages/flutter/examples/api/test/goldens_io.dart diff --git a/examples/api/test/goldens_web.dart b/packages/flutter/examples/api/test/goldens_web.dart similarity index 100% rename from examples/api/test/goldens_web.dart rename to packages/flutter/examples/api/test/goldens_web.dart diff --git a/examples/api/test/material/about/about_list_tile.0_test.dart b/packages/flutter/examples/api/test/material/about/about_list_tile.0_test.dart similarity index 100% rename from examples/api/test/material/about/about_list_tile.0_test.dart rename to packages/flutter/examples/api/test/material/about/about_list_tile.0_test.dart diff --git a/examples/api/test/material/action_buttons/action_icon_theme.0_test.dart b/packages/flutter/examples/api/test/material/action_buttons/action_icon_theme.0_test.dart similarity index 100% rename from examples/api/test/material/action_buttons/action_icon_theme.0_test.dart rename to packages/flutter/examples/api/test/material/action_buttons/action_icon_theme.0_test.dart diff --git a/examples/api/test/material/action_chip/action_chip.0_test.dart b/packages/flutter/examples/api/test/material/action_chip/action_chip.0_test.dart similarity index 100% rename from examples/api/test/material/action_chip/action_chip.0_test.dart rename to packages/flutter/examples/api/test/material/action_chip/action_chip.0_test.dart diff --git a/examples/api/test/material/animated_icon/animated_icon.0_test.dart b/packages/flutter/examples/api/test/material/animated_icon/animated_icon.0_test.dart similarity index 100% rename from examples/api/test/material/animated_icon/animated_icon.0_test.dart rename to packages/flutter/examples/api/test/material/animated_icon/animated_icon.0_test.dart diff --git a/examples/api/test/material/animated_icon/animated_icons_data.0_test.dart b/packages/flutter/examples/api/test/material/animated_icon/animated_icons_data.0_test.dart similarity index 100% rename from examples/api/test/material/animated_icon/animated_icons_data.0_test.dart rename to packages/flutter/examples/api/test/material/animated_icon/animated_icons_data.0_test.dart diff --git a/examples/api/test/material/app/app.0_test.dart b/packages/flutter/examples/api/test/material/app/app.0_test.dart similarity index 100% rename from examples/api/test/material/app/app.0_test.dart rename to packages/flutter/examples/api/test/material/app/app.0_test.dart diff --git a/examples/api/test/material/app_bar/app_bar.0_test.dart b/packages/flutter/examples/api/test/material/app_bar/app_bar.0_test.dart similarity index 100% rename from examples/api/test/material/app_bar/app_bar.0_test.dart rename to packages/flutter/examples/api/test/material/app_bar/app_bar.0_test.dart diff --git a/examples/api/test/material/app_bar/app_bar.1_test.dart b/packages/flutter/examples/api/test/material/app_bar/app_bar.1_test.dart similarity index 100% rename from examples/api/test/material/app_bar/app_bar.1_test.dart rename to packages/flutter/examples/api/test/material/app_bar/app_bar.1_test.dart diff --git a/examples/api/test/material/app_bar/app_bar.2_test.dart b/packages/flutter/examples/api/test/material/app_bar/app_bar.2_test.dart similarity index 100% rename from examples/api/test/material/app_bar/app_bar.2_test.dart rename to packages/flutter/examples/api/test/material/app_bar/app_bar.2_test.dart diff --git a/examples/api/test/material/app_bar/app_bar.3_test.dart b/packages/flutter/examples/api/test/material/app_bar/app_bar.3_test.dart similarity index 100% rename from examples/api/test/material/app_bar/app_bar.3_test.dart rename to packages/flutter/examples/api/test/material/app_bar/app_bar.3_test.dart diff --git a/examples/api/test/material/app_bar/app_bar.4_test.dart b/packages/flutter/examples/api/test/material/app_bar/app_bar.4_test.dart similarity index 100% rename from examples/api/test/material/app_bar/app_bar.4_test.dart rename to packages/flutter/examples/api/test/material/app_bar/app_bar.4_test.dart diff --git a/examples/api/test/material/app_bar/sliver_app_bar.1_test.dart b/packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.1_test.dart similarity index 100% rename from examples/api/test/material/app_bar/sliver_app_bar.1_test.dart rename to packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.1_test.dart diff --git a/examples/api/test/material/app_bar/sliver_app_bar.2_test.dart b/packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.2_test.dart similarity index 100% rename from examples/api/test/material/app_bar/sliver_app_bar.2_test.dart rename to packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.2_test.dart diff --git a/examples/api/test/material/app_bar/sliver_app_bar.3_test.dart b/packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.3_test.dart similarity index 100% rename from examples/api/test/material/app_bar/sliver_app_bar.3_test.dart rename to packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.3_test.dart diff --git a/examples/api/test/material/app_bar/sliver_app_bar.4_test.dart b/packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.4_test.dart similarity index 100% rename from examples/api/test/material/app_bar/sliver_app_bar.4_test.dart rename to packages/flutter/examples/api/test/material/app_bar/sliver_app_bar.4_test.dart diff --git a/examples/api/test/material/autocomplete/autocomplete.0_test.dart b/packages/flutter/examples/api/test/material/autocomplete/autocomplete.0_test.dart similarity index 100% rename from examples/api/test/material/autocomplete/autocomplete.0_test.dart rename to packages/flutter/examples/api/test/material/autocomplete/autocomplete.0_test.dart diff --git a/examples/api/test/material/autocomplete/autocomplete.1_test.dart b/packages/flutter/examples/api/test/material/autocomplete/autocomplete.1_test.dart similarity index 100% rename from examples/api/test/material/autocomplete/autocomplete.1_test.dart rename to packages/flutter/examples/api/test/material/autocomplete/autocomplete.1_test.dart diff --git a/examples/api/test/material/autocomplete/autocomplete.2_test.dart b/packages/flutter/examples/api/test/material/autocomplete/autocomplete.2_test.dart similarity index 100% rename from examples/api/test/material/autocomplete/autocomplete.2_test.dart rename to packages/flutter/examples/api/test/material/autocomplete/autocomplete.2_test.dart diff --git a/examples/api/test/material/autocomplete/autocomplete.3_test.dart b/packages/flutter/examples/api/test/material/autocomplete/autocomplete.3_test.dart similarity index 100% rename from examples/api/test/material/autocomplete/autocomplete.3_test.dart rename to packages/flutter/examples/api/test/material/autocomplete/autocomplete.3_test.dart diff --git a/examples/api/test/material/autocomplete/autocomplete.4_test.dart b/packages/flutter/examples/api/test/material/autocomplete/autocomplete.4_test.dart similarity index 100% rename from examples/api/test/material/autocomplete/autocomplete.4_test.dart rename to packages/flutter/examples/api/test/material/autocomplete/autocomplete.4_test.dart diff --git a/examples/api/test/material/badge/badge.0_test.dart b/packages/flutter/examples/api/test/material/badge/badge.0_test.dart similarity index 100% rename from examples/api/test/material/badge/badge.0_test.dart rename to packages/flutter/examples/api/test/material/badge/badge.0_test.dart diff --git a/examples/api/test/material/banner/material_banner.0_test.dart b/packages/flutter/examples/api/test/material/banner/material_banner.0_test.dart similarity index 100% rename from examples/api/test/material/banner/material_banner.0_test.dart rename to packages/flutter/examples/api/test/material/banner/material_banner.0_test.dart diff --git a/examples/api/test/material/banner/material_banner.1_test.dart b/packages/flutter/examples/api/test/material/banner/material_banner.1_test.dart similarity index 100% rename from examples/api/test/material/banner/material_banner.1_test.dart rename to packages/flutter/examples/api/test/material/banner/material_banner.1_test.dart diff --git a/examples/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart b/packages/flutter/examples/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart similarity index 100% rename from examples/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart rename to packages/flutter/examples/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart diff --git a/examples/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart b/packages/flutter/examples/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart similarity index 100% rename from examples/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart rename to packages/flutter/examples/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart diff --git a/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart b/packages/flutter/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart similarity index 100% rename from examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart rename to packages/flutter/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart diff --git a/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart b/packages/flutter/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart similarity index 100% rename from examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart rename to packages/flutter/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart diff --git a/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.2_test.dart b/packages/flutter/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.2_test.dart similarity index 100% rename from examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.2_test.dart rename to packages/flutter/examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.2_test.dart diff --git a/examples/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart b/packages/flutter/examples/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart similarity index 100% rename from examples/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart rename to packages/flutter/examples/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart diff --git a/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.0_test.dart b/packages/flutter/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.0_test.dart similarity index 100% rename from examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.0_test.dart rename to packages/flutter/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.0_test.dart diff --git a/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.1_test.dart b/packages/flutter/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.1_test.dart similarity index 100% rename from examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.1_test.dart rename to packages/flutter/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.1_test.dart diff --git a/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart b/packages/flutter/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart similarity index 100% rename from examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart rename to packages/flutter/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart diff --git a/examples/api/test/material/button_style/button_style.0_test.dart b/packages/flutter/examples/api/test/material/button_style/button_style.0_test.dart similarity index 100% rename from examples/api/test/material/button_style/button_style.0_test.dart rename to packages/flutter/examples/api/test/material/button_style/button_style.0_test.dart diff --git a/examples/api/test/material/card/card.0_test.dart b/packages/flutter/examples/api/test/material/card/card.0_test.dart similarity index 100% rename from examples/api/test/material/card/card.0_test.dart rename to packages/flutter/examples/api/test/material/card/card.0_test.dart diff --git a/examples/api/test/material/card/card.1_test.dart b/packages/flutter/examples/api/test/material/card/card.1_test.dart similarity index 100% rename from examples/api/test/material/card/card.1_test.dart rename to packages/flutter/examples/api/test/material/card/card.1_test.dart diff --git a/examples/api/test/material/card/card.2_test.dart b/packages/flutter/examples/api/test/material/card/card.2_test.dart similarity index 100% rename from examples/api/test/material/card/card.2_test.dart rename to packages/flutter/examples/api/test/material/card/card.2_test.dart diff --git a/examples/api/test/material/carousel/carousel.0_test.dart b/packages/flutter/examples/api/test/material/carousel/carousel.0_test.dart similarity index 100% rename from examples/api/test/material/carousel/carousel.0_test.dart rename to packages/flutter/examples/api/test/material/carousel/carousel.0_test.dart diff --git a/examples/api/test/material/carousel/carousel.1_test.dart b/packages/flutter/examples/api/test/material/carousel/carousel.1_test.dart similarity index 100% rename from examples/api/test/material/carousel/carousel.1_test.dart rename to packages/flutter/examples/api/test/material/carousel/carousel.1_test.dart diff --git a/examples/api/test/material/checkbox/checkbox.0_test.dart b/packages/flutter/examples/api/test/material/checkbox/checkbox.0_test.dart similarity index 100% rename from examples/api/test/material/checkbox/checkbox.0_test.dart rename to packages/flutter/examples/api/test/material/checkbox/checkbox.0_test.dart diff --git a/examples/api/test/material/checkbox/checkbox.1_test.dart b/packages/flutter/examples/api/test/material/checkbox/checkbox.1_test.dart similarity index 100% rename from examples/api/test/material/checkbox/checkbox.1_test.dart rename to packages/flutter/examples/api/test/material/checkbox/checkbox.1_test.dart diff --git a/examples/api/test/material/checkbox_list_tile/checkbox_list_tile.0_test.dart b/packages/flutter/examples/api/test/material/checkbox_list_tile/checkbox_list_tile.0_test.dart similarity index 100% rename from examples/api/test/material/checkbox_list_tile/checkbox_list_tile.0_test.dart rename to packages/flutter/examples/api/test/material/checkbox_list_tile/checkbox_list_tile.0_test.dart diff --git a/examples/api/test/material/checkbox_list_tile/checkbox_list_tile.1_test.dart b/packages/flutter/examples/api/test/material/checkbox_list_tile/checkbox_list_tile.1_test.dart similarity index 100% rename from examples/api/test/material/checkbox_list_tile/checkbox_list_tile.1_test.dart rename to packages/flutter/examples/api/test/material/checkbox_list_tile/checkbox_list_tile.1_test.dart diff --git a/examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.0_test.dart b/packages/flutter/examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.0_test.dart similarity index 100% rename from examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.0_test.dart rename to packages/flutter/examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.0_test.dart diff --git a/examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.1_test.dart b/packages/flutter/examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.1_test.dart similarity index 100% rename from examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.1_test.dart rename to packages/flutter/examples/api/test/material/checkbox_list_tile/custom_labeled_checkbox.1_test.dart diff --git a/examples/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart b/packages/flutter/examples/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart similarity index 100% rename from examples/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart rename to packages/flutter/examples/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart diff --git a/examples/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart b/packages/flutter/examples/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart similarity index 100% rename from examples/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart rename to packages/flutter/examples/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart diff --git a/examples/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart b/packages/flutter/examples/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart similarity index 100% rename from examples/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart rename to packages/flutter/examples/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart diff --git a/examples/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart b/packages/flutter/examples/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart similarity index 100% rename from examples/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart rename to packages/flutter/examples/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart diff --git a/examples/api/test/material/choice_chip/choice_chip.0_test.dart b/packages/flutter/examples/api/test/material/choice_chip/choice_chip.0_test.dart similarity index 100% rename from examples/api/test/material/choice_chip/choice_chip.0_test.dart rename to packages/flutter/examples/api/test/material/choice_chip/choice_chip.0_test.dart diff --git a/examples/api/test/material/color_scheme/color_scheme.0_test.dart b/packages/flutter/examples/api/test/material/color_scheme/color_scheme.0_test.dart similarity index 100% rename from examples/api/test/material/color_scheme/color_scheme.0_test.dart rename to packages/flutter/examples/api/test/material/color_scheme/color_scheme.0_test.dart diff --git a/examples/api/test/material/color_scheme/dynamic_content_color.0_test.dart b/packages/flutter/examples/api/test/material/color_scheme/dynamic_content_color.0_test.dart similarity index 100% rename from examples/api/test/material/color_scheme/dynamic_content_color.0_test.dart rename to packages/flutter/examples/api/test/material/color_scheme/dynamic_content_color.0_test.dart diff --git a/examples/api/test/material/context_menu/editable_text_toolbar_builder.2_test.dart b/packages/flutter/examples/api/test/material/context_menu/editable_text_toolbar_builder.2_test.dart similarity index 100% rename from examples/api/test/material/context_menu/editable_text_toolbar_builder.2_test.dart rename to packages/flutter/examples/api/test/material/context_menu/editable_text_toolbar_builder.2_test.dart diff --git a/examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart b/packages/flutter/examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart similarity index 100% rename from examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart rename to packages/flutter/examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart diff --git a/examples/api/test/material/data_table/data_table.0_test.dart b/packages/flutter/examples/api/test/material/data_table/data_table.0_test.dart similarity index 100% rename from examples/api/test/material/data_table/data_table.0_test.dart rename to packages/flutter/examples/api/test/material/data_table/data_table.0_test.dart diff --git a/examples/api/test/material/data_table/data_table.1_test.dart b/packages/flutter/examples/api/test/material/data_table/data_table.1_test.dart similarity index 100% rename from examples/api/test/material/data_table/data_table.1_test.dart rename to packages/flutter/examples/api/test/material/data_table/data_table.1_test.dart diff --git a/examples/api/test/material/date_picker/custom_calendar_date_picker.0_test.dart b/packages/flutter/examples/api/test/material/date_picker/custom_calendar_date_picker.0_test.dart similarity index 100% rename from examples/api/test/material/date_picker/custom_calendar_date_picker.0_test.dart rename to packages/flutter/examples/api/test/material/date_picker/custom_calendar_date_picker.0_test.dart diff --git a/examples/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart b/packages/flutter/examples/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart similarity index 100% rename from examples/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart rename to packages/flutter/examples/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart diff --git a/examples/api/test/material/date_picker/show_date_picker.0_test.dart b/packages/flutter/examples/api/test/material/date_picker/show_date_picker.0_test.dart similarity index 100% rename from examples/api/test/material/date_picker/show_date_picker.0_test.dart rename to packages/flutter/examples/api/test/material/date_picker/show_date_picker.0_test.dart diff --git a/examples/api/test/material/date_picker/show_date_picker.1_test.dart b/packages/flutter/examples/api/test/material/date_picker/show_date_picker.1_test.dart similarity index 100% rename from examples/api/test/material/date_picker/show_date_picker.1_test.dart rename to packages/flutter/examples/api/test/material/date_picker/show_date_picker.1_test.dart diff --git a/examples/api/test/material/date_picker/show_date_range_picker.0_test.dart b/packages/flutter/examples/api/test/material/date_picker/show_date_range_picker.0_test.dart similarity index 100% rename from examples/api/test/material/date_picker/show_date_range_picker.0_test.dart rename to packages/flutter/examples/api/test/material/date_picker/show_date_range_picker.0_test.dart diff --git a/examples/api/test/material/dialog/adaptive_alert_dialog.0_test.dart b/packages/flutter/examples/api/test/material/dialog/adaptive_alert_dialog.0_test.dart similarity index 100% rename from examples/api/test/material/dialog/adaptive_alert_dialog.0_test.dart rename to packages/flutter/examples/api/test/material/dialog/adaptive_alert_dialog.0_test.dart diff --git a/examples/api/test/material/dialog/alert_dialog.0_test.dart b/packages/flutter/examples/api/test/material/dialog/alert_dialog.0_test.dart similarity index 100% rename from examples/api/test/material/dialog/alert_dialog.0_test.dart rename to packages/flutter/examples/api/test/material/dialog/alert_dialog.0_test.dart diff --git a/examples/api/test/material/dialog/alert_dialog.1_test.dart b/packages/flutter/examples/api/test/material/dialog/alert_dialog.1_test.dart similarity index 100% rename from examples/api/test/material/dialog/alert_dialog.1_test.dart rename to packages/flutter/examples/api/test/material/dialog/alert_dialog.1_test.dart diff --git a/examples/api/test/material/dialog/dialog.0_test.dart b/packages/flutter/examples/api/test/material/dialog/dialog.0_test.dart similarity index 100% rename from examples/api/test/material/dialog/dialog.0_test.dart rename to packages/flutter/examples/api/test/material/dialog/dialog.0_test.dart diff --git a/examples/api/test/material/dialog/show_dialog.0_test.dart b/packages/flutter/examples/api/test/material/dialog/show_dialog.0_test.dart similarity index 100% rename from examples/api/test/material/dialog/show_dialog.0_test.dart rename to packages/flutter/examples/api/test/material/dialog/show_dialog.0_test.dart diff --git a/examples/api/test/material/dialog/show_dialog.1_test.dart b/packages/flutter/examples/api/test/material/dialog/show_dialog.1_test.dart similarity index 100% rename from examples/api/test/material/dialog/show_dialog.1_test.dart rename to packages/flutter/examples/api/test/material/dialog/show_dialog.1_test.dart diff --git a/examples/api/test/material/dialog/show_dialog.2_test.dart b/packages/flutter/examples/api/test/material/dialog/show_dialog.2_test.dart similarity index 100% rename from examples/api/test/material/dialog/show_dialog.2_test.dart rename to packages/flutter/examples/api/test/material/dialog/show_dialog.2_test.dart diff --git a/examples/api/test/material/divider/divider.0_test.dart b/packages/flutter/examples/api/test/material/divider/divider.0_test.dart similarity index 100% rename from examples/api/test/material/divider/divider.0_test.dart rename to packages/flutter/examples/api/test/material/divider/divider.0_test.dart diff --git a/examples/api/test/material/divider/divider.1_test.dart b/packages/flutter/examples/api/test/material/divider/divider.1_test.dart similarity index 100% rename from examples/api/test/material/divider/divider.1_test.dart rename to packages/flutter/examples/api/test/material/divider/divider.1_test.dart diff --git a/examples/api/test/material/divider/vertical_divider.0_test.dart b/packages/flutter/examples/api/test/material/divider/vertical_divider.0_test.dart similarity index 100% rename from examples/api/test/material/divider/vertical_divider.0_test.dart rename to packages/flutter/examples/api/test/material/divider/vertical_divider.0_test.dart diff --git a/examples/api/test/material/divider/vertical_divider.1_test.dart b/packages/flutter/examples/api/test/material/divider/vertical_divider.1_test.dart similarity index 100% rename from examples/api/test/material/divider/vertical_divider.1_test.dart rename to packages/flutter/examples/api/test/material/divider/vertical_divider.1_test.dart diff --git a/examples/api/test/material/drawer/drawer.0_test.dart b/packages/flutter/examples/api/test/material/drawer/drawer.0_test.dart similarity index 100% rename from examples/api/test/material/drawer/drawer.0_test.dart rename to packages/flutter/examples/api/test/material/drawer/drawer.0_test.dart diff --git a/examples/api/test/material/dropdown/dropdown_button.0_test.dart b/packages/flutter/examples/api/test/material/dropdown/dropdown_button.0_test.dart similarity index 100% rename from examples/api/test/material/dropdown/dropdown_button.0_test.dart rename to packages/flutter/examples/api/test/material/dropdown/dropdown_button.0_test.dart diff --git a/examples/api/test/material/dropdown/dropdown_button.selected_item_builder.0_test.dart b/packages/flutter/examples/api/test/material/dropdown/dropdown_button.selected_item_builder.0_test.dart similarity index 100% rename from examples/api/test/material/dropdown/dropdown_button.selected_item_builder.0_test.dart rename to packages/flutter/examples/api/test/material/dropdown/dropdown_button.selected_item_builder.0_test.dart diff --git a/examples/api/test/material/dropdown/dropdown_button.style.0_test.dart b/packages/flutter/examples/api/test/material/dropdown/dropdown_button.style.0_test.dart similarity index 100% rename from examples/api/test/material/dropdown/dropdown_button.style.0_test.dart rename to packages/flutter/examples/api/test/material/dropdown/dropdown_button.style.0_test.dart diff --git a/examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart b/packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart similarity index 100% rename from examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart rename to packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart diff --git a/examples/api/test/material/dropdown_menu/dropdown_menu.1_test.dart b/packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu.1_test.dart similarity index 100% rename from examples/api/test/material/dropdown_menu/dropdown_menu.1_test.dart rename to packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu.1_test.dart diff --git a/examples/api/test/material/dropdown_menu/dropdown_menu.2_test.dart b/packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu.2_test.dart similarity index 100% rename from examples/api/test/material/dropdown_menu/dropdown_menu.2_test.dart rename to packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu.2_test.dart diff --git a/examples/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart b/packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart similarity index 100% rename from examples/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart rename to packages/flutter/examples/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart diff --git a/examples/api/test/material/elevated_button/elevated_button.0_test.dart b/packages/flutter/examples/api/test/material/elevated_button/elevated_button.0_test.dart similarity index 100% rename from examples/api/test/material/elevated_button/elevated_button.0_test.dart rename to packages/flutter/examples/api/test/material/elevated_button/elevated_button.0_test.dart diff --git a/examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart b/packages/flutter/examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart similarity index 100% rename from examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart rename to packages/flutter/examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart diff --git a/examples/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart b/packages/flutter/examples/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart similarity index 100% rename from examples/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart rename to packages/flutter/examples/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart diff --git a/examples/api/test/material/expansion_tile/expansion_tile.0_test.dart b/packages/flutter/examples/api/test/material/expansion_tile/expansion_tile.0_test.dart similarity index 100% rename from examples/api/test/material/expansion_tile/expansion_tile.0_test.dart rename to packages/flutter/examples/api/test/material/expansion_tile/expansion_tile.0_test.dart diff --git a/examples/api/test/material/expansion_tile/expansion_tile.1_test.dart b/packages/flutter/examples/api/test/material/expansion_tile/expansion_tile.1_test.dart similarity index 100% rename from examples/api/test/material/expansion_tile/expansion_tile.1_test.dart rename to packages/flutter/examples/api/test/material/expansion_tile/expansion_tile.1_test.dart diff --git a/examples/api/test/material/expansion_tile/expansion_tile.2_test.dart b/packages/flutter/examples/api/test/material/expansion_tile/expansion_tile.2_test.dart similarity index 100% rename from examples/api/test/material/expansion_tile/expansion_tile.2_test.dart rename to packages/flutter/examples/api/test/material/expansion_tile/expansion_tile.2_test.dart diff --git a/examples/api/test/material/filled_button/filled_button.0_test.dart b/packages/flutter/examples/api/test/material/filled_button/filled_button.0_test.dart similarity index 100% rename from examples/api/test/material/filled_button/filled_button.0_test.dart rename to packages/flutter/examples/api/test/material/filled_button/filled_button.0_test.dart diff --git a/examples/api/test/material/filter_chip/filter_chip.0_test.dart b/packages/flutter/examples/api/test/material/filter_chip/filter_chip.0_test.dart similarity index 100% rename from examples/api/test/material/filter_chip/filter_chip.0_test.dart rename to packages/flutter/examples/api/test/material/filter_chip/filter_chip.0_test.dart diff --git a/examples/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart b/packages/flutter/examples/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart similarity index 100% rename from examples/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart rename to packages/flutter/examples/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart diff --git a/examples/api/test/material/floating_action_button/floating_action_button.0_test.dart b/packages/flutter/examples/api/test/material/floating_action_button/floating_action_button.0_test.dart similarity index 100% rename from examples/api/test/material/floating_action_button/floating_action_button.0_test.dart rename to packages/flutter/examples/api/test/material/floating_action_button/floating_action_button.0_test.dart diff --git a/examples/api/test/material/floating_action_button/floating_action_button.1_test.dart b/packages/flutter/examples/api/test/material/floating_action_button/floating_action_button.1_test.dart similarity index 100% rename from examples/api/test/material/floating_action_button/floating_action_button.1_test.dart rename to packages/flutter/examples/api/test/material/floating_action_button/floating_action_button.1_test.dart diff --git a/examples/api/test/material/floating_action_button/floating_action_button.2_test.dart b/packages/flutter/examples/api/test/material/floating_action_button/floating_action_button.2_test.dart similarity index 100% rename from examples/api/test/material/floating_action_button/floating_action_button.2_test.dart rename to packages/flutter/examples/api/test/material/floating_action_button/floating_action_button.2_test.dart diff --git a/examples/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart b/packages/flutter/examples/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart similarity index 100% rename from examples/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart rename to packages/flutter/examples/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart diff --git a/examples/api/test/material/icon_alignment/icon_alignment.0_test.dart b/packages/flutter/examples/api/test/material/icon_alignment/icon_alignment.0_test.dart similarity index 100% rename from examples/api/test/material/icon_alignment/icon_alignment.0_test.dart rename to packages/flutter/examples/api/test/material/icon_alignment/icon_alignment.0_test.dart diff --git a/examples/api/test/material/icon_button/icon_button.0_test.dart b/packages/flutter/examples/api/test/material/icon_button/icon_button.0_test.dart similarity index 100% rename from examples/api/test/material/icon_button/icon_button.0_test.dart rename to packages/flutter/examples/api/test/material/icon_button/icon_button.0_test.dart diff --git a/examples/api/test/material/icon_button/icon_button.1_test.dart b/packages/flutter/examples/api/test/material/icon_button/icon_button.1_test.dart similarity index 100% rename from examples/api/test/material/icon_button/icon_button.1_test.dart rename to packages/flutter/examples/api/test/material/icon_button/icon_button.1_test.dart diff --git a/examples/api/test/material/icon_button/icon_button.2_test.dart b/packages/flutter/examples/api/test/material/icon_button/icon_button.2_test.dart similarity index 100% rename from examples/api/test/material/icon_button/icon_button.2_test.dart rename to packages/flutter/examples/api/test/material/icon_button/icon_button.2_test.dart diff --git a/examples/api/test/material/icon_button/icon_button.3_test.dart b/packages/flutter/examples/api/test/material/icon_button/icon_button.3_test.dart similarity index 100% rename from examples/api/test/material/icon_button/icon_button.3_test.dart rename to packages/flutter/examples/api/test/material/icon_button/icon_button.3_test.dart diff --git a/examples/api/test/material/ink/ink.image_clip.0_test.dart b/packages/flutter/examples/api/test/material/ink/ink.image_clip.0_test.dart similarity index 100% rename from examples/api/test/material/ink/ink.image_clip.0_test.dart rename to packages/flutter/examples/api/test/material/ink/ink.image_clip.0_test.dart diff --git a/examples/api/test/material/ink/ink.image_clip.1_test.dart b/packages/flutter/examples/api/test/material/ink/ink.image_clip.1_test.dart similarity index 100% rename from examples/api/test/material/ink/ink.image_clip.1_test.dart rename to packages/flutter/examples/api/test/material/ink/ink.image_clip.1_test.dart diff --git a/examples/api/test/material/ink_well/ink_well.0_test.dart b/packages/flutter/examples/api/test/material/ink_well/ink_well.0_test.dart similarity index 100% rename from examples/api/test/material/ink_well/ink_well.0_test.dart rename to packages/flutter/examples/api/test/material/ink_well/ink_well.0_test.dart diff --git a/examples/api/test/material/input_chip/input_chip.0_test.dart b/packages/flutter/examples/api/test/material/input_chip/input_chip.0_test.dart similarity index 100% rename from examples/api/test/material/input_chip/input_chip.0_test.dart rename to packages/flutter/examples/api/test/material/input_chip/input_chip.0_test.dart diff --git a/examples/api/test/material/input_chip/input_chip.1_test.dart b/packages/flutter/examples/api/test/material/input_chip/input_chip.1_test.dart similarity index 100% rename from examples/api/test/material/input_chip/input_chip.1_test.dart rename to packages/flutter/examples/api/test/material/input_chip/input_chip.1_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.1_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.1_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.1_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.1_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.2_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.2_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.2_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.2_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.3_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.3_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.3_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.3_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.floating_label_style_error.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.floating_label_style_error.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.floating_label_style_error.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.floating_label_style_error.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.helper.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.helper.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.helper.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.helper.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.label.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.label.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.label.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.label.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.label_style_error.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.label_style_error.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.label_style_error.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.label_style_error.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.prefix_icon.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.prefix_icon.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.prefix_icon.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.prefix_icon.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.suffix_icon.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.suffix_icon.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.suffix_icon.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.suffix_icon.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.widget_state.0_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.widget_state.0_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.widget_state.0_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.widget_state.0_test.dart diff --git a/examples/api/test/material/input_decorator/input_decoration.widget_state.1_test.dart b/packages/flutter/examples/api/test/material/input_decorator/input_decoration.widget_state.1_test.dart similarity index 100% rename from examples/api/test/material/input_decorator/input_decoration.widget_state.1_test.dart rename to packages/flutter/examples/api/test/material/input_decorator/input_decoration.widget_state.1_test.dart diff --git a/examples/api/test/material/list_tile/custom_list_item.0_test.dart b/packages/flutter/examples/api/test/material/list_tile/custom_list_item.0_test.dart similarity index 100% rename from examples/api/test/material/list_tile/custom_list_item.0_test.dart rename to packages/flutter/examples/api/test/material/list_tile/custom_list_item.0_test.dart diff --git a/examples/api/test/material/list_tile/custom_list_item.1_test.dart b/packages/flutter/examples/api/test/material/list_tile/custom_list_item.1_test.dart similarity index 100% rename from examples/api/test/material/list_tile/custom_list_item.1_test.dart rename to packages/flutter/examples/api/test/material/list_tile/custom_list_item.1_test.dart diff --git a/examples/api/test/material/list_tile/list_tile.0_test.dart b/packages/flutter/examples/api/test/material/list_tile/list_tile.0_test.dart similarity index 100% rename from examples/api/test/material/list_tile/list_tile.0_test.dart rename to packages/flutter/examples/api/test/material/list_tile/list_tile.0_test.dart diff --git a/examples/api/test/material/list_tile/list_tile.1_test.dart b/packages/flutter/examples/api/test/material/list_tile/list_tile.1_test.dart similarity index 100% rename from examples/api/test/material/list_tile/list_tile.1_test.dart rename to packages/flutter/examples/api/test/material/list_tile/list_tile.1_test.dart diff --git a/examples/api/test/material/list_tile/list_tile.2_test.dart b/packages/flutter/examples/api/test/material/list_tile/list_tile.2_test.dart similarity index 100% rename from examples/api/test/material/list_tile/list_tile.2_test.dart rename to packages/flutter/examples/api/test/material/list_tile/list_tile.2_test.dart diff --git a/examples/api/test/material/list_tile/list_tile.3_test.dart b/packages/flutter/examples/api/test/material/list_tile/list_tile.3_test.dart similarity index 100% rename from examples/api/test/material/list_tile/list_tile.3_test.dart rename to packages/flutter/examples/api/test/material/list_tile/list_tile.3_test.dart diff --git a/examples/api/test/material/list_tile/list_tile.4_test.dart b/packages/flutter/examples/api/test/material/list_tile/list_tile.4_test.dart similarity index 100% rename from examples/api/test/material/list_tile/list_tile.4_test.dart rename to packages/flutter/examples/api/test/material/list_tile/list_tile.4_test.dart diff --git a/examples/api/test/material/list_tile/list_tile.selected.0_test.dart b/packages/flutter/examples/api/test/material/list_tile/list_tile.selected.0_test.dart similarity index 100% rename from examples/api/test/material/list_tile/list_tile.selected.0_test.dart rename to packages/flutter/examples/api/test/material/list_tile/list_tile.selected.0_test.dart diff --git a/examples/api/test/material/material_state/material_state_border_side.0_test.dart b/packages/flutter/examples/api/test/material/material_state/material_state_border_side.0_test.dart similarity index 100% rename from examples/api/test/material/material_state/material_state_border_side.0_test.dart rename to packages/flutter/examples/api/test/material/material_state/material_state_border_side.0_test.dart diff --git a/examples/api/test/material/material_state/material_state_mouse_cursor.0_test.dart b/packages/flutter/examples/api/test/material/material_state/material_state_mouse_cursor.0_test.dart similarity index 100% rename from examples/api/test/material/material_state/material_state_mouse_cursor.0_test.dart rename to packages/flutter/examples/api/test/material/material_state/material_state_mouse_cursor.0_test.dart diff --git a/examples/api/test/material/material_state/material_state_property.0_test.dart b/packages/flutter/examples/api/test/material/material_state/material_state_property.0_test.dart similarity index 100% rename from examples/api/test/material/material_state/material_state_property.0_test.dart rename to packages/flutter/examples/api/test/material/material_state/material_state_property.0_test.dart diff --git a/examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart diff --git a/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart diff --git a/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/menu_anchor.0_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart diff --git a/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/menu_anchor.1_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart diff --git a/examples/api/test/material/menu_anchor/menu_anchor.2_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.2_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/menu_anchor.2_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.2_test.dart diff --git a/examples/api/test/material/menu_anchor/menu_anchor.3_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.3_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/menu_anchor.3_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/menu_anchor.3_test.dart diff --git a/examples/api/test/material/menu_anchor/menu_bar.0_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/menu_bar.0_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/menu_bar.0_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/menu_bar.0_test.dart diff --git a/examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart b/packages/flutter/examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart similarity index 100% rename from examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart rename to packages/flutter/examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart diff --git a/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart b/packages/flutter/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart similarity index 100% rename from examples/api/test/material/navigation_bar/navigation_bar.0_test.dart rename to packages/flutter/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart diff --git a/examples/api/test/material/navigation_bar/navigation_bar.1_test.dart b/packages/flutter/examples/api/test/material/navigation_bar/navigation_bar.1_test.dart similarity index 100% rename from examples/api/test/material/navigation_bar/navigation_bar.1_test.dart rename to packages/flutter/examples/api/test/material/navigation_bar/navigation_bar.1_test.dart diff --git a/examples/api/test/material/navigation_bar/navigation_bar.2_test.dart b/packages/flutter/examples/api/test/material/navigation_bar/navigation_bar.2_test.dart similarity index 100% rename from examples/api/test/material/navigation_bar/navigation_bar.2_test.dart rename to packages/flutter/examples/api/test/material/navigation_bar/navigation_bar.2_test.dart diff --git a/examples/api/test/material/navigation_drawer/navigation_drawer.0_test.dart b/packages/flutter/examples/api/test/material/navigation_drawer/navigation_drawer.0_test.dart similarity index 100% rename from examples/api/test/material/navigation_drawer/navigation_drawer.0_test.dart rename to packages/flutter/examples/api/test/material/navigation_drawer/navigation_drawer.0_test.dart diff --git a/examples/api/test/material/navigation_rail/navigation_rail.0_test.dart b/packages/flutter/examples/api/test/material/navigation_rail/navigation_rail.0_test.dart similarity index 100% rename from examples/api/test/material/navigation_rail/navigation_rail.0_test.dart rename to packages/flutter/examples/api/test/material/navigation_rail/navigation_rail.0_test.dart diff --git a/examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart b/packages/flutter/examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart similarity index 100% rename from examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart rename to packages/flutter/examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart diff --git a/examples/api/test/material/outlined_button/outlined_button.0_test.dart b/packages/flutter/examples/api/test/material/outlined_button/outlined_button.0_test.dart similarity index 100% rename from examples/api/test/material/outlined_button/outlined_button.0_test.dart rename to packages/flutter/examples/api/test/material/outlined_button/outlined_button.0_test.dart diff --git a/examples/api/test/material/page_transitions_theme/page_transitions_theme.0_test.dart b/packages/flutter/examples/api/test/material/page_transitions_theme/page_transitions_theme.0_test.dart similarity index 100% rename from examples/api/test/material/page_transitions_theme/page_transitions_theme.0_test.dart rename to packages/flutter/examples/api/test/material/page_transitions_theme/page_transitions_theme.0_test.dart diff --git a/examples/api/test/material/page_transitions_theme/page_transitions_theme.1_test.dart b/packages/flutter/examples/api/test/material/page_transitions_theme/page_transitions_theme.1_test.dart similarity index 100% rename from examples/api/test/material/page_transitions_theme/page_transitions_theme.1_test.dart rename to packages/flutter/examples/api/test/material/page_transitions_theme/page_transitions_theme.1_test.dart diff --git a/examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart b/packages/flutter/examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart similarity index 100% rename from examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart rename to packages/flutter/examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart diff --git a/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart b/packages/flutter/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart similarity index 100% rename from examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart rename to packages/flutter/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart diff --git a/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart b/packages/flutter/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart similarity index 100% rename from examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart rename to packages/flutter/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart diff --git a/examples/api/test/material/popup_menu/popup_menu.0_test.dart b/packages/flutter/examples/api/test/material/popup_menu/popup_menu.0_test.dart similarity index 100% rename from examples/api/test/material/popup_menu/popup_menu.0_test.dart rename to packages/flutter/examples/api/test/material/popup_menu/popup_menu.0_test.dart diff --git a/examples/api/test/material/popup_menu/popup_menu.1_test.dart b/packages/flutter/examples/api/test/material/popup_menu/popup_menu.1_test.dart similarity index 100% rename from examples/api/test/material/popup_menu/popup_menu.1_test.dart rename to packages/flutter/examples/api/test/material/popup_menu/popup_menu.1_test.dart diff --git a/examples/api/test/material/popup_menu/popup_menu.2_test.dart b/packages/flutter/examples/api/test/material/popup_menu/popup_menu.2_test.dart similarity index 100% rename from examples/api/test/material/popup_menu/popup_menu.2_test.dart rename to packages/flutter/examples/api/test/material/popup_menu/popup_menu.2_test.dart diff --git a/examples/api/test/material/progress_indicator/circular_progress_indicator.0_test.dart b/packages/flutter/examples/api/test/material/progress_indicator/circular_progress_indicator.0_test.dart similarity index 100% rename from examples/api/test/material/progress_indicator/circular_progress_indicator.0_test.dart rename to packages/flutter/examples/api/test/material/progress_indicator/circular_progress_indicator.0_test.dart diff --git a/examples/api/test/material/progress_indicator/circular_progress_indicator.1_test.dart b/packages/flutter/examples/api/test/material/progress_indicator/circular_progress_indicator.1_test.dart similarity index 100% rename from examples/api/test/material/progress_indicator/circular_progress_indicator.1_test.dart rename to packages/flutter/examples/api/test/material/progress_indicator/circular_progress_indicator.1_test.dart diff --git a/examples/api/test/material/progress_indicator/circular_progress_indicator.2_test.dart b/packages/flutter/examples/api/test/material/progress_indicator/circular_progress_indicator.2_test.dart similarity index 100% rename from examples/api/test/material/progress_indicator/circular_progress_indicator.2_test.dart rename to packages/flutter/examples/api/test/material/progress_indicator/circular_progress_indicator.2_test.dart diff --git a/examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart b/packages/flutter/examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart similarity index 100% rename from examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart rename to packages/flutter/examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart diff --git a/examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart b/packages/flutter/examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart similarity index 100% rename from examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart rename to packages/flutter/examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart diff --git a/examples/api/test/material/radio/radio.0_test.dart b/packages/flutter/examples/api/test/material/radio/radio.0_test.dart similarity index 100% rename from examples/api/test/material/radio/radio.0_test.dart rename to packages/flutter/examples/api/test/material/radio/radio.0_test.dart diff --git a/examples/api/test/material/radio/radio.1_test.dart b/packages/flutter/examples/api/test/material/radio/radio.1_test.dart similarity index 100% rename from examples/api/test/material/radio/radio.1_test.dart rename to packages/flutter/examples/api/test/material/radio/radio.1_test.dart diff --git a/examples/api/test/material/radio/radio.toggleable.0_test.dart b/packages/flutter/examples/api/test/material/radio/radio.toggleable.0_test.dart similarity index 100% rename from examples/api/test/material/radio/radio.toggleable.0_test.dart rename to packages/flutter/examples/api/test/material/radio/radio.toggleable.0_test.dart diff --git a/examples/api/test/material/radio_list_tile/custom_labeled_radio.0_test.dart b/packages/flutter/examples/api/test/material/radio_list_tile/custom_labeled_radio.0_test.dart similarity index 100% rename from examples/api/test/material/radio_list_tile/custom_labeled_radio.0_test.dart rename to packages/flutter/examples/api/test/material/radio_list_tile/custom_labeled_radio.0_test.dart diff --git a/examples/api/test/material/radio_list_tile/custom_labeled_radio.1_test.dart b/packages/flutter/examples/api/test/material/radio_list_tile/custom_labeled_radio.1_test.dart similarity index 100% rename from examples/api/test/material/radio_list_tile/custom_labeled_radio.1_test.dart rename to packages/flutter/examples/api/test/material/radio_list_tile/custom_labeled_radio.1_test.dart diff --git a/examples/api/test/material/radio_list_tile/radio_list_tile.0_test.dart b/packages/flutter/examples/api/test/material/radio_list_tile/radio_list_tile.0_test.dart similarity index 100% rename from examples/api/test/material/radio_list_tile/radio_list_tile.0_test.dart rename to packages/flutter/examples/api/test/material/radio_list_tile/radio_list_tile.0_test.dart diff --git a/examples/api/test/material/radio_list_tile/radio_list_tile.1_test.dart b/packages/flutter/examples/api/test/material/radio_list_tile/radio_list_tile.1_test.dart similarity index 100% rename from examples/api/test/material/radio_list_tile/radio_list_tile.1_test.dart rename to packages/flutter/examples/api/test/material/radio_list_tile/radio_list_tile.1_test.dart diff --git a/examples/api/test/material/radio_list_tile/radio_list_tile.toggleable.0_test.dart b/packages/flutter/examples/api/test/material/radio_list_tile/radio_list_tile.toggleable.0_test.dart similarity index 100% rename from examples/api/test/material/radio_list_tile/radio_list_tile.toggleable.0_test.dart rename to packages/flutter/examples/api/test/material/radio_list_tile/radio_list_tile.toggleable.0_test.dart diff --git a/examples/api/test/material/range_slider/range_slider.0_test.dart b/packages/flutter/examples/api/test/material/range_slider/range_slider.0_test.dart similarity index 100% rename from examples/api/test/material/range_slider/range_slider.0_test.dart rename to packages/flutter/examples/api/test/material/range_slider/range_slider.0_test.dart diff --git a/examples/api/test/material/refresh_indicator/refresh_indicator.0_test.dart b/packages/flutter/examples/api/test/material/refresh_indicator/refresh_indicator.0_test.dart similarity index 100% rename from examples/api/test/material/refresh_indicator/refresh_indicator.0_test.dart rename to packages/flutter/examples/api/test/material/refresh_indicator/refresh_indicator.0_test.dart diff --git a/examples/api/test/material/refresh_indicator/refresh_indicator.1_test.dart b/packages/flutter/examples/api/test/material/refresh_indicator/refresh_indicator.1_test.dart similarity index 100% rename from examples/api/test/material/refresh_indicator/refresh_indicator.1_test.dart rename to packages/flutter/examples/api/test/material/refresh_indicator/refresh_indicator.1_test.dart diff --git a/examples/api/test/material/refresh_indicator/refresh_indicator.2_test.dart b/packages/flutter/examples/api/test/material/refresh_indicator/refresh_indicator.2_test.dart similarity index 100% rename from examples/api/test/material/refresh_indicator/refresh_indicator.2_test.dart rename to packages/flutter/examples/api/test/material/refresh_indicator/refresh_indicator.2_test.dart diff --git a/examples/api/test/material/reorderable_list/reorderable_list_view.0_test.dart b/packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.0_test.dart similarity index 100% rename from examples/api/test/material/reorderable_list/reorderable_list_view.0_test.dart rename to packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.0_test.dart diff --git a/examples/api/test/material/reorderable_list/reorderable_list_view.1_test.dart b/packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.1_test.dart similarity index 100% rename from examples/api/test/material/reorderable_list/reorderable_list_view.1_test.dart rename to packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.1_test.dart diff --git a/examples/api/test/material/reorderable_list/reorderable_list_view.2_test.dart b/packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.2_test.dart similarity index 100% rename from examples/api/test/material/reorderable_list/reorderable_list_view.2_test.dart rename to packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.2_test.dart diff --git a/examples/api/test/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0_test.dart b/packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0_test.dart similarity index 100% rename from examples/api/test/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0_test.dart rename to packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.build_default_drag_handles.0_test.dart diff --git a/examples/api/test/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0_test.dart b/packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0_test.dart similarity index 100% rename from examples/api/test/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0_test.dart rename to packages/flutter/examples/api/test/material/reorderable_list/reorderable_list_view.reorderable_list_view_builder.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.1_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.1_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.1_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.1_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.2_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.2_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.2_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.2_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.drawer.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.drawer.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.drawer.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.drawer.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.floating_action_button_animator.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.floating_action_button_animator.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.floating_action_button_animator.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.floating_action_button_animator.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.of.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.of.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.of.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.of.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold.of.1_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold.of.1_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold.of.1_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold.of.1_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_messenger.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_messenger.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_messenger.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_messenger.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_messenger.of.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_messenger.of.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_messenger.of.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_messenger.of.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_messenger.of.1_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_messenger.of.1_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_messenger.of.1_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_messenger.of.1_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.1_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.1_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.1_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.1_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.2_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.2_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.2_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.2_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart diff --git a/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart b/packages/flutter/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart similarity index 100% rename from examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart rename to packages/flutter/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart diff --git a/examples/api/test/material/scrollbar/scrollbar.0_test.dart b/packages/flutter/examples/api/test/material/scrollbar/scrollbar.0_test.dart similarity index 100% rename from examples/api/test/material/scrollbar/scrollbar.0_test.dart rename to packages/flutter/examples/api/test/material/scrollbar/scrollbar.0_test.dart diff --git a/examples/api/test/material/scrollbar/scrollbar.1_test.dart b/packages/flutter/examples/api/test/material/scrollbar/scrollbar.1_test.dart similarity index 100% rename from examples/api/test/material/scrollbar/scrollbar.1_test.dart rename to packages/flutter/examples/api/test/material/scrollbar/scrollbar.1_test.dart diff --git a/examples/api/test/material/search_anchor/search_anchor.0_test.dart b/packages/flutter/examples/api/test/material/search_anchor/search_anchor.0_test.dart similarity index 100% rename from examples/api/test/material/search_anchor/search_anchor.0_test.dart rename to packages/flutter/examples/api/test/material/search_anchor/search_anchor.0_test.dart diff --git a/examples/api/test/material/search_anchor/search_anchor.1_test.dart b/packages/flutter/examples/api/test/material/search_anchor/search_anchor.1_test.dart similarity index 100% rename from examples/api/test/material/search_anchor/search_anchor.1_test.dart rename to packages/flutter/examples/api/test/material/search_anchor/search_anchor.1_test.dart diff --git a/examples/api/test/material/search_anchor/search_anchor.2_test.dart b/packages/flutter/examples/api/test/material/search_anchor/search_anchor.2_test.dart similarity index 100% rename from examples/api/test/material/search_anchor/search_anchor.2_test.dart rename to packages/flutter/examples/api/test/material/search_anchor/search_anchor.2_test.dart diff --git a/examples/api/test/material/search_anchor/search_anchor.3_test.dart b/packages/flutter/examples/api/test/material/search_anchor/search_anchor.3_test.dart similarity index 100% rename from examples/api/test/material/search_anchor/search_anchor.3_test.dart rename to packages/flutter/examples/api/test/material/search_anchor/search_anchor.3_test.dart diff --git a/examples/api/test/material/search_anchor/search_anchor.4_test.dart b/packages/flutter/examples/api/test/material/search_anchor/search_anchor.4_test.dart similarity index 100% rename from examples/api/test/material/search_anchor/search_anchor.4_test.dart rename to packages/flutter/examples/api/test/material/search_anchor/search_anchor.4_test.dart diff --git a/examples/api/test/material/search_anchor/search_bar.0_test.dart b/packages/flutter/examples/api/test/material/search_anchor/search_bar.0_test.dart similarity index 100% rename from examples/api/test/material/search_anchor/search_bar.0_test.dart rename to packages/flutter/examples/api/test/material/search_anchor/search_bar.0_test.dart diff --git a/examples/api/test/material/segmented_button/segmented_button.0_test.dart b/packages/flutter/examples/api/test/material/segmented_button/segmented_button.0_test.dart similarity index 100% rename from examples/api/test/material/segmented_button/segmented_button.0_test.dart rename to packages/flutter/examples/api/test/material/segmented_button/segmented_button.0_test.dart diff --git a/examples/api/test/material/segmented_button/segmented_button.1_test.dart b/packages/flutter/examples/api/test/material/segmented_button/segmented_button.1_test.dart similarity index 100% rename from examples/api/test/material/segmented_button/segmented_button.1_test.dart rename to packages/flutter/examples/api/test/material/segmented_button/segmented_button.1_test.dart diff --git a/examples/api/test/material/selection_area/selection_area.0_test.dart b/packages/flutter/examples/api/test/material/selection_area/selection_area.0_test.dart similarity index 100% rename from examples/api/test/material/selection_area/selection_area.0_test.dart rename to packages/flutter/examples/api/test/material/selection_area/selection_area.0_test.dart diff --git a/examples/api/test/material/selection_area/selection_area.1_test.dart b/packages/flutter/examples/api/test/material/selection_area/selection_area.1_test.dart similarity index 100% rename from examples/api/test/material/selection_area/selection_area.1_test.dart rename to packages/flutter/examples/api/test/material/selection_area/selection_area.1_test.dart diff --git a/examples/api/test/material/selection_area/selection_area.2_test.dart b/packages/flutter/examples/api/test/material/selection_area/selection_area.2_test.dart similarity index 100% rename from examples/api/test/material/selection_area/selection_area.2_test.dart rename to packages/flutter/examples/api/test/material/selection_area/selection_area.2_test.dart diff --git a/examples/api/test/material/shaped_input_border/shaped_input_border.0_test.dart b/packages/flutter/examples/api/test/material/shaped_input_border/shaped_input_border.0_test.dart similarity index 100% rename from examples/api/test/material/shaped_input_border/shaped_input_border.0_test.dart rename to packages/flutter/examples/api/test/material/shaped_input_border/shaped_input_border.0_test.dart diff --git a/examples/api/test/material/slider/slider.0_test.dart b/packages/flutter/examples/api/test/material/slider/slider.0_test.dart similarity index 100% rename from examples/api/test/material/slider/slider.0_test.dart rename to packages/flutter/examples/api/test/material/slider/slider.0_test.dart diff --git a/examples/api/test/material/slider/slider.1_test.dart b/packages/flutter/examples/api/test/material/slider/slider.1_test.dart similarity index 100% rename from examples/api/test/material/slider/slider.1_test.dart rename to packages/flutter/examples/api/test/material/slider/slider.1_test.dart diff --git a/examples/api/test/material/snack_bar/snack_bar.0_test.dart b/packages/flutter/examples/api/test/material/snack_bar/snack_bar.0_test.dart similarity index 100% rename from examples/api/test/material/snack_bar/snack_bar.0_test.dart rename to packages/flutter/examples/api/test/material/snack_bar/snack_bar.0_test.dart diff --git a/examples/api/test/material/snack_bar/snack_bar.1_test.dart b/packages/flutter/examples/api/test/material/snack_bar/snack_bar.1_test.dart similarity index 100% rename from examples/api/test/material/snack_bar/snack_bar.1_test.dart rename to packages/flutter/examples/api/test/material/snack_bar/snack_bar.1_test.dart diff --git a/examples/api/test/material/snack_bar/snack_bar.2_test.dart b/packages/flutter/examples/api/test/material/snack_bar/snack_bar.2_test.dart similarity index 100% rename from examples/api/test/material/snack_bar/snack_bar.2_test.dart rename to packages/flutter/examples/api/test/material/snack_bar/snack_bar.2_test.dart diff --git a/examples/api/test/material/stepper/step_style.0_test.dart b/packages/flutter/examples/api/test/material/stepper/step_style.0_test.dart similarity index 100% rename from examples/api/test/material/stepper/step_style.0_test.dart rename to packages/flutter/examples/api/test/material/stepper/step_style.0_test.dart diff --git a/examples/api/test/material/stepper/stepper.0_test.dart b/packages/flutter/examples/api/test/material/stepper/stepper.0_test.dart similarity index 100% rename from examples/api/test/material/stepper/stepper.0_test.dart rename to packages/flutter/examples/api/test/material/stepper/stepper.0_test.dart diff --git a/examples/api/test/material/stepper/stepper.controls_builder.0_test.dart b/packages/flutter/examples/api/test/material/stepper/stepper.controls_builder.0_test.dart similarity index 100% rename from examples/api/test/material/stepper/stepper.controls_builder.0_test.dart rename to packages/flutter/examples/api/test/material/stepper/stepper.controls_builder.0_test.dart diff --git a/examples/api/test/material/switch/switch.0_test.dart b/packages/flutter/examples/api/test/material/switch/switch.0_test.dart similarity index 100% rename from examples/api/test/material/switch/switch.0_test.dart rename to packages/flutter/examples/api/test/material/switch/switch.0_test.dart diff --git a/examples/api/test/material/switch/switch.1_test.dart b/packages/flutter/examples/api/test/material/switch/switch.1_test.dart similarity index 100% rename from examples/api/test/material/switch/switch.1_test.dart rename to packages/flutter/examples/api/test/material/switch/switch.1_test.dart diff --git a/examples/api/test/material/switch/switch.2_test.dart b/packages/flutter/examples/api/test/material/switch/switch.2_test.dart similarity index 100% rename from examples/api/test/material/switch/switch.2_test.dart rename to packages/flutter/examples/api/test/material/switch/switch.2_test.dart diff --git a/examples/api/test/material/switch/switch.3_test.dart b/packages/flutter/examples/api/test/material/switch/switch.3_test.dart similarity index 100% rename from examples/api/test/material/switch/switch.3_test.dart rename to packages/flutter/examples/api/test/material/switch/switch.3_test.dart diff --git a/examples/api/test/material/switch/switch.4_test.dart b/packages/flutter/examples/api/test/material/switch/switch.4_test.dart similarity index 100% rename from examples/api/test/material/switch/switch.4_test.dart rename to packages/flutter/examples/api/test/material/switch/switch.4_test.dart diff --git a/examples/api/test/material/switch_list_tile/custom_labeled_switch.0_test.dart b/packages/flutter/examples/api/test/material/switch_list_tile/custom_labeled_switch.0_test.dart similarity index 100% rename from examples/api/test/material/switch_list_tile/custom_labeled_switch.0_test.dart rename to packages/flutter/examples/api/test/material/switch_list_tile/custom_labeled_switch.0_test.dart diff --git a/examples/api/test/material/switch_list_tile/custom_labeled_switch.1_test.dart b/packages/flutter/examples/api/test/material/switch_list_tile/custom_labeled_switch.1_test.dart similarity index 100% rename from examples/api/test/material/switch_list_tile/custom_labeled_switch.1_test.dart rename to packages/flutter/examples/api/test/material/switch_list_tile/custom_labeled_switch.1_test.dart diff --git a/examples/api/test/material/switch_list_tile/switch_list_tile.0_test.dart b/packages/flutter/examples/api/test/material/switch_list_tile/switch_list_tile.0_test.dart similarity index 100% rename from examples/api/test/material/switch_list_tile/switch_list_tile.0_test.dart rename to packages/flutter/examples/api/test/material/switch_list_tile/switch_list_tile.0_test.dart diff --git a/examples/api/test/material/switch_list_tile/switch_list_tile.1_test.dart b/packages/flutter/examples/api/test/material/switch_list_tile/switch_list_tile.1_test.dart similarity index 100% rename from examples/api/test/material/switch_list_tile/switch_list_tile.1_test.dart rename to packages/flutter/examples/api/test/material/switch_list_tile/switch_list_tile.1_test.dart diff --git a/examples/api/test/material/tab_controller/tab_controller.1_test.dart b/packages/flutter/examples/api/test/material/tab_controller/tab_controller.1_test.dart similarity index 100% rename from examples/api/test/material/tab_controller/tab_controller.1_test.dart rename to packages/flutter/examples/api/test/material/tab_controller/tab_controller.1_test.dart diff --git a/examples/api/test/material/tabs/tab_bar.0_test.dart b/packages/flutter/examples/api/test/material/tabs/tab_bar.0_test.dart similarity index 100% rename from examples/api/test/material/tabs/tab_bar.0_test.dart rename to packages/flutter/examples/api/test/material/tabs/tab_bar.0_test.dart diff --git a/examples/api/test/material/tabs/tab_bar.1_test.dart b/packages/flutter/examples/api/test/material/tabs/tab_bar.1_test.dart similarity index 100% rename from examples/api/test/material/tabs/tab_bar.1_test.dart rename to packages/flutter/examples/api/test/material/tabs/tab_bar.1_test.dart diff --git a/examples/api/test/material/tabs/tab_bar.2_test.dart b/packages/flutter/examples/api/test/material/tabs/tab_bar.2_test.dart similarity index 100% rename from examples/api/test/material/tabs/tab_bar.2_test.dart rename to packages/flutter/examples/api/test/material/tabs/tab_bar.2_test.dart diff --git a/examples/api/test/material/tabs/tab_bar.3_test.dart b/packages/flutter/examples/api/test/material/tabs/tab_bar.3_test.dart similarity index 100% rename from examples/api/test/material/tabs/tab_bar.3_test.dart rename to packages/flutter/examples/api/test/material/tabs/tab_bar.3_test.dart diff --git a/examples/api/test/material/tabs/tab_bar.indicator_animation.0_test.dart b/packages/flutter/examples/api/test/material/tabs/tab_bar.indicator_animation.0_test.dart similarity index 100% rename from examples/api/test/material/tabs/tab_bar.indicator_animation.0_test.dart rename to packages/flutter/examples/api/test/material/tabs/tab_bar.indicator_animation.0_test.dart diff --git a/examples/api/test/material/tabs/tab_bar.onFocusChange_test.dart b/packages/flutter/examples/api/test/material/tabs/tab_bar.onFocusChange_test.dart similarity index 100% rename from examples/api/test/material/tabs/tab_bar.onFocusChange_test.dart rename to packages/flutter/examples/api/test/material/tabs/tab_bar.onFocusChange_test.dart diff --git a/examples/api/test/material/tabs/tab_bar.onHover_test.dart b/packages/flutter/examples/api/test/material/tabs/tab_bar.onHover_test.dart similarity index 100% rename from examples/api/test/material/tabs/tab_bar.onHover_test.dart rename to packages/flutter/examples/api/test/material/tabs/tab_bar.onHover_test.dart diff --git a/examples/api/test/material/text_button/text_button.0_test.dart b/packages/flutter/examples/api/test/material/text_button/text_button.0_test.dart similarity index 100% rename from examples/api/test/material/text_button/text_button.0_test.dart rename to packages/flutter/examples/api/test/material/text_button/text_button.0_test.dart diff --git a/examples/api/test/material/text_button/text_button.1_test.dart b/packages/flutter/examples/api/test/material/text_button/text_button.1_test.dart similarity index 100% rename from examples/api/test/material/text_button/text_button.1_test.dart rename to packages/flutter/examples/api/test/material/text_button/text_button.1_test.dart diff --git a/examples/api/test/material/text_field/text_field.0_test.dart b/packages/flutter/examples/api/test/material/text_field/text_field.0_test.dart similarity index 100% rename from examples/api/test/material/text_field/text_field.0_test.dart rename to packages/flutter/examples/api/test/material/text_field/text_field.0_test.dart diff --git a/examples/api/test/material/text_field/text_field.1_test.dart b/packages/flutter/examples/api/test/material/text_field/text_field.1_test.dart similarity index 100% rename from examples/api/test/material/text_field/text_field.1_test.dart rename to packages/flutter/examples/api/test/material/text_field/text_field.1_test.dart diff --git a/examples/api/test/material/text_field/text_field.2_test.dart b/packages/flutter/examples/api/test/material/text_field/text_field.2_test.dart similarity index 100% rename from examples/api/test/material/text_field/text_field.2_test.dart rename to packages/flutter/examples/api/test/material/text_field/text_field.2_test.dart diff --git a/examples/api/test/material/text_field/text_field.3_test.dart b/packages/flutter/examples/api/test/material/text_field/text_field.3_test.dart similarity index 100% rename from examples/api/test/material/text_field/text_field.3_test.dart rename to packages/flutter/examples/api/test/material/text_field/text_field.3_test.dart diff --git a/examples/api/test/material/text_form_field/text_form_field.1_test.dart b/packages/flutter/examples/api/test/material/text_form_field/text_form_field.1_test.dart similarity index 100% rename from examples/api/test/material/text_form_field/text_form_field.1_test.dart rename to packages/flutter/examples/api/test/material/text_form_field/text_form_field.1_test.dart diff --git a/examples/api/test/material/text_form_field/text_form_field.2_test.dart b/packages/flutter/examples/api/test/material/text_form_field/text_form_field.2_test.dart similarity index 100% rename from examples/api/test/material/text_form_field/text_form_field.2_test.dart rename to packages/flutter/examples/api/test/material/text_form_field/text_form_field.2_test.dart diff --git a/examples/api/test/material/theme/theme_extension.1_test.dart b/packages/flutter/examples/api/test/material/theme/theme_extension.1_test.dart similarity index 100% rename from examples/api/test/material/theme/theme_extension.1_test.dart rename to packages/flutter/examples/api/test/material/theme/theme_extension.1_test.dart diff --git a/examples/api/test/material/theme_data/theme_data.0_test.dart b/packages/flutter/examples/api/test/material/theme_data/theme_data.0_test.dart similarity index 100% rename from examples/api/test/material/theme_data/theme_data.0_test.dart rename to packages/flutter/examples/api/test/material/theme_data/theme_data.0_test.dart diff --git a/examples/api/test/material/time_picker/show_time_picker.0_test.dart b/packages/flutter/examples/api/test/material/time_picker/show_time_picker.0_test.dart similarity index 100% rename from examples/api/test/material/time_picker/show_time_picker.0_test.dart rename to packages/flutter/examples/api/test/material/time_picker/show_time_picker.0_test.dart diff --git a/examples/api/test/material/toggle_buttons/toggle_buttons.0_test.dart b/packages/flutter/examples/api/test/material/toggle_buttons/toggle_buttons.0_test.dart similarity index 100% rename from examples/api/test/material/toggle_buttons/toggle_buttons.0_test.dart rename to packages/flutter/examples/api/test/material/toggle_buttons/toggle_buttons.0_test.dart diff --git a/examples/api/test/material/toggle_buttons/toggle_buttons.1_test.dart b/packages/flutter/examples/api/test/material/toggle_buttons/toggle_buttons.1_test.dart similarity index 100% rename from examples/api/test/material/toggle_buttons/toggle_buttons.1_test.dart rename to packages/flutter/examples/api/test/material/toggle_buttons/toggle_buttons.1_test.dart diff --git a/examples/api/test/material/tooltip/tooltip.0_test.dart b/packages/flutter/examples/api/test/material/tooltip/tooltip.0_test.dart similarity index 100% rename from examples/api/test/material/tooltip/tooltip.0_test.dart rename to packages/flutter/examples/api/test/material/tooltip/tooltip.0_test.dart diff --git a/examples/api/test/material/tooltip/tooltip.1_test.dart b/packages/flutter/examples/api/test/material/tooltip/tooltip.1_test.dart similarity index 100% rename from examples/api/test/material/tooltip/tooltip.1_test.dart rename to packages/flutter/examples/api/test/material/tooltip/tooltip.1_test.dart diff --git a/examples/api/test/material/tooltip/tooltip.2_test.dart b/packages/flutter/examples/api/test/material/tooltip/tooltip.2_test.dart similarity index 100% rename from examples/api/test/material/tooltip/tooltip.2_test.dart rename to packages/flutter/examples/api/test/material/tooltip/tooltip.2_test.dart diff --git a/examples/api/test/material/tooltip/tooltip.3_test.dart b/packages/flutter/examples/api/test/material/tooltip/tooltip.3_test.dart similarity index 100% rename from examples/api/test/material/tooltip/tooltip.3_test.dart rename to packages/flutter/examples/api/test/material/tooltip/tooltip.3_test.dart diff --git a/examples/api/test/material/widget_state_input_border/widget_state_input_border.0_test.dart b/packages/flutter/examples/api/test/material/widget_state_input_border/widget_state_input_border.0_test.dart similarity index 100% rename from examples/api/test/material/widget_state_input_border/widget_state_input_border.0_test.dart rename to packages/flutter/examples/api/test/material/widget_state_input_border/widget_state_input_border.0_test.dart diff --git a/examples/api/test/painting/axis_direction/axis_direction.0_test.dart b/packages/flutter/examples/api/test/painting/axis_direction/axis_direction.0_test.dart similarity index 100% rename from examples/api/test/painting/axis_direction/axis_direction.0_test.dart rename to packages/flutter/examples/api/test/painting/axis_direction/axis_direction.0_test.dart diff --git a/examples/api/test/painting/borders/border_side.stroke_align.0_test.dart b/packages/flutter/examples/api/test/painting/borders/border_side.stroke_align.0_test.dart similarity index 100% rename from examples/api/test/painting/borders/border_side.stroke_align.0_test.dart rename to packages/flutter/examples/api/test/painting/borders/border_side.stroke_align.0_test.dart diff --git a/examples/api/test/painting/gradient/linear_gradient.0_test.dart b/packages/flutter/examples/api/test/painting/gradient/linear_gradient.0_test.dart similarity index 88% rename from examples/api/test/painting/gradient/linear_gradient.0_test.dart rename to packages/flutter/examples/api/test/painting/gradient/linear_gradient.0_test.dart index 4e5308b45486f..e3eb4fb92e3e5 100644 --- a/examples/api/test/painting/gradient/linear_gradient.0_test.dart +++ b/packages/flutter/examples/api/test/painting/gradient/linear_gradient.0_test.dart @@ -2,6 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines. +@Tags(['reduced-test-set']) +library; + import 'package:flutter/material.dart'; import 'package:flutter_api_samples/painting/gradient/linear_gradient.0.dart' as example; diff --git a/examples/api/test/painting/image_provider/image_provider.0_test.dart b/packages/flutter/examples/api/test/painting/image_provider/image_provider.0_test.dart similarity index 100% rename from examples/api/test/painting/image_provider/image_provider.0_test.dart rename to packages/flutter/examples/api/test/painting/image_provider/image_provider.0_test.dart diff --git a/examples/api/test/painting/linear_border/linear_border.0_test.dart b/packages/flutter/examples/api/test/painting/linear_border/linear_border.0_test.dart similarity index 100% rename from examples/api/test/painting/linear_border/linear_border.0_test.dart rename to packages/flutter/examples/api/test/painting/linear_border/linear_border.0_test.dart diff --git a/examples/api/test/painting/rounded_superellipse_border/rounded_superellipse_border.0_test.dart b/packages/flutter/examples/api/test/painting/rounded_superellipse_border/rounded_superellipse_border.0_test.dart similarity index 100% rename from examples/api/test/painting/rounded_superellipse_border/rounded_superellipse_border.0_test.dart rename to packages/flutter/examples/api/test/painting/rounded_superellipse_border/rounded_superellipse_border.0_test.dart diff --git a/examples/api/test/painting/star_border/star_border.0_test.dart b/packages/flutter/examples/api/test/painting/star_border/star_border.0_test.dart similarity index 100% rename from examples/api/test/painting/star_border/star_border.0_test.dart rename to packages/flutter/examples/api/test/painting/star_border/star_border.0_test.dart diff --git a/examples/api/test/rendering/box/parent_data.0_test.dart b/packages/flutter/examples/api/test/rendering/box/parent_data.0_test.dart similarity index 100% rename from examples/api/test/rendering/box/parent_data.0_test.dart rename to packages/flutter/examples/api/test/rendering/box/parent_data.0_test.dart diff --git a/examples/api/test/rendering/growth_direction/growth_direction.0_test.dart b/packages/flutter/examples/api/test/rendering/growth_direction/growth_direction.0_test.dart similarity index 100% rename from examples/api/test/rendering/growth_direction/growth_direction.0_test.dart rename to packages/flutter/examples/api/test/rendering/growth_direction/growth_direction.0_test.dart diff --git a/examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart b/packages/flutter/examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart similarity index 100% rename from examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart rename to packages/flutter/examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart diff --git a/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart b/packages/flutter/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart similarity index 100% rename from examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart rename to packages/flutter/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart diff --git a/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart b/packages/flutter/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart similarity index 100% rename from examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart rename to packages/flutter/examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart diff --git a/examples/api/test/sample_templates/cupertino.0_test.dart b/packages/flutter/examples/api/test/sample_templates/cupertino.0_test.dart similarity index 100% rename from examples/api/test/sample_templates/cupertino.0_test.dart rename to packages/flutter/examples/api/test/sample_templates/cupertino.0_test.dart diff --git a/examples/api/test/sample_templates/material.0_test.dart b/packages/flutter/examples/api/test/sample_templates/material.0_test.dart similarity index 100% rename from examples/api/test/sample_templates/material.0_test.dart rename to packages/flutter/examples/api/test/sample_templates/material.0_test.dart diff --git a/examples/api/test/sample_templates/widgets.0_test.dart b/packages/flutter/examples/api/test/sample_templates/widgets.0_test.dart similarity index 100% rename from examples/api/test/sample_templates/widgets.0_test.dart rename to packages/flutter/examples/api/test/sample_templates/widgets.0_test.dart diff --git a/examples/api/test/services/binding/handle_request_app_exit.0_test.dart b/packages/flutter/examples/api/test/services/binding/handle_request_app_exit.0_test.dart similarity index 100% rename from examples/api/test/services/binding/handle_request_app_exit.0_test.dart rename to packages/flutter/examples/api/test/services/binding/handle_request_app_exit.0_test.dart diff --git a/examples/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart b/packages/flutter/examples/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart similarity index 100% rename from examples/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart rename to packages/flutter/examples/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart diff --git a/examples/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart b/packages/flutter/examples/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart similarity index 100% rename from examples/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart rename to packages/flutter/examples/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart diff --git a/examples/api/test/services/mouse_cursor/mouse_cursor.0_test.dart b/packages/flutter/examples/api/test/services/mouse_cursor/mouse_cursor.0_test.dart similarity index 100% rename from examples/api/test/services/mouse_cursor/mouse_cursor.0_test.dart rename to packages/flutter/examples/api/test/services/mouse_cursor/mouse_cursor.0_test.dart diff --git a/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0_test.dart b/packages/flutter/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0_test.dart similarity index 100% rename from examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0_test.dart rename to packages/flutter/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.0_test.dart diff --git a/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1_test.dart b/packages/flutter/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1_test.dart similarity index 100% rename from examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1_test.dart rename to packages/flutter/examples/api/test/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1_test.dart diff --git a/examples/api/test/services/text_input/text_input_control.0_test.dart b/packages/flutter/examples/api/test/services/text_input/text_input_control.0_test.dart similarity index 100% rename from examples/api/test/services/text_input/text_input_control.0_test.dart rename to packages/flutter/examples/api/test/services/text_input/text_input_control.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_alternative.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_alternative.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_alternative.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_alternative.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_alternative_fractions.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_alternative_fractions.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_alternative_fractions.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_alternative_fractions.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_case_sensitive_forms.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_case_sensitive_forms.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_case_sensitive_forms.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_case_sensitive_forms.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_character_variant.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_character_variant.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_character_variant.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_character_variant.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_contextual_alternates.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_contextual_alternates.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_contextual_alternates.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_contextual_alternates.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_denominator.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_denominator.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_denominator.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_denominator.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_fractions.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_fractions.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_fractions.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_fractions.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_historical_forms.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_historical_forms.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_historical_forms.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_historical_forms.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_historical_ligatures.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_historical_ligatures.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_historical_ligatures.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_historical_ligatures.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_lining_figures.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_lining_figures.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_lining_figures.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_lining_figures.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_locale_aware.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_locale_aware.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_locale_aware.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_locale_aware.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_notational_forms.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_notational_forms.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_notational_forms.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_notational_forms.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_numerators.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_numerators.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_numerators.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_numerators.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_oldstyle_figures.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_oldstyle_figures.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_oldstyle_figures.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_oldstyle_figures.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_ordinal_forms.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_ordinal_forms.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_ordinal_forms.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_ordinal_forms.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_proportional_figures.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_proportional_figures.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_proportional_figures.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_proportional_figures.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_scientific_inferiors.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_scientific_inferiors.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_scientific_inferiors.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_scientific_inferiors.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_slashed_zero.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_slashed_zero.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_slashed_zero.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_slashed_zero.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_stylistic_alternates.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_alternates.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_stylistic_alternates.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_alternates.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_stylistic_set.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.1_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.1_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_stylistic_set.1_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_stylistic_set.1_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_subscripts.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_subscripts.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_subscripts.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_subscripts.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_superscripts.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_superscripts.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_superscripts.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_superscripts.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_swash.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_swash.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_swash.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_swash.0_test.dart diff --git a/examples/api/test/ui/text/font_feature.font_feature_tabular_figures.0_test.dart b/packages/flutter/examples/api/test/ui/text/font_feature.font_feature_tabular_figures.0_test.dart similarity index 100% rename from examples/api/test/ui/text/font_feature.font_feature_tabular_figures.0_test.dart rename to packages/flutter/examples/api/test/ui/text/font_feature.font_feature_tabular_figures.0_test.dart diff --git a/examples/api/test/widgets/actions/action.action_overridable.0_test.dart b/packages/flutter/examples/api/test/widgets/actions/action.action_overridable.0_test.dart similarity index 100% rename from examples/api/test/widgets/actions/action.action_overridable.0_test.dart rename to packages/flutter/examples/api/test/widgets/actions/action.action_overridable.0_test.dart diff --git a/examples/api/test/widgets/actions/action_listener.0_test.dart b/packages/flutter/examples/api/test/widgets/actions/action_listener.0_test.dart similarity index 100% rename from examples/api/test/widgets/actions/action_listener.0_test.dart rename to packages/flutter/examples/api/test/widgets/actions/action_listener.0_test.dart diff --git a/examples/api/test/widgets/actions/actions.0_test.dart b/packages/flutter/examples/api/test/widgets/actions/actions.0_test.dart similarity index 100% rename from examples/api/test/widgets/actions/actions.0_test.dart rename to packages/flutter/examples/api/test/widgets/actions/actions.0_test.dart diff --git a/examples/api/test/widgets/actions/focusable_action_detector.0_test.dart b/packages/flutter/examples/api/test/widgets/actions/focusable_action_detector.0_test.dart similarity index 100% rename from examples/api/test/widgets/actions/focusable_action_detector.0_test.dart rename to packages/flutter/examples/api/test/widgets/actions/focusable_action_detector.0_test.dart diff --git a/examples/api/test/widgets/animated_grid/animated_grid.0_test.dart b/packages/flutter/examples/api/test/widgets/animated_grid/animated_grid.0_test.dart similarity index 100% rename from examples/api/test/widgets/animated_grid/animated_grid.0_test.dart rename to packages/flutter/examples/api/test/widgets/animated_grid/animated_grid.0_test.dart diff --git a/examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart b/packages/flutter/examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart similarity index 100% rename from examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart rename to packages/flutter/examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart diff --git a/examples/api/test/widgets/animated_list/animated_list.0_test.dart b/packages/flutter/examples/api/test/widgets/animated_list/animated_list.0_test.dart similarity index 100% rename from examples/api/test/widgets/animated_list/animated_list.0_test.dart rename to packages/flutter/examples/api/test/widgets/animated_list/animated_list.0_test.dart diff --git a/examples/api/test/widgets/animated_list/animated_list_separated.0_test.dart b/packages/flutter/examples/api/test/widgets/animated_list/animated_list_separated.0_test.dart similarity index 100% rename from examples/api/test/widgets/animated_list/animated_list_separated.0_test.dart rename to packages/flutter/examples/api/test/widgets/animated_list/animated_list_separated.0_test.dart diff --git a/examples/api/test/widgets/animated_list/sliver_animated_list.0_test.dart b/packages/flutter/examples/api/test/widgets/animated_list/sliver_animated_list.0_test.dart similarity index 100% rename from examples/api/test/widgets/animated_list/sliver_animated_list.0_test.dart rename to packages/flutter/examples/api/test/widgets/animated_list/sliver_animated_list.0_test.dart diff --git a/examples/api/test/widgets/animated_size/animated_size.0_test.dart b/packages/flutter/examples/api/test/widgets/animated_size/animated_size.0_test.dart similarity index 100% rename from examples/api/test/widgets/animated_size/animated_size.0_test.dart rename to packages/flutter/examples/api/test/widgets/animated_size/animated_size.0_test.dart diff --git a/examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart b/packages/flutter/examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart similarity index 100% rename from examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart rename to packages/flutter/examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart diff --git a/examples/api/test/widgets/app/widgets_app.widgets_app.0_test.dart b/packages/flutter/examples/api/test/widgets/app/widgets_app.widgets_app.0_test.dart similarity index 100% rename from examples/api/test/widgets/app/widgets_app.widgets_app.0_test.dart rename to packages/flutter/examples/api/test/widgets/app/widgets_app.widgets_app.0_test.dart diff --git a/examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.0_test.dart b/packages/flutter/examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.0_test.dart similarity index 100% rename from examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.0_test.dart rename to packages/flutter/examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.0_test.dart diff --git a/examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.1_test.dart b/packages/flutter/examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.1_test.dart similarity index 100% rename from examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.1_test.dart rename to packages/flutter/examples/api/test/widgets/app_lifecycle_listener/app_lifecycle_listener.1_test.dart diff --git a/examples/api/test/widgets/async/future_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/async/future_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/async/future_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/async/future_builder.0_test.dart diff --git a/examples/api/test/widgets/async/stream_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/async/stream_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/async/stream_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/async/stream_builder.0_test.dart diff --git a/examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart b/packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart similarity index 100% rename from examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart rename to packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart diff --git a/examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart b/packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart similarity index 100% rename from examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart rename to packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart diff --git a/examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart b/packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart similarity index 100% rename from examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart rename to packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart diff --git a/examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart b/packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart similarity index 100% rename from examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart rename to packages/flutter/examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart diff --git a/examples/api/test/widgets/autofill/autofill_group.0_test.dart b/packages/flutter/examples/api/test/widgets/autofill/autofill_group.0_test.dart similarity index 100% rename from examples/api/test/widgets/autofill/autofill_group.0_test.dart rename to packages/flutter/examples/api/test/widgets/autofill/autofill_group.0_test.dart diff --git a/examples/api/test/widgets/basic/absorb_pointer.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/absorb_pointer.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/absorb_pointer.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/absorb_pointer.0_test.dart diff --git a/examples/api/test/widgets/basic/aspect_ratio.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/aspect_ratio.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/aspect_ratio.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/aspect_ratio.0_test.dart diff --git a/examples/api/test/widgets/basic/aspect_ratio.1_test.dart b/packages/flutter/examples/api/test/widgets/basic/aspect_ratio.1_test.dart similarity index 100% rename from examples/api/test/widgets/basic/aspect_ratio.1_test.dart rename to packages/flutter/examples/api/test/widgets/basic/aspect_ratio.1_test.dart diff --git a/examples/api/test/widgets/basic/aspect_ratio.2_test.dart b/packages/flutter/examples/api/test/widgets/basic/aspect_ratio.2_test.dart similarity index 100% rename from examples/api/test/widgets/basic/aspect_ratio.2_test.dart rename to packages/flutter/examples/api/test/widgets/basic/aspect_ratio.2_test.dart diff --git a/examples/api/test/widgets/basic/clip_rrect.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/clip_rrect.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/clip_rrect.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/clip_rrect.0_test.dart diff --git a/examples/api/test/widgets/basic/clip_rrect.1_test.dart b/packages/flutter/examples/api/test/widgets/basic/clip_rrect.1_test.dart similarity index 100% rename from examples/api/test/widgets/basic/clip_rrect.1_test.dart rename to packages/flutter/examples/api/test/widgets/basic/clip_rrect.1_test.dart diff --git a/examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart diff --git a/examples/api/test/widgets/basic/expanded.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/expanded.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/expanded.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/expanded.0_test.dart diff --git a/examples/api/test/widgets/basic/expanded.1_test.dart b/packages/flutter/examples/api/test/widgets/basic/expanded.1_test.dart similarity index 100% rename from examples/api/test/widgets/basic/expanded.1_test.dart rename to packages/flutter/examples/api/test/widgets/basic/expanded.1_test.dart diff --git a/examples/api/test/widgets/basic/fitted_box.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/fitted_box.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/fitted_box.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/fitted_box.0_test.dart diff --git a/examples/api/test/widgets/basic/flow.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/flow.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/flow.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/flow.0_test.dart diff --git a/examples/api/test/widgets/basic/fractionally_sized_box.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/fractionally_sized_box.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/fractionally_sized_box.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/fractionally_sized_box.0_test.dart diff --git a/examples/api/test/widgets/basic/ignore_pointer.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/ignore_pointer.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/ignore_pointer.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/ignore_pointer.0_test.dart diff --git a/examples/api/test/widgets/basic/indexed_stack.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/indexed_stack.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/indexed_stack.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/indexed_stack.0_test.dart diff --git a/examples/api/test/widgets/basic/listener.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/listener.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/listener.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/listener.0_test.dart diff --git a/examples/api/test/widgets/basic/mouse_region.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/mouse_region.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/mouse_region.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/mouse_region.0_test.dart diff --git a/examples/api/test/widgets/basic/mouse_region.on_exit.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/mouse_region.on_exit.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/mouse_region.on_exit.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/mouse_region.on_exit.0_test.dart diff --git a/examples/api/test/widgets/basic/mouse_region.on_exit.1_test.dart b/packages/flutter/examples/api/test/widgets/basic/mouse_region.on_exit.1_test.dart similarity index 100% rename from examples/api/test/widgets/basic/mouse_region.on_exit.1_test.dart rename to packages/flutter/examples/api/test/widgets/basic/mouse_region.on_exit.1_test.dart diff --git a/examples/api/test/widgets/basic/offstage.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/offstage.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/offstage.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/offstage.0_test.dart diff --git a/examples/api/test/widgets/basic/overflowbox.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/overflowbox.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/overflowbox.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/overflowbox.0_test.dart diff --git a/examples/api/test/widgets/basic/physical_shape.0_test.dart b/packages/flutter/examples/api/test/widgets/basic/physical_shape.0_test.dart similarity index 100% rename from examples/api/test/widgets/basic/physical_shape.0_test.dart rename to packages/flutter/examples/api/test/widgets/basic/physical_shape.0_test.dart diff --git a/examples/api/test/widgets/binding/widget_binding_observer.0_test.dart b/packages/flutter/examples/api/test/widgets/binding/widget_binding_observer.0_test.dart similarity index 100% rename from examples/api/test/widgets/binding/widget_binding_observer.0_test.dart rename to packages/flutter/examples/api/test/widgets/binding/widget_binding_observer.0_test.dart diff --git a/examples/api/test/widgets/color_filter/color_filtered.0_test.dart b/packages/flutter/examples/api/test/widgets/color_filter/color_filtered.0_test.dart similarity index 100% rename from examples/api/test/widgets/color_filter/color_filtered.0_test.dart rename to packages/flutter/examples/api/test/widgets/color_filter/color_filtered.0_test.dart diff --git a/examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart b/packages/flutter/examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart similarity index 100% rename from examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart rename to packages/flutter/examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart diff --git a/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart diff --git a/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart b/packages/flutter/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart similarity index 100% rename from examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart rename to packages/flutter/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart diff --git a/examples/api/test/widgets/dismissible/dismissible.0_test.dart b/packages/flutter/examples/api/test/widgets/dismissible/dismissible.0_test.dart similarity index 100% rename from examples/api/test/widgets/dismissible/dismissible.0_test.dart rename to packages/flutter/examples/api/test/widgets/dismissible/dismissible.0_test.dart diff --git a/examples/api/test/widgets/drag_target/draggable.0_test.dart b/packages/flutter/examples/api/test/widgets/drag_target/draggable.0_test.dart similarity index 100% rename from examples/api/test/widgets/drag_target/draggable.0_test.dart rename to packages/flutter/examples/api/test/widgets/drag_target/draggable.0_test.dart diff --git a/examples/api/test/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0_test.dart b/packages/flutter/examples/api/test/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0_test.dart similarity index 100% rename from examples/api/test/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0_test.dart rename to packages/flutter/examples/api/test/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0_test.dart diff --git a/examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart b/packages/flutter/examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart similarity index 100% rename from examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart rename to packages/flutter/examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart diff --git a/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart b/packages/flutter/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart similarity index 100% rename from examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart rename to packages/flutter/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart diff --git a/examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart b/packages/flutter/examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart similarity index 100% rename from examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart rename to packages/flutter/examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart diff --git a/examples/api/test/widgets/editable_text/text_editing_controller.1_test.dart b/packages/flutter/examples/api/test/widgets/editable_text/text_editing_controller.1_test.dart similarity index 100% rename from examples/api/test/widgets/editable_text/text_editing_controller.1_test.dart rename to packages/flutter/examples/api/test/widgets/editable_text/text_editing_controller.1_test.dart diff --git a/examples/api/test/widgets/expansible/expansible.0_test.dart b/packages/flutter/examples/api/test/widgets/expansible/expansible.0_test.dart similarity index 100% rename from examples/api/test/widgets/expansible/expansible.0_test.dart rename to packages/flutter/examples/api/test/widgets/expansible/expansible.0_test.dart diff --git a/examples/api/test/widgets/focus_manager/focus_node.0_test.dart b/packages/flutter/examples/api/test/widgets/focus_manager/focus_node.0_test.dart similarity index 100% rename from examples/api/test/widgets/focus_manager/focus_node.0_test.dart rename to packages/flutter/examples/api/test/widgets/focus_manager/focus_node.0_test.dart diff --git a/examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart b/packages/flutter/examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart similarity index 100% rename from examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart rename to packages/flutter/examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart diff --git a/examples/api/test/widgets/focus_scope/focus.0_test.dart b/packages/flutter/examples/api/test/widgets/focus_scope/focus.0_test.dart similarity index 100% rename from examples/api/test/widgets/focus_scope/focus.0_test.dart rename to packages/flutter/examples/api/test/widgets/focus_scope/focus.0_test.dart diff --git a/examples/api/test/widgets/focus_scope/focus.1_test.dart b/packages/flutter/examples/api/test/widgets/focus_scope/focus.1_test.dart similarity index 100% rename from examples/api/test/widgets/focus_scope/focus.1_test.dart rename to packages/flutter/examples/api/test/widgets/focus_scope/focus.1_test.dart diff --git a/examples/api/test/widgets/focus_scope/focus.2_test.dart b/packages/flutter/examples/api/test/widgets/focus_scope/focus.2_test.dart similarity index 100% rename from examples/api/test/widgets/focus_scope/focus.2_test.dart rename to packages/flutter/examples/api/test/widgets/focus_scope/focus.2_test.dart diff --git a/examples/api/test/widgets/focus_scope/focus_scope.0_test.dart b/packages/flutter/examples/api/test/widgets/focus_scope/focus_scope.0_test.dart similarity index 100% rename from examples/api/test/widgets/focus_scope/focus_scope.0_test.dart rename to packages/flutter/examples/api/test/widgets/focus_scope/focus_scope.0_test.dart diff --git a/examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart b/packages/flutter/examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart similarity index 100% rename from examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart rename to packages/flutter/examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart diff --git a/examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart b/packages/flutter/examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart similarity index 100% rename from examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart rename to packages/flutter/examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart diff --git a/examples/api/test/widgets/form/form.0_test.dart b/packages/flutter/examples/api/test/widgets/form/form.0_test.dart similarity index 100% rename from examples/api/test/widgets/form/form.0_test.dart rename to packages/flutter/examples/api/test/widgets/form/form.0_test.dart diff --git a/examples/api/test/widgets/form/form.1_test.dart b/packages/flutter/examples/api/test/widgets/form/form.1_test.dart similarity index 100% rename from examples/api/test/widgets/form/form.1_test.dart rename to packages/flutter/examples/api/test/widgets/form/form.1_test.dart diff --git a/examples/api/test/widgets/framework/build_owner.0_test.dart b/packages/flutter/examples/api/test/widgets/framework/build_owner.0_test.dart similarity index 100% rename from examples/api/test/widgets/framework/build_owner.0_test.dart rename to packages/flutter/examples/api/test/widgets/framework/build_owner.0_test.dart diff --git a/examples/api/test/widgets/framework/error_widget.0_test.dart b/packages/flutter/examples/api/test/widgets/framework/error_widget.0_test.dart similarity index 100% rename from examples/api/test/widgets/framework/error_widget.0_test.dart rename to packages/flutter/examples/api/test/widgets/framework/error_widget.0_test.dart diff --git a/examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart b/packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart similarity index 100% rename from examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart rename to packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart diff --git a/examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart b/packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart similarity index 100% rename from examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart rename to packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart diff --git a/examples/api/test/widgets/gesture_detector/gesture_detector.2_test.dart b/packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.2_test.dart similarity index 100% rename from examples/api/test/widgets/gesture_detector/gesture_detector.2_test.dart rename to packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.2_test.dart diff --git a/examples/api/test/widgets/gesture_detector/gesture_detector.3_test.dart b/packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.3_test.dart similarity index 100% rename from examples/api/test/widgets/gesture_detector/gesture_detector.3_test.dart rename to packages/flutter/examples/api/test/widgets/gesture_detector/gesture_detector.3_test.dart diff --git a/examples/api/test/widgets/hardware_keyboard/key_event_manager.0_test.dart b/packages/flutter/examples/api/test/widgets/hardware_keyboard/key_event_manager.0_test.dart similarity index 100% rename from examples/api/test/widgets/hardware_keyboard/key_event_manager.0_test.dart rename to packages/flutter/examples/api/test/widgets/hardware_keyboard/key_event_manager.0_test.dart diff --git a/examples/api/test/widgets/heroes/hero.0_test.dart b/packages/flutter/examples/api/test/widgets/heroes/hero.0_test.dart similarity index 100% rename from examples/api/test/widgets/heroes/hero.0_test.dart rename to packages/flutter/examples/api/test/widgets/heroes/hero.0_test.dart diff --git a/examples/api/test/widgets/heroes/hero.1_test.dart b/packages/flutter/examples/api/test/widgets/heroes/hero.1_test.dart similarity index 100% rename from examples/api/test/widgets/heroes/hero.1_test.dart rename to packages/flutter/examples/api/test/widgets/heroes/hero.1_test.dart diff --git a/examples/api/test/widgets/image/image.error_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/image/image.error_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/image/image.error_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/image/image.error_builder.0_test.dart diff --git a/examples/api/test/widgets/image/image.frame_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/image/image.frame_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/image/image.frame_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/image/image.frame_builder.0_test.dart diff --git a/examples/api/test/widgets/image/image.loading_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/image/image.loading_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/image/image.loading_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/image/image.loading_builder.0_test.dart diff --git a/examples/api/test/widgets/implicit_animations/animated_align.0_test.dart b/packages/flutter/examples/api/test/widgets/implicit_animations/animated_align.0_test.dart similarity index 100% rename from examples/api/test/widgets/implicit_animations/animated_align.0_test.dart rename to packages/flutter/examples/api/test/widgets/implicit_animations/animated_align.0_test.dart diff --git a/examples/api/test/widgets/implicit_animations/animated_container.0_test.dart b/packages/flutter/examples/api/test/widgets/implicit_animations/animated_container.0_test.dart similarity index 100% rename from examples/api/test/widgets/implicit_animations/animated_container.0_test.dart rename to packages/flutter/examples/api/test/widgets/implicit_animations/animated_container.0_test.dart diff --git a/examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart b/packages/flutter/examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart similarity index 100% rename from examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart rename to packages/flutter/examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart diff --git a/examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart b/packages/flutter/examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart similarity index 100% rename from examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart rename to packages/flutter/examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart diff --git a/examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart b/packages/flutter/examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart similarity index 100% rename from examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart rename to packages/flutter/examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart diff --git a/examples/api/test/widgets/implicit_animations/animated_slide.0_test.dart b/packages/flutter/examples/api/test/widgets/implicit_animations/animated_slide.0_test.dart similarity index 100% rename from examples/api/test/widgets/implicit_animations/animated_slide.0_test.dart rename to packages/flutter/examples/api/test/widgets/implicit_animations/animated_slide.0_test.dart diff --git a/examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart b/packages/flutter/examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart similarity index 100% rename from examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart rename to packages/flutter/examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart diff --git a/examples/api/test/widgets/inherited_model/inherited_model.0_test.dart b/packages/flutter/examples/api/test/widgets/inherited_model/inherited_model.0_test.dart similarity index 100% rename from examples/api/test/widgets/inherited_model/inherited_model.0_test.dart rename to packages/flutter/examples/api/test/widgets/inherited_model/inherited_model.0_test.dart diff --git a/examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart b/packages/flutter/examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart similarity index 100% rename from examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart rename to packages/flutter/examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart diff --git a/examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart b/packages/flutter/examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart similarity index 100% rename from examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart rename to packages/flutter/examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart diff --git a/examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart b/packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart similarity index 100% rename from examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart rename to packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart diff --git a/examples/api/test/widgets/interactive_viewer/interactive_viewer.builder.0_test.dart b/packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/interactive_viewer/interactive_viewer.builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.builder.0_test.dart diff --git a/examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart b/packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart similarity index 100% rename from examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart rename to packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart diff --git a/examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart b/packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart similarity index 100% rename from examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart rename to packages/flutter/examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart diff --git a/examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart b/packages/flutter/examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart similarity index 100% rename from examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart rename to packages/flutter/examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart diff --git a/examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart b/packages/flutter/examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart similarity index 100% rename from examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart rename to packages/flutter/examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart diff --git a/examples/api/test/widgets/keep_alive/keep_alive.0_test.dart b/packages/flutter/examples/api/test/widgets/keep_alive/keep_alive.0_test.dart similarity index 100% rename from examples/api/test/widgets/keep_alive/keep_alive.0_test.dart rename to packages/flutter/examples/api/test/widgets/keep_alive/keep_alive.0_test.dart diff --git a/examples/api/test/widgets/layout_builder/layout_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/layout_builder/layout_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/layout_builder/layout_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/layout_builder/layout_builder.0_test.dart diff --git a/examples/api/test/widgets/magnifier/magnifier.0_test.dart b/packages/flutter/examples/api/test/widgets/magnifier/magnifier.0_test.dart similarity index 94% rename from examples/api/test/widgets/magnifier/magnifier.0_test.dart rename to packages/flutter/examples/api/test/widgets/magnifier/magnifier.0_test.dart index 8d69c88b4f757..04e39ea6397d6 100644 --- a/examples/api/test/widgets/magnifier/magnifier.0_test.dart +++ b/packages/flutter/examples/api/test/widgets/magnifier/magnifier.0_test.dart @@ -2,6 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines. +@Tags(['reduced-test-set']) +library; + import 'package:flutter/material.dart'; import 'package:flutter_api_samples/widgets/magnifier/magnifier.0.dart' as example; diff --git a/examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart b/packages/flutter/examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart similarity index 100% rename from examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart rename to packages/flutter/examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart diff --git a/examples/api/test/widgets/navigator/navigator.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/navigator.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/navigator.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/navigator.0_test.dart diff --git a/examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart diff --git a/examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart diff --git a/examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart diff --git a/examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart diff --git a/examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart diff --git a/examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart diff --git a/examples/api/test/widgets/navigator/restorable_route_future.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator/restorable_route_future.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator/restorable_route_future.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator/restorable_route_future.0_test.dart diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart b/packages/flutter/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart similarity index 100% rename from examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart rename to packages/flutter/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart b/packages/flutter/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart similarity index 100% rename from examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart rename to packages/flutter/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart diff --git a/examples/api/test/widgets/navigator_utils.dart b/packages/flutter/examples/api/test/widgets/navigator_utils.dart similarity index 100% rename from examples/api/test/widgets/navigator_utils.dart rename to packages/flutter/examples/api/test/widgets/navigator_utils.dart diff --git a/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart b/packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart similarity index 100% rename from examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart rename to packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart diff --git a/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart b/packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart similarity index 100% rename from examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart rename to packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart diff --git a/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart b/packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart similarity index 100% rename from examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart rename to packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart diff --git a/examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart b/packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart similarity index 100% rename from examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart rename to packages/flutter/examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart diff --git a/examples/api/test/widgets/notification_listener/notification.0_test.dart b/packages/flutter/examples/api/test/widgets/notification_listener/notification.0_test.dart similarity index 100% rename from examples/api/test/widgets/notification_listener/notification.0_test.dart rename to packages/flutter/examples/api/test/widgets/notification_listener/notification.0_test.dart diff --git a/examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart b/packages/flutter/examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart similarity index 100% rename from examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart rename to packages/flutter/examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart diff --git a/examples/api/test/widgets/overlay/overlay.0_test.dart b/packages/flutter/examples/api/test/widgets/overlay/overlay.0_test.dart similarity index 100% rename from examples/api/test/widgets/overlay/overlay.0_test.dart rename to packages/flutter/examples/api/test/widgets/overlay/overlay.0_test.dart diff --git a/examples/api/test/widgets/overlay/overlay_portal.0_test.dart b/packages/flutter/examples/api/test/widgets/overlay/overlay_portal.0_test.dart similarity index 100% rename from examples/api/test/widgets/overlay/overlay_portal.0_test.dart rename to packages/flutter/examples/api/test/widgets/overlay/overlay_portal.0_test.dart diff --git a/examples/api/test/widgets/overlay/overlay_portal.1_test.dart b/packages/flutter/examples/api/test/widgets/overlay/overlay_portal.1_test.dart similarity index 100% rename from examples/api/test/widgets/overlay/overlay_portal.1_test.dart rename to packages/flutter/examples/api/test/widgets/overlay/overlay_portal.1_test.dart diff --git a/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart b/packages/flutter/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart similarity index 100% rename from examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart rename to packages/flutter/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart diff --git a/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart b/packages/flutter/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart similarity index 100% rename from examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart rename to packages/flutter/examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart diff --git a/examples/api/test/widgets/page/page_can_pop.0_test.dart b/packages/flutter/examples/api/test/widgets/page/page_can_pop.0_test.dart similarity index 100% rename from examples/api/test/widgets/page/page_can_pop.0_test.dart rename to packages/flutter/examples/api/test/widgets/page/page_can_pop.0_test.dart diff --git a/examples/api/test/widgets/page_storage/page_storage.0_test.dart b/packages/flutter/examples/api/test/widgets/page_storage/page_storage.0_test.dart similarity index 100% rename from examples/api/test/widgets/page_storage/page_storage.0_test.dart rename to packages/flutter/examples/api/test/widgets/page_storage/page_storage.0_test.dart diff --git a/examples/api/test/widgets/page_transitions_builder/page_transitions_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/page_transitions_builder/page_transitions_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/page_transitions_builder/page_transitions_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/page_transitions_builder/page_transitions_builder.0_test.dart diff --git a/examples/api/test/widgets/page_view/page_view.0_test.dart b/packages/flutter/examples/api/test/widgets/page_view/page_view.0_test.dart similarity index 100% rename from examples/api/test/widgets/page_view/page_view.0_test.dart rename to packages/flutter/examples/api/test/widgets/page_view/page_view.0_test.dart diff --git a/examples/api/test/widgets/page_view/page_view.1_test.dart b/packages/flutter/examples/api/test/widgets/page_view/page_view.1_test.dart similarity index 100% rename from examples/api/test/widgets/page_view/page_view.1_test.dart rename to packages/flutter/examples/api/test/widgets/page_view/page_view.1_test.dart diff --git a/examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart b/packages/flutter/examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart similarity index 100% rename from examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart rename to packages/flutter/examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart diff --git a/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart b/packages/flutter/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart similarity index 100% rename from examples/api/test/widgets/pop_scope/pop_scope.0_test.dart rename to packages/flutter/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart diff --git a/examples/api/test/widgets/pop_scope/pop_scope.1_test.dart b/packages/flutter/examples/api/test/widgets/pop_scope/pop_scope.1_test.dart similarity index 100% rename from examples/api/test/widgets/pop_scope/pop_scope.1_test.dart rename to packages/flutter/examples/api/test/widgets/pop_scope/pop_scope.1_test.dart diff --git a/examples/api/test/widgets/preferred_size/preferred_size.0_test.dart b/packages/flutter/examples/api/test/widgets/preferred_size/preferred_size.0_test.dart similarity index 100% rename from examples/api/test/widgets/preferred_size/preferred_size.0_test.dart rename to packages/flutter/examples/api/test/widgets/preferred_size/preferred_size.0_test.dart diff --git a/examples/api/test/widgets/radio_group/radio_group.0_test.dart b/packages/flutter/examples/api/test/widgets/radio_group/radio_group.0_test.dart similarity index 100% rename from examples/api/test/widgets/radio_group/radio_group.0_test.dart rename to packages/flutter/examples/api/test/widgets/radio_group/radio_group.0_test.dart diff --git a/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart b/packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart similarity index 100% rename from examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart rename to packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart diff --git a/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.1_test.dart b/packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.1_test.dart similarity index 100% rename from examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.1_test.dart rename to packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.1_test.dart diff --git a/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.2_test.dart b/packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.2_test.dart similarity index 100% rename from examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.2_test.dart rename to packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.2_test.dart diff --git a/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.3_test.dart b/packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.3_test.dart similarity index 100% rename from examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.3_test.dart rename to packages/flutter/examples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.3_test.dart diff --git a/examples/api/test/widgets/raw_tooltip/raw_tooltip.0_test.dart b/packages/flutter/examples/api/test/widgets/raw_tooltip/raw_tooltip.0_test.dart similarity index 100% rename from examples/api/test/widgets/raw_tooltip/raw_tooltip.0_test.dart rename to packages/flutter/examples/api/test/widgets/raw_tooltip/raw_tooltip.0_test.dart diff --git a/examples/api/test/widgets/repeating_animation_builder/repeating_animation_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/repeating_animation_builder/repeating_animation_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/repeating_animation_builder/repeating_animation_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/repeating_animation_builder/repeating_animation_builder.0_test.dart diff --git a/examples/api/test/widgets/restoration/restoration_mixin.0_test.dart b/packages/flutter/examples/api/test/widgets/restoration/restoration_mixin.0_test.dart similarity index 100% rename from examples/api/test/widgets/restoration/restoration_mixin.0_test.dart rename to packages/flutter/examples/api/test/widgets/restoration/restoration_mixin.0_test.dart diff --git a/examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart b/packages/flutter/examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart similarity index 100% rename from examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart rename to packages/flutter/examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart diff --git a/examples/api/test/widgets/routes/flexible_route_transitions.0_test.dart b/packages/flutter/examples/api/test/widgets/routes/flexible_route_transitions.0_test.dart similarity index 100% rename from examples/api/test/widgets/routes/flexible_route_transitions.0_test.dart rename to packages/flutter/examples/api/test/widgets/routes/flexible_route_transitions.0_test.dart diff --git a/examples/api/test/widgets/routes/flexible_route_transitions.1_test.dart b/packages/flutter/examples/api/test/widgets/routes/flexible_route_transitions.1_test.dart similarity index 100% rename from examples/api/test/widgets/routes/flexible_route_transitions.1_test.dart rename to packages/flutter/examples/api/test/widgets/routes/flexible_route_transitions.1_test.dart diff --git a/examples/api/test/widgets/routes/local_history_entry.0_test.dart b/packages/flutter/examples/api/test/widgets/routes/local_history_entry.0_test.dart similarity index 100% rename from examples/api/test/widgets/routes/local_history_entry.0_test.dart rename to packages/flutter/examples/api/test/widgets/routes/local_history_entry.0_test.dart diff --git a/examples/api/test/widgets/routes/popup_route.0_test.dart b/packages/flutter/examples/api/test/widgets/routes/popup_route.0_test.dart similarity index 100% rename from examples/api/test/widgets/routes/popup_route.0_test.dart rename to packages/flutter/examples/api/test/widgets/routes/popup_route.0_test.dart diff --git a/examples/api/test/widgets/routes/route_observer.0_test.dart b/packages/flutter/examples/api/test/widgets/routes/route_observer.0_test.dart similarity index 100% rename from examples/api/test/widgets/routes/route_observer.0_test.dart rename to packages/flutter/examples/api/test/widgets/routes/route_observer.0_test.dart diff --git a/examples/api/test/widgets/routes/show_general_dialog.0_test.dart b/packages/flutter/examples/api/test/widgets/routes/show_general_dialog.0_test.dart similarity index 100% rename from examples/api/test/widgets/routes/show_general_dialog.0_test.dart rename to packages/flutter/examples/api/test/widgets/routes/show_general_dialog.0_test.dart diff --git a/examples/api/test/widgets/safe_area/safe_area.0_test.dart b/packages/flutter/examples/api/test/widgets/safe_area/safe_area.0_test.dart similarity index 100% rename from examples/api/test/widgets/safe_area/safe_area.0_test.dart rename to packages/flutter/examples/api/test/widgets/safe_area/safe_area.0_test.dart diff --git a/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_end_notification/scroll_end_notification.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.0_test.dart diff --git a/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.1_test.dart b/packages/flutter/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.1_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_end_notification/scroll_end_notification.1_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_end_notification/scroll_end_notification.1_test.dart diff --git a/examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart diff --git a/examples/api/test/widgets/scroll_position/is_scrolling_listener.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_position/is_scrolling_listener.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_position/is_scrolling_listener.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_position/is_scrolling_listener.0_test.dart diff --git a/examples/api/test/widgets/scroll_position/scroll_controller_notification.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_position/scroll_controller_notification.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_position/scroll_controller_notification.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_position/scroll_controller_notification.0_test.dart diff --git a/examples/api/test/widgets/scroll_position/scroll_controller_on_attach.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_position/scroll_controller_on_attach.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_position/scroll_controller_on_attach.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_position/scroll_controller_on_attach.0_test.dart diff --git a/examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart diff --git a/examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart b/packages/flutter/examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart diff --git a/examples/api/test/widgets/scroll_view/grid_view.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_view/grid_view.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_view/grid_view.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_view/grid_view.0_test.dart diff --git a/examples/api/test/widgets/scroll_view/list_view.0_test.dart b/packages/flutter/examples/api/test/widgets/scroll_view/list_view.0_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_view/list_view.0_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_view/list_view.0_test.dart diff --git a/examples/api/test/widgets/scroll_view/list_view.1_test.dart b/packages/flutter/examples/api/test/widgets/scroll_view/list_view.1_test.dart similarity index 100% rename from examples/api/test/widgets/scroll_view/list_view.1_test.dart rename to packages/flutter/examples/api/test/widgets/scroll_view/list_view.1_test.dart diff --git a/examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart b/packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart similarity index 100% rename from examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart rename to packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart diff --git a/examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart b/packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart similarity index 100% rename from examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart rename to packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart diff --git a/examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart b/packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart similarity index 100% rename from examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart rename to packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart diff --git a/examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart b/packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart similarity index 100% rename from examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart rename to packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart diff --git a/examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart b/packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart similarity index 100% rename from examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart rename to packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart diff --git a/examples/api/test/widgets/selectable_region/selectable_region.0_test.dart b/packages/flutter/examples/api/test/widgets/selectable_region/selectable_region.0_test.dart similarity index 100% rename from examples/api/test/widgets/selectable_region/selectable_region.0_test.dart rename to packages/flutter/examples/api/test/widgets/selectable_region/selectable_region.0_test.dart diff --git a/examples/api/test/widgets/selection_container/selection_container.0_test.dart b/packages/flutter/examples/api/test/widgets/selection_container/selection_container.0_test.dart similarity index 100% rename from examples/api/test/widgets/selection_container/selection_container.0_test.dart rename to packages/flutter/examples/api/test/widgets/selection_container/selection_container.0_test.dart diff --git a/examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart b/packages/flutter/examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart similarity index 100% rename from examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart rename to packages/flutter/examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart diff --git a/examples/api/test/widgets/sensitive_content/sensitive_content.0_test.dart b/packages/flutter/examples/api/test/widgets/sensitive_content/sensitive_content.0_test.dart similarity index 100% rename from examples/api/test/widgets/sensitive_content/sensitive_content.0_test.dart rename to packages/flutter/examples/api/test/widgets/sensitive_content/sensitive_content.0_test.dart diff --git a/examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart b/packages/flutter/examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart similarity index 100% rename from examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart rename to packages/flutter/examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart diff --git a/examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart b/packages/flutter/examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart similarity index 100% rename from examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart rename to packages/flutter/examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart diff --git a/examples/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart b/packages/flutter/examples/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart similarity index 100% rename from examples/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart rename to packages/flutter/examples/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart diff --git a/examples/api/test/widgets/shortcuts/character_activator.0_test.dart b/packages/flutter/examples/api/test/widgets/shortcuts/character_activator.0_test.dart similarity index 100% rename from examples/api/test/widgets/shortcuts/character_activator.0_test.dart rename to packages/flutter/examples/api/test/widgets/shortcuts/character_activator.0_test.dart diff --git a/examples/api/test/widgets/shortcuts/logical_key_set.0_test.dart b/packages/flutter/examples/api/test/widgets/shortcuts/logical_key_set.0_test.dart similarity index 100% rename from examples/api/test/widgets/shortcuts/logical_key_set.0_test.dart rename to packages/flutter/examples/api/test/widgets/shortcuts/logical_key_set.0_test.dart diff --git a/examples/api/test/widgets/shortcuts/shortcuts.0_test.dart b/packages/flutter/examples/api/test/widgets/shortcuts/shortcuts.0_test.dart similarity index 100% rename from examples/api/test/widgets/shortcuts/shortcuts.0_test.dart rename to packages/flutter/examples/api/test/widgets/shortcuts/shortcuts.0_test.dart diff --git a/examples/api/test/widgets/shortcuts/shortcuts.1_test.dart b/packages/flutter/examples/api/test/widgets/shortcuts/shortcuts.1_test.dart similarity index 100% rename from examples/api/test/widgets/shortcuts/shortcuts.1_test.dart rename to packages/flutter/examples/api/test/widgets/shortcuts/shortcuts.1_test.dart diff --git a/examples/api/test/widgets/shortcuts/single_activator.0_test.dart b/packages/flutter/examples/api/test/widgets/shortcuts/single_activator.0_test.dart similarity index 100% rename from examples/api/test/widgets/shortcuts/single_activator.0_test.dart rename to packages/flutter/examples/api/test/widgets/shortcuts/single_activator.0_test.dart diff --git a/examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart b/packages/flutter/examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart similarity index 100% rename from examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart rename to packages/flutter/examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart diff --git a/examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart b/packages/flutter/examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart similarity index 100% rename from examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart rename to packages/flutter/examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart diff --git a/examples/api/test/widgets/sliver/decorated_sliver.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/decorated_sliver.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/decorated_sliver.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/decorated_sliver.0_test.dart diff --git a/examples/api/test/widgets/sliver/decorated_sliver.1_test.dart b/packages/flutter/examples/api/test/widgets/sliver/decorated_sliver.1_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/decorated_sliver.1_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/decorated_sliver.1_test.dart diff --git a/examples/api/test/widgets/sliver/pinned_header_sliver.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/pinned_header_sliver.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/pinned_header_sliver.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/pinned_header_sliver.0_test.dart diff --git a/examples/api/test/widgets/sliver/pinned_header_sliver.1_test.dart b/packages/flutter/examples/api/test/widgets/sliver/pinned_header_sliver.1_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/pinned_header_sliver.1_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/pinned_header_sliver.1_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_constrained_cross_axis.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_constrained_cross_axis.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_constrained_cross_axis.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_constrained_cross_axis.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_cross_axis_group.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_cross_axis_group.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_cross_axis_group.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_cross_axis_group.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_floating_header.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_floating_header.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_floating_header.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_floating_header.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_list.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_list.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_list.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_list.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_main_axis_group.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_main_axis_group.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_main_axis_group.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_main_axis_group.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_opacity.1_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_opacity.1_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_opacity.1_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_opacity.1_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_resizing_header.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_resizing_header.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_resizing_header.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_resizing_header.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_tree.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_tree.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_tree.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_tree.0_test.dart diff --git a/examples/api/test/widgets/sliver/sliver_tree.1_test.dart b/packages/flutter/examples/api/test/widgets/sliver/sliver_tree.1_test.dart similarity index 100% rename from examples/api/test/widgets/sliver/sliver_tree.1_test.dart rename to packages/flutter/examples/api/test/widgets/sliver/sliver_tree.1_test.dart diff --git a/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart b/packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart similarity index 100% rename from examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart rename to packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart diff --git a/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart b/packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart similarity index 100% rename from examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart rename to packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart diff --git a/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart b/packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart similarity index 100% rename from examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart rename to packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart diff --git a/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart b/packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart similarity index 100% rename from examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart rename to packages/flutter/examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart diff --git a/examples/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart b/packages/flutter/examples/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart similarity index 100% rename from examples/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart rename to packages/flutter/examples/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart diff --git a/examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart b/packages/flutter/examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart similarity index 100% rename from examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart rename to packages/flutter/examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart diff --git a/examples/api/test/widgets/system_context_menu/system_context_menu.1_test.dart b/packages/flutter/examples/api/test/widgets/system_context_menu/system_context_menu.1_test.dart similarity index 100% rename from examples/api/test/widgets/system_context_menu/system_context_menu.1_test.dart rename to packages/flutter/examples/api/test/widgets/system_context_menu/system_context_menu.1_test.dart diff --git a/examples/api/test/widgets/table/table.0_test.dart b/packages/flutter/examples/api/test/widgets/table/table.0_test.dart similarity index 100% rename from examples/api/test/widgets/table/table.0_test.dart rename to packages/flutter/examples/api/test/widgets/table/table.0_test.dart diff --git a/examples/api/test/widgets/tap_region/tap_region.0_test.dart b/packages/flutter/examples/api/test/widgets/tap_region/tap_region.0_test.dart similarity index 100% rename from examples/api/test/widgets/tap_region/tap_region.0_test.dart rename to packages/flutter/examples/api/test/widgets/tap_region/tap_region.0_test.dart diff --git a/examples/api/test/widgets/tap_region/tap_region.1_test.dart b/packages/flutter/examples/api/test/widgets/tap_region/tap_region.1_test.dart similarity index 100% rename from examples/api/test/widgets/tap_region/tap_region.1_test.dart rename to packages/flutter/examples/api/test/widgets/tap_region/tap_region.1_test.dart diff --git a/examples/api/test/widgets/tap_region/text_field_tap_region.0_test.dart b/packages/flutter/examples/api/test/widgets/tap_region/text_field_tap_region.0_test.dart similarity index 100% rename from examples/api/test/widgets/tap_region/text_field_tap_region.0_test.dart rename to packages/flutter/examples/api/test/widgets/tap_region/text_field_tap_region.0_test.dart diff --git a/examples/api/test/widgets/text/text.0_test.dart b/packages/flutter/examples/api/test/widgets/text/text.0_test.dart similarity index 100% rename from examples/api/test/widgets/text/text.0_test.dart rename to packages/flutter/examples/api/test/widgets/text/text.0_test.dart diff --git a/examples/api/test/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0_test.dart b/packages/flutter/examples/api/test/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0_test.dart similarity index 100% rename from examples/api/test/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0_test.dart rename to packages/flutter/examples/api/test/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0_test.dart diff --git a/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart b/packages/flutter/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart similarity index 96% rename from examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart rename to packages/flutter/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart index 9335e6af7c88c..e2da420a604b0 100644 --- a/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart +++ b/packages/flutter/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart @@ -2,6 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines. +@Tags(['reduced-test-set']) +library; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_api_samples/widgets/text_magnifier/text_magnifier.0.dart' diff --git a/examples/api/test/widgets/transitions/align_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/align_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/align_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/align_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/animated_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/animated_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/animated_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/animated_builder.0_test.dart diff --git a/examples/api/test/widgets/transitions/animated_widget.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/animated_widget.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/animated_widget.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/animated_widget.0_test.dart diff --git a/examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/fade_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/fade_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/fade_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/fade_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/listenable_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/listenable_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/listenable_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/listenable_builder.0_test.dart diff --git a/examples/api/test/widgets/transitions/listenable_builder.1_test.dart b/packages/flutter/examples/api/test/widgets/transitions/listenable_builder.1_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/listenable_builder.1_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/listenable_builder.1_test.dart diff --git a/examples/api/test/widgets/transitions/listenable_builder.2_test.dart b/packages/flutter/examples/api/test/widgets/transitions/listenable_builder.2_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/listenable_builder.2_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/listenable_builder.2_test.dart diff --git a/examples/api/test/widgets/transitions/listenable_builder.3_test.dart b/packages/flutter/examples/api/test/widgets/transitions/listenable_builder.3_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/listenable_builder.3_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/listenable_builder.3_test.dart diff --git a/examples/api/test/widgets/transitions/matrix_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/matrix_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/matrix_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/matrix_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/positioned_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/positioned_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/positioned_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/positioned_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/rotation_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/rotation_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/rotation_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/rotation_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/scale_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/scale_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/scale_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/scale_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/size_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/size_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/size_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/size_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/slide_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/slide_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/slide_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/slide_transition.0_test.dart diff --git a/examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart b/packages/flutter/examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart similarity index 100% rename from examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart rename to packages/flutter/examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart diff --git a/examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart diff --git a/examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart b/packages/flutter/examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart similarity index 100% rename from examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart rename to packages/flutter/examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart diff --git a/examples/api/test/widgets/value_listenable_builder/value_listenable_builder.0_test.dart b/packages/flutter/examples/api/test/widgets/value_listenable_builder/value_listenable_builder.0_test.dart similarity index 100% rename from examples/api/test/widgets/value_listenable_builder/value_listenable_builder.0_test.dart rename to packages/flutter/examples/api/test/widgets/value_listenable_builder/value_listenable_builder.0_test.dart diff --git a/examples/api/test/widgets/widget_state/widget_state_border_side.0_test.dart b/packages/flutter/examples/api/test/widgets/widget_state/widget_state_border_side.0_test.dart similarity index 100% rename from examples/api/test/widgets/widget_state/widget_state_border_side.0_test.dart rename to packages/flutter/examples/api/test/widgets/widget_state/widget_state_border_side.0_test.dart diff --git a/examples/api/test/widgets/widget_state/widget_state_mouse_cursor.0_test.dart b/packages/flutter/examples/api/test/widgets/widget_state/widget_state_mouse_cursor.0_test.dart similarity index 100% rename from examples/api/test/widgets/widget_state/widget_state_mouse_cursor.0_test.dart rename to packages/flutter/examples/api/test/widgets/widget_state/widget_state_mouse_cursor.0_test.dart diff --git a/examples/api/test/widgets/widget_state/widget_state_outlined_border.0_test.dart b/packages/flutter/examples/api/test/widgets/widget_state/widget_state_outlined_border.0_test.dart similarity index 100% rename from examples/api/test/widgets/widget_state/widget_state_outlined_border.0_test.dart rename to packages/flutter/examples/api/test/widgets/widget_state/widget_state_outlined_border.0_test.dart diff --git a/examples/api/test/widgets/widget_state/widget_state_property.0_test.dart b/packages/flutter/examples/api/test/widgets/widget_state/widget_state_property.0_test.dart similarity index 100% rename from examples/api/test/widgets/widget_state/widget_state_property.0_test.dart rename to packages/flutter/examples/api/test/widgets/widget_state/widget_state_property.0_test.dart diff --git a/examples/api/test/widgets/windows/popup.0_test.dart b/packages/flutter/examples/api/test/widgets/windows/popup.0_test.dart similarity index 100% rename from examples/api/test/widgets/windows/popup.0_test.dart rename to packages/flutter/examples/api/test/widgets/windows/popup.0_test.dart diff --git a/examples/api/test/widgets/windows/satellite.0_test.dart b/packages/flutter/examples/api/test/widgets/windows/satellite.0_test.dart similarity index 100% rename from examples/api/test/widgets/windows/satellite.0_test.dart rename to packages/flutter/examples/api/test/widgets/windows/satellite.0_test.dart diff --git a/examples/api/test/widgets/windows/tooltip.0_test.dart b/packages/flutter/examples/api/test/widgets/windows/tooltip.0_test.dart similarity index 100% rename from examples/api/test/widgets/windows/tooltip.0_test.dart rename to packages/flutter/examples/api/test/widgets/windows/tooltip.0_test.dart diff --git a/examples/api/test/widgets/windows/window_manager.0_test.dart b/packages/flutter/examples/api/test/widgets/windows/window_manager.0_test.dart similarity index 100% rename from examples/api/test/widgets/windows/window_manager.0_test.dart rename to packages/flutter/examples/api/test/widgets/windows/window_manager.0_test.dart diff --git a/examples/api/test_driver/integration_test.dart b/packages/flutter/examples/api/test_driver/integration_test.dart similarity index 100% rename from examples/api/test_driver/integration_test.dart rename to packages/flutter/examples/api/test_driver/integration_test.dart diff --git a/examples/api/web/favicon.png b/packages/flutter/examples/api/web/favicon.png similarity index 100% rename from examples/api/web/favicon.png rename to packages/flutter/examples/api/web/favicon.png diff --git a/examples/api/web/icons/Icon-192.png b/packages/flutter/examples/api/web/icons/Icon-192.png similarity index 100% rename from examples/api/web/icons/Icon-192.png rename to packages/flutter/examples/api/web/icons/Icon-192.png diff --git a/examples/api/web/icons/Icon-512.png b/packages/flutter/examples/api/web/icons/Icon-512.png similarity index 100% rename from examples/api/web/icons/Icon-512.png rename to packages/flutter/examples/api/web/icons/Icon-512.png diff --git a/examples/api/web/index.html b/packages/flutter/examples/api/web/index.html similarity index 100% rename from examples/api/web/index.html rename to packages/flutter/examples/api/web/index.html diff --git a/examples/api/web/manifest.json b/packages/flutter/examples/api/web/manifest.json similarity index 100% rename from examples/api/web/manifest.json rename to packages/flutter/examples/api/web/manifest.json diff --git a/examples/api/windows/.gitignore b/packages/flutter/examples/api/windows/.gitignore similarity index 100% rename from examples/api/windows/.gitignore rename to packages/flutter/examples/api/windows/.gitignore diff --git a/examples/api/windows/CMakeLists.txt b/packages/flutter/examples/api/windows/CMakeLists.txt similarity index 100% rename from examples/api/windows/CMakeLists.txt rename to packages/flutter/examples/api/windows/CMakeLists.txt diff --git a/examples/api/windows/flutter/CMakeLists.txt b/packages/flutter/examples/api/windows/flutter/CMakeLists.txt similarity index 100% rename from examples/api/windows/flutter/CMakeLists.txt rename to packages/flutter/examples/api/windows/flutter/CMakeLists.txt diff --git a/examples/api/windows/runner/CMakeLists.txt b/packages/flutter/examples/api/windows/runner/CMakeLists.txt similarity index 100% rename from examples/api/windows/runner/CMakeLists.txt rename to packages/flutter/examples/api/windows/runner/CMakeLists.txt diff --git a/examples/api/windows/runner/Runner.rc b/packages/flutter/examples/api/windows/runner/Runner.rc similarity index 100% rename from examples/api/windows/runner/Runner.rc rename to packages/flutter/examples/api/windows/runner/Runner.rc diff --git a/examples/api/windows/runner/flutter_window.cpp b/packages/flutter/examples/api/windows/runner/flutter_window.cpp similarity index 100% rename from examples/api/windows/runner/flutter_window.cpp rename to packages/flutter/examples/api/windows/runner/flutter_window.cpp diff --git a/examples/api/windows/runner/flutter_window.h b/packages/flutter/examples/api/windows/runner/flutter_window.h similarity index 100% rename from examples/api/windows/runner/flutter_window.h rename to packages/flutter/examples/api/windows/runner/flutter_window.h diff --git a/examples/api/windows/runner/main.cpp b/packages/flutter/examples/api/windows/runner/main.cpp similarity index 100% rename from examples/api/windows/runner/main.cpp rename to packages/flutter/examples/api/windows/runner/main.cpp diff --git a/examples/api/windows/runner/resource.h b/packages/flutter/examples/api/windows/runner/resource.h similarity index 100% rename from examples/api/windows/runner/resource.h rename to packages/flutter/examples/api/windows/runner/resource.h diff --git a/examples/api/windows/runner/runner.exe.manifest b/packages/flutter/examples/api/windows/runner/runner.exe.manifest similarity index 100% rename from examples/api/windows/runner/runner.exe.manifest rename to packages/flutter/examples/api/windows/runner/runner.exe.manifest diff --git a/examples/api/windows/runner/utils.cpp b/packages/flutter/examples/api/windows/runner/utils.cpp similarity index 100% rename from examples/api/windows/runner/utils.cpp rename to packages/flutter/examples/api/windows/runner/utils.cpp diff --git a/examples/api/windows/runner/utils.h b/packages/flutter/examples/api/windows/runner/utils.h similarity index 100% rename from examples/api/windows/runner/utils.h rename to packages/flutter/examples/api/windows/runner/utils.h diff --git a/examples/api/windows/runner/win32_window.cpp b/packages/flutter/examples/api/windows/runner/win32_window.cpp similarity index 100% rename from examples/api/windows/runner/win32_window.cpp rename to packages/flutter/examples/api/windows/runner/win32_window.cpp diff --git a/examples/api/windows/runner/win32_window.h b/packages/flutter/examples/api/windows/runner/win32_window.h similarity index 100% rename from examples/api/windows/runner/win32_window.h rename to packages/flutter/examples/api/windows/runner/win32_window.h diff --git a/pubspec.yaml b/pubspec.yaml index d19d75af74729..fb510bd5d1936 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,6 @@ workspace: - dev/tools/gen_keycodes - dev/tools/vitool - dev/tracing_tests - - examples/api - examples/flutter_view - examples/hello_world - examples/image_list @@ -78,6 +77,7 @@ workspace: - packages/flutter_localizations - packages/flutter_test - packages/flutter_web_plugins + - packages/flutter/examples/api - packages/flutter/test_private - packages/flutter/test_private/test - packages/fuchsia_remote_debug_protocol From 2cb26f7500f7a17e87f00d0f3077f1eccad3ed91 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 7 Aug 2026 16:49:01 -0400 Subject: [PATCH 136/330] [Tool] Fix SwiftPM race condition during parallel Xcode builds (#188451) Xcode builds multi-target applications in parallel, invoking the Flutter build pipeline concurrently. This leads to a destructive race condition in `generatePluginsSwiftPackage` where one process deletes the ephemeral packages directory while another is writing to it, causing `FileSystemException` or `PathExistsException`. This change implements: 1. Process-safe file locking (`.swift_pm.lock`) with retry loops to serialize directory preparation across parallel builds. 2. Non-destructive, incremental cleanup of obsolete symlinks rather than deleting the entire directory. 3. Content-aware write skipping for `Package.swift` and placeholder source files to avoid redundant writes and prevent unnecessary Xcode project re-indexing. Fixes https://github.com/flutter/flutter/issues/188446 --- .../lib/src/base/file_system.dart | 68 ++++ .../lib/src/flutter_plugins.dart | 1 + .../lib/src/macos/cocoapod_utils.dart | 1 + .../lib/src/macos/swift_package_manager.dart | 186 ++++++---- .../lib/src/macos/swift_packages.dart | 39 +- .../macos/cocoapod_utils_test.dart | 14 +- .../macos/swift_package_manager_test.dart | 335 +++++++++++++++++- 7 files changed, 571 insertions(+), 73 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index 6e1a75fea7101..53724f70a7a72 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.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:async'; + import 'package:file/file.dart'; import 'package:file/local.dart' as local_fs; import 'package:meta/meta.dart'; import 'common.dart'; import 'io.dart'; +import 'logger.dart'; import 'platform.dart'; import 'process.dart'; import 'signals.dart'; +import 'terminal.dart'; // package:file/local.dart must not be exported. This exposes LocalFileSystem, // which we override to ensure that temporary directories are cleaned up when @@ -273,3 +277,67 @@ class LocalFileSystem extends local_fs.LocalFileSystem { @visibleForTesting Directory get superSystemTempDirectory => super.systemTempDirectory; } + +extension FileSystemLocking on FileSystem { + /// Runs [scope] while holding an exclusive file lock on the file at [lockPath]. + /// + /// The lock is released after [scope] completes, even if it throws. + /// + /// If the lock cannot be acquired immediately, it will retry every 50ms. + Future runLocked({ + required String lockPath, + required FutureOr Function() scope, + Logger? logger, + String? traceMessage, + String? warningMessage, + }) async { + final File lockFile = file(lockPath); + var printed = false; + RandomAccessFile? openedFile; + while (true) { + try { + lockFile.parent.createSync(recursive: true); + openedFile = lockFile.openSync(mode: FileMode.write); + openedFile.lockSync(); + break; + } on UnimplementedError { + logger?.printTrace(traceMessage ?? 'Locking not supported (UnimplementedError).'); + break; + } on UnsupportedError { + logger?.printTrace(traceMessage ?? 'Locking not supported (UnsupportedError).'); + break; + } on FileSystemException catch (e) { + final lockFailed = openedFile != null; + if (openedFile != null) { + try { + openedFile.closeSync(); + } on FileSystemException catch (_) {} + openedFile = null; + } + if (!printed) { + final details = lockFailed ? '' : ' (Error: $e)'; + logger?.printTrace( + traceMessage ?? 'Waiting to obtain lock of directory: ${lockFile.path}$details', + ); + logger?.printWarning( + warningMessage ?? 'Waiting for another flutter command to release the lock...', + color: TerminalColor.grey, + fatal: false, + ); + printed = true; + } + await Future.delayed(const Duration(milliseconds: 50)); + } + } + + try { + return await scope(); + } finally { + if (openedFile != null) { + try { + openedFile.closeSync(); + } on FileSystemException {} // ignore: empty_catches + } + } + } +} diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index 1d52046878f6e..3af0fb0848407 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -1457,6 +1457,7 @@ Future injectPlugins( templateRenderer: globals.templateRenderer, processUtils: globals.processUtils, config: globals.config, + logger: globals.logger, ), fileSystem: globals.fs, featureFlags: featureFlags, diff --git a/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart b/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart index 5a676b734d0ec..55f38ff954d64 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart @@ -62,6 +62,7 @@ Future processPodsIfNeeded( templateRenderer: globals.templateRenderer, processUtils: globals.processUtils, config: globals.config, + logger: globals.logger, ); final FlutterDarwinPlatform platform = xcodeProject is IosProject ? FlutterDarwinPlatform.ios 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 6c2d83670af80..1339079a273be 100644 --- a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -8,6 +8,7 @@ import '../base/common.dart'; import '../base/config.dart'; import '../base/error_handling_io.dart'; import '../base/file_system.dart'; +import '../base/logger.dart'; import '../base/process.dart'; import '../base/template.dart'; import '../base/version.dart'; @@ -43,88 +44,118 @@ class SwiftPackageManager { required TemplateRenderer templateRenderer, required ProcessUtils processUtils, required Config config, + Logger? logger, }) : _fileSystem = fileSystem, _templateRenderer = templateRenderer, _processUtils = processUtils, - _config = config; + _config = config, + _logger = logger; final FileSystem _fileSystem; final TemplateRenderer _templateRenderer; final ProcessUtils _processUtils; final Config _config; + final Logger? _logger; - /// Creates a Swift Package called 'FlutterGeneratedPluginSwiftPackage' that - /// has dependencies on Flutter plugins that are compatible with Swift - /// Package Manager. Future generatePluginsSwiftPackage( List plugins, FlutterDarwinPlatform platform, XcodeBasedProject project, { bool flutterAsADependency = true, }) async { - final Directory symlinkDirectory = project.relativeSwiftPackagesDirectory; - ErrorHandlingFileSystem.deleteIfExists(symlinkDirectory, recursive: true); - symlinkDirectory.createSync(recursive: true); - - final ( - List packageDependencies, - List targetDependencies, - ) = _dependenciesForPlugins( - plugins: plugins, - platform: platform, - symlinkDirectory: symlinkDirectory, - pathRelativeTo: project.flutterPluginSwiftPackageDirectory.path, - ); + final String lockPath = project.ephemeralDirectory.childFile('.swift_pm.lock').path; + await _fileSystem.runLocked( + lockPath: lockPath, + logger: _logger, + traceMessage: + 'Waiting to be able to obtain lock of Swift Package Manager directory: $lockPath', + warningMessage: + 'Waiting for another flutter command to release the Swift Package Manager lock...', + scope: () async { + final Directory symlinkDirectory = project.relativeSwiftPackagesDirectory; + try { + symlinkDirectory.createSync(recursive: true); + } on FileSystemException catch (e) { + if (!_fileSystem.isDirectorySync(symlinkDirectory.path)) { + throwToolExit( + 'Failed to create Swift Packages directory at "${symlinkDirectory.path}": $e', + ); + } + } - // If there aren't any Swift Package plugins and the project hasn't been - // migrated yet, don't generate a Swift package or migrate the app since - // it's not needed. If the project has already been migrated, regenerate - // the Package.swift even if there are no dependencies in case there - // were dependencies previously. - if (packageDependencies.isEmpty && !project.flutterPluginSwiftPackageInProjectSettings) { - return; - } + final ( + List packageDependencies, + List targetDependencies, + Set expectedBasenames, + ) = _dependenciesForPlugins( + plugins: plugins, + platform: platform, + symlinkDirectory: symlinkDirectory, + pathRelativeTo: project.flutterPluginSwiftPackageDirectory.path, + ); - // Add Flutter framework Swift package dependency - if (flutterAsADependency) { - final ( - SwiftPackagePackageDependency flutterFrameworkPackageDependency, - SwiftPackageTargetDependency flutterFrameworkTargetDependency, - ) = _dependencyForFlutterFramework( - pathRelativeTo: project.flutterPluginSwiftPackageDirectory.path, - platform: platform, - project: project, - ); - packageDependencies.add(flutterFrameworkPackageDependency); - targetDependencies.add(flutterFrameworkTargetDependency); - } + // If there aren't any Swift Package plugins and the project hasn't been + // migrated yet, don't generate a Swift package or migrate the app since + // it's not needed. If the project has already been migrated, regenerate + // the Package.swift even if there are no dependencies in case there + // were dependencies previously. + if (packageDependencies.isEmpty && !project.flutterPluginSwiftPackageInProjectSettings) { + _cleanStaleSymlinks( + symlinkDirectory: symlinkDirectory, + expectedBasenames: expectedBasenames, + keepFlutterFramework: false, + ); + return; + } - // FlutterGeneratedPluginSwiftPackage must be statically linked to ensure - // any dynamic dependencies are linked to Runner and prevent undefined symbols. - final generatedProduct = SwiftPackageProduct.library( - name: kFlutterGeneratedPluginSwiftPackageName, - targets: [kFlutterGeneratedPluginSwiftPackageName], - libraryType: SwiftPackageLibraryType.static, - ); + // Add Flutter framework Swift package dependency + if (flutterAsADependency) { + final ( + SwiftPackagePackageDependency flutterFrameworkPackageDependency, + SwiftPackageTargetDependency flutterFrameworkTargetDependency, + ) = _dependencyForFlutterFramework( + pathRelativeTo: project.flutterPluginSwiftPackageDirectory.path, + platform: platform, + project: project, + ); + packageDependencies.add(flutterFrameworkPackageDependency); + targetDependencies.add(flutterFrameworkTargetDependency); + } - final generatedTarget = SwiftPackageTarget.defaultTarget( - name: kFlutterGeneratedPluginSwiftPackageName, - dependencies: targetDependencies, - ); + // FlutterGeneratedPluginSwiftPackage must be statically linked to ensure + // any dynamic dependencies are linked to Runner and prevent undefined symbols. + final generatedProduct = SwiftPackageProduct.library( + name: kFlutterGeneratedPluginSwiftPackageName, + targets: [kFlutterGeneratedPluginSwiftPackageName], + libraryType: SwiftPackageLibraryType.static, + ); - final pluginsPackage = SwiftPackage( - manifest: project.flutterPluginSwiftPackageManifest, - name: kFlutterGeneratedPluginSwiftPackageName, - platforms: [platform.supportedPackagePlatform], - products: [generatedProduct], - dependencies: packageDependencies, - targets: [generatedTarget], - templateRenderer: _templateRenderer, + final generatedTarget = SwiftPackageTarget.defaultTarget( + name: kFlutterGeneratedPluginSwiftPackageName, + dependencies: targetDependencies, + ); + + final pluginsPackage = SwiftPackage( + manifest: project.flutterPluginSwiftPackageManifest, + name: kFlutterGeneratedPluginSwiftPackageName, + platforms: [platform.supportedPackagePlatform], + products: [generatedProduct], + dependencies: packageDependencies, + targets: [generatedTarget], + templateRenderer: _templateRenderer, + ); + pluginsPackage.createSwiftPackage(); + + _cleanStaleSymlinks( + symlinkDirectory: symlinkDirectory, + expectedBasenames: expectedBasenames, + keepFlutterFramework: flutterAsADependency, + ); + }, ); - pluginsPackage.createSwiftPackage(); } - (List, List) + (List, List, Set) _dependenciesForPlugins({ required List plugins, required FlutterDarwinPlatform platform, @@ -133,6 +164,7 @@ class SwiftPackageManager { }) { final packageDependencies = []; final targetDependencies = []; + final expectedBasenames = {}; for (final plugin in plugins) { final String? pluginSwiftPackageManifestPath = plugin.pluginSwiftPackageManifestPath( @@ -174,6 +206,7 @@ class SwiftPackageManager { final Link pluginSymlink = symlinkDirectory.childLink(basename); _createPluginSymlink(pluginSymlink: pluginSymlink, packagePath: packagePath); + expectedBasenames.add(basename); final String packageRelativePath = _fileSystem.path.relative( pluginSymlink.path, @@ -195,7 +228,7 @@ class SwiftPackageManager { ), ); } - return (packageDependencies, targetDependencies); + return (packageDependencies, targetDependencies, expectedBasenames); } /// Safely creates a symlink at [pluginSymlink] pointing to [packagePath]. @@ -454,4 +487,37 @@ class SwiftPackageManager { manifestContents.replaceFirst(oldSupportedPlatform, newSupportedPlatform), ); } + + /// Cleans up any stale or unreferenced plugin symlinks from the project's + /// symlink directory. + /// + /// When plugins are removed from `pubspec.yaml` or updated to versions that + /// no longer use Swift Package Manager, their old symlinks remain on disk. + /// This method removes them to prevent Xcode compilation failures caused by + /// trying to resolve broken or obsolete symlinks. + void _cleanStaleSymlinks({ + required Directory symlinkDirectory, + required Set expectedBasenames, + required bool keepFlutterFramework, + }) { + if (!symlinkDirectory.existsSync()) { + return; + } + try { + for (final FileSystemEntity entity in symlinkDirectory.listSync()) { + final String name = _fileSystem.path.basename(entity.path); + if (name == 'FlutterFramework') { + if (!keepFlutterFramework) { + ErrorHandlingFileSystem.deleteIfExists(entity, recursive: true); + } + continue; + } + if (!expectedBasenames.contains(name)) { + ErrorHandlingFileSystem.deleteIfExists(entity, recursive: true); + } + } + } on FileSystemException catch (_) { + // Ignore errors, as another process might be concurrently modifying the directory. + } + } } diff --git a/packages/flutter_tools/lib/src/macos/swift_packages.dart b/packages/flutter_tools/lib/src/macos/swift_packages.dart index 5b7613675e09f..c1849f891becf 100644 --- a/packages/flutter_tools/lib/src/macos/swift_packages.dart +++ b/packages/flutter_tools/lib/src/macos/swift_packages.dart @@ -18,7 +18,10 @@ const _swiftPackageTemplate = ''' import PackageDescription -{{#hasSwiftCodeBefore}}\n{{swiftCodeBefore}}\n\n{{/hasSwiftCodeBefore}} +{{#hasSwiftCodeBefore}} +{{swiftCodeBefore}} + +{{/hasSwiftCodeBefore}} let package = Package( name: "{{packageName}}", {{#platforms}} @@ -120,11 +123,17 @@ class SwiftPackage { final Directory targetDirectory = _manifest.parent .childDirectory('Sources') .childDirectory(target.name); - if (generateEmptySources && - (!targetDirectory.existsSync() || targetDirectory.listSync().isEmpty)) { + if (generateEmptySources) { final File requiredSwiftFile = targetDirectory.childFile('${target.name}.swift'); - requiredSwiftFile.createSync(recursive: true); - requiredSwiftFile.writeAsStringSync(_swiftPackageSourceTemplate); + // Skip creating placeholder sources if sources already exist in the + // target directory to avoid unnecessary file writes during build. + final bool hasSources = + requiredSwiftFile.existsSync() || + (targetDirectory.existsSync() && targetDirectory.listSync().isNotEmpty); + if (!hasSources) { + requiredSwiftFile.createSync(recursive: true); + requiredSwiftFile.writeAsStringSync(_swiftPackageSourceTemplate); + } } } @@ -132,8 +141,24 @@ class SwiftPackage { _swiftPackageTemplate, _templateContext, ); - _manifest.createSync(recursive: true); - _manifest.writeAsStringSync(renderedTemplate); + + // Skip writing Package.swift if the existing file content is identical to + // renderedTemplate. Preserving file modification time (mtime) prevents + // Xcode and Swift Package Manager from invalidating caches and re-resolving + // dependencies during parallel builds. + var shouldWrite = true; + try { + if (_manifest.existsSync() && _manifest.readAsStringSync() == renderedTemplate) { + shouldWrite = false; + } + } on FileSystemException { + // If reading fails, write it anyway. + } + + if (shouldWrite) { + _manifest.createSync(recursive: true); + _manifest.writeAsStringSync(renderedTemplate); + } } String? _formatPlatforms() { diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart index b31b284e0ad5c..a2fc6b6204294 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart @@ -502,8 +502,11 @@ class FakeMacOSProject extends Fake implements MacOSProject { hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); @override - Directory get flutterSwiftPackagesDirectory => - hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages'); + Directory get ephemeralDirectory => + hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral'); + + @override + Directory get flutterSwiftPackagesDirectory => ephemeralDirectory.childDirectory('Packages'); @override Directory get relativeSwiftPackagesDirectory => @@ -551,8 +554,11 @@ class FakeIosProject extends Fake implements IosProject { hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); @override - Directory get flutterSwiftPackagesDirectory => - hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages'); + Directory get ephemeralDirectory => + hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral'); + + @override + Directory get flutterSwiftPackagesDirectory => ephemeralDirectory.childDirectory('Packages'); @override Directory get relativeSwiftPackagesDirectory => 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 c928b0c37bbae..8ef2f11e06a61 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 @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; @@ -738,6 +739,173 @@ let package = Package( ); }); }); + + group('concurrency and optimization', () { + late MemoryFileSystem fs; + late LockTrackingFileSystem trackingFs; + late FakeProcessManager processManager; + late BufferLogger logger; + late FakeXcodeProject project; + late SwiftPackageManager spm; + + setUp(() { + fs = MemoryFileSystem.test(); + trackingFs = LockTrackingFileSystem(fs); + processManager = FakeProcessManager.any(); + logger = BufferLogger.test(); + project = FakeXcodeProject(platform: platform.name, fileSystem: trackingFs); + + spm = SwiftPackageManager( + fileSystem: trackingFs, + templateRenderer: const MustacheTemplateRenderer(), + processUtils: ProcessUtils(processManager: processManager, logger: logger), + config: FakeConfig(), + logger: logger, + ); + }); + + testWithoutContext('acquires and releases lock successfully', () async { + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + + await spm.generatePluginsSwiftPackage([], platform, project); + + expect(trackingFs.lockCount, 1); + expect(trackingFs.unlockCount, 1); + expect(trackingFs.lockAttempts, 1); + }); + + testWithoutContext( + 'retries lock on FileSystemException and eventually succeeds', + () async { + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + + trackingFs.throwErrorOnLock = true; + trackingFs.throwErrorOnLockTimes = 2; // Fail twice, succeed on 3rd attempt + + await spm.generatePluginsSwiftPackage([], platform, project); + + expect(trackingFs.lockCount, 1); + expect(trackingFs.unlockCount, 3); // Closed on every retry + final release + expect(trackingFs.lockAttempts, 3); // 2 failures + 1 success + + // Verify the warning was printed to the logger + expect( + logger.warningText, + contains( + 'Waiting for another flutter command to release the Swift Package Manager lock...', + ), + ); + }, + ); + + testWithoutContext('proceeds without lock on UnimplementedError', () async { + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + + trackingFs.throwUnimplementedOnLock = true; + + await spm.generatePluginsSwiftPackage([], platform, project); + + expect(trackingFs.lockCount, 0); // No successful locks recorded + expect(trackingFs.unlockCount, 1); // Still closed + expect(trackingFs.lockAttempts, 1); // 1 attempt that threw + }); + + testWithoutContext( + 'non-destructive symlink update: preserves active, creates new, deletes obsolete', + () async { + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + + final plugin1 = FakePlugin( + name: 'plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + ); + final plugin2 = FakePlugin( + name: 'plugin_2', + platforms: {platform.name: FakePluginPlatform()}, + ); + final plugin3 = FakePlugin( + name: 'plugin_3', + platforms: {platform.name: FakePluginPlatform()}, + ); + + // Pre-populate symlink directory with plugin_1 (active), plugin_3 (obsolete), and a stale file + final Directory symlinkDir = project.relativeSwiftPackagesDirectory; + symlinkDir.createSync(recursive: true); + + final Link link1 = symlinkDir.childLink('plugin_1-1.0.0'); + link1.createSync('${plugin1.path}/${platform.name}/plugin_1'); + + final Link link3 = symlinkDir.childLink('plugin_3-1.0.0'); + link3.createSync('${plugin3.path}/${platform.name}/plugin_3'); + + final File staleFile = symlinkDir.childFile('stale_file.txt'); + staleFile.createSync(); + staleFile.writeAsStringSync('stale content'); + + // Create fake Package.swift manifests for active plugins to prevent them from being skipped + trackingFs + .file('${plugin1.path}/${platform.name}/${plugin1.name}/Package.swift') + .createSync(recursive: true); + trackingFs + .file('${plugin2.path}/${platform.name}/${plugin2.name}/Package.swift') + .createSync(recursive: true); + + // Reset tracking counters after pre-population + trackingFs.linkCreateCount.clear(); + trackingFs.linkDeleteCount.clear(); + + // Run with plugin_1 and plugin_2 + await spm.generatePluginsSwiftPackage([plugin1, plugin2], platform, project); + + // Verify plugin_1 was preserved (not deleted, not recreated) + expect(trackingFs.linkDeleteCount['plugin_1-1.0.0'], isNull); + expect(trackingFs.linkCreateCount['plugin_1-1.0.0'], isNull); + + // Verify plugin_2 was created + expect(trackingFs.linkCreateCount['plugin_2-1.0.0'], 1); + + // Verify plugin_3 was deleted + expect(trackingFs.linkDeleteCount['plugin_3-1.0.0'], 1); + expect(symlinkDir.childLink('plugin_3-1.0.0').existsSync(), isFalse); + + // Verify stale file was deleted + expect(staleFile.existsSync(), isFalse); + }, + ); + + testWithoutContext( + 'optimizes Package.swift generation: does not rewrite if identical', + () async { + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + + final plugin1 = FakePlugin( + name: 'plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + ); + trackingFs + .file('${plugin1.path}/${platform.name}/${plugin1.name}/Package.swift') + .createSync(recursive: true); + + // Reset write count after setup to only track writes during SPM generation + trackingFs.writeCount = 0; + + // First run: generates Package.swift and placeholders + await spm.generatePluginsSwiftPackage([plugin1], platform, project); + expect(trackingFs.writeCount, 4); // 2 Package.swift + 2 placeholder .swift files + + trackingFs.writeCount = 0; + + // Second run with same plugin: should skip writing everything + await spm.generatePluginsSwiftPackage([plugin1], platform, project); + expect(trackingFs.writeCount, 0); // All skipped! + }, + ); + }); }); } }); @@ -760,8 +928,11 @@ class FakeXcodeProject extends Fake implements IosProject { String hostAppProjectName = 'Runner'; @override - Directory get flutterSwiftPackagesDirectory => - hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages'); + Directory get ephemeralDirectory => + hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral'); + + @override + Directory get flutterSwiftPackagesDirectory => ephemeralDirectory.childDirectory('Packages'); @override Directory get relativeSwiftPackagesDirectory => @@ -870,3 +1041,163 @@ class _ErrorInjectingLink extends ForwardingFileSystemEntity with super.createSync(target, recursive: recursive); } } + +class LockTrackingFileSystem extends ForwardingFileSystem { + LockTrackingFileSystem(super.delegate); + + int lockCount = 0; + int unlockCount = 0; + int lockAttempts = 0; + int writeCount = 0; + bool throwErrorOnLock = false; + int throwErrorOnLockTimes = 0; + bool throwUnimplementedOnLock = false; + + final Map linkDeleteCount = {}; + final Map linkCreateCount = {}; + + @override + Directory directory(dynamic path) => _TrackingDirectory(this, delegate.directory(path)); + + @override + File file(dynamic path) => _TrackingFile(this, delegate.file(path)); + + @override + Link link(dynamic path) => _TrackingLink(this, delegate.link(path)); +} + +class _TrackingDirectory extends ForwardingFileSystemEntity + with ForwardingDirectory { + _TrackingDirectory(this._fileSystem, this.delegate); + + final LockTrackingFileSystem _fileSystem; + + @override + final io.Directory delegate; + + @override + FileSystem get fileSystem => _fileSystem; + + @override + File wrapFile(io.File delegate) => _fileSystem.file(delegate.path); + + @override + Directory wrapDirectory(io.Directory delegate) => _fileSystem.directory(delegate.path); + + @override + Link wrapLink(io.Link delegate) => _fileSystem.link(delegate.path); + + @override + Directory childDirectory(String basename) => + fileSystem.directory(fileSystem.path.join(path, basename)); + + @override + File childFile(String basename) => fileSystem.file(fileSystem.path.join(path, basename)); + + @override + Link childLink(String basename) => fileSystem.link(fileSystem.path.join(path, basename)); +} + +class _TrackingFile extends ForwardingFileSystemEntity with ForwardingFile { + _TrackingFile(this._fileSystem, this.delegate); + + final LockTrackingFileSystem _fileSystem; + + @override + final io.File delegate; + + @override + FileSystem get fileSystem => _fileSystem; + + @override + File wrapFile(io.File delegate) => _fileSystem.file(delegate.path); + + @override + Directory wrapDirectory(io.Directory delegate) => _fileSystem.directory(delegate.path); + + @override + Link wrapLink(io.Link delegate) => _fileSystem.link(delegate.path); + + @override + void writeAsStringSync( + String contents, { + FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) { + _fileSystem.writeCount++; + super.writeAsStringSync(contents, mode: mode, encoding: encoding, flush: flush); + } + + @override + RandomAccessFile openSync({FileMode mode = FileMode.read}) { + final RandomAccessFile delegateOpened = super.openSync(mode: mode); + if (path.endsWith('.swift_pm.lock')) { + return _LockTrackingRandomAccessFile(_fileSystem, delegateOpened); + } + return delegateOpened; + } +} + +class _LockTrackingRandomAccessFile extends Fake implements RandomAccessFile { + _LockTrackingRandomAccessFile(this._fileSystem, this._delegate); + + final LockTrackingFileSystem _fileSystem; + final RandomAccessFile _delegate; + + @override + void lockSync([FileLock mode = FileLock.exclusive, int start = 0, int end = -1]) { + _fileSystem.lockAttempts++; + if (_fileSystem.throwUnimplementedOnLock) { + throw UnimplementedError('Lock not supported'); + } + if (_fileSystem.throwErrorOnLock) { + if (_fileSystem.throwErrorOnLockTimes > 0) { + _fileSystem.throwErrorOnLockTimes--; + throw const FileSystemException('Lock failed'); + } + } + _fileSystem.lockCount++; + } + + @override + void closeSync() { + _fileSystem.unlockCount++; + _delegate.closeSync(); + } +} + +class _TrackingLink extends ForwardingFileSystemEntity with ForwardingLink { + _TrackingLink(this._fileSystem, this.delegate); + + final LockTrackingFileSystem _fileSystem; + + @override + final io.Link delegate; + + @override + FileSystem get fileSystem => _fileSystem; + + @override + File wrapFile(io.File delegate) => _fileSystem.file(delegate.path); + + @override + Directory wrapDirectory(io.Directory delegate) => _fileSystem.directory(delegate.path); + + @override + Link wrapLink(io.Link delegate) => _fileSystem.link(delegate.path); + + @override + void createSync(String target, {bool recursive = false}) { + final String name = _fileSystem.path.basename(path); + _fileSystem.linkCreateCount[name] = (_fileSystem.linkCreateCount[name] ?? 0) + 1; + super.createSync(target, recursive: recursive); + } + + @override + void deleteSync({bool recursive = false}) { + final String name = _fileSystem.path.basename(path); + _fileSystem.linkDeleteCount[name] = (_fileSystem.linkDeleteCount[name] ?? 0) + 1; + super.deleteSync(recursive: recursive); + } +} From a783556ebe75dc1cafbbf0a5778f2eeb71f3e166 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Sat, 8 Aug 2026 06:20:42 +0900 Subject: [PATCH 137/330] iOS: Remove the non-merged thread topology (#190698) iOS runs with the platform and UI threads merged, and it's not possible to create an engine with unmerged platform/ui threads, but `FlutterEngine` still included branches to create the unmerged threads when `Settings::merged_platform_ui_thread` wasn't enabled. The option to opt-out of merged threads on iOS was removed in #174408 in Aug 2025, and it had been the default configuration for over a year at that point. Both existing routes to a non-merged thread configuration already failed at startup: * Setting `FLTEnableMergedPlatformUIThread=false` in Info.plist causes an `FML_CHECK` in `FlutterDartProject.mm` to blow up. * Setting `--no-enable-merged-platform-ui-thread` or `--merged-platform-ui-thread=disabled` results in them being parsed into Settings::merged_platform_ui_thread in `SettingsFromCommandLine`, and triggers the same `FML_CHECK`. The only route that didn't trigger an abort() on startup was `-[FlutterDartProject initWithSettings:]`, which is declared in `FlutterDartProject_Internal.h` and isn't part of the framework's public API. This also removes the last place the iOS embedder checked `Settings::enable_impeller`. Now it's simply assumed. This is part of cleanup work intended to simplify the iOS embedder prior to an eventual migration to the embedder API. Issue: https://github.com/flutter/flutter/issues/112232 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/framework/Source/FlutterEngine.mm | 41 +++++++------------ .../ios/framework/Source/FlutterEngineTest.mm | 13 ------ 2 files changed, 14 insertions(+), 40 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index cf7dc0badc47e..6c7edcbff0a9a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -205,7 +205,6 @@ @implementation FlutterEngine { FlutterTextureRegistryRelay* _textureRegistry; FlutterFMLTaskRunner* _platformTaskRunnerWrapper; - FlutterFMLTaskRunner* _uiTaskRunnerWrapper; FlutterFMLTaskRunner* _rasterTaskRunnerWrapper; } @@ -456,7 +455,8 @@ - (FlutterFMLTaskRunner*)platformTaskRunner { } - (FlutterFMLTaskRunner*)uiTaskRunner { - return _uiTaskRunnerWrapper; + // The platform and UI threads are always merged on iOS. + return _platformTaskRunnerWrapper; } - (FlutterFMLTaskRunner*)rasterTaskRunner { @@ -582,7 +582,6 @@ - (void)destroyContext { _threadHost.reset(); _platformViewsController = nil; _platformTaskRunnerWrapper = nil; - _uiTaskRunnerWrapper = nil; _rasterTaskRunnerWrapper = nil; } @@ -806,8 +805,6 @@ - (void)setUpShell:(std::unique_ptr)shell _shell = std::move(shell); _platformTaskRunnerWrapper = [[FlutterFMLTaskRunner alloc] initWithTaskRunner:_shell->GetTaskRunners().GetPlatformTaskRunner()]; - _uiTaskRunnerWrapper = - [[FlutterFMLTaskRunner alloc] initWithTaskRunner:_shell->GetTaskRunners().GetUITaskRunner()]; _rasterTaskRunnerWrapper = [[FlutterFMLTaskRunner alloc] initWithTaskRunner:_shell->GetTaskRunners().GetRasterTaskRunner()]; @@ -835,16 +832,14 @@ + (NSString*)generateThreadLabel:(NSString*)labelPrefix { return [NSString stringWithFormat:@"%@.%zu", labelPrefix, ++s_shellCount]; } -static flutter::ThreadHost MakeThreadHost(NSString* thread_label, - const flutter::Settings& settings) { +static flutter::ThreadHost MakeThreadHost(NSString* thread_label) { // The current thread will be used as the platform thread. Ensure that the message loop is // initialized. fml::MessageLoop::EnsureInitializedForCurrentThread(); + // No dedicated UI thread is created: on iOS the UI thread is always merged onto the platform + // thread. FlutterDartProject rejects any other threading configuration at startup. uint32_t threadHostType = flutter::ThreadHost::Type::kRaster | flutter::ThreadHost::Type::kIo; - if (settings.merged_platform_ui_thread != flutter::Settings::MergedPlatformUIThread::kEnabled) { - threadHostType |= flutter::ThreadHost::Type::kUi; - } if ([FlutterEngine isProfilerEnabled]) { threadHostType = threadHostType | flutter::ThreadHost::Type::kProfiler; @@ -853,10 +848,6 @@ + (NSString*)generateThreadLabel:(NSString*)labelPrefix { flutter::ThreadHost::ThreadHostConfig host_config(thread_label.UTF8String, threadHostType, IOSPlatformThreadConfigSetter); - host_config.ui_config = - fml::Thread::ThreadConfig(flutter::ThreadHost::ThreadHostConfig::MakeThreadName( - flutter::ThreadHost::Type::kUi, thread_label.UTF8String), - fml::Thread::ThreadPriority::kDisplay); host_config.raster_config = fml::Thread::ThreadConfig(flutter::ThreadHost::ThreadHostConfig::MakeThreadName( flutter::ThreadHost::Type::kRaster, thread_label.UTF8String), @@ -907,7 +898,7 @@ - (BOOL)createShell:(NSString*)entrypoint NSString* threadLabel = [FlutterEngine generateThreadLabel:self.labelPrefix]; _threadHost = std::make_shared(); - *_threadHost = MakeThreadHost(threadLabel, settings); + *_threadHost = MakeThreadHost(threadLabel); __weak FlutterEngine* weakSelf = self; flutter::Shell::CreateCallback on_create_platform_view = @@ -927,18 +918,14 @@ - (BOOL)createShell:(NSString*)entrypoint flutter::Shell::CreateCallback on_create_rasterizer = [](flutter::Shell& shell) { return std::make_unique(shell); }; - fml::RefPtr ui_runner; - if (settings.enable_impeller && - settings.merged_platform_ui_thread == flutter::Settings::MergedPlatformUIThread::kEnabled) { - ui_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); - } else { - ui_runner = _threadHost->ui_thread->GetTaskRunner(); - } - flutter::TaskRunners task_runners(threadLabel.UTF8String, // label - fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform - _threadHost->raster_thread->GetTaskRunner(), // raster - ui_runner, // ui - _threadHost->io_thread->GetTaskRunner() // io + // The platform and UI threads are always merged on iOS, so the UI task runner is the platform + // thread's task runner. + fml::RefPtr platform_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); + flutter::TaskRunners task_runners(threadLabel.UTF8String, // label + platform_runner, // platform + _threadHost->raster_thread->GetTaskRunner(), // raster + platform_runner, // ui + _threadHost->io_thread->GetTaskRunner() // io ); // Disable GPU if the app or scene is running in the background. diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 60d78fe1efaac..6bf47196d00e3 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -580,19 +580,6 @@ - (void)testCanMergePlatformAndUIThread { #endif // defined(TARGET_IPHONE_SIMULATOR) && TARGET_IPHONE_SIMULATOR } -- (void)testCanUnMergePlatformAndUIThread { -#if defined(TARGET_IPHONE_SIMULATOR) && TARGET_IPHONE_SIMULATOR - auto settings = FLTDefaultSettingsForBundle(); - settings.merged_platform_ui_thread = flutter::Settings::MergedPlatformUIThread::kDisabled; - FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings]; - FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; - [engine run]; - - XCTAssertNotEqual(engine.shell.GetTaskRunners().GetUITaskRunner(), - engine.shell.GetTaskRunners().GetPlatformTaskRunner()); -#endif // defined(TARGET_IPHONE_SIMULATOR) && TARGET_IPHONE_SIMULATOR -} - - (void)testAddSceneDelegateToRegistrar { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project]; From 4ff86d444fb94c39c38111f4b9be649bb716dff7 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Fri, 7 Aug 2026 14:54:19 -0700 Subject: [PATCH 138/330] [Flutter GPU] Add a project-level Flutter GPU setting to the Linux and Windows embedders (#190684) Adds `fl_dart_project_set_enable_flutter_gpu` (Linux) and `DartProject::set_enable_flutter_gpu` (Windows), forwarding `--enable-flutter-gpu` to the engine the same way the existing project-level Impeller settings do. Release desktop builds ignore engine switches from the environment, so this is the only way a released app on these platforms can enable Flutter GPU, matching the permanent opt-ins the other platforms already have (`FLTEnableFlutterGPU` in Info.plist on iOS and macOS, the `EnableFlutterGPU` manifest flag on Android). ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md --- .../shell/platform/linux/fl_dart_project.cc | 14 +++ .../platform/linux/fl_dart_project_test.cc | 10 ++ .../flutter/shell/platform/linux/fl_engine.cc | 24 +++- .../shell/platform/linux/fl_engine_test.cc | 52 +++++++++ .../public/flutter_linux/fl_dart_project.h | 19 +++ .../client_wrapper/dart_project_unittests.cc | 11 ++ .../windows/client_wrapper/flutter_engine.cc | 1 + .../include/flutter/dart_project.h | 12 ++ .../windows/flutter_project_bundle.cc | 2 + .../platform/windows/flutter_project_bundle.h | 6 + .../flutter_project_bundle_unittests.cc | 13 +++ .../windows/flutter_windows_engine.cc | 10 ++ .../flutter_windows_engine_unittests.cc | 109 ++++++++++++++++++ .../platform/windows/public/flutter_windows.h | 5 + .../testing/flutter_windows_engine_builder.cc | 4 + .../testing/flutter_windows_engine_builder.h | 2 + 16 files changed, 292 insertions(+), 2 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_dart_project.cc b/engine/src/flutter/shell/platform/linux/fl_dart_project.cc index 43d98070a8906..6c626c61105a1 100644 --- a/engine/src/flutter/shell/platform/linux/fl_dart_project.cc +++ b/engine/src/flutter/shell/platform/linux/fl_dart_project.cc @@ -16,6 +16,7 @@ struct _FlDartProject { FlUIThreadPolicy ui_thread_policy; gboolean enable_impeller; + gboolean enable_flutter_gpu; }; G_DEFINE_TYPE(FlDartProject, fl_dart_project, G_TYPE_OBJECT) @@ -145,3 +146,16 @@ gboolean fl_dart_project_get_enable_impeller(FlDartProject* project) { g_return_val_if_fail(FL_IS_DART_PROJECT(project), FALSE); return project->enable_impeller; } + +G_MODULE_EXPORT +void fl_dart_project_set_enable_flutter_gpu(FlDartProject* project, + gboolean enable_flutter_gpu) { + g_return_if_fail(FL_IS_DART_PROJECT(project)); + project->enable_flutter_gpu = enable_flutter_gpu; +} + +G_MODULE_EXPORT +gboolean fl_dart_project_get_enable_flutter_gpu(FlDartProject* project) { + g_return_val_if_fail(FL_IS_DART_PROJECT(project), FALSE); + return project->enable_flutter_gpu; +} diff --git a/engine/src/flutter/shell/platform/linux/fl_dart_project_test.cc b/engine/src/flutter/shell/platform/linux/fl_dart_project_test.cc index dc0d1d2a9498b..93e570e535e79 100644 --- a/engine/src/flutter/shell/platform/linux/fl_dart_project_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_dart_project_test.cc @@ -80,3 +80,13 @@ TEST_F(FlDartProjectTest, EnableImpeller) { fl_dart_project_set_enable_impeller(project, TRUE); EXPECT_TRUE(fl_dart_project_get_enable_impeller(project)); } + +TEST_F(FlDartProjectTest, EnableFlutterGpu) { + EXPECT_FALSE(fl_dart_project_get_enable_flutter_gpu(project)); + + fl_dart_project_set_enable_flutter_gpu(project, TRUE); + EXPECT_TRUE(fl_dart_project_get_enable_flutter_gpu(project)); + + fl_dart_project_set_enable_flutter_gpu(project, FALSE); + EXPECT_FALSE(fl_dart_project_get_enable_flutter_gpu(project)); +} diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 6c7b6d2ae3ccf..f88d08cb05f47 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -812,9 +812,12 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { break; } + const std::vector env_switches = + flutter::GetSwitchesFromEnvironment(); + gboolean enable_impeller = fl_dart_project_get_enable_impeller(self->project); gboolean has_enable_impeller = FALSE; - for (const auto& env_switch : flutter::GetSwitchesFromEnvironment()) { + for (const auto& env_switch : env_switches) { if (env_switch == "--enable-impeller" || env_switch == "--enable-impeller=true") { enable_impeller = TRUE; @@ -828,7 +831,7 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { g_autoptr(GPtrArray) command_line_args = g_ptr_array_new_with_free_func(g_free); g_ptr_array_insert(command_line_args, 0, g_strdup("flutter")); - for (const auto& env_switch : flutter::GetSwitchesFromEnvironment()) { + for (const auto& env_switch : env_switches) { g_ptr_array_add(command_line_args, g_strdup(env_switch.c_str())); } // Linux (and other desktop platforms) always uses SDFs. @@ -838,6 +841,23 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { g_ptr_array_add(command_line_args, g_strdup("--enable-impeller")); } + // Forward the project's Flutter GPU setting unless an environment switch + // already carries it (the switch is presence based, so it is only ever + // added, never negated). + if (fl_dart_project_get_enable_flutter_gpu(self->project)) { + gboolean has_enable_flutter_gpu = FALSE; + for (const auto& env_switch : env_switches) { + if (env_switch == "--enable-flutter-gpu" || + env_switch == "--enable-flutter-gpu=true") { + has_enable_flutter_gpu = TRUE; + break; + } + } + if (!has_enable_flutter_gpu) { + g_ptr_array_add(command_line_args, g_strdup("--enable-flutter-gpu")); + } + } + gchar** dart_entrypoint_args = fl_dart_project_get_dart_entrypoint_arguments(self->project); diff --git a/engine/src/flutter/shell/platform/linux/fl_engine_test.cc b/engine/src/flutter/shell/platform/linux/fl_engine_test.cc index 2e32e301a84bc..dd2528e248223 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine_test.cc @@ -1008,6 +1008,58 @@ TEST_F(FlEngineTest, DisableImpeller) { EXPECT_TRUE(called); } +TEST_F(FlEngineTest, EnableFlutterGpuDefault) { + bool called = false; + fl_engine_get_embedder_api(engine)->Initialize = MOCK_ENGINE_PROC( + Initialize, + ([&called](size_t version, const FlutterRendererConfig* config, + const FlutterProjectArgs* args, void* user_data, + FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { + called = true; + bool has_flutter_gpu_switch = false; + for (int i = 0; i < args->command_line_argc; i++) { + if (strcmp(args->command_line_argv[i], "--enable-flutter-gpu") == 0) { + has_flutter_gpu_switch = true; + } + } + EXPECT_FALSE(has_flutter_gpu_switch); + return kSuccess; + })); + fl_engine_get_embedder_api(engine)->RunInitialized = + MOCK_ENGINE_PROC(RunInitialized, ([](auto engine) { return kSuccess; })); + + StartEngine(); + EXPECT_TRUE(called); +} + +TEST_F(FlEngineTest, EnableFlutterGpu) { + fl_dart_project_set_enable_flutter_gpu(project, TRUE); + + bool called = false; + fl_engine_get_embedder_api(engine)->Initialize = MOCK_ENGINE_PROC( + Initialize, + ([&called](size_t version, const FlutterRendererConfig* config, + const FlutterProjectArgs* args, void* user_data, + FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { + called = true; + bool has_flutter_gpu_switch = false; + for (int i = 0; i < args->command_line_argc; i++) { + if (strcmp(args->command_line_argv[i], "--enable-flutter-gpu") == 0) { + has_flutter_gpu_switch = true; + } + } + EXPECT_TRUE(has_flutter_gpu_switch); + return kSuccess; + })); + fl_engine_get_embedder_api(engine)->RunInitialized = + MOCK_ENGINE_PROC(RunInitialized, ([](auto engine) { return kSuccess; })); + + g_autoptr(GError) error = nullptr; + EXPECT_TRUE(fl_engine_start(engine, &error)); + EXPECT_EQ(error, nullptr); + EXPECT_TRUE(called); +} + TEST_F(FlEngineTest, ChildObjects) { // Check objects exist before engine started. EXPECT_NE(fl_engine_get_binary_messenger(engine), nullptr); diff --git a/engine/src/flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h b/engine/src/flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h index 0b50eac76024b..4b83173a3557d 100644 --- a/engine/src/flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h +++ b/engine/src/flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h @@ -178,6 +178,25 @@ void fl_dart_project_set_enable_impeller(FlDartProject* project, */ gboolean fl_dart_project_get_enable_impeller(FlDartProject* project); +/** + * fl_dart_project_set_enable_flutter_gpu: + * @project: an #FlDartProject. + * @enable_flutter_gpu: whether to enable the Flutter GPU API. + * + * Sets whether the Flutter GPU API (package:flutter_gpu) should be enabled. + * Flutter GPU requires the Impeller renderer. + */ +void fl_dart_project_set_enable_flutter_gpu(FlDartProject* project, + gboolean enable_flutter_gpu); + +/** + * fl_dart_project_get_enable_flutter_gpu: + * @project: an #FlDartProject. + * + * Returns: %TRUE if the Flutter GPU API is enabled. + */ +gboolean fl_dart_project_get_enable_flutter_gpu(FlDartProject* project); + G_END_DECLS #endif // FLUTTER_SHELL_PLATFORM_LINUX_PUBLIC_FLUTTER_LINUX_FL_DART_PROJECT_H_ diff --git a/engine/src/flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc b/engine/src/flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc index bfc151947f673..e2a8746bb20a4 100644 --- a/engine/src/flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc @@ -55,4 +55,15 @@ TEST_F(DartProjectTest, DartEntrypointArguments) { EXPECT_EQ(returned_arguments[2], "arg3"); } +TEST_F(DartProjectTest, EnableFlutterGpu) { + DartProject project(L"test"); + EXPECT_FALSE(project.enable_flutter_gpu()); + + project.set_enable_flutter_gpu(true); + EXPECT_TRUE(project.enable_flutter_gpu()); + + project.set_enable_flutter_gpu(false); + EXPECT_FALSE(project.enable_flutter_gpu()); +} + } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_engine.cc b/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_engine.cc index f5340fc558ea8..fb2a31ddf5761 100644 --- a/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_engine.cc +++ b/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_engine.cc @@ -27,6 +27,7 @@ FlutterEngine::FlutterEngine(const DartProject& project) { project.accessibility_mode()); c_engine_properties.impeller_switch = static_cast(project.impeller_switch()); + c_engine_properties.enable_flutter_gpu = project.enable_flutter_gpu(); const std::vector& entrypoint_args = project.dart_entrypoint_arguments(); diff --git a/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h b/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h index 4a2e830cafb57..2dc37d201fbc9 100644 --- a/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h +++ b/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h @@ -153,6 +153,16 @@ class DartProject { // Defaults to ImpellerSwitch::Default. ImpellerSwitch impeller_switch() const { return impeller_switch_; } + // Sets whether the Flutter GPU API (package:flutter_gpu) is enabled. + // Flutter GPU requires the Impeller renderer. + void set_enable_flutter_gpu(bool enable_flutter_gpu) { + enable_flutter_gpu_ = enable_flutter_gpu; + } + + // Returns whether the Flutter GPU API is enabled. + // Defaults to false. + bool enable_flutter_gpu() const { return enable_flutter_gpu_; } + private: // Accessors for internals are private, so that they can be changed if more // flexible options for project structures are needed later without it @@ -185,6 +195,8 @@ class DartProject { AccessibilityMode accessibility_mode_ = AccessibilityMode::Default; // The Impeller enablement switch. ImpellerSwitch impeller_switch_ = ImpellerSwitch::Default; + // Whether the Flutter GPU API is enabled. + bool enable_flutter_gpu_ = false; }; } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.cc b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.cc index 03f20fb586976..63cfead0e0d64 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.cc @@ -42,6 +42,8 @@ FlutterProjectBundle::FlutterProjectBundle( impeller_switch_ = static_cast(properties.impeller_switch); + enable_flutter_gpu_ = properties.enable_flutter_gpu; + // Resolve any relative paths. if (assets_path_.is_relative() || icu_path_.is_relative() || (!aot_library_path_.empty() && aot_library_path_.is_relative())) { diff --git a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h index 59f7041d39da3..f504ea8555687 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h +++ b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h @@ -97,6 +97,9 @@ class FlutterProjectBundle { // Returns the Impeller enablement switch. FlutterImpellerSwitch impeller_switch() const { return impeller_switch_; } + // Returns whether the Flutter GPU API is enabled. + bool enable_flutter_gpu() const { return enable_flutter_gpu_; } + private: std::filesystem::path assets_path_; std::filesystem::path icu_path_; @@ -124,6 +127,9 @@ class FlutterProjectBundle { // The Impeller enablement switch. FlutterImpellerSwitch impeller_switch_ = FlutterImpellerSwitch::Default; + + // Whether the Flutter GPU API is enabled. + bool enable_flutter_gpu_ = false; }; } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/flutter_project_bundle_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_project_bundle_unittests.cc index 607d1cc707d99..c30d7f1ee8a4d 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_project_bundle_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_project_bundle_unittests.cc @@ -34,6 +34,19 @@ TEST(FlutterProjectBundle, BasicPropertiesRelativePaths) { EXPECT_EQ(project.icu_path().filename().string(), "icudtl.dat"); } +TEST(FlutterProjectBundle, EnableFlutterGpu) { + FlutterDesktopEngineProperties properties = {}; + properties.assets_path = L"foo\\flutter_assets"; + properties.icu_data_path = L"foo\\icudtl.dat"; + + FlutterProjectBundle default_project(properties); + EXPECT_FALSE(default_project.enable_flutter_gpu()); + + properties.enable_flutter_gpu = true; + FlutterProjectBundle project(properties); + EXPECT_TRUE(project.enable_flutter_gpu()); +} + TEST(FlutterProjectBundle, SwitchesEmpty) { FlutterDesktopEngineProperties properties = {}; properties.assets_path = L"foo\\flutter_assets"; diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc index 5275e5b612424..d9f797ca257ba 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -326,6 +326,16 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { switches.push_back("--enable-impeller=false"); } } + if (project_->enable_flutter_gpu()) { + if (std::find(switches.begin(), switches.end(), "--enable-flutter-gpu") == + switches.end() && + std::find(switches.begin(), switches.end(), + "--enable-flutter-gpu=true") == switches.end()) { + // Flutter GPU was enabled programmatically, so forward the switch to + // the engine. + switches.push_back("--enable-flutter-gpu"); + } + } std::transform( switches.begin(), switches.end(), std::back_inserter(argv), [](const std::string& arg) -> const char* { return arg.c_str(); }); diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc index 3932b5e790f05..81a7e02fd28ae 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -534,6 +534,115 @@ TEST_F(FlutterWindowsEngineTest, RunWithProjectFlagEnableImpeller) { modifier.ReleaseEGLManager(); } +TEST_F(FlutterWindowsEngineTest, RunWithProjectFlagEnableFlutterGpu) { + FlutterWindowsEngineBuilder builder{GetContext()}; + builder.SetEnableFlutterGpu(true); + std::unique_ptr engine = builder.Build(); + EngineModifier modifier(engine.get()); + + modifier.embedder_api().NotifyDisplayUpdate = + MOCK_ENGINE_PROC(NotifyDisplayUpdate, + ([](FLUTTER_API_SYMBOL(FlutterEngine) raw_engine, + const FlutterEngineDisplaysUpdateType update_type, + const FlutterEngineDisplay* embedder_displays, + size_t display_count) { return kSuccess; })); + + modifier.embedder_api().UpdateAccessibilityFeatures = MOCK_ENGINE_PROC( + UpdateAccessibilityFeatures, + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, + FlutterAccessibilityFeature flags) { return kSuccess; }); + + modifier.embedder_api().UpdateLocales = MOCK_ENGINE_PROC( + UpdateLocales, ([](auto engine, const FlutterLocale** locales, + size_t locales_count) { return kSuccess; })); + + modifier.embedder_api().SendPlatformMessage = + MOCK_ENGINE_PROC(SendPlatformMessage, + ([](auto engine, auto message) { return kSuccess; })); + + bool run_called = false; + modifier.embedder_api().Run = MOCK_ENGINE_PROC( + Run, ([&run_called](size_t version, const FlutterRendererConfig* config, + const FlutterProjectArgs* args, void* user_data, + FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { + run_called = true; + *engine_out = reinterpret_cast(1); + + bool has_flutter_gpu_switch = false; + for (int i = 0; i < args->command_line_argc; ++i) { + if (strcmp(args->command_line_argv[i], "--enable-flutter-gpu") == 0) { + has_flutter_gpu_switch = true; + } + } + EXPECT_TRUE(has_flutter_gpu_switch); + return kSuccess; + })); + + // Set the EGL manager to !nullptr to test ANGLE rendering. + modifier.SetEGLManager(std::make_unique()); + + engine->Run(); + + EXPECT_TRUE(run_called); + + modifier.embedder_api().Shutdown = [](auto engine) { return kSuccess; }; + modifier.ReleaseEGLManager(); +} + +TEST_F(FlutterWindowsEngineTest, RunWithoutProjectFlagEnableFlutterGpu) { + FlutterWindowsEngineBuilder builder{GetContext()}; + std::unique_ptr engine = builder.Build(); + EngineModifier modifier(engine.get()); + + modifier.embedder_api().NotifyDisplayUpdate = + MOCK_ENGINE_PROC(NotifyDisplayUpdate, + ([](FLUTTER_API_SYMBOL(FlutterEngine) raw_engine, + const FlutterEngineDisplaysUpdateType update_type, + const FlutterEngineDisplay* embedder_displays, + size_t display_count) { return kSuccess; })); + + modifier.embedder_api().UpdateAccessibilityFeatures = MOCK_ENGINE_PROC( + UpdateAccessibilityFeatures, + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, + FlutterAccessibilityFeature flags) { return kSuccess; }); + + modifier.embedder_api().UpdateLocales = MOCK_ENGINE_PROC( + UpdateLocales, ([](auto engine, const FlutterLocale** locales, + size_t locales_count) { return kSuccess; })); + + modifier.embedder_api().SendPlatformMessage = + MOCK_ENGINE_PROC(SendPlatformMessage, + ([](auto engine, auto message) { return kSuccess; })); + + bool run_called = false; + modifier.embedder_api().Run = MOCK_ENGINE_PROC( + Run, ([&run_called](size_t version, const FlutterRendererConfig* config, + const FlutterProjectArgs* args, void* user_data, + FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { + run_called = true; + *engine_out = reinterpret_cast(1); + + bool has_flutter_gpu_switch = false; + for (int i = 0; i < args->command_line_argc; ++i) { + if (strcmp(args->command_line_argv[i], "--enable-flutter-gpu") == 0) { + has_flutter_gpu_switch = true; + } + } + EXPECT_FALSE(has_flutter_gpu_switch); + return kSuccess; + })); + + // Set the EGL manager to !nullptr to test ANGLE rendering. + modifier.SetEGLManager(std::make_unique()); + + engine->Run(); + + EXPECT_TRUE(run_called); + + modifier.embedder_api().Shutdown = [](auto engine) { return kSuccess; }; + modifier.ReleaseEGLManager(); +} + TEST_F(FlutterWindowsEngineTest, RunWithProjectFlagDisableImpeller) { FlutterWindowsEngineBuilder builder{GetContext()}; builder.SetImpellerSwitch(DisabledImpeller); diff --git a/engine/src/flutter/shell/platform/windows/public/flutter_windows.h b/engine/src/flutter/shell/platform/windows/public/flutter_windows.h index 54954042f47cd..2194cca17201b 100644 --- a/engine/src/flutter/shell/platform/windows/public/flutter_windows.h +++ b/engine/src/flutter/shell/platform/windows/public/flutter_windows.h @@ -128,6 +128,11 @@ typedef struct { // Policy for enabling the Impeller renderer. FlutterDesktopImpellerSwitch impeller_switch; + + // Whether to enable the Flutter GPU API (package:flutter_gpu). + // Flutter GPU requires the Impeller renderer. + // If not set defaults to false. + bool enable_flutter_gpu; } FlutterDesktopEngineProperties; // ========== View Controller ========== diff --git a/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.cc b/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.cc index 62c1a407ff2e8..28a302a2d6e82 100644 --- a/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.cc +++ b/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.cc @@ -75,6 +75,10 @@ void FlutterWindowsEngineBuilder::SetImpellerSwitch( properties_.impeller_switch = impeller_switch; } +void FlutterWindowsEngineBuilder::SetEnableFlutterGpu(bool enable_flutter_gpu) { + properties_.enable_flutter_gpu = enable_flutter_gpu; +} + void FlutterWindowsEngineBuilder::SetCreateKeyboardHandlerCallbacks( KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state, KeyboardKeyEmbedderHandler::MapVirtualKeyToScanCode map_vk_to_scan) { diff --git a/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h b/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h index a10d11394d7ae..2f2ef89ea7df1 100644 --- a/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h +++ b/engine/src/flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h @@ -33,6 +33,8 @@ class FlutterWindowsEngineBuilder { void SetImpellerSwitch(FlutterDesktopImpellerSwitch impeller_switch); + void SetEnableFlutterGpu(bool enable_flutter_gpu); + void SetWindowsProcTable( std::shared_ptr windows_proc_table); From cda81a73521f85d4283e6acd83329da6c42ad9c9 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Fri, 7 Aug 2026 15:46:32 -0700 Subject: [PATCH 139/330] Migrate flavors_test_ios to Simulators and add UIScene support (#189442) Part of #186414. This PR introduces the `testWithNewIOSSimulator` helper pattern to execute the `flavors_test_ios` on macOS VMs rather than physical lab devices. This also migrates the old flavors test app to adopt the UIScene lifecycle using `FlutterSceneDelegate`. --- .ci.yaml | 5 +- dev/devicelab/bin/tasks/flavors_test_ios.dart | 78 +++++++++++-------- .../lib/tasks/integration_tests.dart | 29 +++++-- .../flavors/ios/Runner/AppDelegate.h | 2 +- .../flavors/ios/Runner/AppDelegate.m | 12 +-- .../flavors/ios/Runner/Info-Free.plist | 21 +++++ .../flavors/ios/Runner/Info-Paid.plist | 21 +++++ 7 files changed, 123 insertions(+), 45 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 2a059bf39e0de..887cfa87b82ec 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -5478,13 +5478,14 @@ targets: ["devicelab", "ios", "mac"] task_name: route_test_ios - - name: Mac_ios flavors_test_ios + - name: Mac flavors_test_ios recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: tags: > - ["devicelab", "ios", "mac"] + ["devicelab", "hostonly", "mac"] task_name: flavors_test_ios - name: Mac_arm64_ios flutter_gallery_ios__compile diff --git a/dev/devicelab/bin/tasks/flavors_test_ios.dart b/dev/devicelab/bin/tasks/flavors_test_ios.dart index 0c81f5bf5f74b..6d94b65aa946f 100644 --- a/dev/devicelab/bin/tasks/flavors_test_ios.dart +++ b/dev/devicelab/bin/tasks/flavors_test_ios.dart @@ -18,37 +18,50 @@ import 'package:standard_message_codec/standard_message_codec.dart'; Future main() async { deviceOperatingSystem = DeviceOperatingSystem.ios; await task(() async { - await createFlavorsTest().call(); - await createIntegrationTestFlavorsTest().call(); - // test install and uninstall of flavors app - final projectDir = '${flutterDirectory.path}/dev/integration_tests/flavors'; - final TaskResult installTestsResult = await inDirectory(projectDir, () async { - final testResults = [ - await _testInstallDebugPaidFlavor(projectDir), - await _testInstallBogusFlavor(), - ]; - - final TaskResult? firstInstallFailure = testResults.firstWhereOrNull( - (TaskResult element) => element.failed, - ); - - return firstInstallFailure ?? TaskResult.success(null); - }); - - await _testFlavorWhenBuiltFromXcode(projectDir); - - return installTestsResult; + String? simulatorDeviceId; + var res = TaskResult.success(null); + try { + await testWithNewIOSSimulator('flavors_test_ios', (String deviceId) async { + simulatorDeviceId = deviceId; + await createFlavorsTest(deviceIdOverride: deviceId).call(); + await createIntegrationTestFlavorsTest(deviceIdOverride: deviceId).call(); + // test install and uninstall of flavors app + final projectDir = '${flutterDirectory.path}/dev/integration_tests/flavors'; + final TaskResult installTestsResult = await inDirectory(projectDir, () async { + final testResults = [ + await _testInstallDebugPaidFlavor(projectDir, deviceId), + await _testInstallBogusFlavor(deviceId), + ]; + + final TaskResult? firstInstallFailure = testResults.firstWhereOrNull( + (TaskResult element) => element.failed, + ); + + return firstInstallFailure ?? TaskResult.success(null); + }); + + if (installTestsResult.failed) { + res = installTestsResult; + return; + } + + res = await _testFlavorWhenBuiltFromXcode(projectDir, deviceId); + }); + } finally { + await removeIOSSimulator(simulatorDeviceId); + } + return res; }); } -Future _testInstallDebugPaidFlavor(String projectDir) async { - await evalFlutter('install', options: ['--flavor', 'paid']); +Future _testInstallDebugPaidFlavor(String projectDir, String deviceId) async { + await evalFlutter('install', options: ['-d', deviceId, '--flavor', 'paid']); final Uint8List assetManifestFileData = File( path.join( projectDir, 'build', 'ios', - 'iphoneos', + 'iphonesimulator', 'Paid App.app', 'Frameworks', 'App.framework', @@ -77,18 +90,21 @@ Future _testInstallDebugPaidFlavor(String projectDir) async { ); } - await flutter('install', options: ['--flavor', 'paid', '--uninstall-only']); + await flutter( + 'install', + options: ['-d', deviceId, '--flavor', 'paid', '--uninstall-only'], + ); return TaskResult.success(null); } -Future _testInstallBogusFlavor() async { +Future _testInstallBogusFlavor(String deviceId) async { final stderr = StringBuffer(); await evalFlutter( 'install', canFail: true, stderr: stderr, - options: ['--flavor', 'bogus'], + options: ['-d', deviceId, '--flavor', 'bogus'], ); final stderrString = stderr.toString(); @@ -102,13 +118,12 @@ Future _testInstallBogusFlavor() async { return TaskResult.success(null); } -Future _testFlavorWhenBuiltFromXcode(String projectDir) async { - final Device device = await devices.workingDevice; +Future _testFlavorWhenBuiltFromXcode(String projectDir, String deviceId) async { await inDirectory(projectDir, () async { // This will put FLAVOR=free in the Flutter/Generated.xcconfig file await flutter( 'build', - options: ['ios', '--config-only', '--debug', '--flavor', 'free'], + options: ['ios', '--simulator', '--config-only', '--debug', '--flavor', 'free'], ); }); @@ -126,7 +141,7 @@ Future _testFlavorWhenBuiltFromXcode(String projectDir) async { // Delete app bundle before build to ensure checks below do not use previously // built bundle. - final appPath = '$projectDir/$buildDir/$configuration-iphoneos/$productName.app'; + final appPath = '$projectDir/$buildDir/$configuration-iphonesimulator/$productName.app'; final appBundle = Directory(appPath); if (appBundle.existsSync()) { appBundle.deleteSync(recursive: true); @@ -134,7 +149,7 @@ Future _testFlavorWhenBuiltFromXcode(String projectDir) async { if (!await runXcodeBuild( platformDirectory: path.join(projectDir, 'ios'), - destination: 'id=${device.deviceId}', + destination: 'id=$deviceId', testName: 'flavors_test_ios', configuration: configuration, scheme: 'paid', @@ -155,6 +170,7 @@ Future _testFlavorWhenBuiltFromXcode(String projectDir) async { // Despite FLAVOR=free being in the Generated.xcconfig, the flavor found in // the test should be "paid" because it was built with the "Debug Paid" configuration. return createFlavorsTest( + deviceIdOverride: deviceId, extraOptions: ['--flavor', 'paid', '--use-application-binary=$appPath'], ).call(); } diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index e307cf20a1276..279b88fc44be2 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -22,21 +22,30 @@ TaskFunction createPlatformInteractionTest() { ).call; } -TaskFunction createFlavorsTest({Map? environment, List? extraOptions}) { +TaskFunction createFlavorsTest({ + Map? environment, + List? extraOptions, + String? deviceIdOverride, +}) { return DriverTest( '${flutterDirectory.path}/dev/integration_tests/flavors', 'lib/main.dart', extraOptions: extraOptions ?? ['--flavor', 'paid'], environment: environment, + deviceIdOverride: deviceIdOverride, ).call; } -TaskFunction createIntegrationTestFlavorsTest({Map? environment}) { +TaskFunction createIntegrationTestFlavorsTest({ + Map? environment, + String? deviceIdOverride, +}) { return IntegrationTest( '${flutterDirectory.path}/dev/integration_tests/flavors', 'integration_test/integration_test.dart', extraOptions: ['--flavor', 'paid'], environment: environment, + deviceIdOverride: deviceIdOverride, ).call; } @@ -333,6 +342,7 @@ class IntegrationTest { this.environment, this.setup, this.tearDown, + this.deviceIdOverride, }); final String testDirectory; @@ -341,6 +351,7 @@ class IntegrationTest { final List createPlatforms; final bool withTalkBack; final Map? environment; + final String? deviceIdOverride; /// Run before flutter drive with the result from devices.workingDevice. final Future Function(Device device)? setup; @@ -350,9 +361,15 @@ class IntegrationTest { Future call() { return inDirectory(testDirectory, () async { - final Device device = await devices.workingDevice; - await device.unlock(); - final String deviceId = device.deviceId; + String deviceId; + Device? selectedDevice; + if (deviceIdOverride != null) { + deviceId = deviceIdOverride!; + } else { + selectedDevice = await devices.workingDevice; + await selectedDevice.unlock(); + deviceId = selectedDevice.deviceId; + } await flutter('packages', options: ['get']); await setup?.call(await devices.workingDevice); @@ -364,7 +381,7 @@ class IntegrationTest { } if (withTalkBack) { - if (device is! AndroidDevice) { + if (selectedDevice is! AndroidDevice) { return TaskResult.failure( 'A test that enables TalkBack can only be run on Android devices', ); diff --git a/dev/integration_tests/flavors/ios/Runner/AppDelegate.h b/dev/integration_tests/flavors/ios/Runner/AppDelegate.h index a78a945cd2ef8..d6d78a2392c2e 100644 --- a/dev/integration_tests/flavors/ios/Runner/AppDelegate.h +++ b/dev/integration_tests/flavors/ios/Runner/AppDelegate.h @@ -5,6 +5,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/dev/integration_tests/flavors/ios/Runner/AppDelegate.m b/dev/integration_tests/flavors/ios/Runner/AppDelegate.m index 7ef36e523eddf..3d0fe3f888251 100644 --- a/dev/integration_tests/flavors/ios/Runner/AppDelegate.m +++ b/dev/integration_tests/flavors/ios/Runner/AppDelegate.m @@ -8,16 +8,18 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; - FlutterMethodChannel* flavorChannel = [FlutterMethodChannel methodChannelWithName:@"flavor" binaryMessenger:controller]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +- (void)didInitializeImplicitFlutterEngine:(NSObject*)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; + + FlutterMethodChannel* flavorChannel = [FlutterMethodChannel methodChannelWithName:@"flavor" binaryMessenger:engineBridge.applicationRegistrar.messenger]; [flavorChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { NSString* flavor = (NSString*)[[NSBundle mainBundle].infoDictionary valueForKey:@"Flavor"]; result(flavor); }]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end diff --git a/dev/integration_tests/flavors/ios/Runner/Info-Free.plist b/dev/integration_tests/flavors/ios/Runner/Info-Free.plist index b6853d58c582f..881a1b229cad2 100644 --- a/dev/integration_tests/flavors/ios/Runner/Info-Free.plist +++ b/dev/integration_tests/flavors/ios/Runner/Info-Free.plist @@ -43,5 +43,26 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + diff --git a/dev/integration_tests/flavors/ios/Runner/Info-Paid.plist b/dev/integration_tests/flavors/ios/Runner/Info-Paid.plist index b6853d58c582f..881a1b229cad2 100644 --- a/dev/integration_tests/flavors/ios/Runner/Info-Paid.plist +++ b/dev/integration_tests/flavors/ios/Runner/Info-Paid.plist @@ -43,5 +43,26 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + From a6266833e3b7e23a6a36ec7df8982c217a881677 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Fri, 7 Aug 2026 20:03:51 -0500 Subject: [PATCH 140/330] Update instructions for creating a tooling redirect (#190324) - Update the link to the firebase.json to its own short link since the file might move soon. - Adjust the prose a bit to account for the `flutter/website` repository containing multiple websites now. - Minor clean up around the doc. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/contributing/use-reliable-links.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/contributing/use-reliable-links.md b/docs/contributing/use-reliable-links.md index 8573b10c43faa..ff4cbebea5e2e 100644 --- a/docs/contributing/use-reliable-links.md +++ b/docs/contributing/use-reliable-links.md @@ -7,11 +7,11 @@ sometimes known as a permalink. Even if you update the link in the repo if it changes or breaks, outdated versions might remain in older versions of tools, docs, etc. Sometimes the link destination itself can add an appropriate redirect, -but sometimes it can't and other times it's destination is not the best choice. +but sometimes it can't and other times its destination isn't the best choice. - If you're not sure if you need a more reliable link, check out [Situations to consider](#situations-to-consider). -- If your destination does not already have a stable link, +- If your destination doesn't already have a stable link, and you think it would benefit from one, follow the instructions in [Create a reliable link](#create-a-reliable-link). @@ -48,9 +48,9 @@ starting with `/to/`, such as `flutter.dev/to/gesture-disambiguation`. Before creating a new tooling link, verify that an appropriate one doesn't exist already. -To see what redirects exist already, check the `/to/` entries in -[`flutter/website/firebase.json`][flutter-redirects] and -[`dart-lang/site-www/firebase.json`][dart-redirects]. +To see what redirects exist already, check the `/to/` entries in the +Flutter documentation website's [`firebase.json` file][flutter-redirects] and +the Dart website's [`firebase.json` file][dart-redirects]. If an appropriate tooling redirect doesn't exist already, create one following these steps: @@ -101,5 +101,5 @@ create one following these steps: > a tooling redirect might not be necessary. > The same goes for links that are only needed for a short time. -[flutter-redirects]: https://github.com/flutter/website/blob/main/firebase.json +[flutter-redirects]: https://flutter.dev/to/site-redirects [dart-redirects]: https://github.com/dart-lang/site-www/blob/main/firebase.json From e359ab98d83bd8251233b8748fb4d51734976cf9 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 8 Aug 2026 01:37:16 -0400 Subject: [PATCH 141/330] Roll Dart SDK from 3928c443a387 to b608f3238a0f (2 revisions) (#190764) https://dart.googlesource.com/sdk.git/+log/3928c443a387..b608f3238a0f 2026-08-07 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-108.0.dev 2026-08-07 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-107.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC codefu@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPS b/DEPS index 7860cfbcfe050..aba7f691c4a86 100644 --- a/DEPS +++ b/DEPS @@ -55,12 +55,12 @@ 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': '3928c443a387af72255c2bb324e989f36f4d1ec1', + 'dart_revision': 'b608f3238a0f6500f16774e7ffa7b810adfcd0c4', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py 'dart_binaryen_rev': '9926156a583cec3d22d521232b31c70fa9a87dc1', - 'dart_boringssl_rev': '7515be5ccd601ae0433d3682ba7737592b402014', + 'dart_boringssl_rev': '619e6bc3deda4e0d2fe4ad3fc439ae4fc1e00caf', 'dart_core_rev': '4a5ae2bc9db1f39fac071f1a6fade64bd155f734', 'dart_devtools_rev': '21f1838f3a9b138ac377efb953ca5a53c8832e75', 'dart_ecosystem_rev': 'ed9c592c1d35106c0a8a52044426515017a60646', From 652d221fb903e7815b99bb52d3c0ecd85aa02023 Mon Sep 17 00:00:00 2001 From: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:39:06 +0800 Subject: [PATCH 142/330] Fix non-independant tests in binding_test.dart (#188963) Part of https://github.com/flutter/flutter/issues/85160 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter_test/test/bindings_test.dart | 42 +++++++------------ .../test/http_overrides/test_test.dart | 17 ++++++++ .../http_overrides/test_widgets_test.dart | 17 ++++++++ 3 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 packages/flutter_test/test/http_overrides/test_test.dart create mode 100644 packages/flutter_test/test/http_overrides/test_widgets_test.dart diff --git a/packages/flutter_test/test/bindings_test.dart b/packages/flutter_test/test/bindings_test.dart index b217c69acc886..4eb66519153b7 100644 --- a/packages/flutter_test/test/bindings_test.dart +++ b/packages/flutter_test/test/bindings_test.dart @@ -2,15 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(gspencergoog): Remove this tag once this test's state leaks/test -// dependencies have been fixed. -// https://github.com/flutter/flutter/issues/85160 -// Fails with "flutter test --test-randomize-ordering-seed=20210721" -@Tags(['no-shuffle']) -library; - import 'dart:async'; -import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -44,28 +36,22 @@ void main() { }); }); - // The next three tests must run in order -- first using `test`, then `testWidgets`, then `test` again. - - var order = 0; - - test('Initializes httpOverrides and testTextInput', () async { - assert(order == 0); - expect(binding.testTextInput, isNotNull); - expect(binding.testTextInput.isRegistered, isFalse); - expect(HttpOverrides.current, isNotNull); - order += 1; - }); + group('testTextInput', () { + setUp(() { + expect(binding.testTextInput, isNotNull); + expect(binding.testTextInput.isRegistered, isFalse); + }); + tearDown(() { + expect(binding.testTextInput.isRegistered, isFalse); + }); - testWidgets('Registers testTextInput', (WidgetTester tester) async { - assert(order == 1); - expect(tester.testTextInput.isRegistered, isTrue); - order += 1; - }); + testWidgets('testWidgets registers testTextInput', (WidgetTester tester) async { + expect(tester.testTextInput.isRegistered, isTrue); + }); - test('Unregisters testTextInput', () async { - assert(order == 2); - expect(binding.testTextInput.isRegistered, isFalse); - order += 1; + test('test does not register testTextInput', () async { + expect(binding.testTextInput.isRegistered, isFalse); + }); }); testWidgets('timeStamp should be accurate to microsecond precision', (WidgetTester tester) async { diff --git a/packages/flutter_test/test/http_overrides/test_test.dart b/packages/flutter_test/test/http_overrides/test_test.dart new file mode 100644 index 0000000000000..958d00a0f4c8e --- /dev/null +++ b/packages/flutter_test/test/http_overrides/test_test.dart @@ -0,0 +1,17 @@ +// 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 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + setUp(() { + expect(HttpOverrides.current, isNull); + }); + + test('test does not register HttpOverrides.current', () { + expect(HttpOverrides.current, isNull); + }); +} diff --git a/packages/flutter_test/test/http_overrides/test_widgets_test.dart b/packages/flutter_test/test/http_overrides/test_widgets_test.dart new file mode 100644 index 0000000000000..509ebeba45a3d --- /dev/null +++ b/packages/flutter_test/test/http_overrides/test_widgets_test.dart @@ -0,0 +1,17 @@ +// 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 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + setUp(() { + expect(HttpOverrides.current, isNotNull); + }); + + testWidgets('testWidgets registers HttpOverrides.current', (WidgetTester tester) async { + expect(HttpOverrides.current, isNotNull); + }); +} From f69633edddff9f9a530c09824e05b6947e854d50 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 8 Aug 2026 02:46:38 -0400 Subject: [PATCH 143/330] Roll Fuchsia Test Scripts from vcANVO8VIDQHasH1X... to k7zairCweHf0pULkj... (#190782) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/fuchsia-test-scripts-flutter Please CC chrome-fuchsia-engprod@google.com,codefu@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index aba7f691c4a86..61c91e922c636 100644 --- a/DEPS +++ b/DEPS @@ -199,7 +199,7 @@ vars = { # The version / instance id of the cipd:chromium/fuchsia/test-scripts which # will be used altogether with fuchsia-sdk to setup the build / test # environment. - 'fuchsia_test_scripts_version': 'vcANVO8VIDQHasH1X_XRoSYLvx7fNwvTbDM1NT9TwA4C', + 'fuchsia_test_scripts_version': 'k7zairCweHf0pULkjCYUmHY77xtMc39w0_VHs6AFBAwC', # The version / instance id of the cipd:chromium/fuchsia/gn-sdk which will be # used altogether with fuchsia-sdk to generate gn based build rules. From c671a99f9e301ee8738a0ee4761b142d209b7f3c Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 8 Aug 2026 05:12:37 -0400 Subject: [PATCH 144/330] Roll Skia from 5dddf11de22a to 508fc9e7f9ad (4 revisions) (#190794) https://skia.googlesource.com/skia.git/+log/5dddf11de22a..508fc9e7f9ad 2026-08-08 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-07 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from e8c6af24af68 to 47bfc06ded40 (9 revisions) 2026-08-07 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-07 skia-autoroll@skia-public.iam.gserviceaccount.com Manual roll Dawn from 1c9c16c9ad1c to bf6225076ff5 (8 revisions) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC alexisdavidc@google.com,codefu@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 61c91e922c636..6ae892b9b228e 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '5dddf11de22a1c185766102e0c132a8887b7f7da', + 'skia_revision': '508fc9e7f9ad1b6c8b6ed11b260f97e7bfbb363f', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 3594a632450f5c1563636e97f34ef05203b2e8c8 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 8 Aug 2026 06:13:32 -0400 Subject: [PATCH 145/330] Roll Dart SDK from b608f3238a0f to 923415105b1e (1 revision) (#190796) https://dart.googlesource.com/sdk.git/+log/b608f3238a0f..923415105b1e 2026-08-08 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-109.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC codefu@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 6ae892b9b228e..24fac885102f7 100644 --- a/DEPS +++ b/DEPS @@ -55,7 +55,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': 'b608f3238a0f6500f16774e7ffa7b810adfcd0c4', + 'dart_revision': '923415105b1ede89acf70c6e0589d33af7a01750', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From 472e7c609e89a28acb14dfde8e21c574d783a3d5 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 8 Aug 2026 11:49:36 -0400 Subject: [PATCH 146/330] Roll Fuchsia Linux SDK from zGBigY0YYrKHxPKN-... to QU9W0ggjnGo4yjuSB... (#190803) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/fuchsia-linux-sdk-flutter Please CC codefu@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 24fac885102f7..1b90564b56cf5 100644 --- a/DEPS +++ b/DEPS @@ -830,7 +830,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'zGBigY0YYrKHxPKN-zAHWAJgqEUOHjPyJc3aZ8gSjT8C' + 'version': 'QU9W0ggjnGo4yjuSBgeKRXPDjwFrI3AmNKByaBHq7yoC' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From bea08b59e4d67d3deefe621b669bbcda3b396fe3 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 8 Aug 2026 12:56:24 -0400 Subject: [PATCH 147/330] Roll Dart SDK from 923415105b1e to 7239be9b8b07 (1 revision) (#190801) https://dart.googlesource.com/sdk.git/+log/923415105b1e..7239be9b8b07 2026-08-08 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-110.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC codefu@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 1b90564b56cf5..38fb254ab7b4a 100644 --- a/DEPS +++ b/DEPS @@ -55,7 +55,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': '923415105b1ede89acf70c6e0589d33af7a01750', + 'dart_revision': '7239be9b8b0785050117bc94d668fd697f7e2cba', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From f2d640ef01561447051f582059295a68ca2046ae Mon Sep 17 00:00:00 2001 From: Lau Ching Jun Date: Sat, 8 Aug 2026 22:43:37 -0700 Subject: [PATCH 148/330] Instruct gemini code assist not to comment on syntax error (#189930) Gemini doesn't know new Dart syntax, and is only adding noise in that case. --- .gemini/styleguide.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index b8a71a120e923..b095a5f134b68 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -29,6 +29,10 @@ flutter/flutter repository. It is based on the more comprehensive official - **Search for counter-examples**: Identify scenarios or edge cases that the proposed code does not handle. If a counter-example is found, propose a test case to demonstrate the gap. - **Suggest simplification and refactoring**: Assess whether the code can be made simpler or refactored to enhance readability and maintainability. +### What Not to Report + +- **Do not report syntax errors**: Leave the detection of syntax errors to the analyzer. The review agent should focus on higher-level concerns such as logic, design, and maintainability rather than issues that automated tooling already catches. + ## General Philosophy - **Optimize for readability**: Code is read more often than it is written. From cf0462b06fad98d1936b47dac167c1477dc039c6 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 9 Aug 2026 10:19:30 -0400 Subject: [PATCH 149/330] Roll Skia from 508fc9e7f9ad to e32c12990ac6 (1 revision) (#190812) https://skia.googlesource.com/skia.git/+log/508fc9e7f9ad..e32c12990ac6 2026-08-09 skia-autoroll@skia-public.iam.gserviceaccount.com Roll SKP CIPD package from 571 to 572 If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC codefu@google.com,jmbetancourt@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 38fb254ab7b4a..db94e8eead130 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '508fc9e7f9ad1b6c8b6ed11b260f97e7bfbb363f', + 'skia_revision': 'e32c12990ac6c9865959c812f5bee1c98d0df497', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 28e6279c3580382dbd1ba599e19c681d3debcc70 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 9 Aug 2026 13:36:19 -0400 Subject: [PATCH 150/330] Roll Fuchsia Linux SDK from QU9W0ggjnGo4yjuSB... to 2r7d_UHIzM8jEP68B... (#190813) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/fuchsia-linux-sdk-flutter Please CC codefu@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index db94e8eead130..b2f8503ea4f8c 100644 --- a/DEPS +++ b/DEPS @@ -830,7 +830,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'QU9W0ggjnGo4yjuSBgeKRXPDjwFrI3AmNKByaBHq7yoC' + 'version': '2r7d_UHIzM8jEP68BCIEXXl40b5qqZ3COoolX2gyOCsC' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From 114eb184d66d7ca136d89eb8ec09f61aff265062 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 9 Aug 2026 18:22:06 -0400 Subject: [PATCH 151/330] Roll Skia from e32c12990ac6 to 2eed75b95604 (1 revision) (#190814) https://skia.googlesource.com/skia.git/+log/e32c12990ac6..2eed75b95604 2026-08-09 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 47bfc06ded40 to 4a3af97d047f (1 revision) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC codefu@google.com,jmbetancourt@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index b2f8503ea4f8c..2aa64844ad673 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': 'e32c12990ac6c9865959c812f5bee1c98d0df497', + 'skia_revision': '2eed75b956045eb8603d3690a1e84bc582a2135d', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 0abb61186ebfa4e998ac32537127cd41b9133d46 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 10 Aug 2026 12:24:41 +0900 Subject: [PATCH 152/330] Scenarios: Deduplicate scenario name declarations (#190818) Previously, adding a scenario to the iOS scenario app meant registering it in three places: * the Dart factory in `scenarios.dart` * the launch argument in `SceneDelegate.m` * the golden identifier in `GoldenTestManager.m` The two Obj-C tables each had a comment asking whoever edits it to keep the other in sync. I missed one of the three adding scenarios recently, which is what prompted this cleanup. It seems better to enforce this through code than through comments. The mapping is pretty mechanical: drop the leading `--` and swap `-` for `_`. Both files now derive the name instead of looking it up. All 38 of `GoldenTestManager`'s entries were derivable that way, as were all but four of `SceneDelegate`'s 58. Those were the `--gesture-*` arguments, which dropped the `platform_view_` prefix that are included in the names of the scenarios they select. I've renamed those to `--platform-view-gesture-*` to match, which lets us delete those tables completely. `SceneDelegate` keeps a set of the arguments that select a scenario, since it still has to tell those apart from behaviour flags like `--screen-before-flutter` and can't look up from the Dart registry. The good news is these can no longer disagree, since neither of them stores any names. The gesture rename is safe since those arguments only occur in `PlatformViewGestureRecognizerTests.m`, and those tests assert on accessibility labels. They don't go through `GoldenTestManager` so there are no golden images named after them that need renaming. We now blow up on unrecognised `--` arguments rather than silently falling through to a bare `UIViewController`. As I (accidentally) discovered when trying to add a new platformview clip test, a scenario registered in Dart but missing from `SceneDelegate` used to show up thirty seconds later as a golden test timeout pointing at the platform view and the engine; it now complains about the argument and tells you where to register it. There's deliberately no allowlist of known non-scenario flags alongside that check. `--screen-before-flutter` has its own branch, and the two remaining behaviour flags, `--maskview-blocking` and `--with-continuous-texture`, only ever accompany a scenario argument, so we never reach the check while one is set. Passing either on its own now raises, which seems like the right outcome given it selects no scenario and would previously have left you looking at an empty view controller. Also discovered the `launchArgsMap` global from `GoldenTestManager` was dead code. It was exported from the header, then shadowed by a static local that held the real table, and was never assigned to or read. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/Scenarios/Scenarios/SceneDelegate.m | 170 +++++++++--------- .../ScenariosUITests/GoldenTestManager.h | 6 +- .../ScenariosUITests/GoldenTestManager.m | 67 +------ .../PlatformViewGestureRecognizerTests.m | 10 +- 4 files changed, 99 insertions(+), 154 deletions(-) diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m index e7627335b2ca6..4631e79a4dabe 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m @@ -57,96 +57,96 @@ - (void)scene:(UIScene*)scene if ([processArguments containsObject:@"--maskview-blocking"]) { self.window.tintColor = UIColor.systemPinkColor; } - NSDictionary* launchArgsMap = @{ - // The golden test args should match `GoldenTestManager`. - @"--locale-initialization" : @"locale_initialization", - @"--platform-view" : @"platform_view", - @"--platform-view-no-overlay-intersection" : @"platform_view_no_overlay_intersection", - @"--platform-view-two-intersecting-overlays" : @"platform_view_two_intersecting_overlays", - @"--platform-view-partial-intersection" : @"platform_view_partial_intersection", - @"--platform-view-one-overlay-two-intersecting-overlays" : - @"platform_view_one_overlay_two_intersecting_overlays", - @"--platform-view-multiple-without-overlays" : @"platform_view_multiple_without_overlays", - @"--platform-view-max-overlays" : @"platform_view_max_overlays", - @"--platform-view-surrounding-layers-fractional-coordinate" : - @"platform_view_surrounding_layers_fractional_coordinate", - @"--platform-view-partial-intersection-fractional-coordinate" : - @"platform_view_partial_intersection_fractional_coordinate", - @"--platform-view-multiple" : @"platform_view_multiple", - @"--platform-view-multiple-background-foreground" : - @"platform_view_multiple_background_foreground", - @"--platform-view-cliprect" : @"platform_view_cliprect", - @"--platform-view-cliprect-multiple-clips" : @"platform_view_cliprect_multiple_clips", - @"--platform-view-cliprrect" : @"platform_view_cliprrect", - @"--platform-view-cliprrect-multiple-clips" : @"platform_view_cliprrect_multiple_clips", - @"--platform-view-large-cliprrect" : @"platform_view_large_cliprrect", - @"--platform-view-large-cliprrect-multiple-clips" : - @"platform_view_large_cliprrect_multiple_clips", - @"--platform-view-clippath" : @"platform_view_clippath", - @"--platform-view-clippath-multiple-clips" : @"platform_view_clippath_multiple_clips", - @"--platform-view-cliprrect-with-transform" : @"platform_view_cliprrect_with_transform", - @"--platform-view-cliprrect-with-transform-multiple-clips" : - @"platform_view_cliprrect_with_transform_multiple_clips", - @"--platform-view-large-cliprrect-with-transform" : - @"platform_view_large_cliprrect_with_transform", - @"--platform-view-large-cliprrect-with-transform-multiple-clips" : - @"platform_view_large_cliprrect_with_transform_multiple_clips", - @"--platform-view-cliprect-with-transform" : @"platform_view_cliprect_with_transform", - @"--platform-view-cliprect-with-transform-multiple-clips" : - @"platform_view_cliprect_with_transform_multiple_clips", - @"--platform-view-clippath-with-transform" : @"platform_view_clippath_with_transform", - @"--platform-view-clippath-with-transform-multiple-clips" : - @"platform_view_clippath_with_transform_multiple_clips", - @"--platform-view-transform" : @"platform_view_transform", - @"--platform-view-opacity" : @"platform_view_opacity", - @"--platform-view-with-other-backdrop-filter" : @"platform_view_with_other_backdrop_filter", - @"--two-platform-views-with-other-backdrop-filter" : - @"two_platform_views_with_other_backdrop_filter", - @"--platform-view-with-negative-backdrop-filter" : - @"platform_view_with_negative_backdrop_filter", - @"--platform-view-rotate" : @"platform_view_rotate", - @"--non-full-screen-flutter-view-platform-view" : @"non_full_screen_flutter_view_platform_view", - @"--gesture-reject-after-touches-ended" : @"platform_view_gesture_reject_after_touches_ended", - @"--gesture-reject-eager" : @"platform_view_gesture_reject_eager", - @"--gesture-accept" : @"platform_view_gesture_accept", - @"--gesture-accept-with-overlapping-platform-views" : - @"platform_view_gesture_accept_with_overlapping_platform_views", - @"--tap-status-bar" : @"tap_status_bar", - @"--animated-color-square" : @"animated_color_square", - @"--solid-blue" : @"solid_blue", - @"--platform-view-with-continuous-texture" : @"platform_view_with_continuous_texture", - @"--bogus-font-text" : @"bogus_font_text", - @"--spawn-engine-works" : @"spawn_engine_works", - @"--pointer-events" : @"pointer_events", - @"--platform-view-scrolling-under-widget" : @"platform_view_scrolling_under_widget", - @"--platform-views-with-clips-scrolling" : @"platform_views_with_clips_scrolling", - @"--platform-views-with-clips-scrolling-multiple-clips" : - @"platform_views_with_clips_scrolling_multiple_clips", - @"--platform-view-cliprect-after-moved" : @"platform_view_cliprect_after_moved", - @"--platform-view-cliprect-after-moved-multiple-clips" : - @"platform_view_cliprect_after_moved_multiple_clips", - @"--two-platform-view-clip-rect" : @"two_platform_view_clip_rect", - @"--two-platform-view-clip-rect-multiple-clips" : @"two_platform_view_clip_rect_multiple_clips", - @"--two-platform-view-clip-rrect" : @"two_platform_view_clip_rrect", - @"--two-platform-view-clip-rrect-multiple-clips" : - @"two_platform_view_clip_rrect_multiple_clips", - @"--two-platform-view-clip-path" : @"two_platform_view_clip_path", - @"--two-platform-view-clip-path-multiple-clips" : @"two_platform_view_clip_path_multiple_clips", - @"--darwin-system-font" : @"darwin_system_font", - }; - __block NSString* flutterViewControllerTestName = nil; - [launchArgsMap - enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL* stop) { - if ([processArguments containsObject:argument]) { - flutterViewControllerTestName = testName; - *stop = YES; - } - }]; + NSSet* scenarioArguments = [NSSet setWithArray:@[ + @"--animated-color-square", + @"--bogus-font-text", + @"--darwin-system-font", + @"--locale-initialization", + @"--non-full-screen-flutter-view-platform-view", + @"--platform-view", + @"--platform-view-clippath", + @"--platform-view-clippath-multiple-clips", + @"--platform-view-clippath-with-transform", + @"--platform-view-clippath-with-transform-multiple-clips", + @"--platform-view-cliprect", + @"--platform-view-cliprect-after-moved", + @"--platform-view-cliprect-after-moved-multiple-clips", + @"--platform-view-cliprect-multiple-clips", + @"--platform-view-cliprect-with-transform", + @"--platform-view-cliprect-with-transform-multiple-clips", + @"--platform-view-cliprrect", + @"--platform-view-cliprrect-multiple-clips", + @"--platform-view-cliprrect-with-transform", + @"--platform-view-cliprrect-with-transform-multiple-clips", + @"--platform-view-gesture-accept", + @"--platform-view-gesture-accept-with-overlapping-platform-views", + @"--platform-view-gesture-reject-after-touches-ended", + @"--platform-view-gesture-reject-eager", + @"--platform-view-large-cliprrect", + @"--platform-view-large-cliprrect-multiple-clips", + @"--platform-view-large-cliprrect-with-transform", + @"--platform-view-large-cliprrect-with-transform-multiple-clips", + @"--platform-view-max-overlays", + @"--platform-view-multiple", + @"--platform-view-multiple-background-foreground", + @"--platform-view-multiple-without-overlays", + @"--platform-view-no-overlay-intersection", + @"--platform-view-one-overlay-two-intersecting-overlays", + @"--platform-view-opacity", + @"--platform-view-partial-intersection", + @"--platform-view-partial-intersection-fractional-coordinate", + @"--platform-view-rotate", + @"--platform-view-scrolling-under-widget", + @"--platform-view-surrounding-layers-fractional-coordinate", + @"--platform-view-transform", + @"--platform-view-two-intersecting-overlays", + @"--platform-view-with-continuous-texture", + @"--platform-view-with-negative-backdrop-filter", + @"--platform-view-with-other-backdrop-filter", + @"--platform-views-with-clips-scrolling", + @"--platform-views-with-clips-scrolling-multiple-clips", + @"--pointer-events", + @"--solid-blue", + @"--spawn-engine-works", + @"--tap-status-bar", + @"--two-platform-view-clip-path", + @"--two-platform-view-clip-path-multiple-clips", + @"--two-platform-view-clip-rect", + @"--two-platform-view-clip-rect-multiple-clips", + @"--two-platform-view-clip-rrect", + @"--two-platform-view-clip-rrect-multiple-clips", + @"--two-platform-views-with-other-backdrop-filter", + ]]; + + // We derive the Dart scenario name from the launch argument: + // * drop the leading "--" + // * swap "-" for "_" + // The GoldenTestManager golden name is derived exactly the same way. + NSString* flutterViewControllerTestName = nil; + for (NSString* argument in processArguments) { + if ([scenarioArguments containsObject:argument]) { + flutterViewControllerTestName = + [[argument substringFromIndex:2] stringByReplacingOccurrencesOfString:@"-" + withString:@"_"]; + break; + } + } if (flutterViewControllerTestName) { [self setupFlutterViewControllerTest:flutterViewControllerTestName]; } else if ([processArguments containsObject:@"--screen-before-flutter"]) { self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; } else { + // No scenario was selected. + // Bail out immediately on any unrecognized `--` argument and let the user know how to register + // a new scenario. + for (NSString* argument in processArguments) { + if ([argument hasPrefix:@"--"]) { + [NSException raise:NSInvalidArgumentException + format:@"Unrecognised scenario argument \"%@\". Add it to scenarioArguments in " + @"SceneDelegate.m, and register the scenario in scenarios.dart.", + argument]; + } + } self.window.rootViewController = [[UIViewController alloc] init]; } diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.h b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.h index 4fd156d4b0589..521a674870a01 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.h +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.h @@ -11,7 +11,6 @@ NS_ASSUME_NONNULL_BEGIN -extern NSDictionary* launchArgsMap; const extern double kDefaultRmseThreshold; // Manages a `GoldenPlatformViewTests`. @@ -24,9 +23,10 @@ const extern double kDefaultRmseThreshold; @property(readonly, copy, nonatomic) NSString* identifier; @property(readonly, copy, nonatomic) NSString* launchArg; -// Initilize with launchArg. +// Initialize with a launch argument. // -// Crahes if the launchArg is not mapped in `Appdelegate.launchArgsMap`. +// The golden identifier is derived from `launchArg` by dropping the leading "--" and +// replacing "-" with "_"; `SceneDelegate` derives the scenario name the same way. - (instancetype)initWithLaunchArg:(NSString*)launchArg; // Take a sceenshot of the test app and check it has the same pixels with diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m index a0333c3073f77..e21d5a608b1ed 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m @@ -13,72 +13,17 @@ @interface GoldenTestManager () @implementation GoldenTestManager -NSDictionary* launchArgsMap; const double kDefaultRmseThreshold = 0.5; - (instancetype)initWithLaunchArg:(NSString*)launchArg { self = [super init]; if (self) { - // The launchArgsMap should match the one in the `PlatformVieGoldenTestManager`. - static NSDictionary* launchArgsMap; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - launchArgsMap = @{ - @"--platform-view" : @"platform_view", - @"--platform-view-multiple" : @"platform_view_multiple", - @"--platform-view-multiple-background-foreground" : - @"platform_view_multiple_background_foreground", - @"--platform-view-cliprect" : @"platform_view_cliprect", - @"--platform-view-cliprect-multiple-clips" : @"platform_view_cliprect_multiple_clips", - @"--platform-view-cliprrect" : @"platform_view_cliprrect", - @"--platform-view-cliprrect-multiple-clips" : @"platform_view_cliprrect_multiple_clips", - @"--platform-view-large-cliprrect" : @"platform_view_large_cliprrect", - @"--platform-view-large-cliprrect-multiple-clips" : - @"platform_view_large_cliprrect_multiple_clips", - @"--platform-view-clippath" : @"platform_view_clippath", - @"--platform-view-clippath-multiple-clips" : @"platform_view_clippath_multiple_clips", - @"--platform-view-cliprrect-with-transform" : @"platform_view_cliprrect_with_transform", - @"--platform-view-cliprrect-with-transform-multiple-clips" : - @"platform_view_cliprrect_with_transform_multiple_clips", - @"--platform-view-large-cliprrect-with-transform" : - @"platform_view_large_cliprrect_with_transform", - @"--platform-view-large-cliprrect-with-transform-multiple-clips" : - @"platform_view_large_cliprrect_with_transform_multiple_clips", - @"--platform-view-cliprect-with-transform" : @"platform_view_cliprect_with_transform", - @"--platform-view-cliprect-with-transform-multiple-clips" : - @"platform_view_cliprect_with_transform_multiple_clips", - @"--platform-view-clippath-with-transform" : @"platform_view_clippath_with_transform", - @"--platform-view-clippath-with-transform-multiple-clips" : - @"platform_view_clippath_with_transform_multiple_clips", - @"--platform-view-transform" : @"platform_view_transform", - @"--platform-view-opacity" : @"platform_view_opacity", - @"--platform-view-with-other-backdrop-filter" : @"platform_view_with_other_backdrop_filter", - @"--two-platform-views-with-other-backdrop-filter" : - @"two_platform_views_with_other_backdrop_filter", - @"--platform-view-with-negative-backdrop-filter" : - @"platform_view_with_negative_backdrop_filter", - @"--platform-view-rotate" : @"platform_view_rotate", - @"--non-full-screen-flutter-view-platform-view" : - @"non_full_screen_flutter_view_platform_view", - @"--bogus-font-text" : @"bogus_font_text", - @"--spawn-engine-works" : @"spawn_engine_works", - @"--platform-view-cliprect-after-moved" : @"platform_view_cliprect_after_moved", - @"--platform-view-cliprect-after-moved-multiple-clips" : - @"platform_view_cliprect_after_moved_multiple_clips", - @"--two-platform-view-clip-rect" : @"two_platform_view_clip_rect", - @"--two-platform-view-clip-rect-multiple-clips" : - @"two_platform_view_clip_rect_multiple_clips", - @"--two-platform-view-clip-rrect" : @"two_platform_view_clip_rrect", - @"--two-platform-view-clip-rrect-multiple-clips" : - @"two_platform_view_clip_rrect_multiple_clips", - @"--two-platform-view-clip-path" : @"two_platform_view_clip_path", - @"--two-platform-view-clip-path-multiple-clips" : - @"two_platform_view_clip_path_multiple_clips", - @"--app-extension" : @"app_extension", - @"--darwin-system-font" : @"darwin_system_font", - }; - }); - _identifier = launchArgsMap[launchArg]; + // We derive the golden identifier from the launch argument: + // * drop the leading "--" + // * swap "-" for "_" + // The SceneDelegate Dart scenario name is derived exactly the same way. + _identifier = [[launchArg substringFromIndex:2] stringByReplacingOccurrencesOfString:@"-" + withString:@"_"]; NSString* impeller = @"impeller_"; NSNumber* enableImpeller = [[NSBundle bundleWithIdentifier:@"dev.flutter.Scenarios"] diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m index 5bd831f0948b5..756053fd249e5 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m @@ -18,7 +18,7 @@ - (void)setUp { - (void)testRejectPolicyUtilTouchesEnded { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--gesture-reject-after-touches-ended" ]; + app.launchArguments = @[ @"--platform-view-gesture-reject-after-touches-ended" ]; [app launch]; NSPredicate* predicateToFindPlatformView = @@ -50,7 +50,7 @@ - (void)testRejectPolicyUtilTouchesEnded { - (void)testRejectPolicyEager { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--gesture-reject-eager" ]; + app.launchArguments = @[ @"--platform-view-gesture-reject-eager" ]; [app launch]; NSPredicate* predicateToFindPlatformView = @@ -86,7 +86,7 @@ - (void)testRejectPolicyEager { - (void)testAccept { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--gesture-accept" ]; + app.launchArguments = @[ @"--platform-view-gesture-accept" ]; [app launch]; NSPredicate* predicateToFindPlatformView = @@ -121,7 +121,7 @@ - (void)testAccept { - (void)testGestureWithMaskViewBlockingPlatformView { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--gesture-accept", @"--maskview-blocking" ]; + app.launchArguments = @[ @"--platform-view-gesture-accept", @"--maskview-blocking" ]; [app launch]; NSPredicate* predicateToFindPlatformView = @@ -166,7 +166,7 @@ - (XCUICoordinate*)getNormalizedCoordinate:(XCUIApplication*)app point:(CGVector - (void)testGestureWithOverlappingPlatformViews { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--gesture-accept-with-overlapping-platform-views" ]; + app.launchArguments = @[ @"--platform-view-gesture-accept-with-overlapping-platform-views" ]; [app launch]; XCUIElement* foreground = app.otherElements[@"platform_view[0]"]; From 4b4c3ec878059897bcfcf4e250c53803b8f69398 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 10 Aug 2026 14:32:11 +0900 Subject: [PATCH 153/330] Scenarios: Fix locale test on hosts with several languages (#190821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `testNoLocalePrepend` asserted that the host has exactly one preferred language: NSArray* preferredLocales = [NSLocale preferredLanguages]; XCTAssertEqual(preferredLocales.count, 1); and then built the expected value from `preferredLocales.firstObject` alone. The scenario publishes the whole list dart:ui received as its semantics label, so on a host configured with more than one language, the app will report a list (e.g. `[ja_JP, en_CA]`) while the test looks for `[ja_JP]`. The goal is to check that we got the right set of locales, but hardcoding the count check to one isn't the right way to do it and causes the test to fail for any developer with more than one language in their locales list. Given that multilingual developers outnumber unilingual ones, this is arguably a bug in the test, or at least an annoyance. We now build the expected value from every preferred language, joined the way Dart formats the list, and the count assertion is gone. Tested on my machine which uses 日本語, by English (Canada), where dart:ui reports `[ja_JP, en_CA]`. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../LocalizationInitializationTest.m | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/LocalizationInitializationTest.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/LocalizationInitializationTest.m index a0196b9b3b038..65112fa311a71 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/LocalizationInitializationTest.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/LocalizationInitializationTest.m @@ -26,15 +26,20 @@ - (void)testNoLocalePrepend { NSTimeInterval timeout = 10.0; // The locales received by dart:ui are exposed onBeginFrame via semantics label. - // There should only be one locale. The list should consist of the default - // locale provided by the iOS app. - NSArray* preferredLocales = [NSLocale preferredLanguages]; - XCTAssertEqual(preferredLocales.count, 1); - // Dart connects the locale parts with `_` while iOS connects them with `-`. - // Converts to dart format before comparing. - NSString* localeDart = [preferredLocales.firstObject stringByReplacingOccurrencesOfString:@"-" - withString:@"_"]; - NSString* expectedIdentifier = [NSString stringWithFormat:@"[%@]", localeDart]; + // The list should consist of the default system locale(s) provided by iOS. + // + // The locales iOS reports are of the form `en-CA`. `dart:ui` reports locales + // in the form `en_CA` and formats them as a list `[first, second, ...]`. + // + // Since this test is sensitive to device locale setup, we can't assume any + // particular locale, or any particular number of locales. + NSMutableArray* dartLocales = [NSMutableArray array]; + for (NSString* localeIdentifier in [NSLocale preferredLanguages]) { + [dartLocales addObject:[localeIdentifier stringByReplacingOccurrencesOfString:@"-" + withString:@"_"]]; + } + NSString* expectedIdentifier = + [NSString stringWithFormat:@"[%@]", [dartLocales componentsJoinedByString:@", "]]; XCUIElement* textInputSemanticsObject = [self.application.textFields matchingIdentifier:expectedIdentifier].element; XCTAssertTrue([textInputSemanticsObject waitForExistenceWithTimeout:timeout]); From 2dc0d10cc543d3b38c8d468493825f7e6badadeb Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 10 Aug 2026 14:45:59 +0900 Subject: [PATCH 154/330] iOS: Fix flake when backgrounding platform views (#190816) Previously, `bringLayersIntoView:withCompositionOrder:` started with a `FML_DCHECK(self.flutterView)`, which aborts when an app with platform views on screen is backgrounded. Before flutter/engine#53826, `SubmitFrame` ran entirely on the platform thread. It started with `FML_DCHECK([[NSThread currentThread] isMainThread])` and bailed out early when `flutter_view_` was null. `BringLayersIntoView` sat in the same (synchronous) scope as that check, which ensured the assertion passed. That change split the method in two: the null check stayed with the raster thread work in `SubmitFrame`, and the UIKit work moved into `PerformSubmit`, posted to the platform thread. The isMainThread assertion moved over with it; the `flutter_view_` one didn't, so ended up being reached on a different thread at a later point than the check that was guarding it. Since `flutterView` is a weak ref that `surfaceUpdated:NO` clears on backgrounding, it might be gone by the time the posted task runs. We now check and bail out early instead, before `_previousCompositionOrder` is cleared, since nothing was attached this frame. `applyMutators` already does this in the same deferred task. This doesn't change opt builds (including debug opt builds), which drop `DCHECK`s at compile time. No new tests: this fixes a flake that `MultiplePlatformViewsBackgroundForegroundTest` in the iOS Scenario app keeps hitting in my local testing. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/framework/Source/FlutterPlatformViewsController.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm index 5b5d19327a57e..7349d124c5fb0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm @@ -946,8 +946,10 @@ - (void)performSubmit:(const LayersMap&)platformViewLayers - (void)bringLayersIntoView:(const LayersMap&)layerMap withCompositionOrder:(const std::vector&)compositionOrder { - FML_DCHECK(self.flutterView); UIView* flutterView = self.flutterView; + if (flutterView == nil) { + return; + } _previousCompositionOrder.clear(); NSMutableArray* desiredPlatformSubviews = [NSMutableArray array]; From 1cd7c6b603b44092bcf20f4a20a00951a6e9ff3e Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 10 Aug 2026 02:36:27 -0400 Subject: [PATCH 155/330] Roll Skia from 2eed75b95604 to 12b57b93c76d (1 revision) (#190825) https://skia.googlesource.com/skia.git/+log/2eed75b95604..12b57b93c76d 2026-08-10 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from acdd3c9e9e9f to a026d3da2361 If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC codefu@google.com,jmbetancourt@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 2aa64844ad673..c2d59cc013f71 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '2eed75b956045eb8603d3690a1e84bc582a2135d', + 'skia_revision': '12b57b93c76d860b7049788d632cb6964e79239f', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 842ce889e595f4672cd0a66d1f225c31945d8f12 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 10 Aug 2026 13:27:18 +0200 Subject: [PATCH 156/330] [cross imports] flutter/test/widgets/app_test.dart, flutter/test/widgets/inherited_test.dart, flutter_test/test/accessibility_window_test.dart (#190650) This PR fixes some more cross imports, in `flutter/test/widgets/app_test.dart`, `flutter/test/widgets/inherited_test.dart` and `flutter_test/test/accessibility_window_test.dart`. Part of https://github.com/flutter/flutter/issues/177415 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- dev/bots/check_tests_cross_imports.dart | 3 - packages/flutter/test/widgets/app_test.dart | 101 ++++++++++---- .../flutter/test/widgets/inherited_test.dart | 132 ++++++++++++------ .../lib/src/test_widgets_app.dart | 31 ++++ .../test/accessibility_window_test.dart | 29 ++-- .../test/test_widgets_app_test.dart | 62 ++++++++ 6 files changed, 266 insertions(+), 92 deletions(-) diff --git a/dev/bots/check_tests_cross_imports.dart b/dev/bots/check_tests_cross_imports.dart index 1810b2696d1c8..48d4c334087e8 100644 --- a/dev/bots/check_tests_cross_imports.dart +++ b/dev/bots/check_tests_cross_imports.dart @@ -120,11 +120,9 @@ class TestsCrossImportChecker { static final Set knownWidgetsCrossImports = { 'packages/flutter/test/widgets/page_transitions_test.dart', 'packages/flutter/test/widgets/routes_test.dart', - 'packages/flutter/test/widgets/app_test.dart', 'packages/flutter/test/widgets/routes_transition_test.dart', 'packages/flutter/test/widgets/editable_text_test.dart', 'packages/flutter/test/widgets/scrollbar_test.dart', - 'packages/flutter/test/widgets/inherited_test.dart', 'packages/flutter/test/widgets/heroes_test.dart', 'packages/flutter/test/widgets/drawer_test.dart', 'packages/flutter/test/widgets/nested_scroll_view_test.dart', @@ -188,7 +186,6 @@ class TestsCrossImportChecker { 'packages/flutter_test/lib/src/widget_tester.dart', 'packages/flutter_test/lib/src/finders.dart', 'packages/flutter_test/lib/src/matchers.dart', - 'packages/flutter_test/test/accessibility_window_test.dart', 'packages/flutter_test/test/widget_tester_test.dart', 'packages/flutter_test/test/accessibility_test.dart', 'packages/flutter_test/test/finders_test.dart', diff --git a/packages/flutter/test/widgets/app_test.dart b/packages/flutter/test/widgets/app_test.dart index bccf5051dc401..0a6c886dd5469 100644 --- a/packages/flutter/test/widgets/app_test.dart +++ b/packages/flutter/test/widgets/app_test.dart @@ -3,10 +3,12 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'test_page_tester.dart'; + class TestIntent extends Intent { const TestIntent(); } @@ -44,14 +46,12 @@ void main() { WidgetsApp( key: key, builder: (BuildContext context, Widget? child) { - return Material( - child: Checkbox( - value: checked, - autofocus: true, - onChanged: (bool? value) { - checked = value; - }, - ), + return _BasicCheckbox( + value: checked, + autofocus: true, + onChanged: (bool? value) { + checked = value; + }, ); }, color: const Color(0xFF123456), @@ -76,14 +76,12 @@ void main() { SingleActivator(LogicalKeyboardKey.space): TestIntent(), }, builder: (BuildContext context, Widget? child) { - return Material( - child: Checkbox( - value: checked, - autofocus: true, - onChanged: (bool? value) { - checked = value; - }, - ), + return _BasicCheckbox( + value: checked, + autofocus: true, + onChanged: (bool? value) { + checked = value; + }, ); }, color: const Color(0xFF123456), @@ -105,14 +103,12 @@ void main() { await tester.pumpWidget( WidgetsApp( builder: (BuildContext context, Widget? child) { - return Material( - child: Checkbox( - value: checked, - autofocus: true, - onChanged: (bool? value) { - checked = value; - }, - ), + return _BasicCheckbox( + value: checked, + autofocus: true, + onChanged: (bool? value) { + checked = value; + }, ); }, color: const Color(0xFF123456), @@ -189,7 +185,7 @@ void main() { await expectFlutterError( key: key, tester: tester, - widget: MaterialApp(navigatorKey: key, home: Container(), onGenerateRoute: (_) => null), + widget: TestWidgetsApp(navigatorKey: key, home: Container(), onGenerateRoute: (_) => null), errorMessage: 'FlutterError\n' ' Could not find a generator for route RouteSettings("/path", null)\n' @@ -213,7 +209,7 @@ void main() { await expectFlutterError( key: key, tester: tester, - widget: MaterialApp( + widget: TestWidgetsApp( navigatorKey: key, home: Container(), onGenerateRoute: (_) => null, @@ -555,8 +551,8 @@ void main() { late final List? localesArg; late final Iterable supportedLocalesArg; await tester.pumpWidget( - MaterialApp( - // This uses a MaterialApp because it introduces some actual localizations. + TestWidgetsApp( + localizationsDelegates: const >[TestLocalizationsDelegate()], localeListResolutionCallback: (List? locales, Iterable supportedLocales) { localesArg = locales; supportedLocalesArg = supportedLocales; @@ -869,8 +865,8 @@ class SimpleNavigatorRouterDelegate extends RouterDelegate pages: >[ // We need at least two pages for the pop to propagate through. // Otherwise, the navigator will bubble the pop to the system navigator. - const MaterialPage(child: Text('base')), - MaterialPage( + const TestPage(child: Text('base')), + TestPage( key: ValueKey(routeInformation.uri.toString()), child: builder(context, routeInformation), ), @@ -878,3 +874,46 @@ class SimpleNavigatorRouterDelegate extends RouterDelegate ); } } + +class TestLocalizationsDelegate extends LocalizationsDelegate { + const TestLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => locale.languageCode == 'en'; + + @override + Future load(Locale locale) { + return SynchronousFuture(const Object()); + } + + @override + bool shouldReload(TestLocalizationsDelegate old) => false; +} + +class _BasicCheckbox extends StatefulWidget { + const _BasicCheckbox({required this.value, required this.onChanged, this.autofocus = false}); + + final bool? value; + final ValueChanged? onChanged; + final bool autofocus; + + @override + State<_BasicCheckbox> createState() => _BasicCheckboxState(); +} + +class _BasicCheckboxState extends State<_BasicCheckbox> { + @override + Widget build(BuildContext context) { + return Actions( + actions: >{ + ActivateIntent: CallbackAction( + onInvoke: (_) { + widget.onChanged?.call(!(widget.value ?? false)); + return null; + }, + ), + }, + child: Focus(autofocus: widget.autofocus, child: const SizedBox()), + ); + } +} diff --git a/packages/flutter/test/widgets/inherited_test.dart b/packages/flutter/test/widgets/inherited_test.dart index 865cf202d71cc..e4f2e59d0226e 100644 --- a/packages/flutter/test/widgets/inherited_test.dart +++ b/packages/flutter/test/widgets/inherited_test.dart @@ -2,12 +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/rendering.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'test_widgets.dart'; +const _kWhite = Color(0xFFFFFFFF); +const _kBlack = Color(0xFF000000); + class TestInherited extends InheritedWidget { const TestInherited({super.key, required super.child, this.shouldNotify = true}); @@ -55,32 +58,82 @@ class ChangeNotifierInherited extends InheritedNotifier { const ChangeNotifierInherited({super.key, required super.child, super.notifier}); } -class ThemedCard extends SingleChildRenderObjectWidget { - const ThemedCard({super.key}) : super(child: const SizedBox.expand()); +@immutable +class TestWidgetData { + const TestWidgetData({ + this.color, + this.elevation, + this.shadowColor, + this.shape, + this.clipBehavior, + }); + + final Color? color; + final double? elevation; + final Color? shadowColor; + final ShapeBorder? shape; + final Clip? clipBehavior; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is TestWidgetData && + other.color == color && + other.elevation == elevation && + other.shadowColor == shadowColor && + other.shape == shape && + other.clipBehavior == clipBehavior; + } + + @override + int get hashCode => Object.hash(color, elevation, shadowColor, shape, clipBehavior); +} + +class TestDataWidget extends InheritedWidget { + const TestDataWidget({super.key, required super.child, required this.data}); + + final TestWidgetData data; + + static TestWidgetData of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()?.data ?? + const TestWidgetData(); + } + + @override + bool updateShouldNotify(TestDataWidget oldWidget) => data != oldWidget.data; +} + +class ThemedWidget extends SingleChildRenderObjectWidget { + const ThemedWidget({super.key}) : super(child: const SizedBox.expand()); @override RenderPhysicalShape createRenderObject(BuildContext context) { - final CardThemeData cardTheme = CardTheme.of(context); + final TestWidgetData data = TestDataWidget.of(context); return RenderPhysicalShape( - clipper: ShapeBorderClipper(shape: cardTheme.shape ?? const RoundedRectangleBorder()), - clipBehavior: cardTheme.clipBehavior ?? Clip.antiAlias, - color: cardTheme.color ?? Colors.white, - elevation: cardTheme.elevation ?? 0.0, - shadowColor: cardTheme.shadowColor ?? Colors.black, + clipper: ShapeBorderClipper(shape: data.shape ?? const RoundedRectangleBorder()), + clipBehavior: data.clipBehavior ?? Clip.antiAlias, + color: data.color ?? _kWhite, + elevation: data.elevation ?? 0.0, + shadowColor: data.shadowColor ?? _kBlack, ); } @override void updateRenderObject(BuildContext context, RenderPhysicalShape renderObject) { - final CardThemeData cardTheme = CardTheme.of(context); + final TestWidgetData data = TestDataWidget.of(context); renderObject - ..clipper = ShapeBorderClipper(shape: cardTheme.shape ?? const RoundedRectangleBorder()) - ..clipBehavior = cardTheme.clipBehavior ?? Clip.antiAlias - ..color = cardTheme.color ?? Colors.white - ..elevation = cardTheme.elevation ?? 0.0 - ..shadowColor = cardTheme.shadowColor ?? Colors.black; + ..clipper = ShapeBorderClipper(shape: data.shape ?? const RoundedRectangleBorder()) + ..clipBehavior = data.clipBehavior ?? Clip.antiAlias + ..color = data.color ?? _kWhite + ..elevation = data.elevation ?? 0.0 + ..shadowColor = data.shadowColor ?? _kBlack; } } @@ -523,30 +576,30 @@ void main() { }); testWidgets('InheritedWidgets can trigger RenderObject updates', (WidgetTester tester) async { - var cardThemeData = const CardThemeData(color: Colors.white); + var data = const TestWidgetData(color: _kWhite); late StateSetter setState; - // Verifies that the "themed card" is rendered + // Verifies that the "themed widget" is rendered // with the appropriate inherited theme data. - void expectCardToMatchTheme() { - final RenderPhysicalShape renderShape = tester.renderObject(find.byType(ThemedCard)); + void expectWidgetToMatchTheme() { + final RenderPhysicalShape renderShape = tester.renderObject(find.byType(ThemedWidget)); - if (cardThemeData.color != null) { - expect(renderShape.color, cardThemeData.color); + if (data.color != null) { + expect(renderShape.color, data.color); } - if (cardThemeData.elevation != null) { - expect(renderShape.elevation, cardThemeData.elevation); + if (data.elevation != null) { + expect(renderShape.elevation, data.elevation); } - if (cardThemeData.shadowColor != null) { - expect(renderShape.shadowColor, cardThemeData.shadowColor); + if (data.shadowColor != null) { + expect(renderShape.shadowColor, data.shadowColor); } - if (cardThemeData.shape != null) { + if (data.shape != null) { final CustomClipper? clipper = renderShape.clipper; expect(clipper, isA()); - expect((clipper! as ShapeBorderClipper).shape, cardThemeData.shape); + expect((clipper! as ShapeBorderClipper).shape, data.shape); } - if (cardThemeData.clipBehavior != null) { - expect(renderShape.clipBehavior, cardThemeData.clipBehavior); + if (data.clipBehavior != null) { + expect(renderShape.clipBehavior, data.clipBehavior); } } @@ -554,38 +607,35 @@ void main() { StatefulBuilder( builder: (BuildContext context, StateSetter stateSetter) { setState = stateSetter; - return Theme( - data: ThemeData(cardTheme: cardThemeData), - child: const ThemedCard(), - ); + return TestDataWidget(data: data, child: const ThemedWidget()); }, ), ); - expectCardToMatchTheme(); + expectWidgetToMatchTheme(); setState(() { - cardThemeData = const CardThemeData( + data = const TestWidgetData( shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), ); }); await tester.pump(); - expectCardToMatchTheme(); + expectWidgetToMatchTheme(); setState(() { - cardThemeData = const CardThemeData(clipBehavior: Clip.hardEdge); + data = const TestWidgetData(clipBehavior: Clip.hardEdge); }); await tester.pump(); - expectCardToMatchTheme(); + expectWidgetToMatchTheme(); setState(() { - cardThemeData = const CardThemeData( + data = const TestWidgetData( elevation: 5.0, - shadowColor: Colors.blueGrey, + shadowColor: Color(0xFF0000FF), shape: ContinuousRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))), clipBehavior: Clip.antiAliasWithSaveLayer, ); }); await tester.pump(); - expectCardToMatchTheme(); + expectWidgetToMatchTheme(); }); } diff --git a/packages/flutter_test/lib/src/test_widgets_app.dart b/packages/flutter_test/lib/src/test_widgets_app.dart index d383b7a3f3f0b..0e67fc1fb9985 100644 --- a/packages/flutter_test/lib/src/test_widgets_app.dart +++ b/packages/flutter_test/lib/src/test_widgets_app.dart @@ -68,6 +68,7 @@ class TestWidgetsApp extends StatelessWidget { this.home, this.initialRoute, this.onGenerateRoute, + this.onUnknownRoute, this.navigatorObservers = const [], this.routes = const {}, this.color = const Color(0xFFFFFFFF), @@ -77,6 +78,8 @@ class TestWidgetsApp extends StatelessWidget { this.shortcuts, this.actions, this.restorationScopeId, + this.localizationsDelegates, + this.localeListResolutionCallback, }); /// A key to use when building the [Navigator]. @@ -140,6 +143,14 @@ class TestWidgetsApp extends StatelessWidget { /// * [WidgetsApp.onGenerateRoute], the equivalent property in [WidgetsApp]. final RouteFactory? onGenerateRoute; + /// Called when [onGenerateRoute] fails to generate a route, except for the + /// [initialRoute]. + /// + /// See also: + /// + /// * [WidgetsApp.onUnknownRoute], the equivalent property in [WidgetsApp]. + final RouteFactory? onUnknownRoute; + /// A list of [NavigatorObserver] for the app's [Navigator]. /// /// Defaults to an empty list. @@ -250,6 +261,23 @@ class TestWidgetsApp extends StatelessWidget { /// * [WidgetsApp.restorationScopeId], the equivalent property in [WidgetsApp]. final String? restorationScopeId; + /// The delegates for this app's [Localizations] widget. + /// + /// See also: + /// + /// * [WidgetsApp.localizationsDelegates], the equivalent property in [WidgetsApp]. + final Iterable>? localizationsDelegates; + + /// The callback responsible for choosing the app's locale + /// when the app is started, and when the user changes the + /// device's locale. + /// + /// See also: + /// + /// * [WidgetsApp.localeListResolutionCallback], the equivalent property in [WidgetsApp]. + /// * [basicLocaleListResolution], the default locale resolution algorithm. + final LocaleListResolutionCallback? localeListResolutionCallback; + static PageRoute _defaultPageRouteBuilder(RouteSettings settings, WidgetBuilder builder) { return PageRouteBuilder( settings: settings, @@ -279,12 +307,15 @@ class TestWidgetsApp extends StatelessWidget { home: home, initialRoute: initialRoute, onGenerateRoute: onGenerateRoute, + onUnknownRoute: onUnknownRoute, routes: routes, pageRouteBuilder: pageRouteBuilder, builder: builder, shortcuts: shortcuts, actions: actions, restorationScopeId: restorationScopeId, + localizationsDelegates: localizationsDelegates, + localeListResolutionCallback: localeListResolutionCallback, ); } } diff --git a/packages/flutter_test/test/accessibility_window_test.dart b/packages/flutter_test/test/accessibility_window_test.dart index f9c20b9bcf947..bbd8e6df4eb0e 100644 --- a/packages/flutter_test/test/accessibility_window_test.dart +++ b/packages/flutter_test/test/accessibility_window_test.dart @@ -2,24 +2,23 @@ // 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'; void main() { + const kOrange = Color(0xFFFF9800); + const kOrangeAccent = Color(0xFFFFAB40); + testWidgets('Fails correctly with configured screen size - small', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.2; tester.view.physicalSize = const Size(250, 300); addTearDown(tester.view.reset); - final Widget invalidButton = ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.orange, - backgroundColor: Colors.orangeAccent, - ), - child: const Text('Button'), + const Widget invalidButton = ColoredBox( + color: kOrangeAccent, + child: Text('Button', style: TextStyle(color: kOrange)), ); - await tester.pumpWidget(MaterialApp(home: Scaffold(body: invalidButton))); + await tester.pumpWidget(const TestWidgetsApp(home: SizedBox.expand(child: invalidButton))); final Evaluation result = await textContrastGuideline.evaluate(tester); expect(result.passed, false); @@ -30,15 +29,11 @@ void main() { tester.view.physicalSize = const Size(2500, 3000); addTearDown(tester.view.reset); - final Widget invalidButton = ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.orange, - backgroundColor: Colors.orangeAccent, - ), - child: const Text('Button'), + const Widget invalidButton = ColoredBox( + color: kOrangeAccent, + child: Text('Button', style: TextStyle(color: kOrange)), ); - await tester.pumpWidget(MaterialApp(home: Scaffold(body: invalidButton))); + await tester.pumpWidget(const TestWidgetsApp(home: SizedBox.expand(child: invalidButton))); final Evaluation result = await textContrastGuideline.evaluate(tester); expect(result.passed, false); diff --git a/packages/flutter_test/test/test_widgets_app_test.dart b/packages/flutter_test/test/test_widgets_app_test.dart index b2b7afb6248a8..dcb09207c5f60 100644 --- a/packages/flutter_test/test/test_widgets_app_test.dart +++ b/packages/flutter_test/test/test_widgets_app_test.dart @@ -698,5 +698,67 @@ void main() { final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp)); expect(widgetsApp.onGenerateRoute, isNull); }); + + testWidgets('onUnknownRoute defaults to null', (WidgetTester tester) async { + await tester.pumpWidget(const TestWidgetsApp(home: Placeholder())); + + final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp)); + expect(widgetsApp.onUnknownRoute, isNull); + }); + + testWidgets('onUnknownRoute is passed to WidgetsApp', (WidgetTester tester) async { + await tester.pumpWidget( + TestWidgetsApp(home: const Placeholder(), onUnknownRoute: (settings) => null), + ); + + final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp)); + expect(widgetsApp.onUnknownRoute, isNotNull); + }); + + testWidgets('localizationsDelegates defaults to null', (WidgetTester tester) async { + await tester.pumpWidget(const TestWidgetsApp(home: Placeholder())); + + final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp)); + expect(widgetsApp.localizationsDelegates, isNull); + }); + + testWidgets('localizationsDelegates is passed to WidgetsApp', (WidgetTester tester) async { + final delegates = >[]; + + await tester.pumpWidget( + TestWidgetsApp(home: const Placeholder(), localizationsDelegates: delegates), + ); + + final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp)); + expect(identical(widgetsApp.localizationsDelegates, delegates), isTrue); + }); + + testWidgets('localeListResolutionCallback defaults to null', (WidgetTester tester) async { + await tester.pumpWidget(const TestWidgetsApp(home: Placeholder())); + + final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp)); + expect(widgetsApp.localeListResolutionCallback, isNull); + }); + + testWidgets('localeListResolutionCallback is passed to WidgetsApp', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const TestWidgetsApp( + home: Placeholder(), + localeListResolutionCallback: testLocalResolutionCallback, + ), + ); + + final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp)); + expect( + identical(widgetsApp.localeListResolutionCallback, testLocalResolutionCallback), + isTrue, + ); + }); }); } + +Locale? testLocalResolutionCallback(List? locales, Iterable supportedLocales) { + return null; +} From 467ec59b25eb62afddb5183f20bdf9ceabeab379 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 10 Aug 2026 07:29:10 -0400 Subject: [PATCH 157/330] Roll Skia from 12b57b93c76d to 2ad03d882c48 (4 revisions) (#190832) https://skia.googlesource.com/skia.git/+log/12b57b93c76d..2ad03d882c48 2026-08-10 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 4a3af97d047f to 37e841bbd2f9 (1 revision) 2026-08-10 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-10 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Skia Infra from 2c6128eaca86 to 262acfdc6d6e (10 revisions) 2026-08-10 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from bf6225076ff5 to 5b79878f746c (9 revisions) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC codefu@google.com,jmbetancourt@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index c2d59cc013f71..d33995b128bdf 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '12b57b93c76d860b7049788d632cb6964e79239f', + 'skia_revision': '2ad03d882c4870850b99905ecf21854f5dd8cd4a', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 89d4c51ceecc2d92b1fe4d383940c341982d9e95 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:21:24 -0700 Subject: [PATCH 158/330] Fixes popuntilwithresult drops results (#190596) as title, it is a typo and should have checked current route's will handle pop internally ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/src/widgets/navigator.dart | 2 +- .../flutter/test/widgets/navigator_test.dart | 205 ++++++++++++------ 2 files changed, 138 insertions(+), 69 deletions(-) diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 680cc9e7835a5..5d5a54c9dc3fe 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -5716,7 +5716,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res (_RouteEntry e) => _RouteEntry.isPresentPredicate(e) && e != candidate, ); - if (next != null && !next.route.willHandlePopInternally && predicate(next.route)) { + if (next != null && !candidate.route.willHandlePopInternally && predicate(next.route)) { pop(result); } else { pop(); diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index d1cd8f16cd291..c0f8280c2f6f9 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -1342,6 +1342,77 @@ void main() { expect(secondReturnValue, isNull); }); + testWidgets( + 'popUntilWithResult returns value to the last popped route when destination route has local history entries', + (WidgetTester tester) async { + bool? firstReturnValue; + bool? secondReturnValue; + + Widget buildPage(String id, VoidCallback? onTap) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Center(child: Text(id, textDirection: TextDirection.ltr)), + ); + } + + await tester.pumpWidget( + TestWidgetsApp( + initialRoute: '/', + onGenerateRoute: (RouteSettings settings) { + final String? routeName = settings.name; + + switch (routeName) { + case '/': + return TestRoute( + settings: settings, + builder: (BuildContext context) => buildPage('/', () async { + ModalRoute.of(context)!.addLocalHistoryEntry(LocalHistoryEntry()); + firstReturnValue = await Navigator.pushNamed(context, '/A'); + }), + ); + case '/A': + return TestRoute( + settings: settings, + builder: (BuildContext context) => buildPage('A', () async { + secondReturnValue = await Navigator.pushNamed(context, '/B'); + }), + ); + case '/B': + return TestRoute( + settings: settings, + builder: (BuildContext context) => buildPage('B', () async { + Navigator.popUntilWithResult( + context, + (Route route) => route.isFirst, + true, + ); + }), + ); + default: + return null; + } + }, + ), + ); + expect(find.text('/'), findsOneWidget); + + await tester.tap(find.text('/')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('A')); + await tester.pumpAndSettle(); + expect(find.text('B'), findsOneWidget); + + await tester.tap(find.text('B')); + await tester.pumpAndSettle(); + expect(find.text('/'), findsOneWidget); + + expect(firstReturnValue, isTrue); + expect(secondReturnValue, isNull); + }, + ); + testWidgets('pushAndRemoveUntil triggers secondaryAnimation', (WidgetTester tester) async { final routes = { '/': (BuildContext context) => OnTapPage( @@ -6265,84 +6336,82 @@ void main() { clear(); }); - testWidgets( - 'Navigator focus restoration reports error to FlutterError', - (WidgetTester tester) async { - final errorDetails = []; - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - errorDetails.add(details); - }; + testWidgets('Navigator focus restoration reports error to FlutterError', ( + WidgetTester tester, + ) async { + final errorDetails = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + errorDetails.add(details); + }; - try { - // Mock accessibility channel to throw error. - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockDecodedMessageHandler(SystemChannels.accessibility, ( - dynamic message, - ) async { - final map = message as Map; - if (map['type'] == 'focus') { - throw Exception('Focus restoration failed'); - } - return null; - }); + try { + // Mock accessibility channel to throw error. + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(SystemChannels.accessibility, ( + dynamic message, + ) async { + final map = message as Map; + if (map['type'] == 'focus') { + throw Exception('Focus restoration failed'); + } + return null; + }); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder( - builder: (BuildContext context) { - return ElevatedButton( - onPressed: () { - Navigator.push( - context, - NoAnimationPageRoute( - pageBuilder: (BuildContext context) => Scaffold( - appBar: AppBar(title: const Text('Second Route')), - body: ElevatedButton( - onPressed: () => Navigator.pop(context), - child: const Text('Pop'), - ), + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () { + Navigator.push( + context, + NoAnimationPageRoute( + pageBuilder: (BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Second Route')), + body: ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text('Pop'), ), ), - ); - }, - child: const Text('Push'), - ); - }, - ), + ), + ); + }, + child: const Text('Push'), + ); + }, ), ), - ); + ), + ); - // Record the last focus in route entry. - ServicesBinding.instance.accessibilityFocus.value = 123; - await tester.pump(); + // Record the last focus in route entry. + ServicesBinding.instance.accessibilityFocus.value = 123; + await tester.pump(); - // Push second route. - await tester.tap(find.text('Push')); - await tester.pumpAndSettle(); + // Push second route. + await tester.tap(find.text('Push')); + await tester.pumpAndSettle(); - // Now we are on the second route. - // Pop it. - await tester.tap(find.text('Pop')); - await tester.pumpAndSettle(); + // Now we are on the second route. + // Pop it. + await tester.tap(find.text('Pop')); + await tester.pumpAndSettle(); - expect(errorDetails.length, 1); - expect(errorDetails[0].exception.toString(), contains('Focus restoration failed')); - expect(errorDetails[0].library, 'widgets library'); - expect( - errorDetails[0].context.toString(), - contains('while restoring focus in the navigator'), - ); - } finally { - FlutterError.onError = oldHandler; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockDecodedMessageHandler(SystemChannels.accessibility, null); - } - }, - variant: TargetPlatformVariant.only(TargetPlatform.iOS), - ); + expect(errorDetails.length, 1); + expect(errorDetails[0].exception.toString(), contains('Focus restoration failed')); + expect(errorDetails[0].library, 'widgets library'); + expect( + errorDetails[0].context.toString(), + contains('while restoring focus in the navigator'), + ); + } finally { + FlutterError.onError = oldHandler; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(SystemChannels.accessibility, null); + } + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); testWidgets('Navigator.pop throws FlutterError when popped with mismatched type', ( WidgetTester tester, From 1f41b0ceedd938e67105bbb8efb49473853ba785 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 10 Aug 2026 08:14:06 -0700 Subject: [PATCH 159/330] [flutter_tools] Anchor package version extraction regex in Wasm dry-run (#190691) Anchor the Wasm dry-run package version regex to `hosted/pub.dev/-` so that version-like numbers in ancestor directories (e.g. Flutter SDK version paths like `/flutter/3.22.0/`) are not matched as the package version. Fixes https://github.com/flutter/flutter/issues/190644 --- .../lib/src/build_system/targets/web.dart | 14 ++- .../targets/web_dry_run_test.dart | 109 ++++++++++++++++++ 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 41b89a8f65246..9862f1cf78b63 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -523,11 +523,15 @@ class Dart2WasmTarget extends Dart2WebTarget { final Set privatePackages = {}; for (final Package package in packageConfigPackages.packages) { final String packageName = package.name; - if (package.root.toString().contains('hosted/pub.dev')) { - final String? packageVersion = RegExp( - r'([0-9]+\.[0-9]+\.[0-9]+(?:-[\w\.-]+)?)', - ).firstMatch(package.root.toString())?.group(1); - hostedPackages[packageName] = packageVersion ?? '?'; + if (package.root.pathSegments.where((String s) => s.isNotEmpty).toList() case [ + ..., + 'hosted', + _, + final packageFolder, + ] when packageFolder.startsWith('$packageName-')) { + // Hosted package directories in .pub-cache follow '-'. + // Substring past the package name and hyphen to extract the version. + hostedPackages[packageName] = packageFolder.substring(packageName.length + 1); } else { privatePackages.add(packageName); } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart index 3ea85d5f4f2a2..2065c4eda9f0d 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_dry_run_test.dart @@ -578,4 +578,113 @@ package:morelong/some/path.dart 9:20 - dart:html unsupported (0) expect(event.eventData['E0'], 'foo:${_fakePackageVersions['foo']}'); }), ); + test( + 'wasm dry run extracts correct package version when ancestor directory contains numbers', + () => testbed.run(() async { + writePackageConfigFiles( + directory: fs.currentDirectory, + packages: {'foo': 'file:///opt/flutter/3.22.0/.pub-cache/hosted/pub.dev/foo-1.0.0'}, + mainLibName: 'my_app', + ); + + processManager.addCommand( + FakeCommand( + command: commandArgs, + exitCode: 254, + stdout: ''' +Found incompatibilities with WebAssembly. + +package:foo/some/path.dart 6:1 - dart:html unsupported (0) +''', + ), + ); + final Dart2WasmTarget target = createTarget(); + await target.build(environment); + + expect(fakeAnalytics.sentEvents, hasLength(1)); + + final Event event = fakeAnalytics.sentEvents[0]; + expect(event.eventName, equals(DashEvent.flutterWasmDryRunPackage)); + expect(event.eventData, hasLength(3)); + expect(event.eventData['result'], 'findings'); + expect(event.eventData['exitCode'], 254); + expect(event.eventData['E0'], 'foo:1.0.0'); + }), + ); + + test( + 'wasm dry run extracts package version from custom pub mirrors', + () => testbed.run(() async { + writePackageConfigFiles( + directory: fs.currentDirectory, + packages: { + 'foo': 'file:///opt/flutter/3.22.0/.pub-cache/hosted/pub.flutter-io.cn/foo-1.0.0/', + }, + mainLibName: 'my_app', + ); + + processManager.addCommand( + FakeCommand( + command: commandArgs, + exitCode: 254, + stdout: ''' +Found incompatibilities with WebAssembly. + +package:foo/some/path.dart 6:1 - dart:html unsupported (0) +''', + ), + ); + final Dart2WasmTarget target = createTarget(); + await target.build(environment); + + expect(fakeAnalytics.sentEvents, hasLength(1)); + + final Event event = fakeAnalytics.sentEvents[0]; + expect(event.eventName, equals(DashEvent.flutterWasmDryRunPackage)); + expect(event.eventData, hasLength(3)); + expect(event.eventData['result'], 'findings'); + expect(event.eventData['exitCode'], 254); + expect(event.eventData['E0'], 'foo:1.0.0'); + }), + ); + + test( + 'wasm dry run extracts package versions across multiple distinct hosted domains', + () => testbed.run(() async { + writePackageConfigFiles( + directory: fs.currentDirectory, + packages: { + 'foo': 'file:///pubcache/.pub-cache/hosted/pub.dev/foo-1.0.0', + 'bar': 'file:///pubcache/.pub-cache/hosted/pub.flutter-io.cn/bar-2.0.0', + 'baz': 'file:///pubcache/.pub-cache/hosted/custom.repo.org%47/baz-3.0.0/', + }, + mainLibName: 'my_app', + ); + + processManager.addCommand( + FakeCommand( + command: commandArgs, + exitCode: 254, + stdout: ''' +Found incompatibilities with WebAssembly. + +package:foo/some/path.dart 6:1 - dart:html unsupported (0) +package:bar/some/path.dart 8:1 - dart:html unsupported (0) +package:baz/some/path.dart 10:1 - dart:html unsupported (0) +''', + ), + ); + final Dart2WasmTarget target = createTarget(); + await target.build(environment); + + expect(fakeAnalytics.sentEvents, hasLength(1)); + + final Event event = fakeAnalytics.sentEvents[0]; + expect(event.eventName, equals(DashEvent.flutterWasmDryRunPackage)); + expect(event.eventData, hasLength(3)); + expect(event.eventData['result'], 'findings'); + expect(event.eventData['exitCode'], 254); + expect(event.eventData['E0'], 'baz:3.0.0,bar:2.0.0,foo:1.0.0'); + }), + ); } From 221059e0b624498a2acb46d97b7a46b8901b6436 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:15:54 -0500 Subject: [PATCH 160/330] Override deployment target in CocoaPod dependencies when they're less than Xcode's minimum (#190677) Xcode 27 fails to build when dependencies have a deployment target lower than 15 for iOS and 12 for macOS. Previously it was just a warning. This changes our CocoaPod logic to remove the deployment target of dependencies when it's lower than the minimum to also include transitive dependencies. Fixes https://github.com/flutter/flutter/issues/190676. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- dev/devicelab/lib/tasks/plugin_tests.dart | 2 ++ packages/flutter_tools/bin/podhelper.rb | 25 +++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart index f65c9c20f5353..ffd33ee96073a 100644 --- a/dev/devicelab/lib/tasks/plugin_tests.dart +++ b/dev/devicelab/lib/tasks/plugin_tests.dart @@ -459,6 +459,8 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = "Classes", "Classes/**/*.{h,m}" s.dependency 'plugintest' + s.ios.deployment_target = '12.0' + s.osx.deployment_target = '10.14' end '''); diff --git a/packages/flutter_tools/bin/podhelper.rb b/packages/flutter_tools/bin/podhelper.rb index e09c48dfddf6a..f7308a6f86cbd 100644 --- a/packages/flutter_tools/bin/podhelper.rb +++ b/packages/flutter_tools/bin/podhelper.rb @@ -82,6 +82,13 @@ def flutter_additional_ios_build_settings(target) # ARC code targeting iOS 8 does not build on Xcode 14.3. Force to at least iOS 9. build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0' if force_to_arc_supported_min + # Suppress warning/error when pod supports a version lower than the minimum supported by Xcode. + # Xcode 27+ produces an error while previous versions produced a warning. + # When it's just a warning, it's harmless but confusing--it's not a bad thing for dependencies to support a lower version. + # When deleted, the deployment version will inherit from the higher version derived from the 'Runner' target. + # If the pod only supports a higher version, do not delete to correctly produce an error. + build_configuration.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' if inherit_deployment_target + # Skip other updates if it does not depend on Flutter (including transitive dependency) next unless depends_on_flutter(target, 'Flutter') @@ -103,11 +110,6 @@ def flutter_additional_ios_build_settings(target) build_configuration.build_settings['OTHER_LDFLAGS'] = '$(inherited) -framework Flutter' build_configuration.build_settings['CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER'] = 'NO' - # Suppress warning when pod supports a version lower than the minimum supported by Xcode (Xcode 12 - iOS 9). - # This warning is harmless but confusing--it's not a bad thing for dependencies to support a lower version. - # When deleted, the deployment version will inherit from the higher version derived from the 'Runner' target. - # If the pod only supports a higher version, do not delete to correctly produce an error. - build_configuration.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' if inherit_deployment_target # Override legacy Xcode 11 style VALID_ARCHS[sdk=iphonesimulator*]=x86_64 and prefer Xcode 12 EXCLUDED_ARCHS. build_configuration.build_settings['VALID_ARCHS[sdk=iphonesimulator*]'] = '$(ARCHS_STANDARD)' @@ -128,8 +130,9 @@ def flutter_additional_macos_build_settings(target) (deployment_target_major.to_i < 10) || (deployment_target_major.to_i == 10 && deployment_target_minor.to_i < 11) - # Suppress warning when pod supports a version lower than the minimum supported by the latest stable version of Xcode (currently 12.0). - # This warning is harmless but confusing--it's not a bad thing for dependencies to support a lower version. + # Suppress warning/error when pod supports a version lower than the minimum supported by the latest stable version of Xcode (currently 12.0). + # Xcode 27+ produces an error while previous versions produced a warning. + # When it's just a warning, it's harmless but confusing--it's not a bad thing for dependencies to support a lower version. inherit_deployment_target = !target.deployment_target.blank? && (deployment_target_major.to_i < 12) @@ -152,6 +155,10 @@ def flutter_additional_macos_build_settings(target) # ARC code targeting macOS 10.10 does not build on Xcode 14.3. Force to at least macOS 10.11. build_configuration.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.11' if force_to_arc_supported_min + # When deleted, the deployment version will inherit from the higher version derived from the 'Runner' target. + # If the pod only supports a higher version, do not delete to correctly produce an error. + build_configuration.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET' if inherit_deployment_target + # Skip other updates if it does not depend on Flutter (including transitive dependency) next unless depends_on_flutter(target, 'FlutterMacOS') @@ -167,10 +174,6 @@ def flutter_additional_macos_build_settings(target) end end - # When deleted, the deployment version will inherit from the higher version derived from the 'Runner' target. - # If the pod only supports a higher version, do not delete to correctly produce an error. - build_configuration.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET' if inherit_deployment_target - # Avoid error about Pods-Runner not supporting provisioning profiles. # Framework signing is handled at the app layer, not per framework, so disallow individual signing. build_configuration.build_settings.delete 'EXPANDED_CODE_SIGN_IDENTITY' From 42cca23f5da06b4aa03502281c49272030ceafc4 Mon Sep 17 00:00:00 2001 From: stuartmorgan-g Date: Mon, 10 Aug 2026 11:19:09 -0400 Subject: [PATCH 161/330] Update packages gardener instructions (#190463) Updates the packages gardener rotation instructions and stable-release-update instructions to cover the new core-packages repository. Also makes the stable-release-update steps part of the gardener rotation, as it was previously not formally owned. --- ...ting-Packages-repo-for-a-stable-release.md | 40 ++++++++++++++----- docs/infra/Packages-Gardener-Rotation.md | 27 ++++++++++--- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/docs/ecosystem/release/Updating-Packages-repo-for-a-stable-release.md b/docs/ecosystem/release/Updating-Packages-repo-for-a-stable-release.md index c2813bf88b6b7..370e5fe34f675 100644 --- a/docs/ecosystem/release/Updating-Packages-repo-for-a-stable-release.md +++ b/docs/ecosystem/release/Updating-Packages-repo-for-a-stable-release.md @@ -1,27 +1,47 @@ -This page describes the process of updating flutter/packages after a stable Flutter release. Hotfix releases don't require any changes, since the auto-roller will update the [pinned stable version](https://github.com/flutter/packages/blob/main/.ci/flutter_stable.version), but full stable releases (roughly once per quarter) require manual updates to the repository: +This page describes the process of updating flutter/packages and flutter/core-packages after a stable Flutter release. + +# Repository Updates + +## flutter/packages + +Hotfix releases don't require any changes, since the auto-roller will update the [pinned stable version](https://github.com/flutter/packages/blob/main/.ci/flutter_stable.version), but full stable releases (roughly once per quarter) require manual updates to the repository: * [The stable pin](https://github.com/flutter/packages/blob/main/.ci/flutter_stable.version) needs to be updated. The autoroller will open a PR, but because it includes a separate commit for every Flutter commit since the last stable, it will overwhelm the CLA check and it will fail. Either the CLA check can be overridden (which is safe since the source repo enforces the CLA), or a new manual PR can be made that updates the hash. -* The [Flutter Dart version mapping](https://github.com/flutter/packages/blob/b4985e25fe0763ece3cfd7af58e0e8c9b9f04fc4/script/tool/lib/src/common/core.dart#L59-L71) needs to be updated. The [Flutter SDK releases page](https://docs.flutter.dev/release/archive) is a useful reference. +* The [Flutter Dart version mapping](https://github.com/flutter/packages/blob/3498b9d7b67143a68dc43b90951acb577e92f64e/script/tool/lib/src/common/core.dart#L70-L106) needs to be updated. The [Flutter SDK releases page](https://docs.flutter.dev/release/archive) is a useful reference. * In addition to adding the new release, add the last bugfix version of the previous stable, for the next step. -* The [N-1 and N-2 legacy analysis tests](https://github.com/flutter/packages/blob/b4985e25fe0763ece3cfd7af58e0e8c9b9f04fc4/.ci.yaml#L223-L237) need to be updated. We generally use the latest bugfix versions for these tests. -* The [minimum allowed Flutter version](https://github.com/flutter/packages/blob/b4985e25fe0763ece3cfd7af58e0e8c9b9f04fc4/.ci/targets/repo_checks.yaml#L19) for the repo needs to be updated to the N-2 version. (We generally use .0 here, not the latest hotfix, under the assumption that there are not going to be analysis-breaking changes in a hotfix.) +* The [N-1 and N-2 legacy analysis tests](https://github.com/flutter/packages/blob/3498b9d7b67143a68dc43b90951acb577e92f64e/.ci.yaml#L290-L304) need to be updated. We generally use the latest bugfix versions for these tests. +* The [minimum allowed Flutter version](https://github.com/flutter/packages/blob/3498b9d7b67143a68dc43b90951acb577e92f64e/.repo_tool_config.yaml#L12) for the repo needs to be updated to the N-2 version. (We generally use .0 here, not the latest hotfix, under the assumption that there are not going to be analysis-breaking changes in a hotfix.) * This should ideally be done in the same PR as the previous step, since that is the point at which we no longer have any coverage of the previous minimum version. * All packages need to be updated to that minimum version. This can be trivially done with the repo tooling. E.g.: - `dart run script/tool/bin/flutter_plugin_tools.dart update-min-sdk --flutter-min=3.7.0` + `dart run script/tool/bin/flutter_plugin_tools.dart update-min-sdk --flutter-min=3.44.0` + + * Per [repo policy](../contributing/README.md#version), we do not version-bump these changes, so the associated `update-release-info` command should use `--version=next`. A convenient way to run the `update-release-info` command on only the necessary packages is to make the `update-min-sdk` run its own commit, then use a command like: - * Per [repo policy](../contributing/README.md#version), we do not version-bump these changes, so the associated `update-release-info` command should use `--version=next`. A convenient way to run the `update-release-info` command on only the necessary packages is to make the `update-min-sdk` run its own commit, then use `--base-branch HEAD^ --run-on-changed-packages` to target only the packages changed in that commit. + `dart run script/tool/bin/flutter_plugin_tools.dart update-release-info --version=next --changelog="Updates minimum supported SDK version to Flutter 3.44/Dart 3.12." --base-branch HEAD^ --run-on-changed-packages` + + to target only the packages changed in that commit. Some manual cleanup will be needed to remove SDK bump lines from the previous `stable` in packages that have not released in the meantime, to avoid having multiple SDK bumps in the same changelog entry. * This must be done in the same PR as the previous step, or CI will fail. * The [release action](https://github.com/flutter/packages/blob/e7d812cefce083fa09762d25cd42303737d05b9f/.github/workflows/release.yml#L34) should be updated to use the new stable. -Many of these steps can be done separately, but they can also be done in combined PRs (as few as one). As an example, 3.13 was done in two PRs: [#4370](https://github.com/flutter/packages/pull/4730) and [#4371](https://github.com/flutter/packages/pull/4731). +Many of these steps can be done separately, but it's often easiest to combine them into a single PR ([example](https://github.com/flutter/packages/pull/11741)). + +## flutter/core-packages + +flutter/core-packages needs the same conceptual changes as flutter/packages, but the CI configuration is different: +* There is no single `stable` pin, nor separate tasks for N-1/N-2 testing. Instead, each GitHub Action that references a specific Dart version needs to be updated. For example: + * [The multi-version Dart analysis and test matrix](https://github.com/flutter/core-packages/blob/6e41b6679caec9c229e65a71169a6fee1e3e3825/.github/workflows/multi_version_tasks.yaml#L31), which should include the corresponding stable, N-1, and N-2 Dart versions for each of the Flutter versions in flutter/packages. + * [Dart unit tests on Windows](https://github.com/flutter/core-packages/blob/6e41b6679caec9c229e65a71169a6fee1e3e3825/.github/workflows/windows_unit_tests.yaml#L36) + * [The release action](https://github.com/flutter/core-packages/blob/6e41b6679caec9c229e65a71169a6fee1e3e3825/.github/workflows/release.yml#L31) +* The repo-level tool configuration sets [a minimum Dart version](https://github.com/flutter/core-packages/blob/6e41b6679caec9c229e65a71169a6fee1e3e3825/.repo_tool_config.yaml#L14) rather than a minimum Flutter version. +* Currently the repository tooling does not have a `--dart-min` option for `update-min-sdk`, so you will need to run `update-min-sdk` using your local copy of the repo tooling that includes the version mapping changes from the flutter/packages steps, and pass the same `--flutter-min` (which will then map to the correct Dart minimum). -### Issue Updates +# Issue Updates Sweep all [`p: waiting for stable update` issues](https://github.com/flutter/flutter/labels/p%3A%20waiting%20for%20stable%20update), and update those that are now unblocked to indicate that they can now be addressed (removing the label). For any that are about deprecated API usage, upgrade them to `P1`, and either find an owner for them or remove the owning team's `triaged-*` label, leaving a comment that the deprecated API usage needs to be removed ASAP to minimize future disruption to package clients. * The motivation for treating these as P1 is that many clients do not update their packages (in particular, their transitive dependencies) frequently, so the further in advance of the eventual API *removal* the publishing of an update is, the fewer clients will have build errors on future updates of Flutter. -### PR Updates +# PR Updates -Similarly sweep all [`p: waiting for stable update` PRs](https://github.com/flutter/packages/labels/waiting%20for%20stable%20update) and comment and remove labels as necessary. \ No newline at end of file +Similarly sweep all [`p: waiting for stable update` PRs](https://github.com/flutter/packages/labels/waiting%20for%20stable%20update) and comment and remove labels as necessary. diff --git a/docs/infra/Packages-Gardener-Rotation.md b/docs/infra/Packages-Gardener-Rotation.md index 08cc0abb4e0f7..9c095b9451657 100644 --- a/docs/infra/Packages-Gardener-Rotation.md +++ b/docs/infra/Packages-Gardener-Rotation.md @@ -2,13 +2,14 @@ The packages gardener role currently makes use of several tools and communicatio ## Objective -The packages gardener’s role is to eliminate impediments to engineering velocity on teams working on the [flutter/packages] repository, and to minimize the latency with which critical fixes arrive in our customers' hands. To that end, we maintain a rotation so that there is a clear owner and point of contact for flutter/packages issues, and so that engineers can plan their work around an assumption of reduced productivity during their rotation. +The packages gardener’s role is to eliminate impediments to engineering velocity on teams working on the [flutter/packages] and [flutter/core-packages] repositories, and to minimize the latency with which critical fixes arrive in our customers' hands. To that end, we maintain a rotation so that there is a clear owner and point of contact for flutter/packages issues, and so that engineers can plan their work around an assumption of reduced productivity during their rotation. The packages gardener's core responsibilities are: -* Keep the flutter/packages tree green to keep developers, and the release process unblocked. +* Keep the flutter/packages and flutter/core-packages trees green to keep developers and the release process unblocked. * Keep the various packages rollers (see below) rolling. * Ensure others are informed of issues which may affect them. * Ensure bugs/investigations are delegated and taken care of by the right person, which may be themself. +* Update the repositories for any stable releases (see below). * Update the list of known deprecations (see below). The gardener's responsibilities do not include: @@ -16,7 +17,7 @@ The gardener's responsibilities do not include: * Personally investigate issues or fix bugs, unless the right person to investigate and fix is the gardener themself. As such, the gardener should use their trowel and hat to: -* Aggressively roll back problematic changes (in all relevant repositories including [flutter/packages] and [flutter/flutter]). +* Aggressively roll back problematic changes (in all relevant repositories including [flutter/packages], [flutter/core-packages], and [flutter/flutter]). * Delegate investigations/issues to the right engineer(s) on the team, and follow-up. * Detect infrastructure issues that cause test failures and flakiness and report to the infrastructure team. * Notify the [Flutter framework gardener] when a framework change causes the flutter/packages roller to fail in a way that can’t easily be fixed locally. @@ -37,7 +38,7 @@ Please briefly describe any issues you encountered during your week of gardening Below are the tasks that should be done routinely while gardening. -### Dashboard +### flutter/packages dashboard Open the [packages build dashboard]. 1. If the tree is closed, identify which test shards are failing. If there are yellow boxes with an exclamation point, that means that the failed tests are automatically re-running themselves. The tree is not fully closed until there are solid red boxes or red boxes with exclamation points. You can begin investigation as soon as you notice the tree going red, but it is suggested not to begin escalation until re-runs have completed. 1. Identify which test within the shard failed, and try to locate obvious errors or failures in the logs. @@ -50,6 +51,11 @@ Open the [packages build dashboard]. Unmute the [hackers-ecosystem channel] and [hackers-infra channel] on [Discord]. Contributors are encouraged to escalate tree closures to you. Respond there as quickly as possible. +### flutter/core-packages tree status +1. Check the [flutter/core-packages build status] (core-packages does not currently have a Flutter dashboard), which should show a green checkmark for the latest commit. +1. If it doesn't, click the red X to see what failed, and find the relevant errors. +1. Proceed as with flutter/packages failures above. + ### Rollers Check that all of the auto-rollers are running: @@ -63,6 +69,10 @@ If a roller's status is not `running`, contact the person who paused it and work If a roller is failing, check the recent runs to see why, and take action to ensure that the roller starts succeeding again. If the issue will take a while to resolve, pause the roller while resolving it, and include an explanation of why. +### Stable release + +If there is a stable release (a full new release, not a hotfix release), both flutter/packages and flutter/core-packages need to be updated to ensure that they are testing the new set of `stable`, N-1, and N-2 releases. See [these instructions][update for stable release] for steps to follow. + ### Deprecations Our analysis options do not flag deprecated API usage ([context][deprecation context]), but it’s important that we not leave deprecated API usage in our packages for any longer than necessary, since when the APIs are eventually removed anyone still using versions of the package predating the fix will get build errors that many developers find confusing and hard to resolve. @@ -79,8 +89,10 @@ Once during your rotation, do a manual check for any new deprecations: * If it's easy to determine, include the version that the replacement API will be available in the issue description. * Exception: If a deprecation warning is from a package integration test that is testing a deprecated API from that package (which does not count as `deprecated_member_use_from_same_package` since the example is technically a different package), annotate it with an `ignore` instead, so it doesn’t show up in this manual check in the future. +These steps should be done for both flutter/packages and flutter/core-packages. + #### Consider fixing deprecated APIs -If old deprecations have reached the point where they can be fixed without losing support for stable, considering using some of your gardening time to replace the deprecated API usage. +If old deprecations have reached the point where they can be fixed without losing support for stable, consider using some of your gardening time to replace the deprecated API usage. ## Handling failures @@ -106,7 +118,7 @@ If the commit landed within the last 24 hours: If the commit could not be automatically reverted: 1. Create a revert pull request from the bad merged pull request via the "Revert" button at the bottom. 1. Add the `revert` label to the PR to allow the bot to land it without approval. -1. Add the original author to the as a reviewer so they are notified. If they are not a member of [flutter-hackers], also include the original pull request reviewers. +1. Add the original author as a reviewer so they are notified. If they are not a member of [flutter-hackers], also include the original pull request reviewers. 1. In "Related Issues" add a link to any GitHub issues that describe the failure. 1. @ mention the author in the [hackers-ecosystem channel] with a link to the revert pull request. If they are unavailable, send an email. If they are not a [Flutter committer][flutter-hackers] and are not on Discord, escalate to the reviewers of the original pull request. 1. As soon as analysis test passes, merge it. You do not need to wait for all presubmit tests to pass, or for an LGTM. @@ -122,6 +134,7 @@ If you see a test failure that appears to be a flake: 1. If the test has neither been recently introduced, nor recently changed, disable the test. The test owner will turn it back on or delete the test as part of their investigation. [flutter/packages]: https://github.com/flutter/packages +[flutter/core-packages]: https://github.com/flutter/core-packages [flutter/flutter]: https://github.com/flutter/flutter [Flutter framework gardener]: /docs/infra/Flutter-Framework-Gardener-Rotation.md [Flutter issues]: https://github.com/flutter/flutter/issues @@ -129,6 +142,7 @@ If you see a test failure that appears to be a flake: [deprecated api issues]: https://github.com/flutter/flutter/labels/p%3A%20deprecated%20api [flutter-hackers]: https://github.com/orgs/flutter/teams/flutter-hackers [packages build dashboard]: https://flutter-dashboard.appspot.com/#/build?repo=packages +[flutter/core-packages build status]: https://github.com/flutter/core-packages/commits/main/ [Discord]: https://discord.gg/BS8KZyg [hackers-ecosystem channel]: https://discord.com/channels/608014603317936148/608020293944082452 [hackers-infra channel]: https://discord.com/channels/608014603317936148/608021351567065092 @@ -139,4 +153,5 @@ If you see a test failure that appears to be a flake: [packages-to-flutter roller]: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll [packages gardening journal]: https://goto.google.com/flutter-packages-gardener-journal [flutter-stable-to-packages roller]: https://autoroll.skia.org/r/flutter-stable-packages +[update for stable release]: /docs/ecosystem/release/Updating-Packages-repo-for-a-stable-release.md [On-call scheduling for Flutter]: https://docs.google.com/document/d/1i-11by4J3zvxWG3qLMm4MKfJ8DrjIJna6DPA9tBmJWc/edit#heading=h.w8bl5vic6x95 From 404001390f7d14df53e192fb8c764122ee22f494 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 10 Aug 2026 11:31:34 -0400 Subject: [PATCH 162/330] Roll Skia from 2ad03d882c48 to 6549c09b1ffb (1 revision) (#190840) https://skia.googlesource.com/skia.git/+log/2ad03d882c48..6549c09b1ffb 2026-08-10 robertphillips@google.com [graphite] Add threading to the PipelineManager If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC codefu@google.com,jmbetancourt@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index d33995b128bdf..801a4d322a527 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '2ad03d882c4870850b99905ecf21854f5dd8cd4a', + 'skia_revision': '6549c09b1ffb41b612afc25a4a626964a4af739d', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From a574a29ac045070a042d4a98f50ef0b528114d53 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 10 Aug 2026 11:53:09 -0400 Subject: [PATCH 163/330] Roll Packages from fc22143c4490 to 1861b68e9695 (4 revisions) (#190844) https://github.com/flutter/packages/compare/fc22143c4490...1861b68e9695 2026-08-09 engine-flutter-autoroll@skia.org Manual roll Flutter from 2757a77a73df to e52f01c920ad (47 revisions) (flutter/packages#12401) 2026-08-08 engine-flutter-autoroll@skia.org Manual roll Flutter from 2a230d1e8037 to 2757a77a73df (14 revisions) (flutter/packages#12393) 2026-08-07 21270878+elliette@users.noreply.github.com [material_ui] Enable localized `time_picker_test` cases (flutter/packages#12391) 2026-08-07 jmccandless@google.com [material_ui] Main example (flutter/packages#12336) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- bin/internal/flutter_packages.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index b31b925e6988c..6b5d61f416c3b 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -fc22143c4490b61cc2aa925751ec246ef1a86ec1 +1861b68e9695df1f592bd1b922888cdc120d1d7a From c6e9db837d6aeaffabd204f04a51d6c0d9a2da88 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 11 Aug 2026 01:02:07 +0900 Subject: [PATCH 164/330] ci: Extract shared expected calls in prepare_package tests (#190817) Three of the `ArchiveCreator` tests each carried their own copy of the same ~60 line map of expected subprocess invocations, differing only in the architecture `dart --version` reports and the name of the output archive. This extracts that map into an `expectedCalls` helper parameterised on those two values, and drops the redundant re-construction of `creator` that two of them did on top of `setUp`. Also adds an `expectedEnvironment` helper and passes it through `convertResults`. We never verified the environment `ArchiveCreator` runs its subprocesses with, so the `sets PUB_CACHE properly` test wasn't actually checking `PUB_CACHE`. The `non-strict mode calls the right commands` and `fails if binary is not codesigned` tests build nearly the same map, but differ in enough other ways that I've left them as-is. This is pre-factoring for the `--target_arch` option added in a followup patch, which introduces further variants of these expectations. Issue: https://github.com/flutter/flutter/issues/189144 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- dev/bots/test/prepare_package_test.dart | 177 +++++++----------------- 1 file changed, 47 insertions(+), 130 deletions(-) diff --git a/dev/bots/test/prepare_package_test.dart b/dev/bots/test/prepare_package_test.dart index a962174a47953..ed64a41168fec 100644 --- a/dev/bots/test/prepare_package_test.dart +++ b/dev/bots/test/prepare_package_test.dart @@ -155,68 +155,23 @@ void main() { tryToDelete(tempDir); }); - test('sets PUB_CACHE properly', () async { + /// The subprocesses expected during archive creation, keyed by command-line. + /// + /// [dartArch] is the architecture `dart --version` reports for the Dart + /// SDK downloaded into the archive. [archiveArch] is the architecture + /// expected in the archive filename, or null for x64, which is unadorned. + Map?> expectedCalls({ + required String dartArch, + String? archiveArch, + }) { final String createBase = path.join(tempDir.absolute.path, 'create_'); + final archPrefix = archiveArch == null ? '' : '${archiveArch}_'; final String archiveName = path.join( tempDir.absolute.path, - 'flutter_${platformName}_v1.2.3-beta${platform.isLinux ? '.tar.xz' : '.zip'}', + 'flutter_${platformName}_${archPrefix}v1.2.3-beta' + '${platform.isLinux ? '.tar.xz' : '.zip'}', ); - - processManager.addCommands( - convertResults(?>{ - 'git clone -b beta https://flutter.googlesource.com/mirrors/flutter': null, - 'git reset --hard $testRef': null, - 'git remote set-url origin https://github.com/flutter/flutter.git': null, - 'git gc --prune=now --aggressive': null, - 'git describe --tags --exact-match $testRef': [ - ProcessResult(0, 0, 'v1.2.3', ''), - ], - '$flutter --version --machine': [ - ProcessResult(0, 0, '{"dartSdkVersion": "3.2.1"}', ''), - ProcessResult(0, 0, '{"dartSdkVersion": "3.2.1"}', ''), - ], - '$dart --version': [ - ProcessResult( - 0, - 0, - 'Dart SDK version: 2.17.0-63.0.beta (beta) (Wed Jan 26 03:48:52 2022 -0800) on "${platformName}_x64"', - '', - ), - ], - if (platform.isWindows) '7za x ${path.join(tempDir.path, 'mingit.zip')}': null, - '$flutter doctor': null, - '$flutter update-packages': null, - '$flutter precache': null, - '$flutter ide-config': null, - '$flutter create --template=app ${createBase}app': null, - '$flutter create --template=package ${createBase}package': null, - '$flutter create --template=plugin ${createBase}plugin': null, - '$flutter pub cache list': [ProcessResult(0, 0, '{"packages":{}}', '')], - 'git clean -f -x -- **/.packages': null, - 'git clean -f -x -- **/.dart_tool/': null, - if (platform.isMacOS) - 'codesign -vvvv --check-notarization ${path.join(tempDir.path, 'flutter', 'bin', 'cache', 'dart-sdk', 'bin', 'dart')}': - null, - if (platform.isWindows) 'attrib -h .git': null, - if (platform.isWindows) - '7za a -tzip -mx=9 $archiveName flutter': null - else if (platform.isMacOS) - 'zip -r -9 --symlinks $archiveName flutter': null - else if (platform.isLinux) - 'tar cJf $archiveName --verbose flutter': null, - }), - ); - await creator.initializeRepo(); - await creator.createArchive(); - }); - - test('calls the right commands for archive output', () async { - final String createBase = path.join(tempDir.absolute.path, 'create_'); - final String archiveName = path.join( - tempDir.absolute.path, - 'flutter_${platformName}_v1.2.3-beta${platform.isLinux ? '.tar.xz' : '.zip'}', - ); - final calls = ?>{ + return ?>{ 'git clone -b beta https://flutter.googlesource.com/mirrors/flutter': null, 'git reset --hard $testRef': null, 'git remote set-url origin https://github.com/flutter/flutter.git': null, @@ -232,7 +187,7 @@ void main() { ProcessResult( 0, 0, - 'Dart SDK version: 2.17.0-63.0.beta (beta) (Wed Jan 26 03:48:52 2022 -0800) on "${platformName}_x64"', + 'Dart SDK version: 2.17.0-63.0.beta (beta) (Wed Jan 26 03:48:52 2022 -0800) on "${platformName}_$dartArch"', '', ), ], @@ -258,81 +213,35 @@ void main() { else if (platform.isLinux) 'tar cJf $archiveName --verbose flutter': null, }; - processManager.addCommands(convertResults(calls)); - creator = ArchiveCreator( - tempDir, - tempDir, - testRef, - Branch.beta, - fs: fs, - processManager: processManager, - subprocessOutput: false, - platform: platform, - httpReader: fakeHttpReader, + } + + /// The environment expected on every [ArchiveCreator] subprocess. + /// + /// This is the ambient environment, plus the archive's own pub cache. + Map expectedEnvironment() { + return { + ...platform.environment, + 'PUB_CACHE': path.join(tempDir.path, '.pub-cache'), + }; + } + + test('sets PUB_CACHE properly', () async { + processManager.addCommands( + convertResults(expectedCalls(dartArch: 'x64'), environment: expectedEnvironment()), ); await creator.initializeRepo(); await creator.createArchive(); }); + test('calls the right commands for archive output', () async { + processManager.addCommands(convertResults(expectedCalls(dartArch: 'x64'))); + await creator.initializeRepo(); + await creator.createArchive(); + }); + test('adds the arch name to the archive for non-x64', () async { - final String createBase = path.join(tempDir.absolute.path, 'create_'); - final String archiveName = path.join( - tempDir.absolute.path, - 'flutter_${platformName}_arm64_v1.2.3-beta${platform.isLinux ? '.tar.xz' : '.zip'}', - ); - final calls = ?>{ - 'git clone -b beta https://flutter.googlesource.com/mirrors/flutter': null, - 'git reset --hard $testRef': null, - 'git remote set-url origin https://github.com/flutter/flutter.git': null, - 'git gc --prune=now --aggressive': null, - 'git describe --tags --exact-match $testRef': [ - ProcessResult(0, 0, 'v1.2.3', ''), - ], - '$flutter --version --machine': [ - ProcessResult(0, 0, '{"dartSdkVersion": "3.2.1"}', ''), - ProcessResult(0, 0, '{"dartSdkVersion": "3.2.1"}', ''), - ], - '$dart --version': [ - ProcessResult( - 0, - 0, - 'Dart SDK version: 2.17.0-63.0.beta (beta) (Wed Jan 26 03:48:52 2022 -0800) on "${platformName}_arm64"', - '', - ), - ], - if (platform.isWindows) '7za x ${path.join(tempDir.path, 'mingit.zip')}': null, - '$flutter doctor': null, - '$flutter update-packages': null, - '$flutter precache': null, - '$flutter ide-config': null, - '$flutter create --template=app ${createBase}app': null, - '$flutter create --template=package ${createBase}package': null, - '$flutter create --template=plugin ${createBase}plugin': null, - '$flutter pub cache list': [ProcessResult(0, 0, '{"packages":{}}', '')], - 'git clean -f -x -- **/.packages': null, - 'git clean -f -x -- **/.dart_tool/': null, - if (platform.isMacOS) - 'codesign -vvvv --check-notarization ${path.join(tempDir.path, 'flutter', 'bin', 'cache', 'dart-sdk', 'bin', 'dart')}': - null, - if (platform.isWindows) 'attrib -h .git': null, - if (platform.isWindows) - '7za a -tzip -mx=9 $archiveName flutter': null - else if (platform.isMacOS) - 'zip -r -9 --symlinks $archiveName flutter': null - else if (platform.isLinux) - 'tar cJf $archiveName --verbose flutter': null, - }; - processManager.addCommands(convertResults(calls)); - creator = ArchiveCreator( - tempDir, - tempDir, - testRef, - Branch.beta, - fs: fs, - processManager: processManager, - subprocessOutput: false, - platform: platform, - httpReader: fakeHttpReader, + processManager.addCommands( + convertResults(expectedCalls(dartArch: 'arm64', archiveArch: 'arm64')), ); await creator.initializeRepo(); await creator.createArchive(); @@ -1228,18 +1137,26 @@ void main() { } } -List convertResults(Map?> results) { +/// Converts a map of command lines to their results into [FakeCommand]s. +/// +/// If [environment] is given, each command is additionally expected to run with +/// exactly that environment. +List convertResults( + Map?> results, { + Map? environment, +}) { final commands = []; for (final String key in results.keys) { final List? candidates = results[key]; final List args = key.split(' '); if (candidates == null) { - commands.add(FakeCommand(command: args)); + commands.add(FakeCommand(command: args, environment: environment)); } else { for (final ProcessResult result in candidates) { commands.add( FakeCommand( command: args, + environment: environment, exitCode: result.exitCode, stderr: result.stderr.toString(), stdout: result.stdout.toString(), From c9c1537899b9594da3197165a23c9639d714d493 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Mon, 10 Aug 2026 09:04:10 -0700 Subject: [PATCH 165/330] Fix CocoaPods ruby dependency for flavors_test_ios on Mac bot (#190775) This PR adds the missing `ruby` (CocoaPods) dependency to the `flavors_test_ios` target in `.ci.yaml`. Following the migration of this test from the physical device pool (`Mac_ios`) to the generic `Mac` simulator pool (in PR #189442), the target began failing in post-submit bringup with the [error `CocoaPods not installed or not in valid state`](https://ci.chromium.org/ui/p/flutter/builders/staging/Mac%20flavors_test_ios/1/overview). Because the dummy `flavors` app uses the `integration_test` package and contains an `ios/Podfile`, it requires CocoaPods to compile. Generic Mac CI bots do not have CocoaPods installed globally, so it must be explicitly requested in the LUCI `.ci.yaml` dependencies list. *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* Fixes #189442 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index 887cfa87b82ec..20617668768c9 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -5484,6 +5484,10 @@ targets: bringup: true timeout: 60 properties: + dependencies: >- + [ + {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"} + ] tags: > ["devicelab", "hostonly", "mac"] task_name: flavors_test_ios From 972ff40da4c97e29bbe6503697a0b15207ec5ba0 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 10 Aug 2026 17:35:56 +0000 Subject: [PATCH 166/330] [flutter_tools] Support UTF-16 and UTF-8 BOM decoding in --dart-define-from-file (#190286) (#190547) Fixes https://github.com/flutter/flutter/issues/190286 When a valid JSON or `.env` configuration file is saved in UTF-16 LE/BE with a BOM (e.g., Windows PowerShell `Out-File` or `Set-Content` default output), `--dart-define-from-file` previously crashed with an unhandled `FileSystemException` during `readAsStringSync`. This change adds `decodeUtf8OrUtf16` to `utils.dart` to support decoding UTF-16 LE/BE with a BOM, as well as stripping UTF-8 BOMs. Additionally, any decoding exceptions during file loading in `extractDartDefineConfigJsonMap` are caught and converted into a user-friendly `ToolExit` rather than an unhandled crash. ## Testing - Added unit tests in `packages/flutter_tools/test/general.shard/base_utils_test.dart`. - Added regression tests in `packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart`. --- .../flutter_tools/lib/src/base/utils.dart | 56 +++++++++++++++++++ .../lib/src/runner/flutter_command.dart | 11 +++- .../test/general.shard/base_utils_test.dart | 38 +++++++++++++ .../runner/flutter_command_test.dart | 55 ++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart index 1fb4184195e5c..9cb792f61bf18 100644 --- a/packages/flutter_tools/lib/src/base/utils.dart +++ b/packages/flutter_tools/lib/src/base/utils.dart @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/// @docImport 'common.dart'; /// @docImport 'terminal.dart'; library; import 'dart:async'; import 'dart:math' as math; +import 'dart:typed_data'; import 'package:file/file.dart'; import 'package:intl/intl.dart' as intl; @@ -647,3 +649,57 @@ List formatTable(List> table, {String separator = ' • ', return '$indentString$formatted'; }).toList(); } + +/// Decodes a list of bytes into a string, supporting UTF-8 (with or without +/// BOM) and UTF-16 LE/BE (with BOM). +/// +/// Inspects leading Byte Order Mark (BOM) signatures in [bytes] to determine +/// the encoding: +/// +/// * **UTF-16 LE** (`0xFF, 0xFE`): Strips the 2-byte BOM and decodes the +/// remaining payload as 16-bit little-endian code units. +/// * **UTF-16 BE** (`0xFE, 0xFF`): Strips the 2-byte BOM and decodes the +/// remaining payload as 16-bit big-endian code units. +/// * **UTF-8 with BOM** (`0xEF, 0xBB, 0xBF`): Strips the 3-byte BOM and decodes +/// the remaining payload as strict UTF-8. +/// * **Default UTF-8** (no BOM): Decodes the entire byte list as strict UTF-8. +/// +/// Throws a [FormatException] if a UTF-16 byte payload has an odd length after +/// stripping the BOM, or a [ToolExit] if strict UTF-8 decoding fails. +String decodeUtf8OrUtf16(List bytes) { + // Avoid using list pattern matching here (e.g., `[0xFF, 0xFE, ...final payload]`) + // as the rest pattern allocates a copied sublist for the payload. + if (bytes.length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) { + return _decodeUtf16(bytes, 2, Endian.little); + } + if (bytes.length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) { + return _decodeUtf16(bytes, 2, Endian.big); + } + if (bytes.length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) { + return utf8.decode(bytes.sublist(3)); + } + return utf8.decode(bytes); +} + +/// Decodes a UTF-16 byte list [bytes] starting from [offset] after its BOM has +/// been stripped. +/// +/// Reads 16-bit integers according to the specified byte [endian] (either +/// [Endian.little] or [Endian.big]). +/// +/// Throws a [FormatException] if the payload length has an odd number of bytes, +/// as each UTF-16 code unit requires exactly 2 bytes. +String _decodeUtf16(List bytes, int offset, Endian endian) { + final int length = bytes.length - offset; + if (length.isOdd) { + throw const FormatException('UTF-16 data length must be even after BOM'); + } + final Uint8List uint8List = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); + final byteData = ByteData.sublistView(uint8List, offset); + final int count = length ~/ 2; + final codeUnits = Uint16List(count); + for (var i = 0; i < count; i++) { + codeUnits[i] = byteData.getUint16(i * 2, endian); + } + return String.fromCharCodes(codeUnits); +} diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 057ffbb36db8c..a5038b6d02329 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -1768,7 +1768,16 @@ abstract class FlutterCommand extends Command { ); } - final String configRaw = globals.fs.file(path).readAsStringSync(); + String configRaw; + try { + configRaw = decodeUtf8OrUtf16(globals.fs.file(path).readAsBytesSync()); + } on Exception catch (err) { + throwToolExit( + 'Unable to decode the file at path "$path". ' + 'Ensure that the file is encoded in UTF-8 or UTF-16.\n' + 'Error details: $err', + ); + } // Determine whether the file content is JSON or .env format. String configJsonRaw; diff --git a/packages/flutter_tools/test/general.shard/base_utils_test.dart b/packages/flutter_tools/test/general.shard/base_utils_test.dart index b55ffafbc272c..9dba4fc1cab4e 100644 --- a/packages/flutter_tools/test/general.shard/base_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/base_utils_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'package:flutter_tools/src/base/utils.dart'; import '../src/common.dart'; @@ -59,4 +60,41 @@ void main() { expect(list.items, ['a']); }); }); + + group('decodeUtf8OrUtf16', () { + test('decodes UTF-8 without BOM', () { + expect(decodeUtf8OrUtf16(utf8.encode('hello world')), 'hello world'); + }); + + test('decodes UTF-8 with BOM', () { + expect( + decodeUtf8OrUtf16([0xEF, 0xBB, 0xBF, ...utf8.encode('hello world')]), + 'hello world', + ); + }); + + test('decodes UTF-16 LE with BOM', () { + final bytes = [0xFF, 0xFE, 0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F, 0x00]; + expect(decodeUtf8OrUtf16(bytes), 'hello'); + }); + + test('decodes UTF-16 BE with BOM', () { + final bytes = [0xFE, 0xFF, 0x00, 0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F]; + expect(decodeUtf8OrUtf16(bytes), 'hello'); + }); + + test('throws FormatException on odd-length UTF-16 LE payload', () { + final bytes = [0xFF, 0xFE, 0x68]; + expect(() => decodeUtf8OrUtf16(bytes), throwsFormatException); + }); + + test('throws FormatException on odd-length UTF-16 BE payload', () { + final bytes = [0xFE, 0xFF, 0x68]; + expect(() => decodeUtf8OrUtf16(bytes), throwsFormatException); + }); + + test('throws ToolExit on invalid UTF-8 bytes', () { + expect(() => decodeUtf8OrUtf16([0xFF, 0xFF, 0xFF]), throwsToolExit()); + }); + }); } diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart index 88cdae2683ece..66b0029467627 100644 --- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart @@ -1279,6 +1279,61 @@ void main() { }, ); + testUsingContext( + 'parses values from JSON files encoded in UTF-16 LE with BOM', + () async { + fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true); + fileSystem.file('pubspec.yaml').createSync(); + final utf16Bytes = [ + 0xFF, 0xFE, // UTF-16 LE BOM + ...'{ "kInt": 1 }'.codeUnits.expand( + (int codeUnit) => [codeUnit & 0xFF, (codeUnit >> 8) & 0xFF], + ), + ]; + fileSystem.file('config.json').writeAsBytesSync(utf16Bytes); + + await dummyCommandRunner.run(['dummy', '--dart-define-from-file=config.json']); + + final BuildInfo buildInfo = await dummyCommand.getBuildInfo( + forcedBuildMode: BuildMode.debug, + ); + expect(buildInfo.dartDefines, containsAll(const ['kInt=1'])); + }, + overrides: { + FileSystem: () => fileSystem, + Logger: () => logger, + FileSystemUtils: () => fileSystemUtils, + Platform: () => platform, + ProcessManager: () => processManager, + }, + ); + + testUsingContext( + 'throws a ToolExit when the given file cannot be decoded', + () async { + fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true); + fileSystem.file('pubspec.yaml').createSync(); + fileSystem.file('config.json').writeAsBytesSync([0xFF, 0xFF, 0xFF, 0xFF, 0x01]); + + await dummyCommandRunner.run(['dummy', '--dart-define-from-file=config.json']); + expect( + dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug), + throwsToolExit( + message: + 'Unable to decode the file at path "config.json". ' + 'Ensure that the file is encoded in UTF-8 or UTF-16.\n', + ), + ); + }, + overrides: { + FileSystem: () => fileSystem, + Logger: () => logger, + FileSystemUtils: () => fileSystemUtils, + Platform: () => platform, + ProcessManager: () => processManager, + }, + ); + testUsingContext( 'throws a ToolExit when the given JSON file is malformed', () async { From eed6f4bc5596b0f27925fb27d5cc199f70a8521a Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 10 Aug 2026 17:35:56 +0000 Subject: [PATCH 167/330] [flutter_tools] Use JSON_INPUT COMPILE_EXPRESSION for expression evaluation (#190722) ## Description When evaluating expressions in Flutter debug sessions, `DefaultResidentCompiler._compileExpression` was communicating with `frontend_server` using the legacy line-delimited protocol (`compile-expression `). Because `frontend_server`'s state machine transitions out of expression parsing upon receiving the first newline character, multi-line expressions had only their first line evaluated, with subsequent lines misread as variable definitions. This PR updates `DefaultResidentCompiler._compileExpression` in `packages/flutter_tools/lib/src/compile.dart` to use the structured `JSON_INPUT` protocol with `'type': 'COMPILE_EXPRESSION'` (mirroring `COMPILE_EXPRESSION_JS` in `_compileExpressionToJs`), safely encoding multi-line expressions and scope definitions into a single JSON payload. ## Fixes Fixes https://github.com/flutter/flutter/issues/55731 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter_tools/lib/src/compile.dart | 36 ++-- .../compile_expression_test.dart | 175 ++++++++++++++---- 2 files changed, 154 insertions(+), 57 deletions(-) diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index cf8ec8f8c97ef..d91ab00e0eed2 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -1082,25 +1082,25 @@ class DefaultResidentCompiler implements ResidentCompiler { return null; } - final String inputKey = const Uuid().v4(); - server.stdin - ..writeln('compile-expression $inputKey') - ..writeln(request.expression); - request.definitions?.forEach(server.stdin.writeln); - server.stdin.writeln(inputKey); - request.definitionTypes?.forEach(server.stdin.writeln); - server.stdin.writeln(inputKey); - request.typeDefinitions?.forEach(server.stdin.writeln); - server.stdin.writeln(inputKey); - request.typeBounds?.forEach(server.stdin.writeln); - server.stdin.writeln(inputKey); - request.typeDefaults?.forEach(server.stdin.writeln); server.stdin - ..writeln(inputKey) - ..writeln(request.libraryUri ?? '') - ..writeln(request.klass ?? '') - ..writeln(request.method ?? '') - ..writeln(request.isStatic); + ..writeln('JSON_INPUT') + ..writeln( + json.encode({ + 'type': 'COMPILE_EXPRESSION', + 'data': { + 'expression': request.expression, + 'definitions': request.definitions ?? [], + 'definitionTypes': request.definitionTypes ?? [], + 'typeDefinitions': request.typeDefinitions ?? [], + 'typeBounds': request.typeBounds ?? [], + 'typeDefaults': request.typeDefaults ?? [], + 'libraryUri': request.libraryUri ?? '', + 'class': request.klass, + 'method': request.method, + 'static': request.isStatic, + }, + }), + ); return _stdoutHandler.compilerOutput?.future; } diff --git a/packages/flutter_tools/test/general.shard/compile_expression_test.dart b/packages/flutter_tools/test/general.shard/compile_expression_test.dart index 2cfece7bd6466..883718f8a52f8 100644 --- a/packages/flutter_tools/test/general.shard/compile_expression_test.dart +++ b/packages/flutter_tools/test/general.shard/compile_expression_test.dart @@ -70,51 +70,148 @@ void main() { }); testWithoutContext('compile expression can compile single expression', () async { - final compileResponseCompleter = Completer>(); - final compileExpressionResponseCompleter = Completer>(); + final stdoutController = StreamController>(); + processManager.process.stdout = stdoutController.stream; + fileSystem.file('/path/to/main.dart.dill') ..createSync(recursive: true) ..writeAsBytesSync([1, 2, 3, 4]); - processManager.process.stdout = Stream>.fromFutures(>>[ - compileResponseCompleter.future, - compileExpressionResponseCompleter.future, - ]); - compileResponseCompleter.complete( - Future>.value( - utf8.encode('result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'), - ), + final Future compileFuture = generator.recompile( + Uri.file('/path/to/main.dart'), + null, + /* invalidatedFiles */ + outputPath: '/build/', + packageConfig: PackageConfig.empty, + projectRootPath: '', + fs: fileSystem, + ); + stdoutController.add( + utf8.encode('result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'), ); + final CompilerOutput? output = await compileFuture; + expect(frontendServerStdIn.getAndClear(), 'compile file:///path/to/main.dart\n'); + expect(testLogger.errorText, equals('line1\nline2\n')); + expect(output!.outputFilename, equals('/path/to/main.dart.dill')); - await generator - .recompile( - Uri.file('/path/to/main.dart'), - null, - /* invalidatedFiles */ - outputPath: '/build/', - packageConfig: PackageConfig.empty, - projectRootPath: '', - fs: fileSystem, - ) - .then((CompilerOutput? output) { - expect(frontendServerStdIn.getAndClear(), 'compile file:///path/to/main.dart\n'); - expect(testLogger.errorText, equals('line1\nline2\n')); - expect(output!.outputFilename, equals('/path/to/main.dart.dill')); - - compileExpressionResponseCompleter.complete( - Future>.value( - utf8.encode( - 'result def\nline1\nline2\ndef\ndef /path/to/main.dart.dill.incremental 0\n', - ), - ), - ); - generator - .compileExpression('2+2', null, null, null, null, null, null, null, null, false) - .then((CompilerOutput? outputExpression) { - expect(outputExpression, isNotNull); - expect(outputExpression!.expressionData, [1, 2, 3, 4]); - }); - }); + fileSystem.file('/path/to/main.dart.dill.incremental') + ..createSync(recursive: true) + ..writeAsBytesSync([1, 2, 3, 4]); + + final Future expressionFuture = generator.compileExpression( + '2+2', + null, + null, + null, + null, + null, + null, + null, + null, + false, + ); + stdoutController.add( + utf8.encode('result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'), + ); + final CompilerOutput? outputExpression = await expressionFuture; + expect(outputExpression, isNotNull); + expect(outputExpression!.expressionData, [1, 2, 3, 4]); + + final List stdinLines = frontendServerStdIn.getAndClear().trim().split('\n'); + expect(stdinLines, hasLength(2)); + expect(stdinLines[0], 'JSON_INPUT'); + expect( + json.decode(stdinLines[1]), + { + 'type': 'COMPILE_EXPRESSION', + 'data': { + 'expression': '2+2', + 'definitions': [], + 'definitionTypes': [], + 'typeDefinitions': [], + 'typeBounds': [], + 'typeDefaults': [], + 'libraryUri': '', + 'class': null, + 'method': null, + 'static': false, + }, + }, + ); + await stdoutController.close(); + }); + + testWithoutContext('compile expression sends JSON_INPUT for multiline expressions', () async { + final stdoutController = StreamController>(); + processManager.process.stdout = stdoutController.stream; + + fileSystem.file('/path/to/main.dart.dill') + ..createSync(recursive: true) + ..writeAsBytesSync([1, 2, 3, 4]); + fileSystem.file('/path/to/main.dart.dill.incremental') + ..createSync(recursive: true) + ..writeAsBytesSync([1, 2, 3, 4]); + + final Future compileFuture = generator.recompile( + Uri.file('/path/to/main.dart'), + null, + /* invalidatedFiles */ + outputPath: '/build/', + packageConfig: PackageConfig.empty, + projectRootPath: '', + fs: fileSystem, + ); + stdoutController.add( + utf8.encode('result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'), + ); + final CompilerOutput? output = await compileFuture; + expect(frontendServerStdIn.getAndClear(), 'compile file:///path/to/main.dart\n'); + expect(testLogger.errorText, equals('line1\nline2\n')); + expect(output!.outputFilename, equals('/path/to/main.dart.dill')); + + const multilineExpression = 'final a = 1;\nfinal b = 2;\na + b;'; + final Future expressionFuture = generator.compileExpression( + multilineExpression, + ['def1'], + ['int'], + ['TypeDef1'], + ['TypeBound1'], + ['TypeDefault1'], + 'package:foo/foo.dart', + 'FooClass', + 'fooMethod', + false, + ); + stdoutController.add( + utf8.encode('result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'), + ); + final CompilerOutput? outputExpression = await expressionFuture; + + expect(outputExpression, isNotNull); + expect(outputExpression!.expressionData, [1, 2, 3, 4]); + + final List stdinLines = frontendServerStdIn.getAndClear().trim().split('\n'); + expect(stdinLines, hasLength(2)); + expect(stdinLines[0], 'JSON_INPUT'); + expect( + json.decode(stdinLines[1]), + { + 'type': 'COMPILE_EXPRESSION', + 'data': { + 'expression': multilineExpression, + 'definitions': ['def1'], + 'definitionTypes': ['int'], + 'typeDefinitions': ['TypeDef1'], + 'typeBounds': ['TypeBound1'], + 'typeDefaults': ['TypeDefault1'], + 'libraryUri': 'package:foo/foo.dart', + 'class': 'FooClass', + 'method': 'fooMethod', + 'static': false, + }, + }, + ); + await stdoutController.close(); }); testWithoutContext('compile expressions without awaiting', () async { From 55e4f9c4c6ebf66869ad0c5e7b72a7e501171f4b Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Mon, 10 Aug 2026 17:40:03 +0000 Subject: [PATCH 168/330] Refactor `flutter_view` example to not use `material` (#190377) This PR refactors the `flutter_view` example to not use `material`. It also migrates the iOS app to utilize the UIScene lifecycle. Without the migration the app refused to run, and showed the error below. ``` failure in void _UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption(void)_block_invoke (UIApplication_RuntimeIssues.m:106) : Application failed to launch: UIScene life cycle is required for apps built with this SDK. See "Transitioning to the UIKit scene-based life cycle" in the UIKit documentation for more information on migration. ``` ## Android: https://github.com/user-attachments/assets/6b137881-2fbc-4756-8846-d57ec978e9c1 ## iOS: https://github.com/user-attachments/assets/abde493c-2b24-4f3e-8e91-e19f80fc9e56 Part of #190305 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Renzo Olivares --- dev/bots/check_examples_cross_imports.dart | 1 - .../ios/Runner.xcodeproj/project.pbxproj | 6 ++ .../flutter_view/ios/Runner/AppDelegate.m | 9 ++ examples/flutter_view/ios/Runner/Info.plist | 19 ++++ .../flutter_view/ios/Runner/SceneDelegate.h | 11 +++ .../flutter_view/ios/Runner/SceneDelegate.m | 28 ++++++ examples/flutter_view/lib/main.dart | 96 ++++++++++++++----- examples/flutter_view/pubspec.yaml | 1 - 8 files changed, 147 insertions(+), 24 deletions(-) create mode 100644 examples/flutter_view/ios/Runner/SceneDelegate.h create mode 100644 examples/flutter_view/ios/Runner/SceneDelegate.m diff --git a/dev/bots/check_examples_cross_imports.dart b/dev/bots/check_examples_cross_imports.dart index ac8d8fdb94c7d..51b2842f52709 100644 --- a/dev/bots/check_examples_cross_imports.dart +++ b/dev/bots/check_examples_cross_imports.dart @@ -585,7 +585,6 @@ class ExamplesCrossImportChecker { 'packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart', 'packages/flutter/examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart', 'packages/flutter/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart', - 'examples/flutter_view/lib/main.dart', 'examples/texture/lib/main.dart', }; diff --git a/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj b/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj index ec0f531456d7e..04fd6c81a8fae 100644 --- a/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 2DE332E71E55C6D800393FD5 /* MainViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2DE332E61E55C6D800393FD5 /* MainViewController.m */; }; 3B3967051E83383D004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967041E83383D004F5970 /* AppFrameworkInfo.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + A1B2C3D4E5F6000100000001 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6000100000003 /* SceneDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -41,6 +42,8 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + A1B2C3D4E5F6000100000002 /* SceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; + A1B2C3D4E5F6000100000003 /* SceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -96,6 +99,8 @@ 2DD8945E1E5B87AF0010574F /* ic_add.png */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + A1B2C3D4E5F6000100000002 /* SceneDelegate.h */, + A1B2C3D4E5F6000100000003 /* SceneDelegate.m */, 2DE332E81E55C6F100393FD5 /* MainViewController.h */, 2DE332E61E55C6D800393FD5 /* MainViewController.m */, 2D4B11261E55A15A00FF14DB /* NativeViewController.m */, @@ -228,6 +233,7 @@ buildActionMask = 2147483647; files = ( 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + A1B2C3D4E5F6000100000001 /* SceneDelegate.m in Sources */, 97C146F31CF9000F007C117D /* main.m in Sources */, 2D4B11271E55A15A00FF14DB /* NativeViewController.m in Sources */, 2DE332E71E55C6D800393FD5 /* MainViewController.m in Sources */, diff --git a/examples/flutter_view/ios/Runner/AppDelegate.m b/examples/flutter_view/ios/Runner/AppDelegate.m index b3fa342f04a00..6ce3705c80edf 100644 --- a/examples/flutter_view/ios/Runner/AppDelegate.m +++ b/examples/flutter_view/ios/Runner/AppDelegate.m @@ -12,4 +12,13 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } +#pragma mark - UISceneSession lifecycle + +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { + return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; +} + +- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { +} + @end diff --git a/examples/flutter_view/ios/Runner/Info.plist b/examples/flutter_view/ios/Runner/Info.plist index e8267951bd6ec..a70b5eb485496 100644 --- a/examples/flutter_view/ios/Runner/Info.plist +++ b/examples/flutter_view/ios/Runner/Info.plist @@ -43,5 +43,24 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + diff --git a/examples/flutter_view/ios/Runner/SceneDelegate.h b/examples/flutter_view/ios/Runner/SceneDelegate.h new file mode 100644 index 0000000000000..ce1caaaaf194a --- /dev/null +++ b/examples/flutter_view/ios/Runner/SceneDelegate.h @@ -0,0 +1,11 @@ +// 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 + +@interface SceneDelegate : UIResponder + +@property (strong, nonatomic) UIWindow * window; + +@end diff --git a/examples/flutter_view/ios/Runner/SceneDelegate.m b/examples/flutter_view/ios/Runner/SceneDelegate.m new file mode 100644 index 0000000000000..a05f3eb127f62 --- /dev/null +++ b/examples/flutter_view/ios/Runner/SceneDelegate.m @@ -0,0 +1,28 @@ +// 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 "SceneDelegate.h" + +@implementation SceneDelegate + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. +} + +- (void)sceneDidDisconnect:(UIScene *)scene { +} + +- (void)sceneDidBecomeActive:(UIScene *)scene { +} + +- (void)sceneWillResignActive:(UIScene *)scene { +} + +- (void)sceneWillEnterForeground:(UIScene *)scene { +} + +- (void)sceneDidEnterBackground:(UIScene *)scene { +} + +@end diff --git a/examples/flutter_view/lib/main.dart b/examples/flutter_view/lib/main.dart index 3ca4b56b71d5c..d5b8e880178b2 100644 --- a/examples/flutter_view/lib/main.dart +++ b/examples/flutter_view/lib/main.dart @@ -3,8 +3,14 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:flutter/material.dart'; + import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +const Color _grey = Color(0xFF9E9E9E); +const Color _white = Color(0xFFFFFFFF); +const Color _black = Color(0xFF000000); +const Color _blue = Color(0xFF2196F3); void main() { runApp(const FlutterView()); @@ -15,9 +21,23 @@ class FlutterView extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return WidgetsApp( title: 'Flutter View', - theme: ThemeData(primarySwatch: Colors.grey), + color: _grey, + textStyle: const TextStyle(color: _black, decoration: TextDecoration.none), + pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) { + return PageRouteBuilder( + settings: settings, + pageBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return builder(context); + }, + ); + }, home: const MyHomePage(), ); } @@ -60,32 +80,64 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ColoredBox( + color: _white, + child: Stack( children: [ - Expanded( - child: Center( - child: Text( - 'Platform button tapped $_counter time${_counter == 1 ? '' : 's'}.', - style: const TextStyle(fontSize: 17.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Center( + child: Text( + 'Platform button tapped $_counter time${_counter == 1 ? '' : 's'}.', + style: const TextStyle(fontSize: 17.0), + ), + ), ), - ), + Container( + padding: const EdgeInsets.only(bottom: 15.0, left: 5.0), + child: Row( + children: [ + Image.asset('assets/flutter-mark-square-64.png', scale: 1.5), + const Text('Flutter', style: TextStyle(fontSize: 30.0)), + ], + ), + ), + ], ), - Container( - padding: const EdgeInsets.only(bottom: 15.0, left: 5.0), - child: Row( - children: [ - Image.asset('assets/flutter-mark-square-64.png', scale: 1.5), - const Text('Flutter', style: TextStyle(fontSize: 30.0)), - ], + Positioned( + bottom: 16.0, + right: 16.0, + child: _Button( + onPressed: _sendFlutterIncrement, + icon: const Text( + '+', + style: TextStyle(color: _white, fontSize: 28.0, fontWeight: FontWeight.bold), + ), ), ), ], ), - floatingActionButton: FloatingActionButton( - onPressed: _sendFlutterIncrement, - child: const Icon(Icons.add), + ); + } +} + +class _Button extends StatelessWidget { + const _Button({required this.onPressed, required this.icon}); + + final VoidCallback onPressed; + final Widget icon; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: Container( + width: 56.0, + height: 56.0, + decoration: const BoxDecoration(color: _blue, shape: BoxShape.circle), + child: Center(child: icon), ), ); } diff --git a/examples/flutter_view/pubspec.yaml b/examples/flutter_view/pubspec.yaml index 7ba4d4117af75..564760d123a01 100644 --- a/examples/flutter_view/pubspec.yaml +++ b/examples/flutter_view/pubspec.yaml @@ -12,7 +12,6 @@ dependencies: flutter: - uses-material-design: true assets: - assets/flutter-mark-square-64.png From ab7c1db5a494ff77e8ce9d87a0484bd4debda502 Mon Sep 17 00:00:00 2001 From: b-luk <97480502+b-luk@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:10:07 +0000 Subject: [PATCH 169/330] Update `FlutterNativeAssetsBuildRunnerImpl` to add ".exe" suffix to its `_dartExecutable` for Windows (#190761) This fixes the windows_engine_integration_golden_test on CI, which is currently failing (but not blocking the tree because it is still `bringup: true`). This dart executable path is passed into the `NativeAssetsBuildRunner` constructor, part of `package:hooks`. The `NativeAssetsBuildRunner` code on Windows either runs the executable directly (if it ends in .exe), or runs the executable via `cmd` (if it does not end in .exe). On Windows, the dart executable ends in .exe, but it was not passed in with the .exe suffix. This resulted in trying to run it with `cmd`, which does not work properly in some situations. Run before this change: https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Windows%20windows_engine_integration_golden_test/2/overview Run after this change: https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Windows%20windows_engine_integration_golden_test/3/overview Part of #190301 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/src/build_system/targets/native_assets.dart | 1 + .../lib/src/isolated/native_assets/native_assets.dart | 4 +++- .../lib/src/isolated/native_assets/test/native_assets.dart | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart index f729baee6c4ae..4c7b189e0b7da 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart @@ -419,6 +419,7 @@ Future createFlutterNativeAssetsBuildRunner( packageConfig, fileSystem, environment.logger, + environment.platform, runPackageName, includeDevDependencies: includeDevDependencies, pubspecPath, diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart index f021e6380281b..bff4e91d19723 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart @@ -544,6 +544,7 @@ class FlutterNativeAssetsBuildRunnerImpl implements FlutterNativeAssetsBuildRunn this.packageConfig, this.fileSystem, this.logger, + this.platform, this.runPackageName, this.pubspecPath, { required this.includeDevDependencies, @@ -554,6 +555,7 @@ class FlutterNativeAssetsBuildRunnerImpl implements FlutterNativeAssetsBuildRunn final PackageConfig packageConfig; final FileSystem fileSystem; final Logger logger; + final Platform platform; final String runPackageName; /// Include the dev dependencies of [runPackageName]. @@ -582,7 +584,7 @@ class FlutterNativeAssetsBuildRunnerImpl implements FlutterNativeAssetsBuildRunn late final Uri _dartExecutable = fileSystem .directory(Cache.flutterRoot) .uri - .resolve('bin/cache/dart-sdk/bin/dart'); + .resolve('bin/cache/dart-sdk/bin/dart${platform.isWindows ? '.exe' : ''}'); late final packageLayout = PackageLayout.fromPackageConfig( fileSystem, diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart index 73bc965e29e4e..f81f2a4c59afb 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart @@ -44,6 +44,7 @@ Future testCompilerBuildNativeAssets(BuildInfo buildInfo) async { buildInfo.packageConfig, globals.fs, globals.logger, + globals.platform, runPackageName, includeDevDependencies: true, pubspecPath, From 347dff3c2098c18ed623735df69e206777f72009 Mon Sep 17 00:00:00 2001 From: zhongliugo Date: Mon, 10 Aug 2026 18:18:26 +0000 Subject: [PATCH 170/330] [web] Keep the keyboard up during an iOS caret drag (#190014) Fixes #189744 **Problem** On iOS 27, long-press-dragging the selection caret in a Web TextField dismisses the keyboard mid-gesture. WebKit transiently blurs the hidden input with a null relatedTarget while the document keeps focus, then refocuses it a frame later,and the engine reacted to that blink on two listeners that each tore the input down. A plain input ignores the same blur, so the cause is Flutter's reaction, not a WebKit limitation. **Fix** On iOS both listeners now defer their teardown by 100ms and cancel it if the input refocuses, mirroring the existing #155265 deferred close. Done and tap-away never refocus so they still close, and the deferral is narrowed to the exact drag signature so every other focus transition is unaffected. **Demo** Before: https://flutter-demo-52-before.web.app (keyboard dismisses mid caret drag) After: https://flutter-demo-52-after.web.app (keyboard stays up) Repro on iOS 27 Safari: tap the field to raise the keyboard, then long-press and drag the selection caret. Before dismisses; after stays up. Done and tap-away still dismiss. --- .../view_focus_binding.dart | 51 +++++- .../src/engine/text_editing/text_editing.dart | 51 +++++- .../view_focus_binding_test.dart | 169 ++++++++++++++++++ .../web_ui/test/engine/text_editing_test.dart | 127 +++++++++++++ 4 files changed, 396 insertions(+), 2 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart index 3874eb9d43e07..9ccf9789d4027 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart @@ -8,6 +8,12 @@ import 'dart:js_interop'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +/// Overrides `domDocument.hasFocus()` in [ViewFocusBinding] for tests, so the +/// iOS caret-drag deferral tests do not depend on the headless browser +/// reporting the test document as focused. Mirrors +/// `DefaultTextEditingStrategy.debugDocumentHasFocusOverride`. +bool? debugViewFocusDocumentHasFocusOverride; + /// Tracks the [FlutterView]s focus changes. final class ViewFocusBinding { ViewFocusBinding(this._viewManager, this._onViewFocusChange); @@ -20,6 +26,21 @@ final class ViewFocusBinding { StreamSubscription? _onViewCreatedListener; + /// A deferred report of a `focusout` that named no element to gain focus. + /// + /// A native iOS caret/selection drag transiently blurs the focused input to + /// and WebKit refocuses it a frame later. Deferring the report lets + /// that refocus cancel it, so the view is not briefly reported unfocused. + /// See: https://github.com/flutter/flutter/issues/189744 + Timer? _pendingFocusoutTimer; + + /// Whether the document itself still has focus. + /// + /// Reads [debugViewFocusDocumentHasFocusOverride] when set, so tests do not + /// depend on the headless browser reporting the test document as focused. + /// Mirrors `DefaultTextEditingStrategy._documentHasFocus`. + bool get _documentHasFocus => debugViewFocusDocumentHasFocusOverride ?? domDocument.hasFocus(); + void init() { // We need a global listener here to know if the user was pressing "shift" // when the Flutter view receives focus, to move the Flutter focus to the @@ -36,6 +57,7 @@ final class ViewFocusBinding { domDocument.body?.removeEventListener(_keyDown, _handleKeyDown); domDocument.body?.removeEventListener(_keyUp, _handleKeyUp); _onViewCreatedListener?.cancel(); + _pendingFocusoutTimer?.cancel(); } void changeViewFocus(int viewId, ui.ViewFocusState state) { @@ -54,6 +76,9 @@ final class ViewFocusBinding { late final DomEventListener _handleFocusin = createDomEventListener((DomEvent event) { event as DomFocusEvent; + // Focus returned, so a deferred `focusout` was a transient blur; drop it. + _pendingFocusoutTimer?.cancel(); + _pendingFocusoutTimer = null; _handleFocusChange(event.target as DomElement?); }); @@ -70,7 +95,31 @@ final class ViewFocusBinding { } event as DomFocusEvent; - _handleFocusChange(event.relatedTarget as DomElement?); + final willGainFocus = event.relatedTarget as DomElement?; + final target = event.target as DomElement?; + + // On iOS, a native caret/selection drag transiently blurs Flutter's active + // text-editing element to (relatedTarget == null) while the document + // still has focus, and WebKit refocuses it a frame later. Reporting the view + // unfocused on that blink tears the text connection down and drops the + // keyboard. Defer only that precise case, re-deriving from the live focus so + // an immediate refocus is a no-op ([_handleFocusin] cancels the timer). + // Anything else, a non-editing element, a genuine focus loss, or the page + // itself losing focus, reports immediately. + // https://github.com/flutter/flutter/issues/189744 + if (isIosSafari && + willGainFocus == null && + _documentHasFocus && + textEditing.isActiveTextEditingElement(target)) { + _pendingFocusoutTimer?.cancel(); + _pendingFocusoutTimer = Timer(kTransientBlurSettleDelay, () { + _pendingFocusoutTimer = null; + _handleFocusChange(domDocument.activeElement); + }); + return; + } + + _handleFocusChange(willGainFocus); }); late final DomEventListener _handleKeyDown = createDomEventListener((DomEvent event) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index e907a3e98469f..fa74506f646ad 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -12,6 +12,7 @@ import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; +import '../browser_detection.dart' show isIosSafari; import '../configuration.dart'; import '../dom.dart'; import '../mouse/prevent_default.dart'; @@ -50,6 +51,18 @@ bool browserHasAutofillOverlay() => /// transparent. const String transparentTextEditingClass = 'transparentTextEditing'; +/// How long to wait before treating a blur that named no incoming element as a +/// real focus loss. +/// +/// Several browser behaviors blur transiently and restore focus a moment later: +/// backgrounding a tab fires blur before `visibilitychange`, and on iOS a +/// native caret or selection drag blurs the input mid-gesture before WebKit +/// refocuses it. Waiting this long lets those settle before the engine acts. +/// +/// Shared by [DefaultTextEditingStrategy.handleBlur] and [ViewFocusBinding], +/// which defer the same gesture and must not disagree about the window. +const Duration kTransientBlurSettleDelay = Duration(milliseconds: 100); + void _emptyCallback(dynamic _) {} /// These style attributes are constant throughout the life time of an input @@ -1862,7 +1875,7 @@ abstract class DefaultTextEditingStrategy // When a browser tab is backgrounded, the input blur arrives before // visibilitychange. Wait briefly so tab switches can keep the text // connection alive, while ordinary window/iframe blurs still close it. - _pendingBlurConnectionCloseTimer = Timer(const Duration(milliseconds: 100), () { + _pendingBlurConnectionCloseTimer = Timer(kTransientBlurSettleDelay, () { _pendingBlurConnectionCloseTimer = null; if (_documentVisibilityState == 'hidden' || _documentHasFocus) { return; @@ -1871,6 +1884,32 @@ abstract class DefaultTextEditingStrategy }); return; } + // On iOS WebKit, a native caret or selection drag transiently blurs the + // hidden input mid-gesture with `relatedTarget == null` while the document + // still has focus, and WebKit refocuses the input a frame later. Closing + // the connection on that blink drops the keyboard; a plain keeps + // it. Defer the close and skip it if the input has regained focus by the + // time the timer fires. A genuine blur, the Done button or tapping away, + // does not refocus, so it still closes. [ViewFocusBinding] defers the + // matching `focusout` the same way. + // https://github.com/flutter/flutter/issues/189744 + if (isIosSafari) { + _pendingBlurConnectionCloseTimer?.cancel(); + _pendingBlurConnectionCloseTimer = Timer(kTransientBlurSettleDelay, () { + _pendingBlurConnectionCloseTimer = null; + if (domDocument.activeElement == activeDomElement) { + // The input refocused: this was the transient mid-gesture blur. + return; + } + if (_documentVisibilityState == 'hidden') { + // The page was backgrounded (e.g. a tab switch) after the blur was + // scheduled; keep the connection alive, matching the branch above. + return; + } + textEditing.sendTextConnectionClosedToFrameworkIfAny(); + }); + return; + } textEditing.sendTextConnectionClosedToFrameworkIfAny(); } else if (_viewForElement(willGainFocusElement) == activeDomElementView) { // If the focus stays within the same FlutterView, ensure the focus stays @@ -2754,6 +2793,16 @@ class HybridTextEditing { /// Also used to define if a keyboard is needed. bool isEditing = false; + /// Whether [element] is the DOM element currently receiving text input. + /// + /// [ViewFocusBinding] uses this to recognize a `focusout` that originated + /// from the active text-editing element. + /// + /// Prefer this over matching on [textEditingClass]. That class is + /// not guaranteed to be applied by all strategies. + bool isActiveTextEditingElement(DomElement? element) => + isEditing && element != null && element == strategy.domElement; + InputConfiguration? configuration; DefaultTextEditingStrategy? debugTextEditingStrategyOverride; diff --git a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart index 9a4acd9acb675..03e10bc977373 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart @@ -24,6 +24,7 @@ void testMain() { tearDown(() { EngineSemantics.instance.semanticsEnabled = false; + endFakeTextEditing(); }); test('The view is focusable and reachable by keyboard when registered', () async { @@ -268,9 +269,177 @@ void testMain() { expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); }); + + // On iOS a native caret/selection drag transiently blurs Flutter's active + // text-editing element to (relatedTarget == null) while the document + // still has focus, and WebKit refocuses it a frame later. The view-unfocused + // report is deferred so that refocus cancels it. + // Regression test for https://github.com/flutter/flutter/issues/189744 + test('drops the deferred view-unfocused report when the editing input ' + 'refocuses on iOS', () async { + final EngineFlutterView view = createAndRegisterView(dispatcher); + final DomHTMLInputElement input = createDomHTMLInputElement(); + view.dom.rootElement.append(input); + input.focusWithoutScroll(); + beginFakeTextEditing(input); + dispatchedViewFocusEvents.clear(); + + debugEmulateIosSafari = true; + debugViewFocusDocumentHasFocusOverride = true; + try { + // The null-relatedTarget focusout schedules the deferred report; the + // immediate refocus, as WebKit does mid-drag, cancels it. + input.blur(); + input.focusWithoutScroll(); + await Future.delayed(const Duration(milliseconds: 150)); + expect(dispatchedViewFocusEvents, isEmpty); + } finally { + debugEmulateIosSafari = false; + debugViewFocusDocumentHasFocusOverride = null; + } + }); + + // A genuine blur (Done button, tap-away) never refocuses, so the deferred + // report must still fire, carrying the right view and direction. + test('reports the view unfocused on iOS when the editing input does not ' + 'refocus', () async { + final EngineFlutterView view = createAndRegisterView(dispatcher); + final DomHTMLInputElement input = createDomHTMLInputElement(); + view.dom.rootElement.append(input); + input.focusWithoutScroll(); + beginFakeTextEditing(input); + dispatchedViewFocusEvents.clear(); + + debugEmulateIosSafari = true; + debugViewFocusDocumentHasFocusOverride = true; + try { + input.blur(); + await Future.delayed(const Duration(milliseconds: 150)); + final Iterable unfocused = dispatchedViewFocusEvents.where( + (ui.ViewFocusEvent e) => e.state == ui.ViewFocusState.unfocused, + ); + expect(unfocused, hasLength(1)); + expect(unfocused.single.viewId, view.viewId); + expect(unfocused.single.direction, ui.ViewFocusDirection.undefined); + } finally { + debugEmulateIosSafari = false; + debugViewFocusDocumentHasFocusOverride = null; + } + }); + + // The deferral is scoped to Flutter's text-editing element. A null-target + // focusout from any other element must report immediately, so a later + // refocus cannot erase a real focus loss. + test('reports immediately for a non-text-editing element on iOS', () { + final EngineFlutterView view = createAndRegisterView(dispatcher); + final DomElement other = createDomElement('input'); + view.dom.rootElement.append(other); + other.focusWithoutScroll(); + dispatchedViewFocusEvents.clear(); + + debugEmulateIosSafari = true; + // Report the document as focused so the only condition failing is that + // `other` is not the active editing element. Without this the test could + // pass because the headless browser reported the document unfocused, + // which is a different branch than the one under test. + debugViewFocusDocumentHasFocusOverride = true; + try { + other.blur(); + // Not deferred: the unfocused event is present synchronously. + expect( + dispatchedViewFocusEvents.where( + (ui.ViewFocusEvent e) => e.state == ui.ViewFocusState.unfocused, + ), + hasLength(1), + ); + } finally { + debugEmulateIosSafari = false; + debugViewFocusDocumentHasFocusOverride = null; + } + }); + + // The deferral requires the document to still have focus. When focus has + // left the document, such as a window, iframe, or app switch, the + // null-target focusout from the editing element must report immediately so + // the framework is not left believing the view is still focused. + // Regression test for https://github.com/flutter/flutter/issues/189744 + test('reports immediately when the document is not focused on iOS', () { + final EngineFlutterView view = createAndRegisterView(dispatcher); + final DomHTMLInputElement input = createDomHTMLInputElement(); + view.dom.rootElement.append(input); + input.focusWithoutScroll(); + beginFakeTextEditing(input); + dispatchedViewFocusEvents.clear(); + + debugEmulateIosSafari = true; + debugViewFocusDocumentHasFocusOverride = false; + try { + input.blur(); + // Not deferred: with the document unfocused the unfocused event is + // present synchronously. + expect( + dispatchedViewFocusEvents.where( + (ui.ViewFocusEvent e) => e.state == ui.ViewFocusState.unfocused, + ), + hasLength(1), + ); + } finally { + debugEmulateIosSafari = false; + debugViewFocusDocumentHasFocusOverride = null; + } + }); + + // The deferral must key off the engine's editing state, not the + // `flt-text-editing` class, which is not guaranteed to be applied by all + // text editing strategies. Matching on the class would leave the deferral + // dead for strategies that do not apply it. + // Regression test for https://github.com/flutter/flutter/issues/189744 + test('defers on iOS for an editing element with no flt-text-editing class', () async { + final EngineFlutterView view = createAndRegisterView(dispatcher); + final DomHTMLInputElement input = createDomHTMLInputElement(); + view.dom.rootElement.append(input); + input.focusWithoutScroll(); + beginFakeTextEditing(input); + expect( + input.classList.contains(HybridTextEditing.textEditingClass), + isFalse, + reason: 'the semantics path never applies this class', + ); + dispatchedViewFocusEvents.clear(); + + debugEmulateIosSafari = true; + debugViewFocusDocumentHasFocusOverride = true; + try { + input.blur(); + input.focusWithoutScroll(); + await Future.delayed(const Duration(milliseconds: 150)); + expect(dispatchedViewFocusEvents, isEmpty); + } finally { + debugEmulateIosSafari = false; + debugViewFocusDocumentHasFocusOverride = null; + } + }); }); } +/// Makes [element] the engine's active text-editing element, which is what +/// [HybridTextEditing.isActiveTextEditingElement] reports to [ViewFocusBinding]. +/// +/// Sets the real singleton state rather than applying +/// [HybridTextEditing.textEditingClass], so these tests exercise the same signal +/// production code reads. The class is not guaranteed to be applied by all text +/// editing strategies, so keying tests off it would not reflect the production +/// code. +void beginFakeTextEditing(DomHTMLElement element) { + textEditing.isEditing = true; + textEditing.strategy.domElement = element; +} + +void endFakeTextEditing() { + textEditing.isEditing = false; + textEditing.strategy.domElement = null; +} + EngineFlutterView createAndRegisterView(EnginePlatformDispatcher dispatcher) { final DomElement div = createDomElement('div'); final view = EngineFlutterView(dispatcher, div); diff --git a/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart b/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart index 2eb09a3ecdef2..b5a8f7b0bd825 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart @@ -760,6 +760,133 @@ Future testMain() async { spy.tearDown(); }); + // On iOS WebKit, a native caret or selection drag transiently blurs the + // hidden input with `relatedTarget == null` and refocuses it a frame later. + // The connection close is deferred so that refocus cancels it; otherwise the + // keyboard dismisses mid-drag. + // Regression test for https://github.com/flutter/flutter/issues/189744 + test('keeps the text connection open on iOS when the input refocuses after a ' + 'null-relatedTarget blur', () async { + final spy = PlatformMessagesSpy(); + spy.setUp(); + + textEditing.configuration = singlelineConfig; + + final showCompleter = Completer(); + textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); + await showCompleter.future; + expect(textEditing.isEditing, isTrue); + + final DomHTMLElement input = textEditing.strategy.domElement!; + debugEmulateIosSafari = true; + textEditing.strategy.debugDocumentHasFocusOverride = true; + try { + // The blur schedules a deferred close; the immediate refocus, as WebKit + // does mid-drag, must skip it. + input.blur(); + input.focusWithoutScroll(); + await Future.delayed(const Duration(milliseconds: 150)); + expect(connectionClosedMessages(spy), isEmpty); + expect(textEditing.isEditing, isTrue); + } finally { + debugEmulateIosSafari = false; + textEditing.strategy.debugDocumentHasFocusOverride = null; + } + + spy.tearDown(); + }); + + // The Done button and tapping away also blur with `relatedTarget == null`, + // but do not refocus, so the deferred close must still fire. + test('closes the text connection on iOS when the input is not refocused ' + 'after a null-relatedTarget blur', () async { + final spy = PlatformMessagesSpy(); + spy.setUp(); + + textEditing.configuration = singlelineConfig; + + final showCompleter = Completer(); + textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); + await showCompleter.future; + expect(textEditing.isEditing, isTrue); + + final DomHTMLElement input = textEditing.strategy.domElement!; + debugEmulateIosSafari = true; + textEditing.strategy.debugDocumentHasFocusOverride = true; + try { + input.blur(); + await Future.delayed(const Duration(milliseconds: 150)); + expect(connectionClosedMessages(spy), hasLength(1)); + } finally { + debugEmulateIosSafari = false; + textEditing.strategy.debugDocumentHasFocusOverride = null; + } + + spy.tearDown(); + }); + + // The deferral is iOS-only: elsewhere a null-relatedTarget blur closes + // immediately. + test('closes the text connection immediately off iOS on a null-relatedTarget ' + 'blur', () async { + final spy = PlatformMessagesSpy(); + spy.setUp(); + + textEditing.configuration = singlelineConfig; + + final showCompleter = Completer(); + textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); + await showCompleter.future; + expect(textEditing.isEditing, isTrue); + + textEditing.strategy.debugDocumentHasFocusOverride = true; + try { + textEditing.strategy.handleBlur(createDomEvent('Event', 'blur')); + expect(connectionClosedMessages(spy), hasLength(1)); + } finally { + textEditing.strategy.debugDocumentHasFocusOverride = null; + } + + spy.tearDown(); + }); + + // If the page is backgrounded (a tab switch) after the deferred close is + // scheduled, the connection must stay open, matching the issue 155265 policy. + test('keeps the text connection open on iOS when the page hides before the ' + 'deferred close fires', () async { + final spy = PlatformMessagesSpy(); + spy.setUp(); + + textEditing.configuration = singlelineConfig; + + final showCompleter = Completer(); + textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); + await showCompleter.future; + expect(textEditing.isEditing, isTrue); + + final DomHTMLElement input = textEditing.strategy.domElement!; + debugEmulateIosSafari = true; + textEditing.strategy.debugDocumentHasFocusOverride = true; + try { + // Blur without refocusing schedules the deferred close, then the page + // is hidden before it fires. + input.blur(); + textEditing.strategy.debugDocumentVisibilityStateOverride = 'hidden'; + await Future.delayed(const Duration(milliseconds: 150)); + expect(connectionClosedMessages(spy), isEmpty); + expect(textEditing.isEditing, isTrue); + } finally { + debugEmulateIosSafari = false; + textEditing.strategy.debugDocumentHasFocusOverride = null; + textEditing.strategy.debugDocumentVisibilityStateOverride = null; + // Restore focus so this "left blurred" scenario does not leak into the + // next test. + input.focusWithoutScroll(); + } + + spy.tearDown(); + }); + test( 'keeps focus within window/iframe when the focus moves within the flutter view in Chrome and Firefox but not Safari', () async { From 4e516d74647e9e5845405b254c260c21dbd33bd2 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Mon, 10 Aug 2026 18:41:50 +0000 Subject: [PATCH 171/330] Make Xcode workspace cleaning optional during flutter clean (#190091) This PR introduces a new `--include-xcode-workspace` flag to flutter clean which makes Xcode workspace cleaning optional, bypassing the expensive xcodebuild execution that inherently triggers Swift Package resolution over the internet. By default, it will now instantly clean local build directories without polling or cleaning Xcode. Additionally, this updates several Xcode-specific error messages across the codebase to explicitly instruct users to run `flutter clean --include-xcode-workspace` when clearing Xcode's derived data is required *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* Fixes https://github.com/flutter/flutter/issues/183946, https://github.com/flutter/flutter/issues/173940 and https://github.com/flutter/flutter/issues/127708 too *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter_tools/lib/src/commands/clean.dart | 12 ++++++- packages/flutter_tools/lib/src/ios/mac.dart | 4 +-- .../lib/src/macos/swift_package_manager.dart | 4 +-- .../commands.shard/hermetic/clean_test.dart | 35 ++++++++++++++++--- .../test/general.shard/ios/mac_test.dart | 2 +- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart index 59a8558f3b9f1..8e5ce36d8f25c 100644 --- a/packages/flutter_tools/lib/src/commands/clean.dart +++ b/packages/flutter_tools/lib/src/commands/clean.dart @@ -29,6 +29,13 @@ class CleanCommand extends FlutterCommand { 'Also clean the example directory, if one exists. ' 'Useful when developing in a package project.', ); + argParser.addFlag( + 'include-xcode-workspace', + negatable: false, + help: + 'Whether to run "xcodebuild clean" on the Xcode workspace for iOS and macOS projects. ' + "This removes build products and intermediate files from Xcode's build cache and can be slow to complete.", + ); argParser.addFlag( 'stop-gradle', negatable: false, @@ -56,7 +63,10 @@ class CleanCommand extends FlutterCommand { Future runCommand() async { final FlutterProject flutterProject = FlutterProject.current(); final Xcode? xcode = globals.xcode; - final bool cleanXcode = xcode != null && xcode.isInstalledAndMeetsVersionCheck; + final bool userWantsXcodeClean = + boolArg('include-xcode-workspace') || (argResults?.wasParsed('scheme') ?? false); + final bool cleanXcode = + xcode != null && xcode.isInstalledAndMeetsVersionCheck && userWantsXcodeClean; await _cleanProject(flutterProject, cleanXcode: cleanXcode); if (boolArg('include-example')) { diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 4d30668e5d174..9d4761dc9d96a 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -1264,7 +1264,7 @@ Future _handleIssues( } else if (modifiedPrecompiledSource) { logger.printError( '════════════════════════════════════════════════════════════════════════════════\n' - 'A precompiled file has been changed since last built. Please run "flutter clean" to clear ' + 'A precompiled file has been changed since last built. Please run "flutter clean --include-xcode-workspace" to clear ' 'the cache.\n' '════════════════════════════════════════════════════════════════════════════════', ); @@ -1532,7 +1532,7 @@ class _XCResultIssueHandlingResult { final String? missingModule; /// An issue indicates that a source file, such as a header in the Flutter framework, has - /// changed since last built. This requires "flutter clean" to resolve. + /// changed since last built. This requires "flutter clean --include-xcode-workspace" to resolve. final bool modifiedPrecompiledSource; final bool unableToFindArmDestination; 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 1339079a273be..3bef2991799ba 100644 --- a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -236,7 +236,7 @@ class SwiftPackageManager { /// If a symlink already exists and points to the correct target, creation is skipped /// to avoid potential Xcode parallel target build race conditions. /// If creation fails due to sharing violations or locks (e.g., when Xcode is open), - /// throws a descriptive [ToolExit] advising the user to close Xcode and run "flutter clean". + /// throws a descriptive [ToolExit] advising the user to close Xcode and run "flutter clean --include-xcode-workspace". void _createPluginSymlink({required Link pluginSymlink, required String packagePath}) { final FileSystemEntityType type = _fileSystem.typeSync(pluginSymlink.path, followLinks: false); var skipCreation = false; @@ -277,7 +277,7 @@ class SwiftPackageManager { throwToolExit( 'Failed to create Swift Package plugin symlink at "${pluginSymlink.path}" to "$packagePath":\n' '$e\n' - 'If Xcode is currently open, please close Xcode, run "flutter clean", and try building again.', + 'If Xcode is currently open, please close Xcode, run "flutter clean --include-xcode-workspace", and try building again.', ); } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart index 77551757d2037..49e2530de64e3 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart @@ -59,7 +59,7 @@ void main() { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean']); + await runner.run(['clean', '--include-xcode-workspace']); expect(buildDirectory, isNot(exists)); expect(projectUnderTest.dartTool, isNot(exists)); @@ -102,6 +102,31 @@ void main() { }, ); + testUsingContext( + '$CleanCommand does not clean Xcode by default', + () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(fs.currentDirectory, true); + xcodeProjectInterpreter.isInstalled = true; + xcodeProjectInterpreter.version = Version(1000, 0, 0); + final CommandRunner runner = createTestCommandRunner(CleanCommand()); + await runner.run(['clean']); + + expect(buildDirectory, isNot(exists)); + expect(projectUnderTest.dartTool, isNot(exists)); + expect(projectUnderTest.android.ephemeralDirectory, isNot(exists)); + expect(projectUnderTest.ios.ephemeralDirectory, isNot(exists)); + + // The workspaces should be empty since we didn't pass --include-xcode-workspace. + expect(xcodeProjectInterpreter.workspaces, isEmpty); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + Xcode: () => xcode, + XcodeProjectInterpreter: () => xcodeProjectInterpreter, + }, + ); + testUsingContext( '$CleanCommand does not clean the example directory by default', () async { @@ -116,7 +141,7 @@ void main() { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean']); + await runner.run(['clean', '--include-xcode-workspace']); expect(buildDirectory, isNot(exists)); @@ -159,7 +184,7 @@ void main() { xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean', '--include-example']); + await runner.run(['clean', '--include-example', '--include-xcode-workspace']); expect(buildDirectory, isNot(exists)); expect(projectUnderTest.dartTool, isNot(exists)); @@ -209,7 +234,7 @@ void main() { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean', '--include-example']); + await runner.run(['clean', '--include-example', '--include-xcode-workspace']); expect(testLogger.statusText, contains('No example app found')); }, @@ -302,7 +327,7 @@ void main() { final command = CleanCommand(verbose: true); final CommandRunner runner = createTestCommandRunner(command); - await runner.run(['clean']); + await runner.run(['clean', '--include-xcode-workspace']); expect(xcodeProjectInterpreter.workspaces, const [ CleanWorkspaceCall('/ios/Runner.xcworkspace', 'Runner', true), diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index c91ede19d924c..8b7d5cb81c352 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -653,7 +653,7 @@ duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_S expect( logger.errorText, contains( - 'A precompiled file has been changed since last built. Please run "flutter clean" to ' + 'A precompiled file has been changed since last built. Please run "flutter clean --include-xcode-workspace" to ' 'clear the cache.', ), ); From e8a5db89401fc2205423cff9b5fb9aff6bb8ef64 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Mon, 10 Aug 2026 18:43:48 +0000 Subject: [PATCH 172/330] Adjust settings header name for properties that apply to both tooltips and popups (#190053) --- .../multiple_windows/lib/app/window_settings_dialog.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/multiple_windows/lib/app/window_settings_dialog.dart b/examples/multiple_windows/lib/app/window_settings_dialog.dart index d32803604bf0a..84f02d3699c66 100644 --- a/examples/multiple_windows/lib/app/window_settings_dialog.dart +++ b/examples/multiple_windows/lib/app/window_settings_dialog.dart @@ -103,7 +103,7 @@ class _WindowSettingsEditorState extends State<_WindowSettingsEditor> { _buildDivider(), _buildDialogEditor(), _buildDivider(), - _buildTooltipEditor(), + _buildTooltipAndPopupEditor(), ], ), ), @@ -207,10 +207,10 @@ class _WindowSettingsEditorState extends State<_WindowSettingsEditor> { ); } - Widget _buildTooltipEditor() { + Widget _buildTooltipAndPopupEditor() { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16), - title: const Text('Tooltip'), + title: const Text('Tooltips and Popups'), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 4ff8da7c3ba2e290a8bc7cce25f2dc18e6054579 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Mon, 10 Aug 2026 18:45:59 +0000 Subject: [PATCH 173/330] Never use a header bar in the Linux application template (#190120) The original intention was to use a header bar, as this would mean a Flutter application looked the most consistent with other GNOME apps. However, over time this was found to be problematic when using X11, using some tiling window managers and undesirable on other desktops that didn't use this style. This change defaults to not using a header bar and leaves it to the application author to decide if they want to change this either by editing the runner or making use of a plugin that does this for them. This seems like a more manageable solution long term. Fixes https://github.com/flutter/flutter/issues/111453 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../linux.tmpl/runner/my_application.cc.tmpl | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.cc.tmpl b/packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.cc.tmpl index 3b8611caea9fa..e1224f0b76359 100644 --- a/packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.cc.tmpl +++ b/packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.cc.tmpl @@ -1,9 +1,6 @@ #include "my_application.h" #include -#ifdef GDK_WINDOWING_X11 -#include -#endif #include "flutter/generated_plugin_registrant.h" @@ -22,37 +19,21 @@ static void first_frame_cb(MyApplication* self, FlView* view) { // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + gtk_window_set_default_size(window, 1280, 720); - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "{{projectName}}"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "{{projectName}}"); - } + // If you want to use a header bar, uncomment the following lines. + // Header bars are commonly used in GNOME based desktop environments and + // allow additional widgets to be added to the title bar. + // See https://docs.gtk.org/gtk3/class.HeaderBar.html + // GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + // gtk_widget_show(GTK_WIDGET(header_bar)); + // gtk_header_bar_set_show_close_button(header_bar, TRUE); + // gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - gtk_window_set_default_size(window, 1280, 720); + gtk_window_set_title(window, "{{projectName}}"); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments( From 72311a817bdd6da90f0a5e169423e6b3ab5d3794 Mon Sep 17 00:00:00 2001 From: Mohellebi Abdessalem <116356835+AbdeMohlbi@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:04:24 +0000 Subject: [PATCH 174/330] Remove `platforms` key from flutter daemon (#190631) Fixes #140473 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter_tools/lib/src/commands/daemon.dart | 7 +------ .../test/commands.shard/hermetic/daemon_test.dart | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index fb29bc4b12102..a05187c83d9f1 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -599,11 +599,7 @@ class DaemonDomain extends Domain { PlatformType.values.forEach(handlePlatformType); - return { - // TODO(fujino): delete this key https://github.com/flutter/flutter/issues/140473 - 'platforms': platformTypes, - 'platformTypes': platformTypesMap, - }; + return {'platformTypes': platformTypesMap}; } on Exception catch (err, stackTrace) { sendEvent('log', { 'log': 'Failed to parse project metadata', @@ -613,7 +609,6 @@ class DaemonDomain extends Domain { // On any sort of failure, fall back to Android and iOS for backwards // compatibility. return const { - 'platforms': ['android', 'ios'], 'platformTypes': { 'android': {'isSupported': true}, 'ios': {'isSupported': true}, diff --git a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart index 541d2272e8ab3..06aeb295a6658 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart @@ -125,7 +125,6 @@ void main() { expect(response.data['id'], 0); expect(response.data['result'], isNotEmpty); expect(response.data['result']! as Map, const { - 'platforms': ['macos', 'windows'], 'platformTypes': >{ 'web': { 'isSupported': false, From 32384666b443e8609e3655665f2d1fd8726bfd02 Mon Sep 17 00:00:00 2001 From: Ishaq Hassan Date: Mon, 10 Aug 2026 19:41:33 +0000 Subject: [PATCH 175/330] fix: forward --build-name and --build-number to desktop version.json (#190130) On desktop, `flutter build linux --build-name=4.5.6` produces a `flutter_assets/version.json` that still shows the pubspec default (`1.0.0`) instead of the value passed on the command line. The same happens with `--build-number`. That `version.json` (read by `package_info_plus`) is generated by the assemble target's `getVersionInfo`, which already honors the `BuildName` and `BuildNumber` defines. The break is upstream in the desktop build pipeline: those defines never reach `flutter assemble`. `BuildInfo.toEnvironmentConfig()`, which produces the environment map written into the generated CMake config, leaves the build name and number out, and `tool_backend.dart` does not forward them when it invokes assemble. Android and web are unaffected because they go through `toBuildSystemEnvironment()`, which already includes both values. This forwards the two values through the desktop path, following the same pattern already used for split-debug-info and tree-shake-icons. `toEnvironmentConfig()` now emits `BUILD_NAME` and `BUILD_NUMBER` via the existing null-aware map entries, and `tool_backend.dart` reads them and adds `-dBuildName` and `-dBuildNumber` to the assemble invocation next to the other optional defines. Because the map entries are null-aware, the keys are simply absent when the flags are not supplied, so default behavior is unchanged. No change is needed in `linux.dart`, which already applies these defines when they are present. Reproduced by the issue triager and reconfirmed by another user on 3.29.2. I added a `toEnvironmentConfig` unit test in `build_info_test.dart` that asserts the new keys appear when a build name and number are set. The existing encoding test keeps passing because null values stay omitted. Fixes https://github.com/flutter/flutter/issues/152236 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md Co-authored-by: Ben Konyi --- packages/flutter_tools/bin/tool_backend.dart | 4 ++++ packages/flutter_tools/lib/src/build_info.dart | 2 ++ .../test/general.shard/build_info_test.dart | 14 ++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/flutter_tools/bin/tool_backend.dart b/packages/flutter_tools/bin/tool_backend.dart index fb62e172c07a8..c2173d660bb4f 100644 --- a/packages/flutter_tools/bin/tool_backend.dart +++ b/packages/flutter_tools/bin/tool_backend.dart @@ -26,6 +26,8 @@ Future main(List arguments) async { final String? localEngineHost = Platform.environment['LOCAL_ENGINE_HOST']; final String? projectDirectory = Platform.environment['PROJECT_DIR']; final String? splitDebugInfo = Platform.environment['SPLIT_DEBUG_INFO']; + final String? buildName = Platform.environment['BUILD_NAME']; + final String? buildNumber = Platform.environment['BUILD_NUMBER']; final trackWidgetCreation = Platform.environment['TRACK_WIDGET_CREATION'] == 'true'; final treeShakeIcons = Platform.environment['TREE_SHAKE_ICONS'] == 'true'; final verbose = Platform.environment['VERBOSE_SCRIPT_LOGGING'] == 'true'; @@ -98,6 +100,8 @@ or if (codeSizeDirectory != null) '-dCodeSizeDirectory=$codeSizeDirectory', if (flavor != null && flavor.isNotEmpty) '-dFlavor=$flavor', if (splitDebugInfo != null) '-dSplitDebugInfo=$splitDebugInfo', + if (buildName != null) '-dBuildName=$buildName', + if (buildNumber != null) '-dBuildNumber=$buildNumber', if (dartDefines != null) '--DartDefines=$dartDefines', if (extraGenSnapshotOptions != null) '--ExtraGenSnapshotOptions=$extraGenSnapshotOptions', if (frontendServerStarterPath != null) '-dFrontendServerStarterPath=$frontendServerStarterPath', diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 6f2b277cb12c6..4645f89be70f2 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -417,6 +417,8 @@ class BuildInfo { 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions.join(','), if (extraGenSnapshotOptions.isNotEmpty) 'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions.join(','), + 'BUILD_NAME': ?buildName, + 'BUILD_NUMBER': ?buildNumber, 'SPLIT_DEBUG_INFO': ?splitDebugInfoPath, 'TRACK_WIDGET_CREATION': trackWidgetCreation.toString(), 'TREE_SHAKE_ICONS': treeShakeIcons.toString(), diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart index 311dec93df998..311463af08223 100644 --- a/packages/flutter_tools/test/general.shard/build_info_test.dart +++ b/packages/flutter_tools/test/general.shard/build_info_test.dart @@ -292,6 +292,20 @@ void main() { }); }); + testWithoutContext('toEnvironmentConfig includes build name and build number', () { + const buildInfo = BuildInfo( + BuildMode.release, + null, + buildName: '4.5.6', + buildNumber: '7', + treeShakeIcons: false, + packageConfigPath: 'foo/.dart_tool/package_config.json', + ); + + expect(buildInfo.toEnvironmentConfig()['BUILD_NAME'], '4.5.6'); + expect(buildInfo.toEnvironmentConfig()['BUILD_NUMBER'], '7'); + }); + testWithoutContext('toGradleConfig encoding of standard values', () { const buildInfo = BuildInfo( BuildMode.debug, From d2dcd72652afd8c5dccd43ccf7ce2e00b977d5f7 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 10 Aug 2026 20:38:58 +0000 Subject: [PATCH 176/330] iOS: Add golden coverage for rounded superellipse clips (#190826) `pushClipRSuperellipse` on a platform view had no golden coverage: the scenario app has goldens for other clips such as clip rect, clip rrect and clip path in several combinations, and but none for a rounded superellipse clip. A regression to circular corners would have gone unnoticed. iOS renders this clip by setting `CALayer.cornerCurve` to `kCACornerCurveContinuous`. Adds `PlatformViewClipRSuperellipseScenario` and its multiple-clips variant, the two `GoldenPlatformViewTests` subclasses, and goldens. Unlike the rrect scnarios, the radii are uniform on purpose. `cornerCurve` only applies when the clip collapses to a single `cornerRadius`, so asymmetric radii fall back to a mask path where an rrect and a superellipse render identically. I checked manually, and copying the rrect geometry here produced a golden byte-identical to the rrect one. Part of: https://github.com/flutter/flutter/issues/112232 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Scenarios.xcodeproj/project.pbxproj | 8 +++ .../ios/Scenarios/Scenarios/SceneDelegate.m | 2 + .../ScenariosUITests/PlatformViewUITests.m | 36 +++++++++++++ ...one SE (3rd generation)_26.2_simulator.png | Bin 0 -> 20739 bytes ...one SE (3rd generation)_26.2_simulator.png | Bin 0 -> 19162 bytes .../lib/src/platform_view.dart | 50 ++++++++++++++++++ .../ios_scenario_app/lib/src/scenarios.dart | 4 ++ 7 files changed, 100 insertions(+) create mode 100644 engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png create mode 100644 engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index e635155cb93bc..5edf562775fe1 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ ACA6436FAE2CC58E937F6474 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 128C0818EE2178D3BA3B0F59 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; A4BAEBB40F17121B1C245691 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = FDCFFA56528B863D5A009BB6 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; AE63A2E21A061413502E8802 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = E94D88641E8692D6CA754665 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 88F7CFED6991CDC3FA1372E6 /* golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 74D42C7F457635DB1EC6D034 /* golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + BD5589685904671FA2FF24D4 /* golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = EA7AA97891F556E64D056229 /* golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; C6A83A6354E040BD4CD665C6 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = A908293A2041E5C40AF4375C /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; 9910D126B2261FAAB97A44E7 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F2029E1BBD358EE9D69051C6 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; 6BE1A09C122A7646184D351E /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C12F613F8121176E6B4E3 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; @@ -249,6 +251,8 @@ 128C0818EE2178D3BA3B0F59 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; FDCFFA56528B863D5A009BB6 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; E94D88641E8692D6CA754665 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 74D42C7F457635DB1EC6D034 /* golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + EA7AA97891F556E64D056229 /* golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; A908293A2041E5C40AF4375C /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; F2029E1BBD358EE9D69051C6 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; 4F2C12F613F8121176E6B4E3 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; @@ -442,6 +446,8 @@ 128C0818EE2178D3BA3B0F59 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, FDCFFA56528B863D5A009BB6 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, E94D88641E8692D6CA754665 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 74D42C7F457635DB1EC6D034 /* golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + EA7AA97891F556E64D056229 /* golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, A908293A2041E5C40AF4375C /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, F2029E1BBD358EE9D69051C6 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, 4F2C12F613F8121176E6B4E3 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, @@ -648,6 +654,8 @@ ACA6436FAE2CC58E937F6474 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, A4BAEBB40F17121B1C245691 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, AE63A2E21A061413502E8802 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 88F7CFED6991CDC3FA1372E6 /* golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + BD5589685904671FA2FF24D4 /* golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, C6A83A6354E040BD4CD665C6 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, 9910D126B2261FAAB97A44E7 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, 6BE1A09C122A7646184D351E /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m index 4631e79a4dabe..feb0116a40302 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/SceneDelegate.m @@ -64,6 +64,8 @@ - (void)scene:(UIScene*)scene @"--locale-initialization", @"--non-full-screen-flutter-view-platform-view", @"--platform-view", + @"--platform-view-clip-rsuperellipse", + @"--platform-view-clip-rsuperellipse-multiple-clips", @"--platform-view-clippath", @"--platform-view-clippath-multiple-clips", @"--platform-view-clippath-with-transform", diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m index e1c4532107e43..be9e9796ecc44 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m @@ -213,6 +213,42 @@ - (void)testPlatformView { @end +@interface PlatformViewMutationClipRSuperellipseTests : GoldenPlatformViewTests + +@end + +@implementation PlatformViewMutationClipRSuperellipseTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + GoldenTestManager* manager = + [[GoldenTestManager alloc] initWithLaunchArg:@"--platform-view-clip-rsuperellipse"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkPlatformViewGolden]; +} + +@end + +@interface PlatformViewMutationClipRSuperellipseMultipleClipsTests : GoldenPlatformViewTests + +@end + +@implementation PlatformViewMutationClipRSuperellipseMultipleClipsTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + GoldenTestManager* manager = [[GoldenTestManager alloc] + initWithLaunchArg:@"--platform-view-clip-rsuperellipse-multiple-clips"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkPlatformViewGolden]; +} + +@end + @interface PlatformViewMutationLargeClipRRectTests : GoldenPlatformViewTests @end diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clip_rsuperellipse_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000000000000000000000000000000..8f7a1b805fb2e3c8748d0b88a5969aa7d90dd901 GIT binary patch literal 20739 zcmeHvXH-+$)^-R%niN4q+5tsCYADiM6cv!Fi1a4XAt1d5aul#2s5A{tr6@`h5ReiO zJoKhCL5fI|8d_+9TYoec~iV&I+*LQTaC zA=hLCA6Y6k2sOA;fqzJ>8w7TE4}oZaE5yXn*U1U#=j!bbzED4nfzX008~C`sfc~>4 zbmay0KksQ2KpEt`iI%P|xc=3;g4ONc=m5bWiN^iv5|KYdt31>BP#!%qtwR`K^xKW%PcETHA>>m;BcB`qa=T7yAA zKtRp+mb1#WiZgMxZ#g+T1bF(N*7$2x)!<~^ zc)I;(`#+!lXSZtb|0;(XdAce_zHUHU4rGOE$fzBv;6I-IrFb>+JXK8noP4#sy~ry5 z4}t!_!hbaUQ>cGi)9dcLen zj%gVy`zjc4* z?1+aQL&R2O;EyRgV(C;i|AE;IK61Z>RO>$QaE6~&<6Ct=chl#clKJMRp!%9k9Ems(VKs&#Y_XtxrdUuV)9>tThAU_J zaNme8S-g1jdSZX8UT3rmj`;Y=4D0KytsS)6Qs$DTxjz{hvFEQhfk9Wyq#W?`D)p`% z;NYUg^j2NZZddO+?tW6luD$gR7p&zEO*M)PU4AYn8M($;xplD9W<=bK;n~_&mm(2b zpLDELFWN*z6T2ox0}0DL;j4E@dAOE=ciY5Cehq7+1jD>^t52 zQj38m6TyST*E?^TXfOg(izh?Gb!g-3>%D!1Hy zjl_lok&ZBtb_T72A`TYCFEAXHiS{Io+HW@qicR44*BGgl_W2i#+~ zTlzePY&nRub>3q`*g!3pxwe-?^ONW{s2uP2(LP~mdkz%^F05t zGFhB9L3ZW{HUHf=5d+aqU6^q}cO!Ltf1;7bN3ZV{l2#RW9hIrSTWLi>&@vpXPR&V` zU%Op;u(K&vDaFeR6I-X`Jp%EUo=Z>E6KZ_o7Jk-hhv?)Lr>;J+ZcUwZ zEWq+_FFoJ7Z#24vX7LO^ShRCRqK32f6Gg1*50-N$h+6?OcZ8Og$8ZrixKzAzL*#|e z^rs4Nk9#V-GG+=d*mCu5{d{2hZYw$=w~<9%eV3X=V!=u^lDMYqS1ncXW=r!_VC;-K zF2qPjE=|E}r0TY#lCz1IONAgJ3gJL?HBZ{2P%&69rLOYbV*KEs&y~+;^)SO&O+7)> zQJMaejr9Bp>J>g6DasK{jZdOhsoLwdZ8?c?T^Zlyo!Xyk#R}ub)er8A%qEd`@E?x8 zzTcT1;{FE76MV2R0KePovvA$xnWLE*uD*$_TPAdhM;kwX%}#T2;?x!5k_+R7z$Gho zL+8Eyxe3-cP4g0ytNL$r%*?)bXUJV>+iUI}l1T47sHv}U*xNkc{Zw?DH$HFk(sN=i z?`&s|&tCuLyvts9o|w1nY5~TVWU5AYUQGfQ+@h-B`EB)Ac0$Z8lS?8Br@%a~f_c6% z?;5GBD4JhE*`hk8EVqnAshjLs^e?+|@+$lXkSWNBySu@TsX`bdfKc zv_)q4;UTPs^E(+6AxkM%YzGs+qz}@NBO49WX4@|GuhWih$QYAS1T~lsB4_11Nb7KK zsYrhg1?BLnh~<$>T3+8rt@l=A`B}7)G48#UD0_yF>+RWP&P==WrmZgGW-DQhwVmb-_1q~`5`;a zR^`S6Lc!i`ZS5AX%CW8Y2@w*t15G^G1(c@_fXp!(-ej&PZ&H{>zJH(C8>|F+^=1|^ zh)F!r_@T>=Fw(w0cm6|Yu8-$zw`wG+L$aoRBO^CKtyDVvu1v|Cc#+apN~PNNR;4Bn zGtbd*g?#yr6h6AShj%+#3C?__KNjukUspg23)T5|TeByBFZJQ;YksuR7%)OjsNY*? zs$b(#z3`L%5v_*mFN}}jc9BhAhf#FGv5o}V2-cBUKmX8BDI!Z}z!2Y2Gc~4>(i`GU zc%z`v+b!!e9YaMnga&GsbcR@dNwR)FAZ!v)s98TeD$rk_lZapSjbGU5bln~ZI ziH`t|-Mc1HTAz=){D`soa@Qz2MMC+w;Vn1M=dt{#9V}tR4{G3OHoQcqe=q8++;2;h zKL>oIZkeBuvR3-7cutj=s?-X(L0ipnUhxb=}fEZq7rMtU3GR)z* zf|q4o{Ff@axLCwDnj3|d0GdgU-5R~_woD1t9if8K)7_JGw3_y*f5ht8*e2shkPQ`K zO=x&3q;~C=b_sXn6N&mB@>~f*}V3QJ8hxpCCvp4 z{ahE&b%tVJZ-9a_)bXr9dq10^b@B1;jR2IsqyauT3QR;aU8O7tXFGIG$k@x|jByy) z8=UhZwi9mGUF$*wBt1pklbuA#^{8#c5xR1&2l@#a^MEP}@slX*3cm@@mAV`;)}mgd zyZcCOh~HbttU(ka8Tv{;*(cQ#I2o0(XZ_l)XtD_y>j;dh<961jzE^s~%afa7bZ4Z1 zJU^INu_Ch5Cnxlbsjt|aflIq|r>lo;@hbTUgwR7^bl1fM+OLbrU$5EP+VXkpts|!; zUy=zyx6?F|*vcy^G}b<|gVXt4xVR)etBwUCiOUP$v~5#Q7{mSM1+Nf6{4j$?1Yzi3 zLrCt?BsKwry%IWaGIMiO2m0`YgEIpj| z3o&#_O>{=lTLZ6)Q3y6D>ilu3jRzd!=?~1!yNDL9gnNI0Reuki3&DC0m)`vDY7o`%3)X1J8CdrP@8AK2ao<2BiQX9T zD6R{f|C;+p#TN_&{e}K+hSXAM_F~f@=(YfKTTqV5ISdLs%iR3E=232A+U*i#7G>yzB1Dh~G@3f$>o1Cka%3BQF znujEbjwK>Vw|ZaL=rB~i$;s+;v-rgtJDEf|w6&ZL?{#n<0CWUWT@|J@1Ms7$gkmLh ztQ6o-HU-K)NjWP0lwqD7K5Xh8G|}fQtH=T ziT%Dl8BSWWk9Gq!vLpPN;hrq}B5r^_t{CAFHm)JQ_$te!q^SL){8X7#;nOQPq5Z8&z$GjP^5e-VLN$i23mucxps2%L{*;H+^Oc@ z{~ac5(4{gvV>0I~x%`3a{JryusvB(?FDv&)^S|Cd7{1S0CfDN8_t6$OBE}bii}xy5 z5?uwm@Z*`RW>!VX_MM#fR+b|hTn|fEdxnd?zUM4uCzSehHeUcv%o-bDvGhh3Sx*|F zE8@q4Wt_%448yq=sqS)S9JG55ewd^)m~ZWV?Ag3Wg9ly^T0o#Zn1|I>0__8FZXO8n zzQ?r=d-guVN2$(v;rH&J~Y4Y!mPcc{)LF%ui- z4DeRMf`Y7ti25*xORBWgB999YRVt!(CDlcK`X3dRi|MqM>(blFYQTV@P00T2Nkw$6 zGGi(uK9h^Rhiyf8boZ_2*Yc$weHQs5nYVdFsh|NMnC%Y;IU3*a13Gy_$}iO=wd>^m z@Sg4WFiWHFNneSD!^)NPMYwBRd}tk5c*Aksmq!y37WQ8^!#{bP?vmjYxjh^=E_xSk zR056&`+g8Ky`c|4*sIY9_q5Lyw_A!N%z4DOc|^lYj

j^Uzlr0;7t8*@~Kew-)4V zRaM#eSiEvTTxd4OST*@K=5RYIIG>MdxCpAS3*iFjj~}O_m7bO8!Ei6`S4d|_i?%p3 zU^&l;AYRl*o$a%0a)jc=Si2LRm%aMptIr>3qKE5GS2QX1R-Za;OajjF!B8t*BImCf z+k6KNGj8*qaU)EMi{vtO*83QcIur!PEy(7Eu)jdFK0B_t!Q_*PTYWGo@k6@Hr;L{o zSS|yoLG$UHh;%n}QX}~GxMp67$us9?1<{~24e$_ux5u!b)JBivgzhb(u-Q%YiTJJ~ z*@ETr=;NUD_&a3xZMyP+TV*MqW%{|}W%ZTyN6BmS02DsRKX*nt6B;s3h!`?>OCXzXq?EO-R3VI*JhxhBL^y>|2LN(3YBQIqzBMV|Vuv)6Q2?Na-6B3LYa0YHW zY-#p)p^}NE&mRi%1}(?%=JB&ikZBzRpb8)$&YTi|+*7OKf#jJODkwAaKrUV)uhk1E zh;^W64CvxsBdoOD78=*WKkbDV+QQ~YgDzfi4-gq(1p;ocHl325T2QNWjA4&VE9GMD z{KgM>af=4;&L@w`1*B0>FTUNRkg087{K1vFji>9gxp*8u$r4L7dcGa zbjYE&@W-cVK70(*d>H{sZ>zc4UR8_5eqwzi4!SF9caY9!(8pvhbc$yxz9O&tV@%Z9 zM9V+ADv2 zc6`esjZkm8snBXAY}#)q2!*k+vpTVfSIrM*Ay#fwaGdyK`^pWvTrTuyAoFJiWHnyI z*geBK^3L0qW69wS*Atv%gbxWS{cxr@f!pZXnbOZRV(F;y{%a0;fU3~B0;0vYqrz6H z&`AbiJt1h3{o9MWJpU$~Kqb$O8&B1W3o>rN=yHU91i4z`6~2dpNM7vSmHFw(c5e4e zQ3y)_Lh#(`FhkvIPmWh(#YK%Gn-bg}R%86OV7el|nd)}pE0DnrGSR-eFUzXgi{iiyeL+WAa8AkY?- z$27RobU%>gI5t8he)H=+L@3Y<8iO0F8S;{32zIl4i3l=_W->vZ2&^li&XcfTfzcfm z5@t10V%DA zfHpy2fpzm1=9sp7WsdCYwD&0n0T>=IXCf$lg)EYNRou@ukt~UdM}sgi*0X#;`J~7C zi^K`d>R=xoJ|k8gu&0UcpZHecWi{#LiLk$jF5=YoeCm1!NacS&WxSmJI#}cDv;D;b zDUw$EH<#xo9Uj3N)gybVKXnQ`BV-Qq23DWXEO`L-XHFnpqxap7xgPJ4@_aG_CjAsK z|BX>lUUzMmP>&xd>C08}^AkqAkOiX2zD}(KnWCvrcWCXf-A1s>l0G->ZY*#Gz?lZl z4`qrpt_&KR$Z|np?%>e8F37#ITjS6a<6cqDgmxecV5Hx3{uj{i9E9nQW+O=c7? zKBqd9To}ChMZ2ateb|^Q4wf?pUf)UqTzcIM+_i0rE>sZeWvT4QGr$E%CD<-8FH@@kI>M> ztC@)@cgXXmtB?vw_}t$EF@T~ZGmsGjmB_KZUM&_=GDmq-%%?(JbLCo|XHyVKVd>@_ z1`A!VuMxr6k&hY^_`_E}+0+DbjCURZ`}A2ploaD@h)8X^TFD}h9pPppB+oDkK?Rfr z8EnW?4O+3T30!W#53-(2ZulrSJegB}=KxJ2%#`>&YVReJzkM_^5-Mx9%faOvQUrvlMGTI=&Rsx&xX}j?UwausDku? zk7u63*nBi13$YX5mZH0xbDeqTV2NNy{5eK4Sfd9XsSdEt&+^t&oVLL)%gX#F8>5@# z*Ll&eps$(dmgUM)IvyZ7Z3qWOnt|Ics0oAeZ1Hp|_GdmoW6nZKA?wpGwPH_5m`ekF zU;^F>LM=oY-SQj*2S-O5PL7ErpjNgg<`z@9t3KylwqF9)V>uFY)^F+Cdms6}&G|l$ z;bJTsr&6SpYJ>#tS)#oBEOQu^(8;Mm#3^`%tF5V|X;482=+Di1H!n!ImkwYf2#qub zW;Ou>{rM&yUtb#1Z8B`8w>8cTPagkK`(=g3bYY(ORfN=}sky@*EeyQY0AL3J$R@83 zVcRQ@og3eCxj8Ed_i%9h(nt;pa!PbjyoRAyp7qwi^W#WsEDvwGlD+6sU}br4X6~#$ zi6kc)6sHpNEK3*O5pLn$KY#4Z{WCFd_2V_A-vnEN?brp#yMS|2los;0c#yVmX%ubf zhW_@omEDxiKWjmqQOUaR_^w4m%a%q&PvovwmbmHKi4uA$Xe#h1Qlt9UTYSd#tK8hG zCgQ})J=)$DgReuM_DkK?Y+sIrjp0R5-$StD9e~pXp+i3X)Z>i1)a0y&zKKa<%^Z{4 z^nEc{qr8F0RxGug%(yyylomd!*&HxrnlvU1_E(UGINE-#BN?PZ(iFWlQ2W6KJx@SDwnPRuhg8eG_b z_4^{3bY==&>B@f1aa{7tRxb36oBl2$ww+K||4zBkVWQX+{STeSvinM4yv{|9$2j7|Y z$DZ!r%f_Fca7A|Hb@sfL>}-6Ad2egTkJ|dek3@wxHL;B2E-b!p9s~sU0XXi7-0b^G zqbO(auEwb@39AZEkb_`X;wl% zcb2AI2N8b>Xo3OYj^S~0oo?pbpKrbJb{}JI@odE0=8xv|yoes$s*SiYD5IKoPMxvE zQ_M~(CuMy0HFK&VcJt>$0ytn>FNQ;r~gI+f9ptSysr18hiH>5 zP&>$BT9i3QGk$C;+!8)~k*Vo@x=e^^hC($$)Vo`&lzzxt5qN}_>v@Adl0ozRPp~1e z{%p+e)0L`JN2g>)&;MwBA~-oXFc||SY~xsluGbV)f0^K$)dn+IWAiC@uFXjd-B?6ckWUKtTZo z1r!ueP(VS!|A2zDxd(5-uWW>qe+z&@FH?SmfI_WPP(VQe1qBooP*6ZY0R;sV6i`q= jLBanT3It5iB$lTz{`da}?N+R5 literal 0 HcmV?d00001 diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clip_rsuperellipse_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000000000000000000000000000000..435e38d9b27542b1cdd89ab9da75a8be6c44384f GIT binary patch literal 19162 zcmeHPcTiK?)(^)Z0aQZ2Do7A1N(s_Iz$i_SUZgi6bO_Qr#742;1rbGQB3%g5q=N;x zfCAD%I#Q%Vln(he-1hE#@BRCIGj9TuIXh>!wbow0)y|njYHO+-r8z+ZgTaofUQyJA z!JtJLj8XytfjdeiuXMnL!c$k}GAzH9?K}7qfHhLJ)6jsO18oHC5XCXr{zzKz5usp! z9RgPh@DB@jguxHmFqk~J!t`uBuvj-w2b>r9awz65j0#*Cz{ep4`g0_-m~!aPHl+mU zgI&^7P*nxjzh{>sEiVHa(;W4Uyo@x|Wo&S+Le{pp>sTQ_SNHuXFj+qt&~(LmS)=@1 zUEDln{Nzp_jF18C{oCl%sDmM1&T^-XG_+9)I1emJLP%Ih__RC?3WbvOu(gxXRaE|C zI`}4c+QG}qT?UQz_4O6<6&1pH*rU%&OG~4LMbIK5f?$N8r@xz*wV$AyC)e+b{JD-I z*3-tr(cQ}t=Z4x}*ZMln+e_~B>HUrV`um+uTbsZ3argFcIoQS4290&Wx?F@%rc8KRa9;_eCk=V(n%xciK{_j(jMeobT#qpo>f4}?BWo6O- zkq+5?c4f3Y9D%f~_XRG0UiLr)|G4+J;AQvulriwcdMMyr_eJ~R*oa zfac%t0;Q3skwyQdfbul8DRL)ZFbqsp@shqD#ln#DBeO|=-eu3JW(P5c+wDVEsz%g( z4Q5Vfp?kM_N2AO~r8dJRUb&bx?#UFeax(T3aYDIM9a7L3d5G?(@(*iPPaw81+2>Eo8(G=FZ6+g=JK?ylEj$EVnR zWyM4~eAb70)d#BvXr)dwyP%KEQ^`M1s1OFj-$lIG z3M}Pp^wEYMFqcgwu#=YQSd(R?_vm(PqN~TaIQpv`JN@HgLUwz7_48{V^0Tws#9P*^ zkL)dU-K3M>;Kzk-w-Q^Qb5jIuf34lxU9uFuQI?eS>vP=R_s8KiKRR~f_=v=0GzZ)K zi@v-f;>YJBLCc@QZ_5^2E0EA3Z^T>%s{AJ_ymuL;c?PS##bg)RUA=m>u_bc)V=q+~!Z)iwMDEu8S_SEH*JT(>207R)*Ga~Jrl znpZ!Gw(y)ghCb#x=;xQzJ>oYZ8ZEzdH!{m>T(L!NeaFP_nbYQKe1@sS{LU>7mn@f~ zUoz9NJN0>@XjU_Kzm@R`m1v*a8d?{tciL0)BZGx%HZXzPU1+^KX5#eNq(Ha+TGDnx zb3(j@jQ8X0dEv=8Y|E3~>3FpDTut!yslfR+8qXzMM{0tb6NglRj1y>zyS%*bG4hqy zmGstZb+p9S1{trYc|4uID#B=9`&}a(S2p%6s%~+}K6I_9H@w%ep@D}~9{4?d4Hq<# zXSp*@Ft8B#K6_$!VPyE4|NJX{&hgtwi5t^xA*De}!%2~IoCNiOA`fC=i>B*|lP#ee zAG{gY2^Ml?*Xy;h$-TbV?i_=qz>)36n(f8F#q#OurC^*^TlRH}5vNm1fWUGZKLfk` z>ub?!qfz`8h*WF8GC41CF|obcv6$)luOoX-AD@*Gf9~R>&QC`2_RRNt&RVYH@vV;x zp9rxZ%PhJbZ_k20b^OHa#w0rMt&8z_oev*%R%I)H_B6%|JR28yrgnU+`*EZ%;oNs# zmxQrTuV{s4Obcz#Fd=<%WxstYaymB$G|Ra+f44nc{s*2eXml)`BV_p<)%tutQHEs& z39RrZ1I*)Rxleb~7!dZwX5QV49DDA&-_7rIYS{UmocSSVUwx{lme?A;_f0E5zsj5W zR7Y>`&vu2mj6fG>=ao0na{iNXX!W-rKJMCQJR-Jn7?rvV_!2J5SN`azrn^|R_ycP> zQh7C#;6C+pZEbCiP`g(#mmm1;SCz$D(q467W^n4vr^bqK5E~E_tX64<2sYGoob6qZ zeNDXAR;4BpCsfq-#S@R5Kf9x>IFArYu!;xCB|Xq zfwC2Csb9HoF4{R*w>%NF-0FxwcItKa97dFFfCqw)vmmkKNw^OcjMx*(mg9%$jvgHf zT&j+~xaSw?tbc+6`phd_PdoIv_jXhZuY0G4WE_EyS=n;jP$PMXcKxs=*=w!Gs>d@ggC?(tm zH}0T=#y*WGMAUk+gU&;s^KjOaGZ-rg{39EA(q?y_(yhbl?xKnpFc{O%F$=VhVkj~I)rm#tYq-Zju~HZwL|s^8N?eGDIihW2ocfk zKD9869Q=K*Nb*)!rip2Ar-58C`z?$uFmJYpx{u-Qm=VeCd!tkr`(AhFK8N8Os3Sxk z6#3dha0`B9joeZcf4<*Bup2#478VK}I~LS)YGhVsUEbT_ilEKafKy-IAdn6|faiYR ztPn_t+RN8BbZ3GiI*0o+5~XbeIvVPp@M5eULadfSH#WMPq~(8Ggd<+@qnck`2ux(; z%d7h=Il@+h%P5{admo0^1^cI;2)~X%{=#Zh;q`rcgcQO8qU9`Q%;Rk3CV4ob%XCz3 z$-iy3J9jQvzmW@*LQSdojE5~DhWZSuy`#ee94~i!77I1xQB5lAB`yfdM?G&9qF ztfz>90-~ph5TQS2a25WN-T{XTZM-l{uW{i7Cnkjj$Wc%~Pc^LWCUq4tamlQpL_jeN zb06Hkuh-3pkAU|8muUUrZp4WW8nA-^*g-gJ671lP&&{uJde>WwqJX|2B1FF2*X4r0 zWG-86=1*JDumJ|i6c{AQR7$}6o4TBMw77i4BsJ3E8W5>5Z>AfhN^R!R#I5=>*BFB5 zup+TJZwq-Tpreds%kODV`nXW>U{VGF8RgSyGT<-)LS*R?$fufMQO$;3IO6dYtNudl z)h7aHbuOs^PQ<`iN99yt3hkJrNIOLPK{qZ6C=xIpdCcM~JVA4`irDI0k-)4ThGB;* zK9fORfCGk51=s7ND2?hF;0Q%tRI{R_?os*(eaz!XuY!To!j?c~YHps=3d01$0|Eo* zJW(oOt8y@XDk0v9`W1hBXJ@P%Q3fbT02mr@(iDN@i>({4zt^Jw&;+#a0sVfWnsp0? ze?~d*^{f5xyK4}8EtZTiH8mwN=Us>3PH0qf*Ki-;(FlQm!V+pJcsorT z&@@W8JENS#c@SqmiV5S3?I`}Kb`W#EWJF?Lp3J^`kcwf{0RL*8J^Tl`Sp*PMq}!_# zrw?+pvp^@$YM2rZk~WkKfN90AP7w!L+^aJ{kHuW8LC667+zeohyct=HWCbDPjBG*3 zwh>NtG0^|9^DIv9dTl|Cl+s8^mz$eum0_7qv}awow^~|M*s~^pJ5C~aoYk}&t9|oj z{G+?PjM;s*riUD#mBZi5#hIINV!|*sAewC0x*&t!qAB*BG;b+f^ek%SHg;B-?DL-- zc*W1yl#wooTWYQ-lr;@>+U{F%u}%3!<|!SAka9S$+;18n(CE?wv1(?i4ZXu7jJ;M* z^|>YEXC>Z(>Xe*^1x=FBCa12oXdPiyS+`eN$8z1GEh;nEPlQ-O03>9x#@%iPak>jw zBqmq;>^yiiU_MZ>I_zz{v9zImpjEj zsgLm{tjJ>liv-)2VJ3^zvsI&U9D)H=a0;D<&M=G_G)DP!_~)iy#IrT{(*X%K zO~QBMShR)t?YA$sl35*`a$UV4xZ?3P3TOpU>^*h0*WSLC^F>X^M#ie_l5yhC*Ub;- zg|?E~H1CVmoMA?ykl7%KJk9>PG=>3D&Uxl(`bkZawyrd6b$*_>x)^6XvNrzP;%mNU zow#!dWP}WzwDCE5P6>Oc{zK6Bhv#MRo%_~vce^4LFE)T$lsYoUPvs&PMv7> z{w*~+)U(ImXKh^6_V&3xdS!kC@UqmiF6F3o#CNlC(Q3C6u?2 zn%J(-HZ*e+hF9KCw_<$FAWtR)+Um8Ra}PuBrYq=w+-o*v)@ey8c((nXF063AUFfaUKm0lhuT-r#1g-E zqF%?illircqYICRujPto{-jm}vAyEO>qjM0F{bvVUJ8so)$ZKkYljZbA_oa_A50-F z0PHuQtae=cy>P_RC z5d0k2>&|;-=qU5d{Eeu&u0sg>y~At=6F_l5{m_IPTOBQ2>_6qh^(pjK2lPxyC81RK^%w z?tg%}4rFuvP_Nr2JZep8l0WA@GxM<3xe22##grA$HDRA}^CKnj01T{Ig}YVhdChVQ z%3&B?(4{-k<8qerJ?~Sq9QtMI45!EQ3Me^1cJ;HlG8d@);@$2CXWDYVwpoCR1^4Ju zd+}7>R}p}vZjE%mY(fS50-)CftYbp#5&S)IR0sCWC1rpUnTOF~`m&aB9vBNw0Ifr9 z3ZQV3R@c{O#2!7R4M&K9B1ETHnyy1JRNv%aYTDkQe0WW8CV+(tgJJ+A#9VSpiv&6} zd)N2J%$Hf8Vpa;s5eUHXv*}wb(=R6lZ44%J9o^jvf|Rl&K-~tw>_IY~hJlq&lkKWm zO3qsJ&!6k-h9#Ma2;>z&m8#3=fM7C4eoJ>V>YR-d)iE|c8)K)_n3OMo{xsgvjV4+q zu3^dSaaG|#-e&!Gwye-u!0A~{^R`I_DWPV4@kgMxZ%~r_uj{4O&1h5KJ7=wW!FN%> z+CU8~HwKpbppp{yZC|Vn4|xf~v~_hg(^Q8K+x0iuLGT!myvMLrX0-4Kk8^xCRxX@e zN&ZwA{uxO49LS|*{Db3FfkQRQ@Mtz{c~j&1wKR?(z)X+QrU6!t$wpgo9NJtS`$%c$ z7+QuSPJJ$#w-Kd)vH_{tnieBi20rERBE}EcoGi1FBR|*&YC}WIopg^wOiA!UF z;A#LfkKNADs2PsV48Q*6>f7CHnTt1;Npe5h74%$hZ~&dr1ZSE=16Sih))3dTMC~j@ zALePv%Sg>g=2+N_AEtm}fdNxRxiL7r!hEUa;*V0krk!KMT5q_w6Mfa-S5PS9O9e%e z_o;kmt??5IcUbK#o{>7~LOt7(dXKQ@e=;;Jmt4;2cWW+Y8>)UI?(P3Q1~0uBlkaA4gr@F}!569@$gW7CM1*-h7^ zqyyGnL;GyS`QyRkByi40Cw<)^QN-<1_7KqJYF1kXR?U zvoWYmAw%LEcM>yfg`zJm_tFK;%{3M0>OgQ7P)juaMI{%R6<2q!&9-sNxy0{T&%(%? z$*Ki08chgU2FwZZ}cYASz=3f5VC1JOqx>}z92ExD>ji-w20_Z1d$$f zt`fDttGdXs6){nkg|l__krSkaTO_QNl@;fg*T6!E0QC|{>v7Pdy?=y>s??TOOU#>5 zkHzx-gOJ6vrGb?lbjIK;u_!zzdQRS7@ zx2+Gir{D-&iBI0C>$Zr_Zi#KUgisU$URB;|L5UC9`n;cLs~39tLeSzC-7x9cbfe}GVoImw0+efA`>Fnae!zW8xm--ajGhMj`Nl1`Me7Ge8uvN7SCdGZ+xHF34fW=C*3# zIT`z$1@4NaQBSSG6{^~~^*A*#hIC|v60oJ%n5`?qYT~TLk@hN7!AeC9NjO1Ta6Gasr$`JLd1wIM8bKgx$+5iXxc9E>eSke%y_+)vUl#5D;;B8 zDu+vD_hNy8-Tjeyap*11QtvBY5)5w#;)aCao3p&atoScu*S&mwY<>OIe| z1!NSEQ9wok83kk%kWoNJ0T~5k6p&FsMgbWG|6eG$EmyD!gHh@2{}urG6#l8l p _scenarios = { PlatformViewClipRRectScenario(view, id: _viewId++), 'platform_view_cliprrect_multiple_clips': (FlutterView view) => PlatformViewClipRRectMultipleClipsScenario(view, id: _viewId++), + 'platform_view_clip_rsuperellipse': (FlutterView view) => + PlatformViewClipRSuperellipseScenario(view, id: _viewId++), + 'platform_view_clip_rsuperellipse_multiple_clips': (FlutterView view) => + PlatformViewClipRSuperellipseMultipleClipsScenario(view, id: _viewId++), 'platform_view_large_cliprrect': (FlutterView view) => PlatformViewLargeClipRRectScenario(view, id: _viewId++), 'platform_view_large_cliprrect_multiple_clips': (FlutterView view) => From 6116d7c0daabe727c16b5e3bf2935eb8b32f2075 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 10 Aug 2026 20:39:26 +0000 Subject: [PATCH 177/330] iOS: Document and tidy the embedder API flag (#190828) `FLTEnableIOSEmbedderAPI` is an existing (unused) switch for the iOS embedder API migration, but we didn't document its expected behaviour where we used it. This documents its semantics in `FlutterDartProject`: it's opt-in while the migration is in progress, a YES value currently only has the effect of populating `FlutterEngine`'s `FlutterEngineProcTable`. Once the embedder fully supports the embedder API and we've tested for a sufficient period, this will become opt-out and eventually be removed. This also replaces the `NSLog` banner we emitted when the flag is on with `FlutterLogger`, so the message goes through the same logging path as the rest of the embedder and respects the configured log level. Also drops two `settings.enable_software_rendering = true` assignments from `testCanEnableDisableEmbedderAPIThroughInfoPlist`. Those were the last references to that setting anywhere in the iOS embedder; nothing has read it on iOS since the software renderer fallback was removed in flutter/flutter#190590. Issue: https://github.com/flutter/flutter/issues/112232 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../darwin/ios/framework/Source/FlutterDartProject.mm | 9 ++++++++- .../darwin/ios/framework/Source/FlutterEngine.mm | 2 +- .../darwin/ios/framework/Source/FlutterEngineTest.mm | 2 -- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 2ed1a6ec4a3b6..387b4f9139120 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -267,7 +267,14 @@ static BOOL DoesHardwareSupportWideGamut() { CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height * scale; settings.resource_cache_max_bytes_threshold = screenWidth * screenHeight * 12 * 4; - // Whether to enable ios embedder api. + // Whether to run the iOS embedder on top of the embedder API rather than `Shell` directly. + // + // This is an opt-in flag while we add support for running on top of the embedder API. Once the + // embedder API implementation reaches parity with the default implementation directly on `Shell` + // and friends, we'll eventually make this default and support opt-out, before being removed + // altogether. + // + // See: https://github.com/flutter/flutter/issues/112232 NSNumber* enable_embedder_api = [mainBundle objectForInfoDictionaryKey:@"FLTEnableIOSEmbedderAPI"]; // Change the default only if the option is present. diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 6c7edcbff0a9a..23c8b99212eaf 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -248,7 +248,7 @@ - (instancetype)initWithName:(NSString*)labelPrefix _enableEmbedderAPI = _dartProject.settings.enable_embedder_api; if (_enableEmbedderAPI) { - NSLog(@"============== iOS: enable_embedder_api is on =============="); + [FlutterLogger logInfo:@"Embedder API enabled."]; _embedderAPI.struct_size = sizeof(FlutterEngineProcTable); FlutterEngineGetProcAddresses(&_embedderAPI); } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 6bf47196d00e3..be708273b589c 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -375,7 +375,6 @@ - (void)testCanEnableDisableEmbedderAPIThroughInfoPlist { { // Not enable embedder API by default auto settings = FLTDefaultSettingsForBundle(); - settings.enable_software_rendering = true; FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; XCTAssertFalse(engine.enableEmbedderAPI); @@ -386,7 +385,6 @@ - (void)testCanEnableDisableEmbedderAPIThroughInfoPlist { OCMStub([mockMainBundle objectForInfoDictionaryKey:@"FLTEnableIOSEmbedderAPI"]) .andReturn(@"YES"); auto settings = FLTDefaultSettingsForBundle(); - settings.enable_software_rendering = true; FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; XCTAssertTrue(engine.enableEmbedderAPI); From a57db01c46f89e43eef527ce656d9416d8394f05 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 10 Aug 2026 21:19:45 +0000 Subject: [PATCH 178/330] run_ios_tests: Allow running from any directory (#190822) Previously `run_ios_tests.sh` only worked when invoked from `engine/src/flutter` which, to be fair, is what the READMEs tell you to do but there's no reason we need to force that, and we don't for other tests like run_tests.py. This is mostly just post-monorepo-merge cleanup. Two separate things depended on the working directory: * The wrapper was resolving `SCRIPT_DIR` from `BASH_SOURCE` but still passed the Dart entrypoint as a relative path, so the VM would report `No such file or directory`. * `run_ios_tests.dart` called `Engine.tryFindWithin()`, which defaults to the current directory and only walks upward. From the repo root `engine/src` is below the starting point, so the search fails and the script exits with `Must be run from within the engine repository.` Both now resolve from the script's own location, so the documented invocation keeps working and but invoking from any other directory works too. The Dart side matches the existing usage in `tools/header_guard_check/lib/header_guard_check.dart:162`. Also fixed up the READMEs, which had a few other issues... one if which was that I forgot to update them in #190818. No test changes because this *is* test changes. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- docs/engine/testing/Testing-the-engine.md | 2 +- .../ios_scenario_app/bin/run_ios_tests.dart | 4 +++- .../testing/ios_scenario_app/ios/README.md | 19 +++++++++++-------- .../ios/Scenarios/ScenariosUITests/README.md | 4 ++-- .../testing/ios_scenario_app/run_ios_tests.sh | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/engine/testing/Testing-the-engine.md b/docs/engine/testing/Testing-the-engine.md index 6715e963ef317..0bb5479588dc3 100644 --- a/docs/engine/testing/Testing-the-engine.md +++ b/docs/engine/testing/Testing-the-engine.md @@ -295,7 +295,7 @@ than using the real Flutter framework at `flutter/flutter`. The end-to-end test can be executed by running: ```sh -testing/ios_scenario_app/run_ios_tests.sh +engine/src/flutter/testing/ios_scenario_app/run_ios_tests.sh ``` Additional end-to-end instrumented tests can be added to [`testing/ios_scenario_app/ios/Scenarios/ScenariosTests`](/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosTests/). diff --git a/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart b/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart index 8b0309385990e..1e3243a269ebe 100644 --- a/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart +++ b/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart @@ -18,7 +18,9 @@ void main(List args) async { return; } - final Engine? engine = Engine.tryFindWithin(); + // Search from this script's own location rather than the current directory, + // so the script can be run from anywhere. + final Engine? engine = Engine.tryFindWithin(path.dirname(path.fromUri(io.Platform.script))); if (engine == null) { io.stderr.writeln('Must be run from within the engine repository.'); io.exitCode = 1; diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/README.md b/engine/src/flutter/testing/ios_scenario_app/ios/README.md index 2c68562ddf0e7..46cd9de1834e6 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/README.md +++ b/engine/src/flutter/testing/ios_scenario_app/ios/README.md @@ -8,17 +8,18 @@ For example, after building `ios_debug_sim_unopt` (to run on Intel Macs) or `ios run: ```sh -# From the root of the engine repository -$ ./testing/ios_scenario_app/run_ios_tests.sh ios_debug_sim_unopt +$ engine/src/flutter/testing/ios_scenario_app/run_ios_tests.sh ios_debug_sim_unopt ``` or: ```sh -# From the root of the engine repository -$ ./testing/ios_scenario_app/run_ios_tests.sh ios_debug_sim_unopt_arm64 +$ engine/src/flutter/testing/ios_scenario_app/run_ios_tests.sh ios_debug_sim_unopt_arm64 ``` +The paths above are relative to the root of the checkout, but the script +resolves everything from its own location, so it can be run from any directory. + To run or debug in Xcode, open the xcodeproj file located in `/ios_debug_sim_unopt/ios_scenario_app/Scenarios/Scenarios.xcodeproj`. @@ -29,10 +30,12 @@ grep for `run_ios_tests.sh`. ## iOS Platform View Tests -For PlatformView tests on iOS, edit the dictionaries in -[AppDelegate.m](Scenarios/Scenarios/AppDelegate.m) and -[GoldenTestManager.m](Scenarios/ScenariosUITests/GoldenTestManager.m) so that -the correct golden image can be found. Also, add a +For PlatformView tests on iOS, register the scenario in +[scenarios.dart](../lib/src/scenarios.dart) and add its launch argument to +`scenarioArguments` in +[SceneDelegate.m](Scenarios/Scenarios/SceneDelegate.m). The golden identifier +is derived from the launch argument -- drop the leading `--` and swap `-` for +`_` -- so it does not need to be registered anywhere else. Also, add a [GoldenPlatformViewTests](Scenarios/ScenariosUITests/GoldenPlatformViewTests.h) in [PlatformViewUITests.m](Scenarios/ScenariosUITests/PlatformViewUITests.m). diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md index 9e6d9a25942b0..e50ae7280fde6 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md @@ -6,8 +6,8 @@ to communicate with the app. For instance, you won't have access to the view controller or engine instances from within the test code. For this reason, the test code typically uses **launch arguments** to configure -the app (for example, use [launchArgsMap](../Scenarios/AppDelegate.m) to inform -the app which `Scenario` to load), and use UIKit UI components to collect test +the app (for example, use [scenarioArguments](../Scenarios/SceneDelegate.m) to +inform the app which `Scenario` to load), and use UIKit UI components to collect test results (for example, every messsage received on the `display_data` channel adds a new `UITextField` to the app, which will be visible to the test code. See [touches_scenario.dart](../../../lib/src/touches_scenario.dart) for an example). diff --git a/engine/src/flutter/testing/ios_scenario_app/run_ios_tests.sh b/engine/src/flutter/testing/ios_scenario_app/run_ios_tests.sh index 68e1ecab652cf..dc30de34ff093 100755 --- a/engine/src/flutter/testing/ios_scenario_app/run_ios_tests.sh +++ b/engine/src/flutter/testing/ios_scenario_app/run_ios_tests.sh @@ -44,5 +44,5 @@ DART="${DART_BIN}/dart" "$DART" \ --disable-dart-dev \ - testing/ios_scenario_app/bin/run_ios_tests.dart \ + "$SCRIPT_DIR/bin/run_ios_tests.dart" \ "$@" From 380e4ebe3b8958768b59415f2563da19d41dc1e4 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 10 Aug 2026 21:23:20 +0000 Subject: [PATCH 179/330] Roll Fuchsia Linux SDK from 2r7d_UHIzM8jEP68B... to SFq4FVodIOQAS26Lr... (#190862) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/fuchsia-linux-sdk-flutter Please CC codefu@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 801a4d322a527..4c315838fe73b 100644 --- a/DEPS +++ b/DEPS @@ -830,7 +830,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': '2r7d_UHIzM8jEP68BCIEXXl40b5qqZ3COoolX2gyOCsC' + 'version': 'SFq4FVodIOQAS26LraIN4MNNWCZmirSJvNBY-OO-yZsC' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From 91ce4def4943ae1ab4268b4321769c948953648a Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 10 Aug 2026 21:26:54 +0000 Subject: [PATCH 180/330] iOS: test GPU availability and task queue behaviour (#190836) Adds tests for two behaviours the embedder API migration has to preserve, both at the layer a plugin or the framework actually sees rather than the layer the current implementation happens to use: First is GPU availability: Currently, `createShell:` re-derives `isGpuDisabled` from the live view controller or application state immediately before creating the shell, and clobbers whatever the property was set to beforehand. This is an easy thing to get wrong later: the "obvious" handling via `FlutterEngineRun` would be to take the initial GPU availability as a project argument, but passing the property straight through would resurrect exactly the stale value this code goes out of its way to drop. Second is Task queues: `PlatformMessageHandlerIosTest` covers `PlatformMessageHandlerIos` behaviour on its own, but nothing covered the path a plugin takes to reach it, through * `-[FlutterEngine makeBackgroundTaskQueue]` * `-[FlutterEngine setMessageHandlerOnChannel:binaryMessageHandler:taskQueue:]` `EmbedderPlatformMessageHandler` always trampolines to the platform thread, so both halves of that contract will have to be rebuilt explicitly for embedder API migration. The GPU test locks in the current behaviour. I'll address the TODO in the next patch to clean up the API. But this is a nice way to document/verify the behavioural diff in the tests. Issue: https://github.com/flutter/flutter/issues/112232 Issue: https://github.com/flutter/flutter/issues/190835 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/framework/Source/FlutterEngineTest.mm | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index be708273b589c..de4166748e465 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -10,6 +10,8 @@ #import "flutter/common/settings.h" #include "flutter/fml/synchronization/sync_switch.h" +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/lib/ui/window/platform_message_response.h" #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h" @@ -25,6 +27,24 @@ #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" FLUTTER_ASSERT_ARC +namespace { +// A `PlatformMessageResponse` that records completion. +// +// This allows tests to inject an inbound platform message the way the engine's platform view would +// and observe that it was answered. +class TestPlatformMessageResponse : public flutter::PlatformMessageResponse { + public: + static fml::RefPtr Create() { + return fml::AdoptRef(new TestPlatformMessageResponse()); + } + void Complete(std::unique_ptr data) override { is_complete_ = true; } + void CompleteEmpty() override { is_complete_ = true; } + + private: + TestPlatformMessageResponse() = default; +}; +} // namespace + @protocol TestFlutterPluginWithSceneEvents @end @@ -524,6 +544,128 @@ - (void)testLifeCycleNotificationWillEnterForegroundForScene { [mockBundle stopMocking]; } +// Locks in current behaviour for GPU state reset, which is arguably wrong. +// +// If `isGpuDisabled` is set before the engine runs, it is silently discarded, because +// `createShell:` assigns to the property from the live view controller or application state +// immediately before creating the shell. +// +// `createShell:` *does* need to sample the current lifecycle state there. Background/foreground +// notifications only fire on transitions, so an engine created while the app is already +// backgrounded would otherwise never know about it. However, this sampling is implemented as an +// unconditional assignment to a public readwrite property, so it also destroys any state previously +// set by the caller. +// +// TODO(cbracken): https://github.com/flutter/flutter/issues/190835 +// +// Move this sampling to `init` and `setViewController:` and stop `createShell:` from clobbering +// this state, then change this test to asserts the opposite of what it currently does. +- (void)testGpuStateIsDerivedFromApplicationStateWhenShellIsCreated { + FlutterDartProject* project = [[FlutterDartProject alloc] init]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; + + // Claim the GPU is disabled before the shell exists. + // + // There is no view controller and the test host is foregrounded, so creating the shell must + // overwrite this with NO. + engine.isGpuDisabled = YES; + XCTAssertTrue(engine.isGpuDisabled); + + [engine run]; + + XCTAssertFalse(engine.isGpuDisabled); + + BOOL gpuDisabled = YES; + [engine shell].GetIsGpuDisabledSyncSwitch()->Execute( + fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] { + gpuDisabled = NO; + })); + XCTAssertFalse(gpuDisabled); +} + +// Verifies a handler registered against a background FlutterTaskQueue runs off the platform thread. +// +// Using the `FlutterTaskQueue` public API, a plugin may register a channel handler against a +// background queue and expect to be called off the platform thread. `PlatformMessageHandlerIosTest` +// tests the handler in isolation, but doesn't cover the full path an actual plugin takes: +// +// `-[FlutterEngine makeBackgroundTaskQueue]` and +// `-[FlutterEngine setMessageHandlerOnChannel:binaryMessageHandler:taskQueue:]`. +// +// The embedder API has no equivalent concept: `EmbedderPlatformMessageHandler` always trampolines +// to the platform thread, so this contract has to be reproduced explicitly during embedder API +// migration. +// +// `testNilTaskQueueDeliversOnThePlatformThread`, tests the other direction. +- (void)testBackgroundTaskQueueDeliversOffThePlatformThread { + FlutterDartProject* project = [[FlutterDartProject alloc] init]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; + [engine run]; + + NSObject* taskQueue = [engine makeBackgroundTaskQueue]; + XCTAssertNotNil(taskQueue); + + NSString* channel = @"com.example.background"; + XCTestExpectation* didCallHandler = [self expectationWithDescription:@"didCallHandler"]; + FlutterBinaryMessengerConnection connection = + [engine setMessageHandlerOnChannel:channel + binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply reply) { + XCTAssertFalse([NSThread isMainThread]); + reply(nil); + [didCallHandler fulfill]; + } + taskQueue:taskQueue]; + XCTAssertTrue(connection > 0); + + // Deliver a message the way the platform view would on receiving one from the engine. + auto response = TestPlatformMessageResponse::Create(); + engine.platformView->GetPlatformMessageHandlerIos()->HandlePlatformMessage( + std::make_unique(channel.UTF8String, response)); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + XCTAssertTrue(response->is_complete()); + + [engine cleanUpConnection:connection]; +} + +// Verifies a handler registered without a task queue runs on the platform thread. +// +// Together with `testBackgroundTaskQueueDeliversOffThePlatformThread` locks in the invariant that +// the task queue argument, and nothing else, decides the thread. The default is the platform +// thread, which needs to be preserved throughout embedder API migration: this is what allows +// plugins interact with UIKit directly from their channel handlers. +// +// This is the half the embedder API migration is most likely to cause to pass by accident. +// `EmbedderPlatformMessageHandler` trampolines everything to the platform thread, so an +// implementation that never leaves it goes green here and fails only in +// `testBackgroundTaskQueueDeliversOffThePlatformThread`. +- (void)testNilTaskQueueDeliversOnThePlatformThread { + FlutterDartProject* project = [[FlutterDartProject alloc] init]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; + [engine run]; + + NSString* channel = @"com.example.platform"; + XCTestExpectation* didCallHandler = [self expectationWithDescription:@"didCallHandler"]; + FlutterBinaryMessengerConnection connection = + [engine setMessageHandlerOnChannel:channel + binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply reply) { + XCTAssertTrue([NSThread isMainThread]); + reply(nil); + [didCallHandler fulfill]; + } + taskQueue:nil]; + XCTAssertTrue(connection > 0); + + auto response = TestPlatformMessageResponse::Create(); + engine.platformView->GetPlatformMessageHandlerIos()->HandlePlatformMessage( + std::make_unique(channel.UTF8String, response)); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + XCTAssertTrue(response->is_complete()); + + [engine cleanUpConnection:connection]; +} + - (void)testLifeCycleNotificationSceneWillConnect { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; From 392f248287eb5553233c1e73b78919b5f9ff474b Mon Sep 17 00:00:00 2001 From: flutter-pub-roller-bot <137456488+flutter-pub-roller-bot@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:40:07 +0000 Subject: [PATCH 181/330] Roll pub packages (#190838) This PR was generated by `flutter update-packages --force-upgrade`. --- packages/flutter_tools/pubspec.yaml | 4 ++-- pubspec.lock | 4 ++-- pubspec.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 0f27e556cfd49..6fd2663d19a12 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: hooks: 2.1.0 code_assets: 1.2.1 data_assets: 0.20.0 - record_use: 1.0.0 + record_use: 1.1.0 # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so when upgrading @@ -129,4 +129,4 @@ dartdoc: nodoc: true -# PUBSPEC CHECKSUM: 9v55jf +# PUBSPEC CHECKSUM: d0mren diff --git a/pubspec.lock b/pubspec.lock index c87e37b573fc4..762cd8e0aaed9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -910,10 +910,10 @@ packages: dependency: "direct main" description: name: record_use - sha256: af37186ff9ede46fa32f152526c48f46c5b3ad7d3d5a566140cb5df3dc4ed737 + sha256: "3b4ab682aff40175afca6ff090cc5aa7ce9dafe8dfbae1537a6c678e6678baee" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" retry: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fb510bd5d1936..bfe69fc2d82e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -123,7 +123,7 @@ dependencies: googleapis_auth: 2.3.3 hooks: 2.1.0 data_assets: 0.20.0 - record_use: 1.0.0 + record_use: 1.1.0 html: 0.15.6 http: 1.6.0 http_multi_server: 3.2.2 @@ -223,4 +223,4 @@ dependencies: dev_dependencies: ffigen: 20.1.1 -# PUBSPEC CHECKSUM: db69ps +# PUBSPEC CHECKSUM: 7p8si8 From 1dc9ea86aa4a141c84e61a3e7e4917eea1cdcf85 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:56:25 +0000 Subject: [PATCH 182/330] Revert: Make Xcode workspace cleaning optional during flutter clean (#190890) Reverts: [Make Xcode workspace cleaning optional during flutter clean](https://github.com/flutter/flutter/pull/190091) Initiated by: @cbracken Reason for reverting: breaks Mac_arm64 plugin_lint_mac Original PR Author: @okorohelijah Reviewed By: @vashworth The original PR description is provided below: This PR introduces a new `--include-xcode-workspace` flag to flutter clean which makes Xcode workspace cleaning optional, bypassing the expensive xcodebuild execution that inherently triggers Swift Package resolution over the internet. By default, it will now instantly clean local build directories without polling or cleaning Xcode. Additionally, this updates several Xcode-specific error messages across the codebase to explicitly instruct users to run `flutter clean --include-xcode-workspace` when clearing Xcode's derived data is required *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* Fixes https://github.com/flutter/flutter/issues/183946, https://github.com/flutter/flutter/issues/173940 and https://github.com/flutter/flutter/issues/127708 too *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter_tools/lib/src/commands/clean.dart | 12 +------ packages/flutter_tools/lib/src/ios/mac.dart | 4 +-- .../lib/src/macos/swift_package_manager.dart | 4 +-- .../commands.shard/hermetic/clean_test.dart | 35 +++---------------- .../test/general.shard/ios/mac_test.dart | 2 +- 5 files changed, 11 insertions(+), 46 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart index 8e5ce36d8f25c..59a8558f3b9f1 100644 --- a/packages/flutter_tools/lib/src/commands/clean.dart +++ b/packages/flutter_tools/lib/src/commands/clean.dart @@ -29,13 +29,6 @@ class CleanCommand extends FlutterCommand { 'Also clean the example directory, if one exists. ' 'Useful when developing in a package project.', ); - argParser.addFlag( - 'include-xcode-workspace', - negatable: false, - help: - 'Whether to run "xcodebuild clean" on the Xcode workspace for iOS and macOS projects. ' - "This removes build products and intermediate files from Xcode's build cache and can be slow to complete.", - ); argParser.addFlag( 'stop-gradle', negatable: false, @@ -63,10 +56,7 @@ class CleanCommand extends FlutterCommand { Future runCommand() async { final FlutterProject flutterProject = FlutterProject.current(); final Xcode? xcode = globals.xcode; - final bool userWantsXcodeClean = - boolArg('include-xcode-workspace') || (argResults?.wasParsed('scheme') ?? false); - final bool cleanXcode = - xcode != null && xcode.isInstalledAndMeetsVersionCheck && userWantsXcodeClean; + final bool cleanXcode = xcode != null && xcode.isInstalledAndMeetsVersionCheck; await _cleanProject(flutterProject, cleanXcode: cleanXcode); if (boolArg('include-example')) { diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 9d4761dc9d96a..4d30668e5d174 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -1264,7 +1264,7 @@ Future _handleIssues( } else if (modifiedPrecompiledSource) { logger.printError( '════════════════════════════════════════════════════════════════════════════════\n' - 'A precompiled file has been changed since last built. Please run "flutter clean --include-xcode-workspace" to clear ' + 'A precompiled file has been changed since last built. Please run "flutter clean" to clear ' 'the cache.\n' '════════════════════════════════════════════════════════════════════════════════', ); @@ -1532,7 +1532,7 @@ class _XCResultIssueHandlingResult { final String? missingModule; /// An issue indicates that a source file, such as a header in the Flutter framework, has - /// changed since last built. This requires "flutter clean --include-xcode-workspace" to resolve. + /// changed since last built. This requires "flutter clean" to resolve. final bool modifiedPrecompiledSource; final bool unableToFindArmDestination; 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 3bef2991799ba..1339079a273be 100644 --- a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -236,7 +236,7 @@ class SwiftPackageManager { /// If a symlink already exists and points to the correct target, creation is skipped /// to avoid potential Xcode parallel target build race conditions. /// If creation fails due to sharing violations or locks (e.g., when Xcode is open), - /// throws a descriptive [ToolExit] advising the user to close Xcode and run "flutter clean --include-xcode-workspace". + /// throws a descriptive [ToolExit] advising the user to close Xcode and run "flutter clean". void _createPluginSymlink({required Link pluginSymlink, required String packagePath}) { final FileSystemEntityType type = _fileSystem.typeSync(pluginSymlink.path, followLinks: false); var skipCreation = false; @@ -277,7 +277,7 @@ class SwiftPackageManager { throwToolExit( 'Failed to create Swift Package plugin symlink at "${pluginSymlink.path}" to "$packagePath":\n' '$e\n' - 'If Xcode is currently open, please close Xcode, run "flutter clean --include-xcode-workspace", and try building again.', + 'If Xcode is currently open, please close Xcode, run "flutter clean", and try building again.', ); } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart index 49e2530de64e3..77551757d2037 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart @@ -59,7 +59,7 @@ void main() { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean', '--include-xcode-workspace']); + await runner.run(['clean']); expect(buildDirectory, isNot(exists)); expect(projectUnderTest.dartTool, isNot(exists)); @@ -102,31 +102,6 @@ void main() { }, ); - testUsingContext( - '$CleanCommand does not clean Xcode by default', - () async { - final FlutterProject projectUnderTest = setupProjectUnderTest(fs.currentDirectory, true); - xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(1000, 0, 0); - final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean']); - - expect(buildDirectory, isNot(exists)); - expect(projectUnderTest.dartTool, isNot(exists)); - expect(projectUnderTest.android.ephemeralDirectory, isNot(exists)); - expect(projectUnderTest.ios.ephemeralDirectory, isNot(exists)); - - // The workspaces should be empty since we didn't pass --include-xcode-workspace. - expect(xcodeProjectInterpreter.workspaces, isEmpty); - }, - overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - Xcode: () => xcode, - XcodeProjectInterpreter: () => xcodeProjectInterpreter, - }, - ); - testUsingContext( '$CleanCommand does not clean the example directory by default', () async { @@ -141,7 +116,7 @@ void main() { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean', '--include-xcode-workspace']); + await runner.run(['clean']); expect(buildDirectory, isNot(exists)); @@ -184,7 +159,7 @@ void main() { xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean', '--include-example', '--include-xcode-workspace']); + await runner.run(['clean', '--include-example']); expect(buildDirectory, isNot(exists)); expect(projectUnderTest.dartTool, isNot(exists)); @@ -234,7 +209,7 @@ void main() { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); final CommandRunner runner = createTestCommandRunner(CleanCommand()); - await runner.run(['clean', '--include-example', '--include-xcode-workspace']); + await runner.run(['clean', '--include-example']); expect(testLogger.statusText, contains('No example app found')); }, @@ -327,7 +302,7 @@ void main() { final command = CleanCommand(verbose: true); final CommandRunner runner = createTestCommandRunner(command); - await runner.run(['clean', '--include-xcode-workspace']); + await runner.run(['clean']); expect(xcodeProjectInterpreter.workspaces, const [ CleanWorkspaceCall('/ios/Runner.xcworkspace', 'Runner', true), diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index 8b7d5cb81c352..c91ede19d924c 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -653,7 +653,7 @@ duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_S expect( logger.errorText, contains( - 'A precompiled file has been changed since last built. Please run "flutter clean --include-xcode-workspace" to ' + 'A precompiled file has been changed since last built. Please run "flutter clean" to ' 'clear the cache.', ), ); From c575e0063d4f094e8da49d8babd9e834cab69b7a Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 11 Aug 2026 02:12:43 +0000 Subject: [PATCH 183/330] Scenarios: Eliminate an unused import (#190837) GoldenTestManager isn't used anywhere in the app extension tests. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios/Scenarios/ScenariosUITests/AppExtensionTests.m | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/AppExtensionTests.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/AppExtensionTests.m index 140368cc0fbfb..d9ba800189c4b 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/AppExtensionTests.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/AppExtensionTests.m @@ -3,7 +3,6 @@ // found in the LICENSE file. #import -#import "GoldenTestManager.h" @interface AppExtensionTests : XCTestCase @property(nonatomic, strong) XCUIApplication* hostApplication; From 3dd1a077dc6703a58cdc9d2699ba0298ae62a258 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 11 Aug 2026 02:13:01 +0000 Subject: [PATCH 184/330] Scenarios: Fix multiple-clips scenario doc comments (#190830) The `...MultipleClipsScenario` constructors each copy their doc comment from the corresponding single-clip scenario, so none of them mention the extra clip that distinguishes them. Updates each to say it constructs the multiple-clips variant. Spotted by Gemini review bot during review of https://github.com/flutter/flutter/pull/190826. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/src/platform_view.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/engine/src/flutter/testing/ios_scenario_app/lib/src/platform_view.dart b/engine/src/flutter/testing/ios_scenario_app/lib/src/platform_view.dart index 8ae325d5b73e3..a7a9705b85f2e 100644 --- a/engine/src/flutter/testing/ios_scenario_app/lib/src/platform_view.dart +++ b/engine/src/flutter/testing/ios_scenario_app/lib/src/platform_view.dart @@ -544,7 +544,7 @@ class PlatformViewClipRectScenario extends Scenario with _BasePlatformViewScenar /// Platform view with clip rect, with multiple clips. class PlatformViewClipRectMultipleClipsScenario extends Scenario with _BasePlatformViewScenarioMixin { - /// Constructs a platform view with clip rect scenario. + /// Constructs a platform view with clip rect and multiple clips. PlatformViewClipRectMultipleClipsScenario(super.view, {required this.id}); /// The platform view identifier. @@ -616,7 +616,7 @@ class PlatformViewClipRectAfterMovedScenario extends Scenario with _BasePlatform /// The clip rect moves with the same transform matrix with the PlatformView. class PlatformViewClipRectAfterMovedMultipleClipsScenario extends Scenario with _BasePlatformViewScenarioMixin { - /// Constructs a platform view with clip rect scenario. + /// Constructs a platform view with clip rect after moved and multiple clips. PlatformViewClipRectAfterMovedMultipleClipsScenario(super.view, {required this.id}); /// The platform view identifier. @@ -690,7 +690,7 @@ class PlatformViewClipRRectScenario extends PlatformViewScenario { /// Platform view with clip rrect, with multiple clips. class PlatformViewClipRRectMultipleClipsScenario extends PlatformViewScenario { - /// Constructs a platform view with clip rrect scenario. + /// Constructs a platform view with clip rrect and multiple clips. PlatformViewClipRRectMultipleClipsScenario(super.view, {super.id = 0}); @override @@ -796,7 +796,7 @@ class PlatformViewLargeClipRRectScenario extends PlatformViewScenario { /// Platform view with clip rrect, with multiple clips. /// The bounding rect of the rrect is the same as PlatformView and only the corner radii clips the PlatformView. class PlatformViewLargeClipRRectMultipleClipsScenario extends PlatformViewScenario { - /// Constructs a platform view with large clip rrect scenario. + /// Constructs a platform view with large clip rrect and multiple clips. PlatformViewLargeClipRRectMultipleClipsScenario(super.view, {super.id = 0}); @override @@ -844,7 +844,7 @@ class PlatformViewClipPathScenario extends PlatformViewScenario { /// Platform view with clip path, with multiple clips. class PlatformViewClipPathMultipleClipsScenario extends PlatformViewScenario { - /// Constructs a platform view with clip path scenario. + /// Constructs a platform view with clip path and multiple clips. PlatformViewClipPathMultipleClipsScenario(super.view, {super.id = 0}); @override @@ -895,7 +895,7 @@ class PlatformViewClipRectWithTransformScenario extends PlatformViewScenario { /// Platform view with clip rect after transformed, with multiple clips. class PlatformViewClipRectWithTransformMultipleClipsScenario extends PlatformViewScenario { - /// Constructs a platform view with clip rect with transform scenario. + /// Constructs a platform view with clip rect with transform and multiple clips. PlatformViewClipRectWithTransformMultipleClipsScenario(super.view, {super.id = 0}); @override @@ -962,7 +962,7 @@ class PlatformViewClipRRectWithTransformScenario extends PlatformViewScenario { /// Platform view with clip rrect after transformed, with multiple clips. class PlatformViewClipRRectWithTransformMultipleClipsScenario extends PlatformViewScenario { - /// Constructs a platform view with clip rrect with transform scenario. + /// Constructs a platform view with clip rrect with transform and multiple clips. PlatformViewClipRRectWithTransformMultipleClipsScenario(super.view, {super.id = 0}); @override @@ -1041,7 +1041,7 @@ class PlatformViewLargeClipRRectWithTransformScenario extends PlatformViewScenar /// Platform view with clip rrect after transformed, with multiple clips. /// The bounding rect of the rrect is the same as PlatformView and only the corner radii clips the PlatformView. class PlatformViewLargeClipRRectWithTransformMultipleClipsScenario extends PlatformViewScenario { - /// Constructs a platform view with large clip rrect with transform scenario. + /// Constructs a platform view with large clip rrect with transform and multiple clips. PlatformViewLargeClipRRectWithTransformMultipleClipsScenario(super.view, {super.id = 0}); @override @@ -1115,7 +1115,7 @@ class PlatformViewClipPathWithTransformScenario extends PlatformViewScenario { /// Platform view with clip path after transformed, with multiple clips. class PlatformViewClipPathWithTransformMultipleClipsScenario extends PlatformViewScenario { - /// Constructs a platform view with clip path with transform scenario. + /// Constructs a platform view with clip path with transform and multiple clips. PlatformViewClipPathWithTransformMultipleClipsScenario(super.view, {super.id = 0}); @override From c4b3bff19e0fbfd9a530b29f808586caa4579e68 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 11 Aug 2026 03:05:28 +0000 Subject: [PATCH 185/330] Roll Skia from 6549c09b1ffb to 7817fed2e368 (8 revisions) (#190877) https://skia.googlesource.com/skia.git/+log/6549c09b1ffb..7817fed2e368 2026-08-10 nathanasanchez@google.com [Graphite] Support inverse fill for tessellated strokes renderer 2026-08-10 thomsmit@google.com [graphite] Refactor flattenning to expose non-culled mode 2026-08-10 fmalita@google.com Harden in-place SkConvertPixels 2026-08-10 helmut@januschka.com [rust png] Splat Adam7 rows for partial decodes 2026-08-10 robertphillips@google.com [graphite] Stop using ResourceProvider as a PipelineManager intermediary 2026-08-10 robertphillips@google.com [graphite] Fix build 2026-08-10 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-10 sergiog@microsoft.com [rust jpeg] Implement fixed-size reads for growing cursor If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC codefu@google.com,jmbetancourt@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 4c315838fe73b..5ff0daef0fa02 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '6549c09b1ffb41b612afc25a4a626964a4af739d', + 'skia_revision': '7817fed2e368c34917073c61545ff3ec1137589f', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 8d047a715778b507f2d87b96d046135982ed4d8c Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 11 Aug 2026 03:12:13 +0000 Subject: [PATCH 186/330] Roll Dart SDK from 7239be9b8b07 to 558fb298e458 (1 revision) (#190881) https://dart.googlesource.com/sdk.git/+log/7239be9b8b07..558fb298e458 2026-08-10 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-111.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC codefu@google.com,dart-vm-team@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 5ff0daef0fa02..37ba85941be89 100644 --- a/DEPS +++ b/DEPS @@ -55,7 +55,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': '7239be9b8b0785050117bc94d668fd697f7e2cba', + 'dart_revision': '558fb298e458b75551bc19aaecbd1243cb72c45f', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From da7fe066e4257fbce4a7b7f8c804e386d5a2dc99 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:16:24 +0000 Subject: [PATCH 187/330] Adds benchmark for windows shadow text. (#190861) issue https://github.com/flutter/flutter/issues/190395 ## test results on macos Metric | Stock Engine (host_profile) | Patched Engine (text-shadow-cache-by-content) | Improvement --------------------------------------------|--------------------------------------------|-----------------------------------------------|------------------------------------------- Average Raster Time | 6.82 ms | 1.11 ms | ~6.1x faster 90th Percentile Raster Time | 8.66 ms | 1.72 ms | ~5.0x faster 99th Percentile Raster Time | 9.73 ms | 2.53 ms | ~3.8x faster Worst Frame Raster Time | 10.14 ms | 2.67 ms | ~3.8x faster The patch is from https://github.com/flutter/flutter/pull/190681 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .ci.yaml | 14 ++ TESTOWNERS | 1 + .../macrobenchmarks/lib/common.dart | 1 + dev/benchmarks/macrobenchmarks/lib/main.dart | 9 ++ .../lib/src/text_shadow_perf.dart | 133 ++++++++++++++++++ .../test_driver/text_shadow_perf_test.dart | 16 +++ ...ws_text_shadow_perf__timeline_summary.dart | 12 ++ dev/devicelab/lib/tasks/perf_tests.dart | 11 ++ 8 files changed, 197 insertions(+) create mode 100644 dev/benchmarks/macrobenchmarks/lib/src/text_shadow_perf.dart create mode 100644 dev/benchmarks/macrobenchmarks/test_driver/text_shadow_perf_test.dart create mode 100644 dev/devicelab/bin/tasks/windows_text_shadow_perf__timeline_summary.dart diff --git a/.ci.yaml b/.ci.yaml index 20617668768c9..dbf01153595e4 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -7261,6 +7261,20 @@ targets: ] task_name: windows_home_scroll_perf__timeline_summary + - name: Windows windows_text_shadow_perf__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + bringup: true + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + task_name: windows_text_shadow_perf__timeline_summary + - name: Windows_arm64 windows_home_scroll_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false diff --git a/TESTOWNERS b/TESTOWNERS index 027cfc027cf33..185001a65f817 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -333,6 +333,7 @@ /dev/devicelab/bin/tasks/windows_desktop_impeller.dart @jonahwilliams @flutter/engine /dev/devicelab/bin/tasks/windows_engine_integration_golden_test.dart @b-luk @flutter/engine /dev/devicelab/bin/tasks/windows_home_scroll_perf__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/windows_text_shadow_perf__timeline_summary.dart @gaaclarke @flutter/engine /dev/devicelab/bin/tasks/windows_startup_test.dart @loic-sharma @flutter/desktop ## Host only framework tests diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart index fde9f2dc35dff..1060fe627225e 100644 --- a/dev/benchmarks/macrobenchmarks/lib/common.dart +++ b/dev/benchmarks/macrobenchmarks/lib/common.dart @@ -45,6 +45,7 @@ const String kDrawAtlasPageRouteName = '/draw_atlas'; const String kAnimatedAdvancedBlend = '/animated_advanced_blend'; const String kRRectBlurRouteName = '/rrect_blur'; const String kRSuperellipseBlurRouteName = '/rsuperellipse_blur'; +const String kTextShadowPerfRouteName = '/text_shadow_perf'; const String kOpacityPeepholeOneRectRouteName = '$kOpacityPeepholeRouteName/one_big_rect'; const String kOpacityPeepholeColumnOfOpacityRouteName = diff --git a/dev/benchmarks/macrobenchmarks/lib/main.dart b/dev/benchmarks/macrobenchmarks/lib/main.dart index 922ca84fb86f2..afb4e194ada9a 100644 --- a/dev/benchmarks/macrobenchmarks/lib/main.dart +++ b/dev/benchmarks/macrobenchmarks/lib/main.dart @@ -44,6 +44,7 @@ import 'src/simple_animation.dart'; import 'src/simple_scroll.dart'; import 'src/sliders.dart'; import 'src/text.dart'; +import 'src/text_shadow_perf.dart'; import 'src/very_long_picture_scrolling.dart'; const String kMacrobenchmarks = 'Macrobenchmarks'; @@ -118,6 +119,7 @@ class MacrobenchmarksApp extends StatelessWidget { const DrawArcsPage(paintStyle: PaintingStyle.fill), kDrawArcsAllStrokeStylesPageRouteName: (BuildContext context) => const DrawArcsPage(paintStyle: PaintingStyle.stroke), + kTextShadowPerfRouteName: (BuildContext context) => const TextShadowPerfPage(), }, ); } @@ -429,6 +431,13 @@ class HomePage extends StatelessWidget { Navigator.pushNamed(context, kDrawArcsAllStrokeStylesPageRouteName); }, ), + ElevatedButton( + key: const Key(kTextShadowPerfRouteName), + child: const Text('Text Shadow Performance'), + onPressed: () { + Navigator.pushNamed(context, kTextShadowPerfRouteName); + }, + ), ], ), ); diff --git a/dev/benchmarks/macrobenchmarks/lib/src/text_shadow_perf.dart b/dev/benchmarks/macrobenchmarks/lib/src/text_shadow_perf.dart new file mode 100644 index 0000000000000..952b9d12bdc4c --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/text_shadow_perf.dart @@ -0,0 +1,133 @@ +// 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/material.dart'; + +/// Macrobenchmark page reproducing the text shadow scrolling workload from +/// https://github.com/flutter/flutter/issues/190395. +class TextShadowPerfPage extends StatefulWidget { + const TextShadowPerfPage({super.key}); + + @override + State createState() => _TextShadowPerfPageState(); +} + +class _TextShadowPerfPageState extends State + with SingleTickerProviderStateMixin { + late final ScrollController _scrollController; + late final AnimationController _animationController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _animationController = AnimationController(vsync: this, duration: const Duration(seconds: 10)) + ..repeat(reverse: true); + _animationController.addListener(_onAnimationTick); + } + + void _onAnimationTick() { + if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0) { + _scrollController.jumpTo( + _animationController.value * _scrollController.position.maxScrollExtent, + ); + } + } + + @override + void dispose() { + _animationController.removeListener(_onAnimationTick); + _animationController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const columnCount = 7; + const groupsPerColumn = 28; + + return Scaffold( + backgroundColor: const Color(0xff080808), + body: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: _scrollController, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(columnCount, (int column) { + return Expanded( + child: Padding( + padding: const EdgeInsets.all(6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: List.generate(groupsPerColumn, (int row) { + return _TextShadowGroup(index: column * groupsPerColumn + row); + }), + ), + ), + ); + }), + ), + ), + ), + ); + } +} + +class _TextShadowGroup extends StatelessWidget { + const _TextShadowGroup({required this.index}); + + final int index; + + static const List _shadows = [Shadow(blurRadius: 1.0)]; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 230, + child: Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Synthetic text item $index', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + shadows: _shadows, + ), + ), + Row( + children: [ + Text( + '${8 + index % 14}:30', + style: const TextStyle( + color: Color(0xff43ddff), + fontSize: 9, + fontWeight: FontWeight.bold, + shadows: _shadows, + ), + ), + const Spacer(), + Text( + 'Ep ${1 + index % 24}', + style: const TextStyle(color: Colors.white70, fontSize: 9, shadows: _shadows), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/text_shadow_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/text_shadow_perf_test.dart new file mode 100644 index 0000000000000..37d2fc6dd78df --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/text_shadow_perf_test.dart @@ -0,0 +1,16 @@ +// 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:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTest( + 'text_shadow_perf', + kTextShadowPerfRouteName, + pageDelay: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + ); +} diff --git a/dev/devicelab/bin/tasks/windows_text_shadow_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/windows_text_shadow_perf__timeline_summary.dart new file mode 100644 index 0000000000000..e3ab862fd8ee6 --- /dev/null +++ b/dev/devicelab/bin/tasks/windows_text_shadow_perf__timeline_summary.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/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.windows; + await task(createTextShadowPerfTest()); +} diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index 0ba0733666f9d..31e8734bd87af 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -376,6 +376,17 @@ TaskFunction createTextfieldPerfE2ETest() { ).run; } +/// Creates a task that runs the text shadow performance benchmark. +TaskFunction createTextShadowPerfTest({bool? enableImpeller}) { + return PerfTest( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test_driver/run_app.dart', + 'text_shadow_perf', + testDriver: 'test_driver/text_shadow_perf_test.dart', + enableImpeller: enableImpeller, + ).run; +} + TaskFunction createVeryLongPictureScrollingPerfE2ETest({required bool enableImpeller}) { return PerfTest.e2e( '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', From f6d405c8b75ca1c693a5ee840b11ecb93fb2acd4 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Tue, 11 Aug 2026 03:17:50 +0000 Subject: [PATCH 188/330] Remove manual Safari fallback for unhandled universal links (#185430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a universal link (https) opens an iOS app on a cold start, the Dart framework may not be ready to handle routes yet — onGenerateRoute can return null simply because initialization hasn't completed. The engine interprets this as "unhandled" and relays the URL back to iOS via openURL:, which opens Safari. This is incorrect and disruptive. This PR completely removes the relay-to-system fallback for unhandled universal links (for both warm and cold starts). The modern iOS API for scenes (`UISceneDelegate continueUserActivity`) returns `void`, meaning iOS natively no longer expects feedback on whether a link was handled, and natively stopped bouncing unrecognized universal links to Safari. Removing our manual `[UIApplication openURL:]` fallback aligns Flutter's engine with Apple's modern native behavior. **PR with the help of Gemini** Fixes #170665 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../framework/Source/FlutterAppDelegate.mm | 17 ++++---------- .../Source/FlutterAppDelegateTest.mm | 6 +++-- .../framework/Source/FlutterSceneLifeCycle.mm | 23 ++++++------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index 9f3cb2553a402..22f86c652cf63 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -149,14 +149,12 @@ - (BOOL)application:(UIApplication*)application return YES; } - // Relaying to the system here will case an infinite loop, so we don't do it here. - return [self handleOpenURL:url options:options relayToSystemIfUnhandled:NO]; + return [self handleOpenURL:url options:options]; } // Helper function for opening an URL, either with a custom scheme or a http/https scheme. - (BOOL)handleOpenURL:(NSURL*)url - options:(NSDictionary*)options - relayToSystemIfUnhandled:(BOOL)throwBack { + options:(NSDictionary*)options { UIApplication* flutterApplication = FlutterSharedApplication.application; if (flutterApplication == nil) { return NO; @@ -168,13 +166,8 @@ - (BOOL)handleOpenURL:(NSURL*)url FlutterViewController* flutterViewController = [self rootFlutterViewController]; if (flutterViewController) { [flutterViewController.engine sendDeepLinkToFramework:url - completionHandler:^(BOOL success) { - if (!success && throwBack) { - // throw it back to iOS - [flutterApplication openURL:url - options:@{} - completionHandler:nil]; - } + completionHandler:^(BOOL success){ + // no-op. }]; } else { [FlutterLogger logError:@"Attempting to open an URL without a Flutter RootViewController."]; @@ -224,7 +217,7 @@ - (BOOL)application:(UIApplication*)application return YES; } - return [self handleOpenURL:userActivity.webpageURL options:@{} relayToSystemIfUnhandled:YES]; + return [self handleOpenURL:userActivity.webpageURL options:@{}]; } #pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index 8b5271f164365..6d6182414cbe7 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -192,7 +192,7 @@ - (void)testUniversalLinkPushRouteInformation { OCMVerifyAll(self.mockNavigationChannel); } -- (void)testUseNonDeprecatedOpenURLAPI { +- (void)testUniversalLinkDoesNotRelayToSystemWhenUnhandled { OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) .andReturn(@YES); NSUserActivity* userActivity = [[NSUserActivity alloc] initWithActivityType:@"com.example.test"]; @@ -205,13 +205,15 @@ - (void)testUseNonDeprecatedOpenURLAPI { }); id mockApplication = OCMClassMock([UIApplication class]); OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + BOOL result = [self.appDelegate application:[UIApplication sharedApplication] continueUserActivity:userActivity restorationHandler:^(NSArray>* __nullable restorableObjects){ }]; XCTAssertTrue(result); - OCMVerify([mockApplication openURL:[OCMArg any] + // openURL should NOT have been called. + OCMReject([mockApplication openURL:[OCMArg any] options:[OCMArg any] completionHandler:[OCMArg any]]); } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm index 019de11b5bbbd..53f1ba5f6bafd 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm @@ -311,7 +311,7 @@ - (void)scene:(UIScene*)scene openURLContexts:(NSSet*)URLCont continue; } for (UIOpenURLContext* urlContext in URLContexts) { - if ([self handleDeeplink:urlContext.URL flutterEngine:engine relayToSystemIfUnhandled:NO]) { + if ([self handleDeeplink:urlContext.URL flutterEngine:engine]) { break; } } @@ -344,7 +344,7 @@ - (void)scene:(UIScene*)scene continueUserActivity:(NSUserActivity*)userActivity if ([enginesHandledByPlugin containsObject:engine]) { continue; } - [self handleDeeplink:userActivity.webpageURL flutterEngine:engine relayToSystemIfUnhandled:YES]; + [self handleDeeplink:userActivity.webpageURL flutterEngine:engine]; } } @@ -436,9 +436,7 @@ - (void)handleDeeplinkingForEngine:(FlutterEngine*)engine // scene(_:continue:) when the universal link is tapped while your app is running or suspended in // memory. for (NSUserActivity* userActivity in connectionOptions.userActivities) { - if ([self handleDeeplink:userActivity.webpageURL - flutterEngine:engine - relayToSystemIfUnhandled:YES]) { + if ([self handleDeeplink:userActivity.webpageURL flutterEngine:engine]) { return; } } @@ -447,15 +445,13 @@ - (void)handleDeeplinkingForEngine:(FlutterEngine*)engine // the scene:willConnectToSession:options: delegate method after launch, and to // scene:openURLContexts: when your app opens a URL while running or suspended in memory. for (UIOpenURLContext* urlContext in connectionOptions.URLContexts) { - if ([self handleDeeplink:urlContext.URL flutterEngine:engine relayToSystemIfUnhandled:YES]) { + if ([self handleDeeplink:urlContext.URL flutterEngine:engine]) { return; } } } -- (BOOL)handleDeeplink:(NSURL*)url - flutterEngine:(FlutterEngine*)engine - relayToSystemIfUnhandled:(BOOL)throwBack { +- (BOOL)handleDeeplink:(NSURL*)url flutterEngine:(FlutterEngine*)engine { if (!url) { return NO; } @@ -465,13 +461,8 @@ - (BOOL)handleDeeplink:(NSURL*)url } // if deep linking is enabled, send it to the framework [engine sendDeepLinkToFramework:url - completionHandler:^(BOOL success) { - if (!success && throwBack) { - // throw it back to iOS - [FlutterSharedApplication.application openURL:url - options:@{} - completionHandler:nil]; - } + completionHandler:^(BOOL success){ + // no-op. }]; return YES; } From e0bb0f68bdef1236b557297bcde00d0da653322d Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 11 Aug 2026 03:39:24 +0000 Subject: [PATCH 189/330] iOS: Remove the Scenarios app Skia golden test option (#190827) `GoldenTestManager` had an option to choose between an `impeller_` and a bare golden name prefix based on the `FLTEnableImpeller` key in the app's Info.plist, and the app had an `Info_Skia.plist` that set this to false. Both of those have been dead code since 2025-02-25 when we stopped reading `FLTEnableImpeller` in #163808, which switched iOS to the slimpeller variant, and no build configuration ever pointed `INFOPLIST_FILE` at `Info_Skia.plist`. It it was only listed in the project navigator for manual use. The only remaining reader of that flag in the tree is the macOS embedder. This removes the branch, both `Info_Skia.plist` files, and the now-dead `FLTEnableImpeller` key from the Scenarios app. We keep the `impeller_` tag on the goldens even though it's redundant, since that involves touching a bunch of PNGs. If we ever need to do that, we can do it separately. No test changes, or all test changes depending how you look at it, but either way this is dead code removal. Issue: #190041 Issue: #112232 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Info_Skia.plist | 29 -------- .../Scenarios.xcodeproj/project.pbxproj | 2 - .../ios/Scenarios/Scenarios/Info.plist | 2 - .../ios/Scenarios/Scenarios/Info_Skia.plist | 66 ------------------- .../ScenariosUITests/GoldenTestManager.m | 12 +--- 5 files changed, 1 insertion(+), 110 deletions(-) delete mode 100644 engine/src/flutter/testing/ios_scenario_app/ios/FlutterAppExtensionTestHost/FlutterAppExtensionTestHost/Info_Skia.plist delete mode 100644 engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/FlutterAppExtensionTestHost/FlutterAppExtensionTestHost/Info_Skia.plist b/engine/src/flutter/testing/ios_scenario_app/ios/FlutterAppExtensionTestHost/FlutterAppExtensionTestHost/Info_Skia.plist deleted file mode 100644 index aaf5a18cf86c7..0000000000000 --- a/engine/src/flutter/testing/ios_scenario_app/ios/FlutterAppExtensionTestHost/FlutterAppExtensionTestHost/Info_Skia.plist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - FLTEnableImpeller - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 5edf562775fe1..21ade5f52dda0 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -236,7 +236,6 @@ 68D4017B2564859300ECD91A /* ContinuousTexture.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContinuousTexture.h; sourceTree = ""; }; 68D4017C2564859300ECD91A /* ContinuousTexture.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContinuousTexture.m; sourceTree = ""; }; F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iPadGestureTests.m; sourceTree = ""; }; - F72114B628EF99F500184A2D /* Info_Skia.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info_Skia.plist; sourceTree = ""; }; 21C15BDB794F61D0706DF179 /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; AAD032C50FB93A4D46C467D6 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; 5BA530B25CC9089C12F2A472 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; @@ -350,7 +349,6 @@ 0D5CE1B12E4A000200AA0002 /* SceneDelegate.m */, 248D76D322E388380012F0C1 /* Assets.xcassets */, 248D76D822E388380012F0C1 /* Info.plist */, - F72114B628EF99F500184A2D /* Info_Skia.plist */, 248D76D922E388380012F0C1 /* main.m */, 0A57B3BB2323C4BD00DD9521 /* ScreenBeforeFlutter.h */, 0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */, diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist index 8d1e8db786072..4cd2a97364c9b 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info.plist @@ -60,7 +60,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - FLTEnableImpeller - diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist deleted file mode 100644 index 3f0ff4e8967e9..0000000000000 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/Info_Skia.plist +++ /dev/null @@ -1,66 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - SceneDelegate - - - - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - FLTEnableImpeller - - - diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m index e21d5a608b1ed..b8b225445d6a1 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m @@ -25,17 +25,7 @@ - (instancetype)initWithLaunchArg:(NSString*)launchArg { _identifier = [[launchArg substringFromIndex:2] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]; - NSString* impeller = @"impeller_"; - NSNumber* enableImpeller = [[NSBundle bundleWithIdentifier:@"dev.flutter.Scenarios"] - objectForInfoDictionaryKey:@"FLTEnableImpeller"]; - if (enableImpeller != nil && !enableImpeller.boolValue) { - impeller = @""; - NSLog(@"Testing Skia: FLTEnableImpeller is NO"); - } else { - NSLog(@"Testing Impeller"); - } - - NSString* prefix = [NSString stringWithFormat:@"golden_%@_%@", _identifier, impeller]; + NSString* prefix = [NSString stringWithFormat:@"golden_%@_impeller_", _identifier]; _goldenImage = [[GoldenImage alloc] initWithGoldenNamePrefix:prefix]; _launchArg = launchArg; } From 061974f451191b46af36cd17cb625aa34a5ce889 Mon Sep 17 00:00:00 2001 From: Abdelrahman Saed Date: Tue, 11 Aug 2026 03:57:51 +0000 Subject: [PATCH 190/330] Document event ordering for HitTestBehavior.translucent (#190163) ## Description Fixes #28170 This PR adds documentation to `HitTestBehavior.translucent` explaining which listener receives events first when both a translucent target and its descendant are listening to the same pointer event. Currently, the documentation for `translucent` only says that both targets receive events, but does not explain the ordering or how gesture arena competitions are resolved. This has been a source of confusion since [issue #28170](https://github.com/flutter/flutter/issues/28170) was filed. ### Changes - Added a paragraph to `HitTestBehavior.translucent` explaining that: - Both the translucent target and its descendant receive pointer events - Events are dispatched to the most specific target first (the descendant), then to the translucent target - In gesture arena competitions, the descendant typically wins because it enters the arena first (first come, first served) - Added "See also" cross-references to `HitTestResult.path` and `GestureDetector.behavior` for further reading ### Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features: every day submit a PR]. - [x] I listed at least one issue that is owned by the Flutter team or is a valid community contribution in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] All existing and new tests are passing. - [x] I signed the [CLA]. Fixes #28170 [Contributor Guide]: https://github.com/flutter/flutter/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ --------- Co-authored-by: Kate Lovett --- .../flutter/lib/src/rendering/proxy_box.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 83acb614649bb..a4336e881af27 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -155,6 +155,22 @@ enum HitTestBehavior { /// Translucent targets both receive events within their bounds and permit /// targets visually behind them to also receive events. + /// + /// When both a translucent target and its descendant are listening to the + /// same pointer event, both will receive it. Events are dispatched to the + /// most specific target first (the descendant), then to the translucent + /// target. In gesture arena competitions for the same gesture, the + /// descendant typically wins because it enters the arena first (first come, + /// first served). The translucent target's gesture is not invoked unless the + /// descendant's gesture is rejected or the descendant listens to a different + /// gesture. + /// + /// See also: + /// + /// * [HitTestResult.path], which describes the order in which hit test + /// entries receive events. + /// * [GestureDetector.behavior], which configures the hit test behavior + /// used for gesture detection. translucent, } From b5971cabf92bbc126be2a82bc87052af6aeae5a2 Mon Sep 17 00:00:00 2001 From: Hari07 <22373191+Hari-07@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:51:22 +0000 Subject: [PATCH 191/330] Add `ClipPath` optimizations to backdrop clips as well (#189118) - In `clip_path_layer.cc`, `ApplyClip()` has optimizations to convert to more efficient shapes if applicable. This PR extends that behaviour to backdrop clips as well - We previously didn't add the backdrop clip pushes to the mock embedder. This extends the mock embedder, adds coverage for the existing push backdrop clip methods as well as the various cases for backdrop path clip added in this PR Fixes https://github.com/flutter/flutter/issues/188971 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/flow/layers/clip_path_layer.cc | 16 ++++++- .../flow/layers/clip_path_layer_unittests.cc | 43 +++++++++++++++++++ .../flow/layers/clip_rect_layer_unittests.cc | 15 +++++++ .../flow/layers/clip_rrect_layer_unittests.cc | 16 +++++++ .../clip_rsuperellipse_layer_unittests.cc | 17 ++++++++ .../src/flutter/flow/testing/mock_embedder.cc | 24 +++++++++++ .../src/flutter/flow/testing/mock_embedder.h | 16 +++++++ 7 files changed, 146 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/flow/layers/clip_path_layer.cc b/engine/src/flutter/flow/layers/clip_path_layer.cc index dd578ccd7bf20..d8e232424088b 100644 --- a/engine/src/flutter/flow/layers/clip_path_layer.cc +++ b/engine/src/flutter/flow/layers/clip_path_layer.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "flutter/flow/layers/clip_path_layer.h" +#include "flutter/display_list/geometry/dl_geometry_types.h" namespace flutter { @@ -33,7 +34,20 @@ void ClipPathLayer::ApplyClip(LayerStateStack::MutatorContext& mutator) const { void ClipPathLayer::PushClipToEmbeddedNativeViewMutatorStack( ExternalViewEmbedder* view_embedder) const { - view_embedder->PushClipPathToVisitedPlatformViews(clip_shape()); + DlRect rect; + if (clip_shape().IsRect(&rect)) { + view_embedder->PushClipRectToVisitedPlatformViews(rect); + } else if (clip_shape().IsOval(&rect)) { + view_embedder->PushClipRRectToVisitedPlatformViews( + DlRoundRect::MakeOval(rect)); + } else { + DlRoundRect rrect; + if (clip_shape().IsRoundRect(&rrect)) { + view_embedder->PushClipRRectToVisitedPlatformViews(rrect); + } else { + view_embedder->PushClipPathToVisitedPlatformViews(clip_shape()); + } + } } } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_path_layer_unittests.cc b/engine/src/flutter/flow/layers/clip_path_layer_unittests.cc index 275b0fad89a47..9013c36918421 100644 --- a/engine/src/flutter/flow/layers/clip_path_layer_unittests.cc +++ b/engine/src/flutter/flow/layers/clip_path_layer_unittests.cc @@ -707,6 +707,49 @@ TEST_F(ClipPathLayerTest, EmptyClipDoesNotCullPlatformView) { EXPECT_EQ(embedder.painted_views(), std::vector({view_id})); } +// Prerolls a ClipPathLayer with a platform view child and returns the clips +// that the layer pushed to the view embedder. +static std::vector PushedClipsForPath(PrerollContext* context, + const DlPath& clip_path) { + auto platform_view = std::make_shared( + DlPoint(0.0f, 0.0f), DlSize(8.0f, 8.0f), 42); + auto clip = std::make_shared(clip_path, Clip::kHardEdge); + clip->Add(platform_view); + + MockViewEmbedder embedder; + context->view_embedder = &embedder; + clip->Preroll(context); + context->view_embedder = nullptr; + return embedder.pushed_clips(); +} + +TEST_F(ClipPathLayerTest, RectPathPushesClipRectToEmbedder) { + const DlRect rect = DlRect::MakeLTRB(2.0f, 2.0f, 12.0f, 12.0f); + EXPECT_EQ(PushedClipsForPath(preroll_context(), DlPath::MakeRect(rect)), + std::vector({Mutator(rect)})); +} + +TEST_F(ClipPathLayerTest, OvalPathPushesClipRRectToEmbedder) { + const DlRect bounds = DlRect::MakeLTRB(2.0f, 2.0f, 12.0f, 12.0f); + EXPECT_EQ(PushedClipsForPath(preroll_context(), DlPath::MakeOval(bounds)), + std::vector({Mutator(DlRoundRect::MakeOval(bounds))})); +} + +TEST_F(ClipPathLayerTest, RoundRectPathPushesClipRRectToEmbedder) { + const DlRoundRect rrect = DlRoundRect::MakeRectXY( + DlRect::MakeLTRB(2.0f, 2.0f, 12.0f, 12.0f), 3.0f, 3.0f); + EXPECT_EQ(PushedClipsForPath(preroll_context(), DlPath::MakeRoundRect(rrect)), + std::vector({Mutator(rrect)})); +} + +TEST_F(ClipPathLayerTest, ComplexPathPushesClipPathToEmbedder) { + const DlRect bounds = DlRect::MakeLTRB(2.0f, 2.0f, 12.0f, 12.0f); + const DlPath path = + DlPath::MakeRect(bounds) + DlPath::MakeRect(bounds.Expand(-1.0f, -1.0f)); + EXPECT_EQ(PushedClipsForPath(preroll_context(), path), + std::vector({Mutator(path)})); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_rect_layer_unittests.cc b/engine/src/flutter/flow/layers/clip_rect_layer_unittests.cc index 8162dc484f68b..f2ff8f77dadbf 100644 --- a/engine/src/flutter/flow/layers/clip_rect_layer_unittests.cc +++ b/engine/src/flutter/flow/layers/clip_rect_layer_unittests.cc @@ -545,6 +545,21 @@ TEST_F(ClipRectLayerTest, EmptyClipDoesNotCullPlatformView) { EXPECT_EQ(embedder.painted_views(), std::vector({view_id})); } +TEST_F(ClipRectLayerTest, PushesClipRectToEmbedder) { + auto platform_view = std::make_shared( + DlPoint(0.0f, 0.0f), DlSize(8.0f, 8.0f), 42); + const DlRect clip_rect = DlRect::MakeLTRB(2.0f, 2.0f, 12.0f, 12.0f); + auto clip = std::make_shared(clip_rect, Clip::kHardEdge); + clip->Add(platform_view); + + MockViewEmbedder embedder; + preroll_context()->view_embedder = &embedder; + clip->Preroll(preroll_context()); + preroll_context()->view_embedder = nullptr; + + EXPECT_EQ(embedder.pushed_clips(), std::vector({Mutator(clip_rect)})); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_rrect_layer_unittests.cc b/engine/src/flutter/flow/layers/clip_rrect_layer_unittests.cc index 69630c7adac32..525b55d4b5154 100644 --- a/engine/src/flutter/flow/layers/clip_rrect_layer_unittests.cc +++ b/engine/src/flutter/flow/layers/clip_rrect_layer_unittests.cc @@ -638,6 +638,22 @@ TEST_F(ClipRRectLayerTest, EmptyClipDoesNotCullPlatformView) { EXPECT_EQ(embedder.painted_views(), std::vector({view_id})); } +TEST_F(ClipRRectLayerTest, PushesClipRRectToEmbedder) { + auto platform_view = std::make_shared( + DlPoint(0.0f, 0.0f), DlSize(8.0f, 8.0f), 42); + const DlRoundRect clip_rrect = DlRoundRect::MakeRectXY( + DlRect::MakeLTRB(2.0f, 2.0f, 12.0f, 12.0f), 3.0f, 3.0f); + auto clip = std::make_shared(clip_rrect, Clip::kHardEdge); + clip->Add(platform_view); + + MockViewEmbedder embedder; + preroll_context()->view_embedder = &embedder; + clip->Preroll(preroll_context()); + preroll_context()->view_embedder = nullptr; + + EXPECT_EQ(embedder.pushed_clips(), std::vector({Mutator(clip_rrect)})); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc index 8a2bf4a88753a..8fb71c6dad954 100644 --- a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc +++ b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc @@ -622,6 +622,23 @@ TEST_F(ClipRSuperellipseLayerTest, EmptyClipDoesNotCullPlatformView) { EXPECT_EQ(embedder.painted_views(), std::vector({view_id})); } +TEST_F(ClipRSuperellipseLayerTest, PushesClipRSuperellipseToEmbedder) { + auto platform_view = std::make_shared( + DlPoint(0.0f, 0.0f), DlSize(8.0f, 8.0f), 42); + const DlRoundSuperellipse clip_rse = DlRoundSuperellipse::MakeRectXY( + DlRect::MakeLTRB(2.0f, 2.0f, 12.0f, 12.0f), 3.0f, 3.0f); + auto clip = + std::make_shared(clip_rse, Clip::kHardEdge); + clip->Add(platform_view); + + MockViewEmbedder embedder; + preroll_context()->view_embedder = &embedder; + clip->Preroll(preroll_context()); + preroll_context()->view_embedder = nullptr; + + EXPECT_EQ(embedder.pushed_clips(), std::vector({Mutator(clip_rse)})); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/flow/testing/mock_embedder.cc b/engine/src/flutter/flow/testing/mock_embedder.cc index 0fd228080e74d..cff261a72b49b 100644 --- a/engine/src/flutter/flow/testing/mock_embedder.cc +++ b/engine/src/flutter/flow/testing/mock_embedder.cc @@ -47,5 +47,29 @@ DlCanvas* MockViewEmbedder::CompositeEmbeddedView(int64_t view_id) { return canvas; } +// |ExternalViewEmbedder| +void MockViewEmbedder::PushClipRectToVisitedPlatformViews( + const DlRect& clip_rect) { + pushed_clips_.emplace_back(clip_rect); +} + +// |ExternalViewEmbedder| +void MockViewEmbedder::PushClipRRectToVisitedPlatformViews( + const DlRoundRect& clip_rrect) { + pushed_clips_.emplace_back(clip_rrect); +} + +// |ExternalViewEmbedder| +void MockViewEmbedder::PushClipRSuperellipseToVisitedPlatformViews( + const DlRoundSuperellipse& clip_rse) { + pushed_clips_.emplace_back(clip_rse); +} + +// |ExternalViewEmbedder| +void MockViewEmbedder::PushClipPathToVisitedPlatformViews( + const DlPath& clip_path) { + pushed_clips_.emplace_back(clip_path); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/flow/testing/mock_embedder.h b/engine/src/flutter/flow/testing/mock_embedder.h index fd2ac11d6f664..b0ce74de74326 100644 --- a/engine/src/flutter/flow/testing/mock_embedder.h +++ b/engine/src/flutter/flow/testing/mock_embedder.h @@ -41,13 +41,29 @@ class MockViewEmbedder : public ExternalViewEmbedder { // |ExternalViewEmbedder| DlCanvas* CompositeEmbeddedView(int64_t view_id) override; + // |ExternalViewEmbedder| + void PushClipRectToVisitedPlatformViews(const DlRect& clip_rect) override; + + // |ExternalViewEmbedder| + void PushClipRRectToVisitedPlatformViews( + const DlRoundRect& clip_rrect) override; + + // |ExternalViewEmbedder| + void PushClipRSuperellipseToVisitedPlatformViews( + const DlRoundSuperellipse& clip_rse) override; + + // |ExternalViewEmbedder| + void PushClipPathToVisitedPlatformViews(const DlPath& clip_path) override; + std::vector prerolled_views() const { return prerolled_views_; } std::vector painted_views() const { return painted_views_; } + std::vector pushed_clips() const { return pushed_clips_; } private: std::deque contexts_; std::vector prerolled_views_; std::vector painted_views_; + std::vector pushed_clips_; }; } // namespace testing From f20f4b26de25e41096e5c41c6256c4fd291a495b Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 11 Aug 2026 10:01:39 +0000 Subject: [PATCH 192/330] Roll Dart SDK from 558fb298e458 to c86be8702638 (3 revisions) (#190906) https://dart.googlesource.com/sdk.git/+log/558fb298e458..c86be8702638 2026-08-11 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-114.0.dev 2026-08-11 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-113.0.dev 2026-08-11 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-112.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC dart-vm-team@google.com,jimgraham@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 37ba85941be89..72fccfac5e6cd 100644 --- a/DEPS +++ b/DEPS @@ -55,7 +55,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': '558fb298e458b75551bc19aaecbd1243cb72c45f', + 'dart_revision': 'c86be87026381cce96e3c93cb810421ad3d19373', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From 24240702e68311e265718541e01c3639f744bf11 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Tue, 11 Aug 2026 12:54:09 +0000 Subject: [PATCH 193/330] flutter_tools: Use pub check-resolution-up-to-date & fix workspace package_config path (#186740) Fixes: https://github.com/dart-lang/pub/issues/4699 For pub workspaces we had a faulty constructed path based on the workspace_ref, which caused it to not find the package_config.json even if it was there. After fixing that there was still an issue that the flutter tool would check the timestamp of `pubspec.lock` against pubspec.yaml. But for a workspace there will be more than one `pubspec.yaml` to check against. Instead of replicating this logic in flutter we call out to `pub check-resolution-up-to-date` and have pub do the timestamp checking. This will add the overhead of a process invocation to the fast path (and also the slow path) but I think it is worth it for the encapsulation of pub logic into the pub client. --- packages/flutter_tools/lib/src/dart/pub.dart | 112 ++++---- .../test/general.shard/dart/pub_get_test.dart | 257 +++++++++++++++--- .../workspace_pub_get_test.dart | 129 +++++++++ 3 files changed, 405 insertions(+), 93 deletions(-) create mode 100644 packages/flutter_tools/test/integration.shard/workspace_pub_get_test.dart diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart index 2fea38ff61fc8..65ef2800ab100 100644 --- a/packages/flutter_tools/lib/src/dart/pub.dart +++ b/packages/flutter_tools/lib/src/dart/pub.dart @@ -253,53 +253,9 @@ class _DefaultPub implements Pub { PubOutputMode outputMode = PubOutputMode.all, }) async { final String directory = project.directory.path; - - // Here we use pub's private helper file to locate the package_config. - // In pub workspaces pub will generate a `.dart_tool/pub/workspace_ref.json` - // inside each workspace-package that refers to the workspace root where - // .dart_tool/package_config.json is located. - // - // By checking for this file instead of iterating parent directories until - // finding .dart_tool/package_config.json we will not mistakenly find a - // package_config.json from outside the workspace. - // - // TODO(sigurdm): avoid relying on pubs implementation details somehow? - final File workspaceRefFile = project.dartTool - .childDirectory('pub') - .childFile('workspace_ref.json'); - final File packageConfigFile; - if (workspaceRefFile.existsSync()) { - switch (jsonDecode(workspaceRefFile.readAsStringSync())) { - case {'workspaceRoot': final String workspaceRoot}: - packageConfigFile = _fileSystem.file( - _fileSystem.path.join(workspaceRefFile.parent.path, workspaceRoot), - ); - default: - // The workspace_ref.json file was malformed. Attempt to load the - // regular .dart_tool/package_config.json - // - // Most likely this doesn't exist, and we will get a new pub - // resolution. - // - // Alternatively this is a stray file somehow, and it can be ignored. - packageConfigFile = project.dartTool.childFile('package_config.json'); - } - } else { - packageConfigFile = project.dartTool.childFile('package_config.json'); - } - - if (packageConfigFile.existsSync()) { - final Directory workspaceRoot = packageConfigFile.parent.parent; - final File lastVersion = workspaceRoot.childDirectory('.dart_tool').childFile('version'); - final versionFromFile = FlutterVersion( - flutterRoot: Cache.flutterRoot!, - fs: _fileSystem, - git: _git, - ); - final File pubspecYaml = project.pubspecFile; - final File pubLockFile = workspaceRoot.childFile('pubspec.lock'); - - if (shouldSkipThirdPartyGenerator) { + if (shouldSkipThirdPartyGenerator) { + final File? packageConfigFile = findPackageConfigFile(project.directory); + if (packageConfigFile != null && packageConfigFile.existsSync()) { Map packageConfigMap; try { packageConfigMap = @@ -316,23 +272,32 @@ class _DefaultPub implements Pub { return; } } + } - // If the pubspec.yaml is older than the package config file and the last - // flutter version used is the same as the current version skip pub get. - // This will incorrectly skip pub on the master branch if dependencies - // are being added/removed from the flutter framework packages, but this - // can be worked around by manually running pub. - if (checkUpToDate && - pubLockFile.existsSync() && - pubspecYaml.lastModifiedSync().isBefore(pubLockFile.lastModifiedSync()) && - pubspecYaml.lastModifiedSync().isBefore(packageConfigFile.lastModifiedSync()) && - lastVersion.existsSync() && - lastVersion.readAsStringSync() == versionFromFile.frameworkVersion) { - _logger.printTrace('Skipping pub get: version match.'); - return; + var versionMatch = false; + if (checkUpToDate) { + final File? packageConfigFile = findPackageConfigFile(project.directory); + if (packageConfigFile != null && packageConfigFile.existsSync()) { + final Directory workspaceRoot = packageConfigFile.parent.parent; + final File lastVersion = workspaceRoot.childDirectory('.dart_tool').childFile('version'); + final versionFromFile = FlutterVersion( + flutterRoot: Cache.flutterRoot!, + fs: _fileSystem, + git: _git, + ); + versionMatch = + lastVersion.existsSync() && + lastVersion.readAsStringSync() == versionFromFile.frameworkVersion; } } + if (checkUpToDate && + versionMatch && + await _checkResolutionUpToDate(directory, context, flutterRootOverride)) { + _logger.printTrace('Skipping pub get: resolution up-to-date.'); + return; + } + final command = upgrade ? 'upgrade' : 'get'; final args = [ if (_logger.supportsColor) '--color', @@ -355,6 +320,33 @@ class _DefaultPub implements Pub { await _updateVersionAndPackageConfig(project); } + Future _checkResolutionUpToDate( + String directory, + PubContext context, + String? flutterRootOverride, + ) async { + final pubCommand = [ + ..._pubCommand, + '--directory', + _fileSystem.path.relative(directory), + 'check-resolution-up-to-date', + ]; + final Map pubEnvironment = await _createPubEnvironment( + context: context, + flutterRootOverride: flutterRootOverride, + ); + try { + final RunResult result = await _processUtils.run( + pubCommand, + workingDirectory: _fileSystem.path.current, + environment: pubEnvironment, + ); + return result.exitCode == 0; + } on io.ProcessException { + return false; + } + } + /// Runs pub with [arguments] and [ProcessStartMode.inheritStdio] mode. /// /// Uses [ProcessStartMode.normal] and [_stdio] if [Pub.test] constructor diff --git a/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart index 714c3d129a371..e6a9dd6a5d433 100644 --- a/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart +++ b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart @@ -57,6 +57,17 @@ void main() { group('shouldSkipThirdPartyGenerator', () { testUsingContext('does not skip pub get the parameter is false', () async { final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + exitCode: 1, + ), const FakeCommand( command: [ 'bin/cache/dart-sdk/bin/dart', @@ -77,6 +88,9 @@ void main() { fileSystem.file('bin/cache/flutter.version.json') ..createSync(recursive: true) ..writeAsStringSync(_generateFlutterVersionJson('b')); + fileSystem.file('.dart_tool/version') + ..createSync(recursive: true) + ..writeAsStringSync('b'); fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' @@ -113,6 +127,17 @@ void main() { 'does not skip pub get if package_config.json has "generator": "pub"', () async { final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + exitCode: 1, + ), const FakeCommand( command: [ 'bin/cache/dart-sdk/bin/dart', @@ -141,7 +166,7 @@ void main() { "generatorVersion": "2.14.0-276.0.dev" } '''); - fileSystem.file('.dart_tool/version').writeAsStringSync('a'); + fileSystem.file('.dart_tool/version').writeAsStringSync('b'); fileSystem.file('bin/cache/flutter.version.json') ..createSync(recursive: true) ..writeAsStringSync(_generateFlutterVersionJson('b')); @@ -170,6 +195,17 @@ void main() { 'does not skip pub get if package_config.json has "generator": "pub"', () async { final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + exitCode: 1, + ), const FakeCommand( command: [ 'bin/cache/dart-sdk/bin/dart', @@ -198,7 +234,7 @@ void main() { "generatorVersion": "2.14.0-276.0.dev" } '''); - fileSystem.file('.dart_tool/version').writeAsStringSync('a'); + fileSystem.file('.dart_tool/version').writeAsStringSync('b'); fileSystem.file('bin/cache/flutter.version.json') ..createSync(recursive: true) ..writeAsStringSync(_generateFlutterVersionJson('b')); @@ -257,41 +293,53 @@ void main() { }); }); - testUsingContext('checkUpToDate skips pub get if the package config is newer than the pubspec ' - 'and the current framework version is the same as the last version', () async { - final processManager = FakeProcessManager.empty(); - final logger = BufferLogger.test(); - final fileSystem = MemoryFileSystem.test(); + testUsingContext( + 'checkUpToDate skips pub get if the resolution is up-to-date and the current framework version is the same as the last version', + () async { + final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + ), + ]); + final logger = BufferLogger.test(); + final fileSystem = MemoryFileSystem.test(); - fileSystem.file('pubspec.yaml').createSync(); - fileSystem.file('pubspec.lock').createSync(); - fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); - fileSystem.file('.dart_tool/version').writeAsStringSync('a'); - fileSystem.file('bin/cache/flutter.version.json') - ..createSync(recursive: true) - ..writeAsStringSync(_generateFlutterVersionJson('a')); + fileSystem.file('pubspec.yaml').createSync(); + fileSystem.file('pubspec.lock').createSync(); + fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); + fileSystem.file('.dart_tool/version').writeAsStringSync('a'); + fileSystem.file('bin/cache/flutter.version.json') + ..createSync(recursive: true) + ..writeAsStringSync(_generateFlutterVersionJson('a')); - final pub = Pub.test( - fileSystem: fileSystem, - logger: logger, - processManager: processManager, - platform: FakePlatform(), - botDetector: const FakeBotDetector(false), - stdio: FakeStdio(), - ); + final pub = Pub.test( + fileSystem: fileSystem, + logger: logger, + processManager: processManager, + platform: FakePlatform(), + botDetector: const FakeBotDetector(false), + stdio: FakeStdio(), + ); - await pub.get( - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - context: PubContext.pubGet, - checkUpToDate: true, - ); + await pub.get( + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + context: PubContext.pubGet, + checkUpToDate: true, + ); - expect(logger.traceText, contains('Skipping pub get: version match.')); - }); + expect(logger.traceText, contains('Skipping pub get: resolution up-to-date.')); + }, + ); testUsingContext( - 'checkUpToDate does not skip pub get if the package config is newer than the pubspec ' - 'but the current framework version is not the same as the last version', + 'checkUpToDate does not skip pub get if the current framework version is not the same as the last version', () async { final processManager = FakeProcessManager.list([ const FakeCommand( @@ -338,8 +386,7 @@ void main() { ); testUsingContext( - 'checkUpToDate does not skip pub get if the package config is newer than the pubspec ' - 'but the current framework version does not exist yet', + 'checkUpToDate does not skip pub get if the last framework version file does not exist', () async { final processManager = FakeProcessManager.list([ const FakeCommand( @@ -384,6 +431,64 @@ void main() { }, ); + testUsingContext( + 'checkUpToDate does not skip pub get if the current framework version matches but resolution needs updating (exit code 1)', + () async { + final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + exitCode: 1, + ), + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'get', + '--example', + ], + ), + ]); + final logger = BufferLogger.test(); + final fileSystem = MemoryFileSystem.test(); + + fileSystem.file('pubspec.yaml').createSync(); + fileSystem.file('pubspec.lock').createSync(); + fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); + fileSystem.file('.dart_tool/version').writeAsStringSync('a'); + fileSystem.file('bin/cache/flutter.version.json') + ..createSync(recursive: true) + ..writeAsStringSync(_generateFlutterVersionJson('a')); + + final pub = Pub.test( + fileSystem: fileSystem, + logger: logger, + processManager: processManager, + platform: FakePlatform(), + botDetector: const FakeBotDetector(false), + stdio: FakeStdio(), + ); + + await pub.get( + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + context: PubContext.pubGet, + checkUpToDate: true, + ); + + expect(processManager, hasNoRemainingExpectations); + expect(fileSystem.file('.dart_tool/version').readAsStringSync(), 'a'); + }, + ); + testUsingContext( 'checkUpToDate does not skip pub get if the package config does not exist', () async { @@ -437,6 +542,17 @@ void main() { () async { final fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + exitCode: 1, + ), const FakeCommand( command: [ 'bin/cache/dart-sdk/bin/dart', @@ -482,6 +598,17 @@ void main() { 'checkUpToDate does not skip pub get if the package config is older that the pubspec', () async { final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + exitCode: 1, + ), const FakeCommand( command: [ 'bin/cache/dart-sdk/bin/dart', @@ -502,6 +629,7 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..setLastModifiedSync(DateTime(1991)); + fileSystem.file('.dart_tool/version').writeAsStringSync('b'); fileSystem.file('bin/cache/flutter.version.json') ..createSync(recursive: true) ..writeAsStringSync(_generateFlutterVersionJson('b')); @@ -530,6 +658,17 @@ void main() { 'checkUpToDate does not skip pub get if the pubspec.lock is older that the pubspec', () async { final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + '.', + 'check-resolution-up-to-date', + ], + exitCode: 1, + ), const FakeCommand( command: [ 'bin/cache/dart-sdk/bin/dart', @@ -1149,6 +1288,58 @@ exit code: 66 ); // because nothing should touch it logger.clear(); }); + + testUsingContext( + 'checkUpToDate works and calls check-resolution-up-to-date in a workspace context', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory pkg = fileSystem.directory('workspace_pkg')..createSync(recursive: true); + + final processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'bin/cache/dart-sdk/bin/dart', + 'pub', + '--suppress-analytics', + '--directory', + 'workspace_pkg', + 'check-resolution-up-to-date', + ], + ), + ]); + final logger = BufferLogger.test(); + + // Workspace package setup + pkg.childFile('pubspec.yaml').createSync(); + fileSystem.file('pubspec.lock').createSync(); + fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); + pkg.childDirectory('.dart_tool').childDirectory('pub').childFile('workspace_ref.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"workspaceRoot": "../../../"}'); + fileSystem.file('bin/cache/flutter.version.json') + ..createSync(recursive: true) + ..writeAsStringSync(_generateFlutterVersionJson('a')); + fileSystem.file('.dart_tool/version').writeAsStringSync('a'); + + final pub = Pub.test( + fileSystem: fileSystem, + logger: logger, + processManager: processManager, + platform: FakePlatform(), + botDetector: const FakeBotDetector(false), + stdio: FakeStdio(), + ); + + await pub.get( + project: FlutterProject.fromDirectoryTest(pkg), + context: PubContext.pubGet, + checkUpToDate: true, + ); + + expect(logger.traceText, contains('Skipping pub get: resolution up-to-date.')); + expect(processManager, hasNoRemainingExpectations); + }, + ); } String _generateFlutterVersionJson(String version) { diff --git a/packages/flutter_tools/test/integration.shard/workspace_pub_get_test.dart b/packages/flutter_tools/test/integration.shard/workspace_pub_get_test.dart new file mode 100644 index 0000000000000..ec100375864af --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/workspace_pub_get_test.dart @@ -0,0 +1,129 @@ +// 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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:process/process.dart'; + +import '../src/common.dart'; +import 'isolated/native_assets_test_utils.dart'; +import 'test_utils.dart'; + +void main() { + const ProcessManager processManager = LocalProcessManager(); + + testWithoutContext( + 'flutter tools correctly skip pub get when resolution is up-to-date in a workspace', + () async { + await inTempDir((Directory tempDir) async { + final Directory workspaceDir = tempDir.childDirectory('my_workspace'); + final Directory appDir = workspaceDir.childDirectory('packages').childDirectory('my_app'); + + // 1. Create workspace root pubspec + workspaceDir.createSync(recursive: true); + workspaceDir.childFile('pubspec.yaml').writeAsStringSync(''' +name: my_workspace +environment: + sdk: ^3.10.0-0 +workspace: + - packages/my_app +'''); + + // 2. Create sub-package app pubspec and main.dart + appDir.createSync(recursive: true); + appDir.childFile('pubspec.yaml').writeAsStringSync(''' +name: my_app +environment: + sdk: ^3.10.0-0 +resolution: workspace +dependencies: + flutter: + sdk: flutter +'''); + appDir.childDirectory('lib').childFile('main.dart') + ..createSync(recursive: true) + ..writeAsStringSync('void main() {}'); + + // 3. Run initial pub get to resolve the workspace + final io.ProcessResult getResult = await processManager.run([ + flutterBin, + '--verbose', + 'pub', + 'get', + ], workingDirectory: appDir.path); + expect( + getResult.exitCode, + 0, + reason: 'Initial pub get failed:\n${getResult.stderr}\n${getResult.stdout}', + ); + + // Validate workspace output structure + expect( + workspaceDir.childDirectory('.dart_tool').childFile('package_config.json').existsSync(), + true, + ); + expect(workspaceDir.childFile('pubspec.lock').existsSync(), true); + expect( + appDir + .childDirectory('.dart_tool') + .childDirectory('pub') + .childFile('workspace_ref.json') + .existsSync(), + true, + ); + + // 4. Run flutter analyze once to populate any SDK caches/unpacks + final io.ProcessResult analyzeResultPrep = await processManager.run([ + flutterBin, + '--verbose', + 'analyze', + ], workingDirectory: appDir.path); + io.stderr.writeln( + 'PREP ANALYZE OUTPUT:\n${analyzeResultPrep.stdout}\n${analyzeResultPrep.stderr}', + ); + + // 5. Run flutter analyze again and verify that it skips pub get + final io.ProcessResult analyzeResult1 = await processManager.run([ + flutterBin, + '--verbose', + 'analyze', + ], workingDirectory: appDir.path); + + expect(analyzeResult1.exitCode, 0); + expect( + analyzeResult1.stdout.toString(), + contains('Skipping pub get: resolution up-to-date.'), + ); + expect(analyzeResult1.stdout.toString(), isNot(contains('get --example'))); + + // 6. Dirty the pubspec to invalidate resolution + appDir.childFile('pubspec.yaml').writeAsStringSync(''' +name: my_app +environment: + sdk: ^3.10.0-0 +resolution: workspace +dependencies: + flutter: + sdk: flutter +# Dirty comment to trigger out-of-date resolution +'''); + + // 7. Run flutter analyze again and verify it does NOT skip pub get + final io.ProcessResult analyzeResult2 = await processManager.run([ + flutterBin, + '--verbose', + 'analyze', + ], workingDirectory: appDir.path); + + expect(analyzeResult2.exitCode, 0); + expect( + analyzeResult2.stdout.toString(), + isNot(contains('Skipping pub get: resolution up-to-date.')), + ); + expect(analyzeResult2.stdout.toString(), contains('get --example')); + }); + }, + ); +} From dc0a602f0ed14fbcd3270ce3ea1ff15f3fe75be8 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 11 Aug 2026 15:50:49 +0000 Subject: [PATCH 194/330] Roll Packages from 1861b68e9695 to aaaf246a1c18 (5 revisions) (#190923) https://github.com/flutter/packages/compare/1861b68e9695...aaaf246a1c18 2026-08-10 jmccandless@google.com [cupertino_ui] Main example (flutter/packages#12380) 2026-08-10 katelovett@google.com Update ci for + releases (flutter/packages#12419) 2026-08-10 engine-flutter-autoroll@skia.org Manual roll Flutter from b766512c65d8 to 27b098811f3b (29 revisions) (flutter/packages#12420) 2026-08-10 engine-flutter-autoroll@skia.org Manual roll Flutter from e52f01c920ad to b766512c65d8 (42 revisions) (flutter/packages#12406) 2026-08-10 41930132+hellohuanlin@users.noreply.github.com [camera]fix test flake due to expectation fulfilled before flag is toggled (flutter/packages#12400) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- bin/internal/flutter_packages.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 6b5d61f416c3b..6bc53c372f369 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -1861b68e9695df1f592bd1b922888cdc120d1d7a +aaaf246a1c18570682055aa08981652c67bde9a8 From 05ee849d12498b91ce7fb11698f221201f0d8abc Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 11 Aug 2026 16:05:29 +0000 Subject: [PATCH 195/330] [flutter_tools] Fix package URI resolution in --experimental-faster-testing (#190875) When `--experimental-faster-testing` is enabled, the runner reads the package config using `Uri.file(flutterProject.directory.path)` which lacked a trailing slash. This caused relative resolution (using `../`) to resolve to the parent of the project directory (one level too high). This fix replaces the base URI with `packageConfigFile.uri` which correctly resolves. A regression test verifying the output `rootUri` was added. Fixes #190784 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter_tools/lib/src/test/runner.dart | 6 +++--- .../test/commands.shard/hermetic/test_test.dart | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart index 7578dcb277bca..e3386e3ed198c 100644 --- a/packages/flutter_tools/lib/src/test/runner.dart +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -216,7 +216,7 @@ interface class FlutterTestRunner { if (packageConfigFile.existsSync()) { projectPackageConfig = PackageConfig.parseBytes( packageConfigFile.readAsBytesSync(), - Uri.file(flutterProject.directory.path), + packageConfigFile.absolute.uri, ); } else { // We can't use this directly, but need to manually check @@ -240,7 +240,7 @@ interface class FlutterTestRunner { .childFile('package_config.json'); final PackageConfig flutterToolsPackageConfig = PackageConfig.parseBytes( flutterToolsPackageConfigFile.readAsBytesSync(), - flutterToolsPackageConfigFile.uri, + flutterToolsPackageConfigFile.absolute.uri, ); final mergedPackages = [...projectPackageConfig.packages]; @@ -629,7 +629,7 @@ class SpawnPlugin extends PlatformPlugin { ); final PackageConfig isolateSpawningTesterPackageConfig = PackageConfig.parseBytes( isolateSpawningTesterPackageConfigFile.readAsBytesSync(), - isolateSpawningTesterPackageConfigFile.uri, + isolateSpawningTesterPackageConfigFile.absolute.uri, ); final File childTestIsolateSpawnerSourceFile = isolateSpawningTesterDirectory.childFile( diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index fc904a53419c4..07fe32b2a145a 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; @@ -669,6 +670,13 @@ resolution: workspace expect(configContents.contains('"name": "test"'), true); expect(configContents.contains('"name": "test_api"'), true); expect(configContents.contains('"name": "test_core"'), true); + + final config = json.decode(configContents) as Map; + final packages = config['packages'] as List; + final Map myApp = packages.cast>().firstWhere( + (Map p) => p['name'] == 'my_app', + ); + expect(myApp['rootUri'], fs.directory('/package').absolute.uri.toString()); }, ); expect(caughtToolExit, true); From 0d95c365c73f5413e3ba1fac56d7a36015586f7a Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 11 Aug 2026 16:41:27 +0000 Subject: [PATCH 196/330] [flutter_tools] Setup Pub Workspace and package structure for flutter_tools sub-packages (#190733) Part of the tool extensibility prototype effort. Establishes the Pub Workspace root in `packages/flutter_tools/pubspec.yaml` and introduces the sub-package hierarchy: - `packages/flutter_tools_core` - `packages/flutter_tools_extension` - `packages/flutter_tools_extension_linux_prototype` Includes unit test coverage verifying workspace configuration in `test/general.shard/pub_workspace_test.dart`. Part of #190745 Part of #190755 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/flutter_tools_core.dart | 13 +++++ .../packages/flutter_tools_core/pubspec.yaml | 14 +++++ .../lib/flutter_tools_extension.dart | 13 +++++ .../flutter_tools_extension/pubspec.yaml | 13 +++++ ...utter_tools_extension_linux_prototype.dart | 12 ++++ .../pubspec.yaml | 18 ++++++ packages/flutter_tools/pubspec.yaml | 13 ++++- .../general.shard/pub_workspace_test.dart | 58 +++++++++++++++++++ pubspec.lock | 21 +++++++ 9 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 packages/flutter_tools/packages/flutter_tools_core/lib/flutter_tools_core.dart create mode 100644 packages/flutter_tools/packages/flutter_tools_core/pubspec.yaml create mode 100644 packages/flutter_tools/packages/flutter_tools_extension/lib/flutter_tools_extension.dart create mode 100644 packages/flutter_tools/packages/flutter_tools_extension/pubspec.yaml create mode 100644 packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/lib/flutter_tools_extension_linux_prototype.dart create mode 100644 packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/pubspec.yaml create mode 100644 packages/flutter_tools/test/general.shard/pub_workspace_test.dart diff --git a/packages/flutter_tools/packages/flutter_tools_core/lib/flutter_tools_core.dart b/packages/flutter_tools/packages/flutter_tools_core/lib/flutter_tools_core.dart new file mode 100644 index 0000000000000..891c430cb5a31 --- /dev/null +++ b/packages/flutter_tools/packages/flutter_tools_core/lib/flutter_tools_core.dart @@ -0,0 +1,13 @@ +// 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. + +/// Core data structures, contracts, and host-side abstractions for Flutter +/// tools extensibility. +/// +/// This library provides platform-agnostic representations of devices, +/// diagnostic validators, project templates, configuration flags, and plugin +/// bindings shared across host tools and extension packages. +library flutter_tools_core; + +// TODO(bkonyi): export core domain models and contracts. diff --git a/packages/flutter_tools/packages/flutter_tools_core/pubspec.yaml b/packages/flutter_tools/packages/flutter_tools_core/pubspec.yaml new file mode 100644 index 0000000000000..56be4a7894966 --- /dev/null +++ b/packages/flutter_tools/packages/flutter_tools_core/pubspec.yaml @@ -0,0 +1,14 @@ +name: flutter_tools_core +description: Core definitions, data models, and host adapters for Flutter tools extensibility. +resolution: workspace + +environment: + sdk: ^3.11.0-0 + +dependencies: + meta: 1.19.0 + +dev_dependencies: + test: 1.31.1 + +# PUBSPEC CHECKSUM: ttq919 diff --git a/packages/flutter_tools/packages/flutter_tools_extension/lib/flutter_tools_extension.dart b/packages/flutter_tools/packages/flutter_tools_extension/lib/flutter_tools_extension.dart new file mode 100644 index 0000000000000..028119e1351ce --- /dev/null +++ b/packages/flutter_tools/packages/flutter_tools_extension/lib/flutter_tools_extension.dart @@ -0,0 +1,13 @@ +// 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. + +/// Generic extension protocol base classes and extension interfaces for Flutter +/// tools extensibility. +/// +/// This package defines the platform-agnostic RPC protocol framing, handshake, +/// and service handler interfaces (`DeviceExtension`, `DiagnosticsExtension`, +/// `ConfigurationExtension`, etc.) implemented by extension authors. +library flutter_tools_extension; + +// TODO(bkonyi): export extension protocol base classes and service interfaces. diff --git a/packages/flutter_tools/packages/flutter_tools_extension/pubspec.yaml b/packages/flutter_tools/packages/flutter_tools_extension/pubspec.yaml new file mode 100644 index 0000000000000..3ad021d8b1540 --- /dev/null +++ b/packages/flutter_tools/packages/flutter_tools_extension/pubspec.yaml @@ -0,0 +1,13 @@ +name: flutter_tools_extension +description: Protocol interfaces and base implementation for Flutter tools extensions. +resolution: workspace + +environment: + sdk: ^3.11.0-0 + +dependencies: + meta: 1.19.0 + flutter_tools_core: + path: ../flutter_tools_core + +# PUBSPEC CHECKSUM: foiaro diff --git a/packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/lib/flutter_tools_extension_linux_prototype.dart b/packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/lib/flutter_tools_extension_linux_prototype.dart new file mode 100644 index 0000000000000..ee0ae6b315683 --- /dev/null +++ b/packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/lib/flutter_tools_extension_linux_prototype.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. + +/// Prototype Linux platform extension package for Flutter tools extensibility. +/// +/// This package encapsulates all Linux-specific device detection, doctor check +/// diagnostics, build handlers, and project templates for the custom Linux +/// extension prototype. +library flutter_tools_extension_linux_prototype; + +// TODO(bkonyi): implement prototype Linux extension entrypoint and services. diff --git a/packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/pubspec.yaml b/packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/pubspec.yaml new file mode 100644 index 0000000000000..3e4d6af5c9e05 --- /dev/null +++ b/packages/flutter_tools/packages/flutter_tools_extension_linux_prototype/pubspec.yaml @@ -0,0 +1,18 @@ +name: flutter_tools_extension_linux_prototype +description: Prototype Linux extension for Flutter tools extensibility. +resolution: workspace + +environment: + sdk: ^3.11.0-0 + +dependencies: + meta: 1.19.0 + flutter_tools_core: + path: ../flutter_tools_core + flutter_tools_extension: + path: ../flutter_tools_extension + +dev_dependencies: + test: 1.31.1 + +# PUBSPEC CHECKSUM: i74lku diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 6fd2663d19a12..692f020774325 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -5,6 +5,11 @@ homepage: https://flutter.dev environment: sdk: ^3.11.0-0 +workspace: + - packages/flutter_tools_core + - packages/flutter_tools_extension + - packages/flutter_tools_extension_linux_prototype + dependencies: # To update these, use "flutter update-packages --force-upgrade". # @@ -22,6 +27,12 @@ dependencies: crypto: 3.0.7 ffi: 2.2.0 file: 7.0.1 + flutter_tools_core: + path: packages/flutter_tools_core + flutter_tools_extension: + path: packages/flutter_tools_extension + flutter_tools_extension_linux_prototype: + path: packages/flutter_tools_extension_linux_prototype flutter_template_images: 5.0.0 html: 0.15.6 http: 1.6.0 @@ -129,4 +140,4 @@ dartdoc: nodoc: true -# PUBSPEC CHECKSUM: d0mren +# PUBSPEC CHECKSUM: abnfae diff --git a/packages/flutter_tools/test/general.shard/pub_workspace_test.dart b/packages/flutter_tools/test/general.shard/pub_workspace_test.dart new file mode 100644 index 0000000000000..94c9b1cce631e --- /dev/null +++ b/packages/flutter_tools/test/general.shard/pub_workspace_test.dart @@ -0,0 +1,58 @@ +// 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_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:yaml/yaml.dart'; + +import '../src/common.dart'; + +void main() { + final FileSystem fs = globals.localFileSystem; + final String flutterToolsRoot = fs.path.join(getFlutterRoot(), 'packages', 'flutter_tools'); + const expectedWorkspaceMembers = [ + 'packages/flutter_tools_core', + 'packages/flutter_tools_extension', + 'packages/flutter_tools_extension_linux_prototype', + ]; + + group('Flutter Tools Pub Workspace', () { + testWithoutContext('root pubspec.yaml declares all sub-packages in workspace', () { + final File pubspecFile = fs.file(fs.path.join(flutterToolsRoot, 'pubspec.yaml')); + expect(pubspecFile.existsSync(), isTrue); + + final Object? yamlContent = loadYaml(pubspecFile.readAsStringSync()); + expect(yamlContent, isA()); + final yamlMap = yamlContent! as YamlMap; + + expect(yamlMap.containsKey('workspace'), isTrue); + final List workspaceList = (yamlMap['workspace'] as YamlList).cast().toList(); + + for (final member in expectedWorkspaceMembers) { + expect(workspaceList, contains(member), reason: 'Expected workspace to include $member'); + } + }); + + testWithoutContext('member pubspec.yaml files declare workspace resolution', () { + for (final member in expectedWorkspaceMembers) { + final File memberPubspec = fs.file(fs.path.join(flutterToolsRoot, member, 'pubspec.yaml')); + expect( + memberPubspec.existsSync(), + isTrue, + reason: 'Expected pubspec.yaml to exist for $member', + ); + + final Object? yamlContent = loadYaml(memberPubspec.readAsStringSync()); + expect(yamlContent, isA()); + final yamlMap = yamlContent! as YamlMap; + + expect( + yamlMap['resolution'], + equals('workspace'), + reason: 'Expected $member to declare resolution: workspace', + ); + } + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 762cd8e0aaed9..29b29050975ae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -410,6 +410,27 @@ packages: relative: true source: path version: "0.0.0" + flutter_tools_core: + dependency: transitive + description: + path: "packages/flutter_tools/packages/flutter_tools_core" + relative: true + source: path + version: "0.0.0" + flutter_tools_extension: + dependency: transitive + description: + path: "packages/flutter_tools/packages/flutter_tools_extension" + relative: true + source: path + version: "0.0.0" + flutter_tools_extension_linux_prototype: + dependency: transitive + description: + path: "packages/flutter_tools/packages/flutter_tools_extension_linux_prototype" + relative: true + source: path + version: "0.0.0" frontend_server_client: dependency: "direct main" description: From def08f458b94eaeea28b261a9dece0fcdfc7265b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0hsan=20G=C3=B6rgel?= Date: Tue, 11 Aug 2026 16:52:10 +0000 Subject: [PATCH 197/330] Wait for web rendering before first-frame event (#189500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `flutter-first-frame` browser event currently fires as soon as the framework sends the `flutter/service_worker` platform message. Web scene rendering is asynchronous, so the event can remove an HTML splash while SkWasm is still rasterizing and briefly expose a blank frame. This change tracks scene renders that are still in progress, waits for them, and dispatches the event on the next browser animation frame. The tests cover both the asynchronous browser-event contract and an explicitly gated scene render, so the latter proves that the event cannot overtake rasterization. Fixes #189499. ## Validation - `felt test --gcs-prod --browser chrome test/engine/window_test.dart test/ui/async_rendering_test.dart` passed in all seven selected suites: - dart2js / CanvasKit (Chromium and full variants) - dart2wasm / CanvasKit - dart2wasm / SkWasm (threaded and forced single-threaded) - dart2wasm / Wimp - `dart analyze` reported no issues for the implementation and both test files. - A minimal SkWasm reproduction exposed a blank compositor frame after the event in 3/3 cold runs before this change. The first visible Flutter frame followed the event by 90.3–143.0 ms. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`), or this change does not alter public API documentation. - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Co-authored-by: zhongliugo --- .../lib/src/engine/platform_dispatcher.dart | 74 ++++++++- .../lib/web_ui/test/engine/window_test.dart | 53 +++++-- .../web_ui/test/ui/async_rendering_test.dart | 144 ++++++++++++++++++ 3 files changed, 254 insertions(+), 17 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index bfc15058b57c3..f6ec6cc9194c6 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -58,6 +58,33 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { final Arena frameArena = Arena(); + /// The number of renders requested by the first frame that have not settled + /// yet. + /// + /// A multi-view app renders one scene per view, so the first frame is not + /// settled until all of them are. A render whose scene is superseded by a + /// newer one counts as settled, because the view will display the newer scene + /// instead. + int _pendingFirstFrameRenders = 0; + + /// Whether the first frame is still being built, and can therefore still + /// request renders. + /// + /// This is false for the rest of the app's lifetime once the first frame ends, + /// so renders requested by later frames are not counted, even while the first + /// frame's own renders are still in flight. + bool _isBuildingFirstFrame = true; + + /// Completes once every render the first frame requested has settled, and is + /// then set to null to stop tracking renders for the rest of the app's + /// lifetime. + /// + /// The framework reports its first frame from a post-frame callback, while web + /// renderers may still be rasterizing that frame asynchronously. The browser + /// event waits on this so it is not sent while the first frame is still + /// rasterizing. + Completer? _firstFrameCompleter = Completer(); + /// The [EnginePlatformDispatcher] singleton. static EnginePlatformDispatcher get instance => _instance; static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher(); @@ -274,6 +301,13 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { invoke(_onDrawFrame, _onDrawFrameZone); _viewsRenderedInCurrentFrame = null; frameArena.collect(); + if (_isBuildingFirstFrame) { + // The first frame has requested every render it is ever going to request. + // A frame that rendered nothing is still a first frame, so there may be + // nothing left to wait for. + _isBuildingFirstFrame = false; + _completeFirstFrameIfRendered(); + } } /// A callback that is invoked when pointer data is available. @@ -563,7 +597,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // Dispatched by the bindings to delay service worker initialization. case 'flutter/service_worker': - domWindow.dispatchEvent(createDomEvent('Event', 'flutter-first-frame')); + unawaited(_dispatchFirstFrameEventAfterRender()); return; case 'flutter/textinput': @@ -756,8 +790,44 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // view hasn't been rendered already in this scope. final bool shouldRender = _viewsRenderedInCurrentFrame?.add(target) ?? false; if (shouldRender) { - await renderer.renderScene(scene, target); + final Future sceneRender = renderer.renderScene(scene, target); + // The first frame ends while this render is still in flight, so remember + // whether it belongs to it instead of asking again below. + final bool isFirstFrameRender = _isBuildingFirstFrame; + if (isFirstFrameRender) { + _pendingFirstFrameRenders++; + } + try { + await sceneRender; + } finally { + if (isFirstFrameRender) { + _pendingFirstFrameRenders--; + _completeFirstFrameIfRendered(); + } + } + } + } + + /// Completes [_firstFrameCompleter] once the first frame is done building and + /// every render it requested has settled. + /// + /// A render that failed counts as settled: [render] reports the failure to + /// its caller, and withholding the browser event would leave an app that hides + /// its loading screen on that event stuck on it forever. + void _completeFirstFrameIfRendered() { + if (_isBuildingFirstFrame || _pendingFirstFrameRenders > 0) { + return; } + _firstFrameCompleter?.complete(); + // Null it out to completely disable tracking for all future frames. + _firstFrameCompleter = null; + } + + Future _dispatchFirstFrameEventAfterRender() async { + await _firstFrameCompleter?.future; + domWindow.requestAnimationFrame((_) { + domWindow.dispatchEvent(createDomEvent('Event', 'flutter-first-frame')); + }); } @override diff --git a/engine/src/flutter/lib/web_ui/test/engine/window_test.dart b/engine/src/flutter/lib/web_ui/test/engine/window_test.dart index 8d3f065886d2d..fab35ddd2a50d 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/window_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/window_test.dart @@ -546,23 +546,46 @@ void testMain() { expect(localeChangedCount, 1); }); - test('dispatches browser event on flutter/service_worker channel', () async { - final completer = Completer(); - domWindow.addEventListener( - 'flutter-first-frame', - createDomEventListener((DomEvent e) => completer.complete()), - ); - final Zone innerZone = Zone.current.fork(); + test('dispatches browser event asynchronously on flutter/service_worker channel', () async { + // Each dispatcher tracks its own first frame, and only the first one. The + // singleton's is already over by now, because previous tests pump frames on + // it, so this test brings its own dispatcher, like 'registration' below, to + // make the frame it pumps really the first one. + final ownDispatcher = EnginePlatformDispatcher(); + addTearDown(ownDispatcher.dispose); + + var eventCount = 0; + final DomEventListener listener = createDomEventListener((DomEvent e) => eventCount++); + domWindow.addEventListener('flutter-first-frame', listener); + addTearDown(() => domWindow.removeEventListener('flutter-first-frame', listener)); + + // The event is dispatched from an animation frame callback, so an animation + // frame is long enough to observe one that was sent. + Future awaitAnimationFrame() { + final completer = Completer(); + domWindow.requestAnimationFrame((_) => Timer.run(completer.complete)); + return completer.future; + } - innerZone.runGuarded(() { - myWindow.sendPlatformMessage( - 'flutter/service_worker', - ByteData(0), - (ByteData? outputData) {}, - ); - }); + ownDispatcher.sendPlatformMessage( + 'flutter/service_worker', + ByteData(0), + (ByteData? outputData) {}, + ); - await expectLater(completer.future, completes); + // The event waits for the first frame to be on screen, which hasn't happened + // yet. + await awaitAnimationFrame(); + expect(eventCount, 0); + + // This frame renders no scene, but a first frame that rendered nothing is + // still a first frame, so ending it releases the event with nothing left to + // wait for. Ending the frame resolves what the dispatch is waiting on + // synchronously, so the dispatch is queued from a microtask, which runs + // before the animation frame below. + ownDispatcher.invokeOnDrawFrame(); + await awaitAnimationFrame(); + expect(eventCount, 1); }); test('sets global html attributes', () { diff --git a/engine/src/flutter/lib/web_ui/test/ui/async_rendering_test.dart b/engine/src/flutter/lib/web_ui/test/ui/async_rendering_test.dart index dd8c28c83012b..ccdc7166d4fe6 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/async_rendering_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/async_rendering_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -89,6 +90,149 @@ void testMain() { printWarning = originalPrintWarning; }); + test('first-frame browser event waits for the first frame to be rendered', () async { + // Each dispatcher tracks only its own first frame, so this test brings its + // own and drives its `onDrawFrame` directly, instead of going through the + // singleton via `renderScene`, to stay independent of what ran before it. + final ownDispatcher = EnginePlatformDispatcher(); + addTearDown(ownDispatcher.dispose); + + final EngineFlutterView view = EnginePlatformDispatcher.instance.implicitView!; + final displayFactory = DisplayCanvasFactory( + createCanvas: () => FakeDisplayCanvas(), + ); + final testRasterizer = TestRasterizer(view, displayFactory) + ..prepareCompleter = Completer(); + final ViewRasterizer originalRasterizer = renderer.rasterizers[view.viewId]!; + renderer.rasterizers[view.viewId] = testRasterizer; + addTearDown(() => renderer.rasterizers[view.viewId] = originalRasterizer); + + var firstFrameEventCount = 0; + final firstFrameEvent = Completer(); + final DomEventListener listener = createDomEventListener((DomEvent event) { + firstFrameEventCount++; + if (!firstFrameEvent.isCompleted) { + firstFrameEvent.complete(); + } + }); + domWindow.addEventListener('flutter-first-frame', listener); + addTearDown(() => domWindow.removeEventListener('flutter-first-frame', listener)); + + // Wait for next frame. + Future waitForAnimationFrame() { + final completer = Completer(); + domWindow.requestAnimationFrame((_) { + Timer.run(completer.complete); + }); + return completer.future; + } + + // `render` only draws anything in an `onDrawFrame` scope, so a frame is a + // scene rendered from within `onDrawFrame`. + Future renderFrame() { + final ui.Scene scene = ui.SceneBuilder().build(); + addTearDown(scene.dispose); + late final Future rendered; + ownDispatcher.onDrawFrame = () { + rendered = ownDispatcher.render(scene, view); + }; + ownDispatcher.invokeOnDrawFrame(); + return rendered; + } + + final Future renderFuture = renderFrame(); + ownDispatcher.sendPlatformMessage( + 'flutter/service_worker', + ByteData(0), + (ByteData? response) {}, + ); + + // The render is parked in `prepareToDraw`, so nothing is on screen yet. The + // event is dispatched from an animation frame callback, so waiting for one + // is enough to observe it if it were sent too early. + await waitForAnimationFrame(); + expect(firstFrameEventCount, 0); + + testRasterizer.prepareCompleter!.complete(); + await renderFuture; + await expectLater(firstFrameEvent.future, completes); + + // Renders requested by later frames are not tracked, and don't dispatch the + // event again, no matter how many frames go by. + await renderFrame(); + await waitForAnimationFrame(); + expect(firstFrameEventCount, 1); + + await renderFrame(); + await waitForAnimationFrame(); + expect(firstFrameEventCount, 1); + }); + + test('first-frame browser event is still sent if a first-frame render fails', () async { + // Each dispatcher tracks only its own first frame, so this test brings its + // own and drives its `onDrawFrame` directly instead of going through + // `renderScene`, to stay independent of what ran before it. + final ownDispatcher = EnginePlatformDispatcher(); + addTearDown(ownDispatcher.dispose); + + final EngineFlutterView view = EnginePlatformDispatcher.instance.implicitView!; + final displayFactory = DisplayCanvasFactory( + createCanvas: () => FakeDisplayCanvas(), + ); + final testRasterizer = TestRasterizer(view, displayFactory) + ..prepareCompleter = Completer(); + final ViewRasterizer originalRasterizer = renderer.rasterizers[view.viewId]!; + renderer.rasterizers[view.viewId] = testRasterizer; + addTearDown(() => renderer.rasterizers[view.viewId] = originalRasterizer); + + var firstFrameEventCount = 0; + final firstFrameEvent = Completer(); + final DomEventListener listener = createDomEventListener((DomEvent event) { + firstFrameEventCount++; + if (!firstFrameEvent.isCompleted) { + firstFrameEvent.complete(); + } + }); + domWindow.addEventListener('flutter-first-frame', listener); + addTearDown(() => domWindow.removeEventListener('flutter-first-frame', listener)); + + // Wait for next frame. + Future waitForAnimationFrame() { + final completer = Completer(); + domWindow.requestAnimationFrame((_) { + Timer.run(completer.complete); + }); + return completer.future; + } + + final ui.Scene scene = ui.SceneBuilder().build(); + addTearDown(scene.dispose); + late final Future renderFuture; + ownDispatcher.onDrawFrame = () { + renderFuture = ownDispatcher.render(scene, view); + }; + ownDispatcher.invokeOnDrawFrame(); + ownDispatcher.sendPlatformMessage( + 'flutter/service_worker', + ByteData(0), + (ByteData? response) {}, + ); + + // The render is parked in `prepareToDraw`, so the first frame is not + // settled yet and the event is withheld. + await waitForAnimationFrame(); + expect(firstFrameEventCount, 0); + + // Failing the render settles the first frame too: `render` reports the + // failure to its caller, and the event still goes out, so an app hiding its + // loading screen on the event is not stuck on a failed frame. + testRasterizer.prepareCompleter!.completeError(StateError('render failed')); + await expectLater(renderFuture, throwsStateError); + await expectLater(firstFrameEvent.future, completes); + await waitForAnimationFrame(); + expect(firstFrameEventCount, 1); + }); + test('disposing platform view during prepareToDraw causes crash in submitFrame', () async { final EngineFlutterView view = EnginePlatformDispatcher.instance.implicitView!; final displayFactory = DisplayCanvasFactory( From 4f85c1701be34db15427a7afa602d851d49420f0 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 11 Aug 2026 16:53:53 +0000 Subject: [PATCH 198/330] test(flutter_tools): deduplicate WebDevFS fixtures and test fakes (#190892) ## Description This PR deduplicates verbose `WebDevFS` constructor setups and repeated test fakes across web general shard tests: - **Shared Test Helper**: Introduces `test/src/fake_web_devfs.dart` providing: - `createWebDevFS` factory helper with standard testbed defaults to eliminate repetitive 25+ parameter boilerplate across test cases. - Shared test fakes (`FakeHttpServer`, `FakeResidentCompiler`, `FakeShaderCompiler`, `FakeDwds`, `FakeAppConnection`, `FakeDebugConnection`, `FakeVmService`). - **Refactored Test Files**: - `test/general.shard/web/devfs_web_test.dart`: Replaced 13 constructor calls with `createWebDevFS` and removed redundant local fake classes. - `test/general.shard/web/devfs_web_ddc_modules_test.dart`: Replaced 6 constructor calls with `createWebDevFS` and removed redundant local fake classes. - **Net LOC Reduction**: -367 lines (+191 / -558 lines). ## Tests - `devfs_web_test.dart` (56/56 passing) - `devfs_web_ddc_modules_test.dart` (42/42 passing) - `test/general.shard/web` (228/228 passing) - `dart analyze --fatal-infos` (clean) --- .../web/devfs_web_ddc_modules_test.dart | 193 +-------- .../general.shard/web/devfs_web_test.dart | 386 +----------------- .../test/src/fake_web_devfs.dart | 178 ++++++++ 3 files changed, 199 insertions(+), 558 deletions(-) create mode 100644 packages/flutter_tools/test/src/fake_web_devfs.dart diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart index bca727bbbb4cf..626c26986cd78 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart @@ -12,10 +12,8 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; -import 'package:flutter_tools/src/build_system/tools/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; -import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/isolated/devfs_web.dart'; import 'package:flutter_tools/src/isolated/release_asset_server.dart'; @@ -28,10 +26,10 @@ import 'package:logging/logging.dart' as logging; import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; import 'package:shelf/shelf.dart'; -import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import '../../src/common.dart'; +import '../../src/fake_web_devfs.dart'; import '../../src/testbed.dart'; const kTransparentImage = [ @@ -775,37 +773,20 @@ void main() { final ResidentCompiler residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', treeShakeIcons: false, packageConfigPath: '.dart_tool/package_config.json', ), - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -878,14 +859,10 @@ void main() { outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', @@ -895,20 +872,8 @@ void main() { enableDwds: true, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -952,32 +917,14 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - buildInfo: BuildInfo.debug, - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - nativeNullAssertions: true, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -995,14 +942,10 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', @@ -1010,23 +953,10 @@ void main() { dartDefines: ['FLUTTER_WEB_USE_SKIA=true'], packageConfigPath: '.dart_tool/package_config.json', ), - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1054,31 +984,15 @@ void main() { final webDevServerConfig = WebDevServerConfig( https: HttpsConfig(certPath: dummyCertPath, certKeyPath: dummyCertKeyPath), ); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, - buildInfo: BuildInfo.debug, - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1221,32 +1135,14 @@ void main() { outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, - buildInfo: BuildInfo.debug, - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1261,78 +1157,3 @@ void main() { await webDevFS.destroy(); }, overrides: {Artifacts: Artifacts.test}); } - -class FakeHttpServer extends Fake implements HttpServer { - bool closed = false; - - @override - Future close({bool force = false}) async { - closed = true; - } -} - -class FakeResidentCompiler extends Fake implements ResidentCompiler { - CompilerOutput? output; - - @override - void addFileSystemRoot(String root) {} - - @override - Future recompile( - Uri mainUri, - List? invalidatedFiles, { - String? outputPath, - PackageConfig? packageConfig, - String? projectRootPath, - FileSystem? fs, - bool suppressErrors = false, - bool checkDartPluginRegistry = false, - File? dartPluginRegistrant, - Uri? nativeAssetsYaml, - bool recompileRestart = false, - }) async { - return output; - } -} - -class FakeShaderCompiler implements DevelopmentShaderCompiler { - const FakeShaderCompiler(); - - @override - void configureCompiler(TargetPlatform? platform) {} - - @override - Future recompileShader(DevFSContent inputShader) { - throw UnimplementedError(); - } - - @override - bool areDependenciesModified(DevFSContent shaderContent) => false; -} - -class FakeDwds extends Fake implements Dwds { - FakeDwds(Iterable connectedAppsIterable) - : connectedApps = Stream.fromIterable(connectedAppsIterable); - - @override - final Stream connectedApps; - - @override - Future debugConnection(AppConnection appConnection) { - return Future.value(FakeDebugConnection()); - } -} - -class FakeAppConnection extends Fake implements AppConnection { - @override - void runMain() {} -} - -class FakeDebugConnection extends Fake implements DebugConnection { - FakeDebugConnection({this.uri = 'http://foo'}); - - @override - final String uri; -} - -class FakeVmService extends Fake implements vm_service.VmService {} diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart index 9ceb9aa1bb64f..1ffcbbe5fa9c4 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart @@ -14,7 +14,6 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; -import 'package:flutter_tools/src/build_system/tools/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/devfs.dart'; @@ -30,10 +29,10 @@ import 'package:flutter_tools/src/web_template.dart'; import 'package:logging/logging.dart' as logging; import 'package:package_config/package_config.dart'; import 'package:shelf/shelf.dart'; -import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import '../../src/common.dart'; +import '../../src/fake_web_devfs.dart'; import '../../src/testbed.dart'; const kTransparentImage = [ @@ -101,31 +100,10 @@ void main() { test( 'WebDevFS.assetPathsToEvict is mutable and can be cleared/modified', () => testbed.run(() { - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, - useSseForDebugProxy: false, - useSseForDebugBackend: false, - useSseForInjectedClient: false, - buildInfo: BuildInfo.debug, - enableDwds: false, - ddsConfig: const DartDevelopmentServiceConfiguration(), + final WebDevFS webDevFS = createWebDevFS( entrypoint: Uri.parse('org-dartlang-app:///main.dart'), - expressionCompiler: null, - chromiumLauncher: null, - nativeNullAssertions: true, - ddcModuleSystem: false, - canaryFeatures: false, - webDevServerConfig: const WebDevServerConfig(), - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - fileSystem: globals.fs, logger: BufferLogger.test(), platform: FakePlatform(), - webCrossOriginIsolation: false, - testMode: true, ); expect(() => webDevFS.assetPathsToEvict.clear(), returnsNormally); @@ -957,37 +935,18 @@ void main() { final ResidentCompiler residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', treeShakeIcons: false, packageConfigPath: '.dart_tool/package_config.json', ), - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - ddcModuleSystem: usesDdcModuleSystem, - canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -1066,32 +1025,7 @@ void main() { final residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, - useSseForDebugProxy: false, - useSseForDebugBackend: false, - useSseForInjectedClient: false, - buildInfo: BuildInfo.debug, - enableDwds: false, - ddsConfig: const DartDevelopmentServiceConfiguration(), - entrypoint: globals.fs.file('lib/main.dart').uri, - expressionCompiler: null, - chromiumLauncher: null, - nativeNullAssertions: true, - ddcModuleSystem: false, - canaryFeatures: false, - webDevServerConfig: const WebDevServerConfig(), - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - fileSystem: globals.fs, - logger: logger, - platform: linux, - webCrossOriginIsolation: false, - testMode: true, - ); + final WebDevFS webDevFS = createWebDevFS(logger: logger, platform: linux); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -1174,32 +1108,7 @@ void main() { final residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, - useSseForDebugProxy: false, - useSseForDebugBackend: false, - useSseForInjectedClient: false, - buildInfo: BuildInfo.debug, - enableDwds: false, - ddsConfig: const DartDevelopmentServiceConfiguration(), - entrypoint: globals.fs.file('lib/main.dart').uri, - expressionCompiler: null, - chromiumLauncher: null, - nativeNullAssertions: true, - ddcModuleSystem: false, - canaryFeatures: false, - webDevServerConfig: const WebDevServerConfig(), - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - fileSystem: globals.fs, - logger: logger, - platform: linux, - webCrossOriginIsolation: false, - testMode: true, - ); + final WebDevFS webDevFS = createWebDevFS(logger: logger, platform: linux); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -1281,32 +1190,7 @@ void main() { final residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, - useSseForDebugProxy: false, - useSseForDebugBackend: false, - useSseForInjectedClient: false, - buildInfo: BuildInfo.debug, - enableDwds: false, - ddsConfig: const DartDevelopmentServiceConfiguration(), - entrypoint: globals.fs.file('lib/main.dart').uri, - expressionCompiler: null, - chromiumLauncher: null, - nativeNullAssertions: true, - ddcModuleSystem: false, - canaryFeatures: false, - webDevServerConfig: const WebDevServerConfig(), - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - fileSystem: globals.fs, - logger: logger, - platform: linux, - webCrossOriginIsolation: false, - testMode: true, - ); + final WebDevFS webDevFS = createWebDevFS(logger: logger, platform: linux); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -1382,32 +1266,7 @@ void main() { final residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, - useSseForDebugProxy: false, - useSseForDebugBackend: false, - useSseForInjectedClient: false, - buildInfo: BuildInfo.debug, - enableDwds: false, - ddsConfig: const DartDevelopmentServiceConfiguration(), - entrypoint: globals.fs.file('lib/main.dart').uri, - expressionCompiler: null, - chromiumLauncher: null, - nativeNullAssertions: true, - ddcModuleSystem: false, - canaryFeatures: false, - webDevServerConfig: const WebDevServerConfig(), - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - fileSystem: globals.fs, - logger: logger, - platform: linux, - webCrossOriginIsolation: false, - testMode: true, - ); + final WebDevFS webDevFS = createWebDevFS(logger: logger, platform: linux); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -1496,37 +1355,18 @@ void main() { final ResidentCompiler residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', treeShakeIcons: false, packageConfigPath: '.dart_tool/package_config.json', ), - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - ddcModuleSystem: usesDdcModuleSystem, - canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -1608,15 +1448,11 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( + final WebDevFS webDevFS = createWebDevFS( // if this is any other value, we will do a real ip lookup - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', @@ -1626,20 +1462,6 @@ void main() { enableDwds: true, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - ddcModuleSystem: usesDdcModuleSystem, - canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1690,32 +1512,12 @@ void main() { outputFile.parent.childFile('a.sources').writeAsStringSync(''); outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - buildInfo: BuildInfo.debug, - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - nativeNullAssertions: true, - ddcModuleSystem: usesDdcModuleSystem, - canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.requireJS.createSync(recursive: true); @@ -1733,14 +1535,10 @@ void main() { outputFile.parent.childFile('a.sources').writeAsStringSync(''); outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', @@ -1748,23 +1546,8 @@ void main() { dartDefines: ['FLUTTER_WEB_USE_SKIA=true'], packageConfigPath: '.dart_tool/package_config.json', ), - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - ddcModuleSystem: usesDdcModuleSystem, - canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1795,31 +1578,13 @@ void main() { host: '::1', https: HttpsConfig(certPath: dummyCertPath, certKeyPath: dummyCertKeyPath), ); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, - buildInfo: BuildInfo.debug, - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - ddcModuleSystem: usesDdcModuleSystem, - canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -2033,32 +1798,12 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, - buildInfo: BuildInfo.debug, - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - ddcModuleSystem: usesDdcModuleSystem, - canaryFeatures: canaryFeatures, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -2293,38 +2038,20 @@ const config = { final residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a.lib.js', 0, []); - const webDevServerConfig = WebDevServerConfig(); - final webDevFS = WebDevFS( - packagesFilePath: '.dart_tool/package_config.json', - urlTunneller: null, + final WebDevFS webDevFS = createWebDevFS( useSseForDebugProxy: true, useSseForDebugBackend: true, useSseForInjectedClient: true, - nativeNullAssertions: true, buildInfo: const BuildInfo( BuildMode.debug, '', treeShakeIcons: false, packageConfigPath: '.dart_tool/package_config.json', ), - enableDwds: false, ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), entrypoint: Uri.base, - testMode: true, - expressionCompiler: null, - chromiumLauncher: null, - // Use DDC library bundle. ddcModuleSystem: true, canaryFeatures: true, - webRenderer: WebRendererMode.canvaskit, - isWasm: false, - useLocalCanvasKit: false, - rootDirectory: globals.fs.currentDirectory, - webDevServerConfig: webDevServerConfig, - fileSystem: globals.fs, - logger: globals.logger, - platform: globals.platform, - webCrossOriginIsolation: false, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -2437,88 +2164,3 @@ const config = { }, overrides: {Artifacts: () => Artifacts.test()}), ); } - -class FakeHttpServer extends Fake implements HttpServer { - bool closed = false; - - @override - Future close({bool force = false}) async { - closed = true; - } -} - -class FakeResidentCompiler extends Fake implements ResidentCompiler { - CompilerOutput? output; - - @override - void addFileSystemRoot(String root) {} - - @override - Future recompile( - Uri mainUri, - List? invalidatedFiles, { - String? outputPath, - PackageConfig? packageConfig, - String? projectRootPath, - FileSystem? fs, - bool suppressErrors = false, - bool checkDartPluginRegistry = false, - File? dartPluginRegistrant, - Uri? nativeAssetsYaml, - bool recompileRestart = false, - }) async { - return output; - } -} - -class FakeShaderCompiler implements DevelopmentShaderCompiler { - const FakeShaderCompiler({this.returnNull = false}); - - final bool returnNull; - - @override - void configureCompiler(TargetPlatform? platform) {} - - @override - Future recompileShader(DevFSContent inputShader) async { - if (returnNull) { - return null; - } - final String source = utf8.decode(await inputShader.contentsAsBytes()); - return DevFSStringContent('compiled_shader: $source'); - } - - @override - bool areDependenciesModified(DevFSContent shaderContent) => false; -} - -class FakeAssetBundle extends Fake implements AssetBundle { - @override - final Map entries = {}; -} - -class FakeDwds extends Fake implements Dwds { - FakeDwds(Iterable connectedAppsIterable) - : connectedApps = Stream.fromIterable(connectedAppsIterable); - - @override - final Stream connectedApps; - - @override - Future debugConnection(AppConnection appConnection) => - Future.value(FakeDebugConnection()); -} - -class FakeAppConnection extends Fake implements AppConnection { - @override - void runMain() {} -} - -class FakeDebugConnection extends Fake implements DebugConnection { - FakeDebugConnection({this.uri = 'http://foo'}); - - @override - final String uri; -} - -class FakeVmService extends Fake implements vm_service.VmService {} diff --git a/packages/flutter_tools/test/src/fake_web_devfs.dart b/packages/flutter_tools/test/src/fake_web_devfs.dart new file mode 100644 index 0000000000000..3bd27276d9b02 --- /dev/null +++ b/packages/flutter_tools/test/src/fake_web_devfs.dart @@ -0,0 +1,178 @@ +// 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 'dart:async'; +import 'dart:io' hide Directory, File; + +import 'package:dwds/dwds.dart'; +import 'package:flutter_tools/src/asset.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/net.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/tools/shader_compiler.dart'; +import 'package:flutter_tools/src/compile.dart'; +import 'package:flutter_tools/src/convert.dart'; +import 'package:flutter_tools/src/devfs.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/isolated/devfs_web.dart'; +import 'package:flutter_tools/src/web/chrome.dart'; +import 'package:flutter_tools/src/web/compile.dart'; +import 'package:flutter_tools/src/web/devfs_config.dart'; +import 'package:package_config/package_config.dart'; +import 'package:test/fake.dart'; +import 'package:vm_service/vm_service.dart' as vm_service; + +/// Helper function to create a [WebDevFS] with sensible testbed defaults. +WebDevFS createWebDevFS({ + String packagesFilePath = '.dart_tool/package_config.json', + UrlTunneller? urlTunneller, + bool useSseForDebugProxy = false, + bool useSseForDebugBackend = false, + bool useSseForInjectedClient = false, + BuildInfo buildInfo = BuildInfo.debug, + bool enableDwds = false, + DartDevelopmentServiceConfiguration ddsConfig = const DartDevelopmentServiceConfiguration(), + Uri? entrypoint, + ExpressionCompiler? expressionCompiler, + ChromiumLauncher? chromiumLauncher, + bool nativeNullAssertions = true, + bool ddcModuleSystem = false, + bool canaryFeatures = false, + WebDevServerConfig webDevServerConfig = const WebDevServerConfig(), + WebRendererMode webRenderer = WebRendererMode.canvaskit, + bool isWasm = false, + bool useLocalCanvasKit = false, + Directory? rootDirectory, + bool useDwdsWebSocketConnection = false, + bool webCrossOriginIsolation = false, + FileSystem? fileSystem, + Logger? logger, + Platform? platform, + bool testMode = true, + Map webDefines = const {}, +}) { + return WebDevFS( + packagesFilePath: packagesFilePath, + urlTunneller: urlTunneller, + useSseForDebugProxy: useSseForDebugProxy, + useSseForDebugBackend: useSseForDebugBackend, + useSseForInjectedClient: useSseForInjectedClient, + buildInfo: buildInfo, + enableDwds: enableDwds, + ddsConfig: ddsConfig, + entrypoint: entrypoint ?? globals.fs.file('lib/main.dart').uri, + expressionCompiler: expressionCompiler, + chromiumLauncher: chromiumLauncher, + nativeNullAssertions: nativeNullAssertions, + ddcModuleSystem: ddcModuleSystem, + canaryFeatures: canaryFeatures, + webDevServerConfig: webDevServerConfig, + webRenderer: webRenderer, + isWasm: isWasm, + useLocalCanvasKit: useLocalCanvasKit, + rootDirectory: rootDirectory ?? globals.fs.currentDirectory, + useDwdsWebSocketConnection: useDwdsWebSocketConnection, + webCrossOriginIsolation: webCrossOriginIsolation, + fileSystem: fileSystem ?? globals.fs, + logger: logger ?? globals.logger, + platform: platform ?? globals.platform, + testMode: testMode, + webDefines: webDefines, + ); +} + +/// A fake [HttpServer] for testing. +class FakeHttpServer extends Fake implements HttpServer { + bool closed = false; + + @override + Future close({bool force = false}) async { + closed = true; + } +} + +/// A fake [ResidentCompiler] for testing. +class FakeResidentCompiler extends Fake implements ResidentCompiler { + CompilerOutput? output; + + @override + void addFileSystemRoot(String root) {} + + @override + Future recompile( + Uri mainUri, + List? invalidatedFiles, { + String? outputPath, + PackageConfig? packageConfig, + String? projectRootPath, + FileSystem? fs, + bool suppressErrors = false, + bool checkDartPluginRegistry = false, + File? dartPluginRegistrant, + Uri? nativeAssetsYaml, + bool recompileRestart = false, + }) async { + return output; + } +} + +/// A fake [DevelopmentShaderCompiler] for testing. +class FakeShaderCompiler implements DevelopmentShaderCompiler { + const FakeShaderCompiler({this.returnNull = false}); + + final bool returnNull; + + @override + void configureCompiler(TargetPlatform? platform) {} + + @override + Future recompileShader(DevFSContent inputShader) async { + if (returnNull) { + return null; + } + final String source = utf8.decode(await inputShader.contentsAsBytes()); + return DevFSStringContent('compiled_shader: $source'); + } + + @override + bool areDependenciesModified(DevFSContent shaderContent) => false; +} + +/// A fake [AssetBundle] for testing. +class FakeAssetBundle extends Fake implements AssetBundle { + @override + final Map entries = {}; +} + +/// A fake [Dwds] service for testing. +class FakeDwds extends Fake implements Dwds { + FakeDwds(Iterable connectedAppsIterable) + : connectedApps = Stream.fromIterable(connectedAppsIterable); + + @override + final Stream connectedApps; + + @override + Future debugConnection(AppConnection appConnection) => + Future.value(FakeDebugConnection()); +} + +/// A fake [AppConnection] for testing. +class FakeAppConnection extends Fake implements AppConnection { + @override + void runMain() {} +} + +/// A fake [DebugConnection] for testing. +class FakeDebugConnection extends Fake implements DebugConnection { + FakeDebugConnection({this.uri = 'http://foo'}); + + @override + final String uri; +} + +/// A fake [vm_service.VmService] for testing. +class FakeVmService extends Fake implements vm_service.VmService {} From b05fabdc6507606fab394e166f732c8d0224b062 Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Tue, 11 Aug 2026 17:00:51 +0000 Subject: [PATCH 199/330] [icon_tree_shaker] Tree-shake material and cupertino with 0 icons (#190905) Bug: * https://github.com/flutter/flutter/issues/190902 Ensures built-in fonts are tree-shaken if 0 icons are used. --- .../targets/icon_tree_shaker.dart | 22 +++- .../targets/icon_tree_shaker_test.dart | 114 ++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart index 3f7356a40aee5..4e66092ef39fb 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart @@ -141,9 +141,12 @@ class IconTreeShaker { familyKeys, ); - if (fonts.length != iconData.length) { + final Set missingFonts = iconData.keys + .where((String key) => !fonts.containsKey(key)) + .toSet(); + if (missingFonts.isNotEmpty) { environment.logger.printStatus( - 'Expected to find fonts for ${iconData.keys}, but found ' + 'Expected to find fonts for $missingFonts, but found ' '${fonts.keys}. This usually means you are referring to ' 'font families in an IconData class but not including them ' 'in the assets section of your pubspec.yaml, are missing ' @@ -155,7 +158,9 @@ class IconTreeShaker { final result = {}; const kSpacePoint = 32; for (final MapEntry(:key, :value) in fonts.entries) { - final List? codePoints = iconData[key]; + final int? fallbackCodePoint = _kKnownIconFontFallbackCodePoints[key]; + final List? codePoints = + iconData[key] ?? (fallbackCodePoint != null ? [fallbackCodePoint] : null); if (codePoints == null) { throw IconTreeShakerException._( 'Expected to font code points for $key, but none were found.', @@ -269,6 +274,14 @@ class IconTreeShaker { 'by providing the --no-tree-shake-icons flag when building your app.'; } + /// Known icon font families that should be subsetted even if 0 icons are recorded. + /// Subsetting unused icon fonts to a single dummy icon ensures that unused fonts + /// are not bundled in their entirety. + static const Map _kKnownIconFontFallbackCodePoints = { + 'MaterialIcons': 57415, // 0xe047, Icons.add + 'packages/cupertino_icons/CupertinoIcons': 62418, // 0xf3d2, CupertinoIcons.chevron_left + }; + /// Returns a map of { fontFamily: relativePath } pairs. Future> _parseFontJson(String fontManifestData, Set families) async { final result = {}; @@ -285,7 +298,8 @@ class IconTreeShaker { 'got: ${map['family']}.', ); } - if (!families.contains(familyKey)) { + if (!families.contains(familyKey) && + !_kKnownIconFontFallbackCodePoints.containsKey(familyKey)) { continue; } final List> fonts = _getList( diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart index 6e6615596b494..d8391d6e98e7c 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart @@ -532,7 +532,12 @@ void main() { targetPlatform: TargetPlatform.android, ); + final stdinSink = CompleterIOSink(); writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult); + resetFontSubsetInvocation(stdinSink: stdinSink); + fileSystem.file(outputPath) + ..createSync(recursive: true) + ..writeAsBytesSync(List.filled(1200, 0)); // Does not throw await iconTreeShaker.subsetFont( input: fileSystem.file(inputPath), @@ -540,6 +545,7 @@ void main() { relativePath: relativePath, ); + expect(stdinSink.getAndClear(), '57415\n'); expect( logger.traceText, contains( @@ -913,6 +919,114 @@ void main() { expect(stdin, contains('59470')); expect(processManager, hasNoRemainingExpectations); }); + + testWithoutContext('Subsets unused CupertinoIcons font to fallback code point', () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); + + const cupertinoFontPath = 'packages/cupertino_icons/assets/CupertinoIcons.ttf'; + const cupertinoManifestJson = + ''' +[ + { + "family": "packages/cupertino_icons/CupertinoIcons", + "fonts": [ + { + "asset": "$cupertinoFontPath" + } + ] + } +] +'''; + fontManifestContent = DevFSStringContent(cupertinoManifestJson); + + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.android, + ); + + // Empty recordings (0 icons used) + writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult); + + final stdinSink = CompleterIOSink(); + fontSubsetArgs = [fontSubsetPath, outputPath, inputPath]; + resetFontSubsetInvocation(stdinSink: stdinSink); + + final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List.filled(2500, 0)); + fileSystem.file(outputPath) + ..createSync(recursive: true) + ..writeAsBytesSync(List.filled(1200, 0)); + + expect( + await iconTreeShaker.subsetFont( + input: inputFont, + outputPath: outputPath, + relativePath: cupertinoFontPath, + ), + true, + ); + + expect(stdinSink.getAndClear(), '62418\n'); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('Does not subset unused non-icon font', () async { + final Environment environment = createEnvironment({ + kIconTreeShakerFlag: 'true', + kBuildMode: 'release', + }); + final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true); + + const customFontPath = 'fonts/Roboto-Regular.ttf'; + const customManifestJson = + ''' +[ + { + "family": "Roboto", + "fonts": [ + { + "asset": "$customFontPath" + } + ] + } +] +'''; + fontManifestContent = DevFSStringContent(customManifestJson); + + final iconTreeShaker = IconTreeShaker( + environment, + fontManifestContent, + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: artifacts, + targetPlatform: TargetPlatform.android, + ); + + // Empty recordings (0 icons used) + writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult); + + final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List.filled(2500, 0)); + + expect( + await iconTreeShaker.subsetFont( + input: inputFont, + outputPath: outputPath, + relativePath: customFontPath, + ), + false, + ); + + expect(processManager, hasNoRemainingExpectations); + }); } const Library iconDataLibrary = Library('package:flutter/src/widgets/icon_data.dart'); From 90cf7be0248f5bb368902b189060502e499bd1b0 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:10:55 +0000 Subject: [PATCH 200/330] Revert: [web] Keep the keyboard up during an iOS caret drag (#190926) Reverts: [[web] Keep the keyboard up during an iOS caret drag](https://github.com/flutter/flutter/pull/190014) Initiated by: @jtmcdole Reason for reverting: Original PR Author: @flutter-zl Reviewed By: @Renzo-Olivares The original PR description is provided below: Fixes #189744 **Problem** On iOS 27, long-press-dragging the selection caret in a Web TextField dismisses the keyboard mid-gesture. WebKit transiently blurs the hidden input with a null relatedTarget while the document keeps focus, then refocuses it a frame later,and the engine reacted to that blink on two listeners that each tore the input down. A plain input ignores the same blur, so the cause is Flutter's reaction, not a WebKit limitation. **Fix** On iOS both listeners now defer their teardown by 100ms and cancel it if the input refocuses, mirroring the existing #155265 deferred close. Done and tap-away never refocus so they still close, and the deferral is narrowed to the exact drag signature so every other focus transition is unaffected. **Demo** Before: https://flutter-demo-52-before.web.app (keyboard dismisses mid caret drag) After: https://flutter-demo-52-after.web.app (keyboard stays up) Repro on iOS 27 Safari: tap the field to raise the keyboard, then long-press and drag the selection caret. Before dismisses; after stays up. Done and tap-away still dismiss. --- .../view_focus_binding.dart | 51 +----- .../src/engine/text_editing/text_editing.dart | 51 +----- .../view_focus_binding_test.dart | 169 ------------------ .../web_ui/test/engine/text_editing_test.dart | 127 ------------- 4 files changed, 2 insertions(+), 396 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart index 9ccf9789d4027..3874eb9d43e07 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart @@ -8,12 +8,6 @@ import 'dart:js_interop'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -/// Overrides `domDocument.hasFocus()` in [ViewFocusBinding] for tests, so the -/// iOS caret-drag deferral tests do not depend on the headless browser -/// reporting the test document as focused. Mirrors -/// `DefaultTextEditingStrategy.debugDocumentHasFocusOverride`. -bool? debugViewFocusDocumentHasFocusOverride; - /// Tracks the [FlutterView]s focus changes. final class ViewFocusBinding { ViewFocusBinding(this._viewManager, this._onViewFocusChange); @@ -26,21 +20,6 @@ final class ViewFocusBinding { StreamSubscription? _onViewCreatedListener; - /// A deferred report of a `focusout` that named no element to gain focus. - /// - /// A native iOS caret/selection drag transiently blurs the focused input to - /// and WebKit refocuses it a frame later. Deferring the report lets - /// that refocus cancel it, so the view is not briefly reported unfocused. - /// See: https://github.com/flutter/flutter/issues/189744 - Timer? _pendingFocusoutTimer; - - /// Whether the document itself still has focus. - /// - /// Reads [debugViewFocusDocumentHasFocusOverride] when set, so tests do not - /// depend on the headless browser reporting the test document as focused. - /// Mirrors `DefaultTextEditingStrategy._documentHasFocus`. - bool get _documentHasFocus => debugViewFocusDocumentHasFocusOverride ?? domDocument.hasFocus(); - void init() { // We need a global listener here to know if the user was pressing "shift" // when the Flutter view receives focus, to move the Flutter focus to the @@ -57,7 +36,6 @@ final class ViewFocusBinding { domDocument.body?.removeEventListener(_keyDown, _handleKeyDown); domDocument.body?.removeEventListener(_keyUp, _handleKeyUp); _onViewCreatedListener?.cancel(); - _pendingFocusoutTimer?.cancel(); } void changeViewFocus(int viewId, ui.ViewFocusState state) { @@ -76,9 +54,6 @@ final class ViewFocusBinding { late final DomEventListener _handleFocusin = createDomEventListener((DomEvent event) { event as DomFocusEvent; - // Focus returned, so a deferred `focusout` was a transient blur; drop it. - _pendingFocusoutTimer?.cancel(); - _pendingFocusoutTimer = null; _handleFocusChange(event.target as DomElement?); }); @@ -95,31 +70,7 @@ final class ViewFocusBinding { } event as DomFocusEvent; - final willGainFocus = event.relatedTarget as DomElement?; - final target = event.target as DomElement?; - - // On iOS, a native caret/selection drag transiently blurs Flutter's active - // text-editing element to (relatedTarget == null) while the document - // still has focus, and WebKit refocuses it a frame later. Reporting the view - // unfocused on that blink tears the text connection down and drops the - // keyboard. Defer only that precise case, re-deriving from the live focus so - // an immediate refocus is a no-op ([_handleFocusin] cancels the timer). - // Anything else, a non-editing element, a genuine focus loss, or the page - // itself losing focus, reports immediately. - // https://github.com/flutter/flutter/issues/189744 - if (isIosSafari && - willGainFocus == null && - _documentHasFocus && - textEditing.isActiveTextEditingElement(target)) { - _pendingFocusoutTimer?.cancel(); - _pendingFocusoutTimer = Timer(kTransientBlurSettleDelay, () { - _pendingFocusoutTimer = null; - _handleFocusChange(domDocument.activeElement); - }); - return; - } - - _handleFocusChange(willGainFocus); + _handleFocusChange(event.relatedTarget as DomElement?); }); late final DomEventListener _handleKeyDown = createDomEventListener((DomEvent event) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index fa74506f646ad..e907a3e98469f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -12,7 +12,6 @@ import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; -import '../browser_detection.dart' show isIosSafari; import '../configuration.dart'; import '../dom.dart'; import '../mouse/prevent_default.dart'; @@ -51,18 +50,6 @@ bool browserHasAutofillOverlay() => /// transparent. const String transparentTextEditingClass = 'transparentTextEditing'; -/// How long to wait before treating a blur that named no incoming element as a -/// real focus loss. -/// -/// Several browser behaviors blur transiently and restore focus a moment later: -/// backgrounding a tab fires blur before `visibilitychange`, and on iOS a -/// native caret or selection drag blurs the input mid-gesture before WebKit -/// refocuses it. Waiting this long lets those settle before the engine acts. -/// -/// Shared by [DefaultTextEditingStrategy.handleBlur] and [ViewFocusBinding], -/// which defer the same gesture and must not disagree about the window. -const Duration kTransientBlurSettleDelay = Duration(milliseconds: 100); - void _emptyCallback(dynamic _) {} /// These style attributes are constant throughout the life time of an input @@ -1875,7 +1862,7 @@ abstract class DefaultTextEditingStrategy // When a browser tab is backgrounded, the input blur arrives before // visibilitychange. Wait briefly so tab switches can keep the text // connection alive, while ordinary window/iframe blurs still close it. - _pendingBlurConnectionCloseTimer = Timer(kTransientBlurSettleDelay, () { + _pendingBlurConnectionCloseTimer = Timer(const Duration(milliseconds: 100), () { _pendingBlurConnectionCloseTimer = null; if (_documentVisibilityState == 'hidden' || _documentHasFocus) { return; @@ -1884,32 +1871,6 @@ abstract class DefaultTextEditingStrategy }); return; } - // On iOS WebKit, a native caret or selection drag transiently blurs the - // hidden input mid-gesture with `relatedTarget == null` while the document - // still has focus, and WebKit refocuses the input a frame later. Closing - // the connection on that blink drops the keyboard; a plain keeps - // it. Defer the close and skip it if the input has regained focus by the - // time the timer fires. A genuine blur, the Done button or tapping away, - // does not refocus, so it still closes. [ViewFocusBinding] defers the - // matching `focusout` the same way. - // https://github.com/flutter/flutter/issues/189744 - if (isIosSafari) { - _pendingBlurConnectionCloseTimer?.cancel(); - _pendingBlurConnectionCloseTimer = Timer(kTransientBlurSettleDelay, () { - _pendingBlurConnectionCloseTimer = null; - if (domDocument.activeElement == activeDomElement) { - // The input refocused: this was the transient mid-gesture blur. - return; - } - if (_documentVisibilityState == 'hidden') { - // The page was backgrounded (e.g. a tab switch) after the blur was - // scheduled; keep the connection alive, matching the branch above. - return; - } - textEditing.sendTextConnectionClosedToFrameworkIfAny(); - }); - return; - } textEditing.sendTextConnectionClosedToFrameworkIfAny(); } else if (_viewForElement(willGainFocusElement) == activeDomElementView) { // If the focus stays within the same FlutterView, ensure the focus stays @@ -2793,16 +2754,6 @@ class HybridTextEditing { /// Also used to define if a keyboard is needed. bool isEditing = false; - /// Whether [element] is the DOM element currently receiving text input. - /// - /// [ViewFocusBinding] uses this to recognize a `focusout` that originated - /// from the active text-editing element. - /// - /// Prefer this over matching on [textEditingClass]. That class is - /// not guaranteed to be applied by all strategies. - bool isActiveTextEditingElement(DomElement? element) => - isEditing && element != null && element == strategy.domElement; - InputConfiguration? configuration; DefaultTextEditingStrategy? debugTextEditingStrategyOverride; diff --git a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart index 03e10bc977373..9a4acd9acb675 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart @@ -24,7 +24,6 @@ void testMain() { tearDown(() { EngineSemantics.instance.semanticsEnabled = false; - endFakeTextEditing(); }); test('The view is focusable and reachable by keyboard when registered', () async { @@ -269,177 +268,9 @@ void testMain() { expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); }); - - // On iOS a native caret/selection drag transiently blurs Flutter's active - // text-editing element to (relatedTarget == null) while the document - // still has focus, and WebKit refocuses it a frame later. The view-unfocused - // report is deferred so that refocus cancels it. - // Regression test for https://github.com/flutter/flutter/issues/189744 - test('drops the deferred view-unfocused report when the editing input ' - 'refocuses on iOS', () async { - final EngineFlutterView view = createAndRegisterView(dispatcher); - final DomHTMLInputElement input = createDomHTMLInputElement(); - view.dom.rootElement.append(input); - input.focusWithoutScroll(); - beginFakeTextEditing(input); - dispatchedViewFocusEvents.clear(); - - debugEmulateIosSafari = true; - debugViewFocusDocumentHasFocusOverride = true; - try { - // The null-relatedTarget focusout schedules the deferred report; the - // immediate refocus, as WebKit does mid-drag, cancels it. - input.blur(); - input.focusWithoutScroll(); - await Future.delayed(const Duration(milliseconds: 150)); - expect(dispatchedViewFocusEvents, isEmpty); - } finally { - debugEmulateIosSafari = false; - debugViewFocusDocumentHasFocusOverride = null; - } - }); - - // A genuine blur (Done button, tap-away) never refocuses, so the deferred - // report must still fire, carrying the right view and direction. - test('reports the view unfocused on iOS when the editing input does not ' - 'refocus', () async { - final EngineFlutterView view = createAndRegisterView(dispatcher); - final DomHTMLInputElement input = createDomHTMLInputElement(); - view.dom.rootElement.append(input); - input.focusWithoutScroll(); - beginFakeTextEditing(input); - dispatchedViewFocusEvents.clear(); - - debugEmulateIosSafari = true; - debugViewFocusDocumentHasFocusOverride = true; - try { - input.blur(); - await Future.delayed(const Duration(milliseconds: 150)); - final Iterable unfocused = dispatchedViewFocusEvents.where( - (ui.ViewFocusEvent e) => e.state == ui.ViewFocusState.unfocused, - ); - expect(unfocused, hasLength(1)); - expect(unfocused.single.viewId, view.viewId); - expect(unfocused.single.direction, ui.ViewFocusDirection.undefined); - } finally { - debugEmulateIosSafari = false; - debugViewFocusDocumentHasFocusOverride = null; - } - }); - - // The deferral is scoped to Flutter's text-editing element. A null-target - // focusout from any other element must report immediately, so a later - // refocus cannot erase a real focus loss. - test('reports immediately for a non-text-editing element on iOS', () { - final EngineFlutterView view = createAndRegisterView(dispatcher); - final DomElement other = createDomElement('input'); - view.dom.rootElement.append(other); - other.focusWithoutScroll(); - dispatchedViewFocusEvents.clear(); - - debugEmulateIosSafari = true; - // Report the document as focused so the only condition failing is that - // `other` is not the active editing element. Without this the test could - // pass because the headless browser reported the document unfocused, - // which is a different branch than the one under test. - debugViewFocusDocumentHasFocusOverride = true; - try { - other.blur(); - // Not deferred: the unfocused event is present synchronously. - expect( - dispatchedViewFocusEvents.where( - (ui.ViewFocusEvent e) => e.state == ui.ViewFocusState.unfocused, - ), - hasLength(1), - ); - } finally { - debugEmulateIosSafari = false; - debugViewFocusDocumentHasFocusOverride = null; - } - }); - - // The deferral requires the document to still have focus. When focus has - // left the document, such as a window, iframe, or app switch, the - // null-target focusout from the editing element must report immediately so - // the framework is not left believing the view is still focused. - // Regression test for https://github.com/flutter/flutter/issues/189744 - test('reports immediately when the document is not focused on iOS', () { - final EngineFlutterView view = createAndRegisterView(dispatcher); - final DomHTMLInputElement input = createDomHTMLInputElement(); - view.dom.rootElement.append(input); - input.focusWithoutScroll(); - beginFakeTextEditing(input); - dispatchedViewFocusEvents.clear(); - - debugEmulateIosSafari = true; - debugViewFocusDocumentHasFocusOverride = false; - try { - input.blur(); - // Not deferred: with the document unfocused the unfocused event is - // present synchronously. - expect( - dispatchedViewFocusEvents.where( - (ui.ViewFocusEvent e) => e.state == ui.ViewFocusState.unfocused, - ), - hasLength(1), - ); - } finally { - debugEmulateIosSafari = false; - debugViewFocusDocumentHasFocusOverride = null; - } - }); - - // The deferral must key off the engine's editing state, not the - // `flt-text-editing` class, which is not guaranteed to be applied by all - // text editing strategies. Matching on the class would leave the deferral - // dead for strategies that do not apply it. - // Regression test for https://github.com/flutter/flutter/issues/189744 - test('defers on iOS for an editing element with no flt-text-editing class', () async { - final EngineFlutterView view = createAndRegisterView(dispatcher); - final DomHTMLInputElement input = createDomHTMLInputElement(); - view.dom.rootElement.append(input); - input.focusWithoutScroll(); - beginFakeTextEditing(input); - expect( - input.classList.contains(HybridTextEditing.textEditingClass), - isFalse, - reason: 'the semantics path never applies this class', - ); - dispatchedViewFocusEvents.clear(); - - debugEmulateIosSafari = true; - debugViewFocusDocumentHasFocusOverride = true; - try { - input.blur(); - input.focusWithoutScroll(); - await Future.delayed(const Duration(milliseconds: 150)); - expect(dispatchedViewFocusEvents, isEmpty); - } finally { - debugEmulateIosSafari = false; - debugViewFocusDocumentHasFocusOverride = null; - } - }); }); } -/// Makes [element] the engine's active text-editing element, which is what -/// [HybridTextEditing.isActiveTextEditingElement] reports to [ViewFocusBinding]. -/// -/// Sets the real singleton state rather than applying -/// [HybridTextEditing.textEditingClass], so these tests exercise the same signal -/// production code reads. The class is not guaranteed to be applied by all text -/// editing strategies, so keying tests off it would not reflect the production -/// code. -void beginFakeTextEditing(DomHTMLElement element) { - textEditing.isEditing = true; - textEditing.strategy.domElement = element; -} - -void endFakeTextEditing() { - textEditing.isEditing = false; - textEditing.strategy.domElement = null; -} - EngineFlutterView createAndRegisterView(EnginePlatformDispatcher dispatcher) { final DomElement div = createDomElement('div'); final view = EngineFlutterView(dispatcher, div); diff --git a/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart b/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart index b5a8f7b0bd825..2eb09a3ecdef2 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart @@ -760,133 +760,6 @@ Future testMain() async { spy.tearDown(); }); - // On iOS WebKit, a native caret or selection drag transiently blurs the - // hidden input with `relatedTarget == null` and refocuses it a frame later. - // The connection close is deferred so that refocus cancels it; otherwise the - // keyboard dismisses mid-drag. - // Regression test for https://github.com/flutter/flutter/issues/189744 - test('keeps the text connection open on iOS when the input refocuses after a ' - 'null-relatedTarget blur', () async { - final spy = PlatformMessagesSpy(); - spy.setUp(); - - textEditing.configuration = singlelineConfig; - - final showCompleter = Completer(); - textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); - await showCompleter.future; - expect(textEditing.isEditing, isTrue); - - final DomHTMLElement input = textEditing.strategy.domElement!; - debugEmulateIosSafari = true; - textEditing.strategy.debugDocumentHasFocusOverride = true; - try { - // The blur schedules a deferred close; the immediate refocus, as WebKit - // does mid-drag, must skip it. - input.blur(); - input.focusWithoutScroll(); - await Future.delayed(const Duration(milliseconds: 150)); - expect(connectionClosedMessages(spy), isEmpty); - expect(textEditing.isEditing, isTrue); - } finally { - debugEmulateIosSafari = false; - textEditing.strategy.debugDocumentHasFocusOverride = null; - } - - spy.tearDown(); - }); - - // The Done button and tapping away also blur with `relatedTarget == null`, - // but do not refocus, so the deferred close must still fire. - test('closes the text connection on iOS when the input is not refocused ' - 'after a null-relatedTarget blur', () async { - final spy = PlatformMessagesSpy(); - spy.setUp(); - - textEditing.configuration = singlelineConfig; - - final showCompleter = Completer(); - textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); - await showCompleter.future; - expect(textEditing.isEditing, isTrue); - - final DomHTMLElement input = textEditing.strategy.domElement!; - debugEmulateIosSafari = true; - textEditing.strategy.debugDocumentHasFocusOverride = true; - try { - input.blur(); - await Future.delayed(const Duration(milliseconds: 150)); - expect(connectionClosedMessages(spy), hasLength(1)); - } finally { - debugEmulateIosSafari = false; - textEditing.strategy.debugDocumentHasFocusOverride = null; - } - - spy.tearDown(); - }); - - // The deferral is iOS-only: elsewhere a null-relatedTarget blur closes - // immediately. - test('closes the text connection immediately off iOS on a null-relatedTarget ' - 'blur', () async { - final spy = PlatformMessagesSpy(); - spy.setUp(); - - textEditing.configuration = singlelineConfig; - - final showCompleter = Completer(); - textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); - await showCompleter.future; - expect(textEditing.isEditing, isTrue); - - textEditing.strategy.debugDocumentHasFocusOverride = true; - try { - textEditing.strategy.handleBlur(createDomEvent('Event', 'blur')); - expect(connectionClosedMessages(spy), hasLength(1)); - } finally { - textEditing.strategy.debugDocumentHasFocusOverride = null; - } - - spy.tearDown(); - }); - - // If the page is backgrounded (a tab switch) after the deferred close is - // scheduled, the connection must stay open, matching the issue 155265 policy. - test('keeps the text connection open on iOS when the page hides before the ' - 'deferred close fires', () async { - final spy = PlatformMessagesSpy(); - spy.setUp(); - - textEditing.configuration = singlelineConfig; - - final showCompleter = Completer(); - textEditing.acceptCommand(const TextInputShow(), showCompleter.complete); - await showCompleter.future; - expect(textEditing.isEditing, isTrue); - - final DomHTMLElement input = textEditing.strategy.domElement!; - debugEmulateIosSafari = true; - textEditing.strategy.debugDocumentHasFocusOverride = true; - try { - // Blur without refocusing schedules the deferred close, then the page - // is hidden before it fires. - input.blur(); - textEditing.strategy.debugDocumentVisibilityStateOverride = 'hidden'; - await Future.delayed(const Duration(milliseconds: 150)); - expect(connectionClosedMessages(spy), isEmpty); - expect(textEditing.isEditing, isTrue); - } finally { - debugEmulateIosSafari = false; - textEditing.strategy.debugDocumentHasFocusOverride = null; - textEditing.strategy.debugDocumentVisibilityStateOverride = null; - // Restore focus so this "left blurred" scenario does not leak into the - // next test. - input.focusWithoutScroll(); - } - - spy.tearDown(); - }); - test( 'keeps focus within window/iframe when the focus moves within the flutter view in Chrome and Firefox but not Safari', () async { From 2132595ad6edc9c04153e7d7d75acaecd3455426 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 11 Aug 2026 17:16:03 +0000 Subject: [PATCH 201/330] [flutter_tools] Add retry and timeout to getFlutterWidgetPreviews in LspPreviewDetector (#190929) Addresses flakiness in `tool_integration_tests` by adding a 5-second timeout and up to 5 retries (with 200ms delay) when calling `dtd.getFlutterWidgetPreviews()`. This handles transient crashes/hangs of the Analysis Server during test execution. Also improves diagnostics in `widget_preview_test_helpers.dart` by closing the stdout stream controller `onDone` and logging the exit code of the `widget-preview` process. Aborts retries early if the detector is disposed or the tool is shutting down to prevent hangs. Fixes https://github.com/flutter/flutter/issues/189496 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../widget_preview/lsp_preview_detector.dart | 47 ++++++++++++++----- .../widget_preview_test_helpers.dart | 29 ++++++++---- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart index ef8d6483ed64d..bf5b1b596e0fd 100644 --- a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart @@ -136,6 +136,10 @@ class LspPreviewDetector { } Future launchAnalysisServer() async { + final String? protocolTrafficLog = platform.environment['FLUTTER_LSP_TRAFFIC_LOG']; + if (protocolTrafficLog != null) { + logger.printTrace('LSP Traffic Log path from env: $protocolTrafficLog'); + } final analysisServer = AnalysisServer( artifacts.getArtifactPath(Artifact.engineDartSdkPath), [projectRoot.path], @@ -145,6 +149,7 @@ class LspPreviewDetector { processManager: processManager, terminal: terminal, suppressAnalytics: suppressAnalytics, + protocolTrafficLog: protocolTrafficLog, ); return analysisServer; } @@ -186,20 +191,38 @@ class LspPreviewDetector { return; } await _analysisServer?.waitForAnalysis(); - try { - final FlutterWidgetPreviews result = await dtd.getFlutterWidgetPreviews(); - onChangeDetected(result); - } catch (e) { + FlutterWidgetPreviews? result; + var retries = 5; + while (retries > 0) { if (_disposed || shutdownHooks.isShuttingDown) { - logger.printTrace('Failed to get widget previews during shutdown: $e'); - } else if (e is StateError || e is Exception) { - logger.printWarning( - 'Lost connection to the Dart Tooling Daemon (DTD). ' - 'Live preview updates are paused. Details: $e', - ); - } else { - rethrow; + break; + } + try { + result = await dtd.getFlutterWidgetPreviews().timeout(const Duration(seconds: 5)); + break; + } catch (e) { + retries--; + if (retries == 0) { + if (_disposed || shutdownHooks.isShuttingDown) { + logger.printTrace('Failed to get widget previews during shutdown: $e'); + } else if (e is StateError || e is Exception) { + logger.printWarning( + 'Lost connection to the Dart Tooling Daemon (DTD). ' + 'Live preview updates are paused. Details: $e', + ); + } else { + rethrow; + } + } else { + logger.printTrace( + 'Failed to get widget previews, retrying in 200ms... ($retries retries left). Error: $e', + ); + await Future.delayed(const Duration(milliseconds: 200)); + } } } + if (result != null) { + onChangeDetected(result); + } } } diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index 47ead4c2b58bc..2cae70dc75610 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -76,13 +76,23 @@ Future> startWidgetPreview({ }); final controller = StreamController.broadcast(); - process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) { - // ignore: avoid_print - print('[stdout] $msg'); - if (!controller.isClosed) { - controller.add(msg); - } - }); + process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen( + (String msg) { + // ignore: avoid_print + print('[stdout] $msg'); + if (!controller.isClosed) { + controller.add(msg); + } + }, + onDone: () { + if (!controller.isClosed) { + controller.close(); + } + }, + ); process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) { // ignore: avoid_print @@ -91,9 +101,8 @@ Future> startWidgetPreview({ unawaited( process.exitCode.then((int exitCode) { - if (!controller.isClosed) { - controller.close(); - } + // ignore: avoid_print + print('widget-preview process exited with code $exitCode'); }), ); From 456270dd7309566e64ad99ec780e54f465ab65fd Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:07:37 +0000 Subject: [PATCH 202/330] Revert: Roll Dart SDK from 558fb298e458 to c86be8702638 (3 revisions) (#190934) Reverts: [Roll Dart SDK from 558fb298e458 to c86be8702638 (3 revisions)](https://github.com/flutter/flutter/pull/190906) Initiated by: @chingjun Reason for reverting: Broke internal tests Original PR Author: @engine-flutter-autoroll Reviewed By: @fluttergithubbot The original PR description is provided below: https://dart.googlesource.com/sdk.git/+log/558fb298e458..c86be8702638 2026-08-11 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-114.0.dev 2026-08-11 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-113.0.dev 2026-08-11 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.14.0-112.0.dev If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/dart-sdk-flutter Please CC dart-vm-team@google.com,jimgraham@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 72fccfac5e6cd..37ba85941be89 100644 --- a/DEPS +++ b/DEPS @@ -55,7 +55,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': 'c86be87026381cce96e3c93cb810421ad3d19373', + 'dart_revision': '558fb298e458b75551bc19aaecbd1243cb72c45f', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From 8e817f5b10eb226c09f5724baabdc7f2731e9f47 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 11 Aug 2026 19:46:14 +0000 Subject: [PATCH 203/330] [flutter_tools] Strip local path and workspace dependencies during update-packages --force-upgrade (#190944) `flutter update-packages --force-upgrade` resolves upgraded hosted dependency versions by creating a temporary directory with copies of `pubspec.yaml` files and running `dart pub upgrade --tighten`. With the introduction of Pub Workspace sub-packages in `flutter_tools` (e.g., `flutter_tools_core`, `flutter_tools_extension`), `dart pub upgrade` failed during version solving because local sub-packages and path dependencies do not exist in the temporary directory. This caused Pub to fail on missing local paths or attempt to resolve unreleased workspace sub-packages from `pub.dev`. This change: - Strips local path dependencies and workspace member dependencies from the temporary pubspec prior to running `pub upgrade --tighten`. - Propagates resolved dependencies to any sub-packages under `packages/flutter_tools/packages/` so shared constraints (e.g. `meta`) remain in sync. - Adds hermetic test coverage in `update_packages_test.dart` for workspace sub-packages and path dependencies. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Victor Sanni --- .../lib/src/commands/update_packages.dart | 82 +++++++++- .../hermetic/update_packages_test.dart | 147 ++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart index b56b4f80869b8..63a1a966b04ee 100644 --- a/packages/flutter_tools/lib/src/commands/update_packages.dart +++ b/packages/flutter_tools/lib/src/commands/update_packages.dart @@ -214,6 +214,17 @@ class UpdatePackagesCommand extends FlutterCommand { ); for (final (:project, :deps) in toolDeps) { _updatePubspec(project.directory, deps); + // When flutter_tools contains sub-packages in its workspace (under packages/), + // also update their pubspecs with the resolved dependencies so that shared + // package constraints (e.g. meta) remain in sync across the workspace. + final Directory subpackagesDir = project.directory.childDirectory('packages'); + if (subpackagesDir.existsSync()) { + for (final FileSystemEntity entity in subpackagesDir.listSync()) { + if (entity is Directory && entity.childFile(_pubspecName).existsSync()) { + _updatePubspec(entity, deps); + } + } + } } } } @@ -280,7 +291,28 @@ class UpdatePackagesCommand extends FlutterCommand { final yamlEditor = YamlEditor(pubspecContents); final ResolvedDependencies oldDeps = _fetchDeps(yamlEditor); final workspacePath = ['workspace']; - if (yamlEditor.parseAt(workspacePath, orElse: () => wrapAsYamlNode(null)).value != null) { + final YamlNode workspaceNode = yamlEditor.parseAt( + workspacePath, + orElse: () => wrapAsYamlNode(null), + ); + final workspaceMembers = {}; + if (workspaceNode is YamlList) { + for (final Object? member in workspaceNode) { + if (member is String) { + String memberName = globals.fs.path.basename(member); + final File memberPubspec = globals.fs.file( + globals.fs.path.join(project.directory.path, member, _pubspecName), + ); + if (memberPubspec.existsSync()) { + try { + memberName = Pubspec.parse(memberPubspec.readAsStringSync()).name; + } on Exception { + // Fall back to basename if parsing fails. + } + } + workspaceMembers.add(memberName); + } + } yamlEditor.remove(workspacePath); } final RelaxMode relaxMode = switch (cherryPicks.isNotEmpty) { @@ -288,6 +320,7 @@ class UpdatePackagesCommand extends FlutterCommand { false => relaxToAny ? RelaxMode.any : RelaxMode.caret, }; _relaxDeps(yamlEditor, relaxMode, pinnedDeps); + _removePathAndWorkspaceDependencies(yamlEditor, project.directory, workspaceMembers); tempPubspec.writeAsStringSync(yamlEditor.toString()); globals.printStatus('Upgrade in $projectTempDir (for project: ${project.manifest.appName})'); await pub.interactively( @@ -309,6 +342,53 @@ class UpdatePackagesCommand extends FlutterCommand { return deps; } + /// Removes local path dependencies and workspace member dependencies from the + /// temporary pubspec before running `pub upgrade --tighten`. + /// + /// `_upgrade` runs `dart pub upgrade` inside an isolated temporary directory + /// where only the project's root `pubspec.yaml` is written. Workspace members + /// and local subpackages (e.g. `flutter_tools_core`) do not exist in the + /// temporary directory. Removing them from the temporary pubspec prevents + /// version solving failures caused by missing local paths or attempting to + /// resolve unreleased workspace subpackages from pub.dev. The actual path + /// dependencies and workspace definitions in the repository are preserved and + /// not affected by this removal. + void _removePathAndWorkspaceDependencies( + YamlEditor yamlEditor, + Directory projectDirectory, + Set workspaceMembers, + ) { + final Directory subpackagesDir = projectDirectory.childDirectory('packages'); + for (final dependencyType in [ + 'dependencies', + 'dev_dependencies', + 'dependency_overrides', + ]) { + final YamlNode node = yamlEditor.parseAt([ + dependencyType, + ], orElse: () => wrapAsYamlNode(null)); + if (node is! YamlMap) { + continue; + } + final toRemove = []; + for (final MapEntry dep in node.entries) { + final Object? value = dep.value; + final packageName = dep.key! as String; + final bool isPathDependency = value is Map && value.containsKey('path'); + final bool isWorkspaceMember = + workspaceMembers.contains(packageName) || + (subpackagesDir.existsSync() && + subpackagesDir.childDirectory(packageName).childFile(_pubspecName).existsSync()); + if (isPathDependency || (value == null && isWorkspaceMember)) { + toRemove.add(packageName); + } + } + for (final packageName in toRemove) { + yamlEditor.remove([dependencyType, packageName]); + } + } + } + void _relaxDeps(YamlEditor yamlEditor, RelaxMode relaxMode, Map fixedDeps) { ResolvedDependencies().forEach( yamlEditor: yamlEditor, diff --git a/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart index 9d66e95a3dfc9..8e9b9bb7584f1 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart @@ -438,6 +438,153 @@ void main() { Logger: () => logger, }, ); + + testUsingContext( + '--force-upgrade succeeds when flutter_tools has workspace subpackages and path dependencies', + () async { + const subpackagePubspecYaml = r''' +name: flutter_tools_core +description: Core utilities for flutter_tools. +resolution: workspace + +environment: + sdk: ^3.7.0-0 + +dependencies: + unified_analytics: 8.0.5 + +# PUBSPEC CHECKSUM: a1b2c3 +'''; + const extensionSubpackagePubspecYaml = r''' +name: flutter_tools_extension +description: Extension utilities for flutter_tools. +resolution: workspace + +environment: + sdk: ^3.7.0-0 + +dependencies: + unified_analytics: 8.0.5 + +# PUBSPEC CHECKSUM: b2c3d4 +'''; + const customSubpackagePubspecYaml = r''' +name: flutter_tools_custom_pkg +description: Custom package for flutter_tools. +resolution: workspace + +environment: + sdk: ^3.7.0-0 + +dependencies: + unified_analytics: 8.0.5 + +# PUBSPEC CHECKSUM: c3d4e5 +'''; + const flutterToolsWithWorkspacePubspecYaml = r''' +name: flutter_tools +description: Examples for flutter +homepage: http://flutter.dev + +version: 1.0.0 + +resolution: workspace + +environment: + sdk: '>=3.2.0-0 <4.0.0' + flutter: ">=2.5.0-6.0.pre.30 <3.0.0" + +workspace: + - packages/flutter_tools_core + - packages/flutter_tools_extension + - packages/custom_core_dir + +dependencies: + test_api: 0.7.4 + flutter: + sdk: flutter + flutter_tools_core: + path: packages/flutter_tools_core + flutter_tools_extension: + flutter_tools_custom_pkg: + + archive: 3.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + unified_analytics: 8.0.5 + +# PUBSPEC CHECKSUM: 6hijp0 +'''; + final Directory coreSubpackageDir = flutterTools + .childDirectory('packages') + .childDirectory('flutter_tools_core'); + coreSubpackageDir.childFile('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(subpackagePubspecYaml); + + final Directory extensionSubpackageDir = flutterTools + .childDirectory('packages') + .childDirectory('flutter_tools_extension'); + extensionSubpackageDir.childFile('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(extensionSubpackagePubspecYaml); + + final Directory customSubpackageDir = flutterTools + .childDirectory('packages') + .childDirectory('custom_core_dir'); + customSubpackageDir.childFile('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(customSubpackagePubspecYaml); + + flutterTools + .childFile('pubspec.yaml') + .writeAsStringSync(flutterToolsWithWorkspacePubspecYaml); + + final command = UpdatePackagesCommand(verboseHelp: false); + await createTestCommandRunner( + command, + ).run(['update-packages', '--force-upgrade', '--update-hashes']); + + final File updatedFlutterToolsPubspec = flutterTools.childFile('pubspec.yaml'); + final parsedToolsPubspec = Pubspec.parse(updatedFlutterToolsPubspec.readAsStringSync()); + expect(parsedToolsPubspec.dependencies['flutter_tools_core'], isA()); + expect( + (parsedToolsPubspec.dependencies['flutter_tools_core']! as PathDependency).path, + 'packages/flutter_tools_core', + ); + expect(parsedToolsPubspec.dependencies.containsKey('flutter_tools_custom_pkg'), isTrue); + expect( + parsedToolsPubspec.dependencies['unified_analytics'], + HostedDependency(version: VersionConstraint.parse('8.0.10')), + ); + + final File updatedCorePubspec = coreSubpackageDir.childFile('pubspec.yaml'); + final parsedCorePubspec = Pubspec.parse(updatedCorePubspec.readAsStringSync()); + expect( + parsedCorePubspec.dependencies['unified_analytics'], + HostedDependency(version: VersionConstraint.parse('8.0.10')), + ); + + final File updatedExtensionPubspec = extensionSubpackageDir.childFile('pubspec.yaml'); + final parsedExtensionPubspec = Pubspec.parse(updatedExtensionPubspec.readAsStringSync()); + expect( + parsedExtensionPubspec.dependencies['unified_analytics'], + HostedDependency(version: VersionConstraint.parse('8.0.10')), + ); + + final File updatedCustomPubspec = customSubpackageDir.childFile('pubspec.yaml'); + final parsedCustomPubspec = Pubspec.parse(updatedCustomPubspec.readAsStringSync()); + expect( + parsedCustomPubspec.dependencies['unified_analytics'], + HostedDependency(version: VersionConstraint.parse('8.0.10')), + ); + }, + overrides: { + Pub: () => pub, + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Cache: () => Cache.test(processManager: processManager), + Logger: () => logger, + }, + ); }); } From 12de6b580ac2bb595268a2f3cf1af2020ba370c5 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Tue, 11 Aug 2026 17:19:39 -0400 Subject: [PATCH 204/330] Address review feedback: Add ApplicationExtension mock helper and fix isHigherThan logic --- .../gradle/src/main/kotlin/VersionFetcher.kt | 2 +- .../src/test/kotlin/FlutterPluginUtilsTest.kt | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt b/packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt index 0632bef409c38..50481d5c07fe8 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt @@ -146,7 +146,7 @@ internal data class CompileSdkVersion( */ fun isHigherThan(other: CompileSdkVersion): Boolean = when { - previewCodename != null && other.previewCodename != null -> false + other.previewCodename != null -> false previewCodename != null && other.apiLevel != null -> true apiLevel != null && other.apiLevel != null -> apiLevel > other.apiLevel else -> false diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index d4f25869100cd..51eb24b864a9d 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -89,6 +89,14 @@ private class TestEnvironment( } class FlutterPluginUtilsTest { + private fun setUpMockAndroidExtension( + project: Project, + ndkVersion: String = "29.0.13846066" + ): ApplicationExtension { + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockAndroidExtension + return mockAndroidExtension + } + companion object { const val EXAMPLE_ENGINE_VERSION = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" @@ -1893,7 +1901,6 @@ class FlutterPluginUtilsTest { val mockCmakeOptions = mockk() val mockNdkBuildOptions = mockk() val mockDefaultConfig = mockk() - every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions .findByType(BaseExtension::class.java)!! @@ -1909,6 +1916,7 @@ class FlutterPluginUtilsTest { every { mockCmakeOptions.path } returns fakeCmakeFile every { mockNdkBuildOptions.path } returns null + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "ignored") verify(exactly = 1) { @@ -1928,7 +1936,6 @@ class FlutterPluginUtilsTest { val mockCmakeOptions = mockk() val mockNdkBuildOptions = mockk() val mockDefaultConfig = mockk() - every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions .findByType(BaseExtension::class.java)!! @@ -1944,6 +1951,7 @@ class FlutterPluginUtilsTest { every { mockCmakeOptions.path } returns null every { mockNdkBuildOptions.path } returns fakeAndroidMkFile + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "ignored") verify(exactly = 1) { @@ -1982,9 +1990,6 @@ class FlutterPluginUtilsTest { every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" every { project.gradle.startParameter.taskNames } returns emptyList() every { project.gradle.startParameter.isOffline } returns false - val mockAndroidExtension = mockk() - every { project.extensions.findByName("android") } returns mockAndroidExtension - every { mockAndroidExtension.ndkVersion } returns "29.0.13846066" every { project.serviceOf() } returns mockExecOperations every { mockExecOperations.exec(capture(execActionSlot)) } answers { File(tempDir.toFile(), "ndk/29.0.13846066/source.properties").apply { @@ -1996,6 +2001,7 @@ class FlutterPluginUtilsTest { every { mockExecResult.assertNormalExitValue() } returns mockExecResult every { mockExecSpec.commandLine(any>()) } returns mockExecSpec + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "/base/path") finalizeDslSlot.captured.invoke(Any()) execActionSlot.captured.execute(mockExecSpec) @@ -2034,10 +2040,8 @@ class FlutterPluginUtilsTest { every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" every { project.gradle.startParameter.taskNames } returns emptyList() - val mockAndroidExtension = mockk() - every { project.extensions.findByName("android") } returns mockAndroidExtension - every { mockAndroidExtension.ndkVersion } returns "29.0.13846066" + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "/base/path") finalizeDslSlot.captured.invoke(Any()) @@ -2058,7 +2062,6 @@ class FlutterPluginUtilsTest { val mockDirectory = mockk() val mockBaseExtension = mockk() var cmakePath: File? = null - every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions every { mockBaseExtension.externalNativeBuild.ndkBuild } returns mockNdkBuildOptions @@ -2082,6 +2085,7 @@ class FlutterPluginUtilsTest { every { mockBuildType.name } returns "Debug" every { mockBuildType.externalNativeBuild.cmake.arguments(any(), any(), any()) } returns Unit + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "/base/path") cmakePath = tempDir.resolve("CMakeLists.txt").toFile() finalizeDslSlot.captured.invoke(Any()) @@ -2124,9 +2128,6 @@ class FlutterPluginUtilsTest { } returns "26.3.11579264" every { project.gradle.startParameter.taskNames } returns emptyList() every { project.gradle.startParameter.isOffline } returns false - val mockAndroidExtension = mockk() - every { project.extensions.findByName("android") } returns mockAndroidExtension - every { mockAndroidExtension.ndkVersion } answers { configuredNdkVersion } every { project.serviceOf() } returns mockExecOperations every { mockExecOperations.exec(capture(execActionSlot)) } answers { File(tempDir.toFile(), "ndk/27.3.13750724/source.properties").apply { @@ -2138,6 +2139,8 @@ class FlutterPluginUtilsTest { every { mockExecResult.assertNormalExitValue() } returns mockExecResult every { mockExecSpec.commandLine(any>()) } returns mockExecSpec + val mockAndroidExtension = setUpMockAndroidExtension(project) + every { mockAndroidExtension.ndkVersion } answers { configuredNdkVersion } FlutterPluginUtils.forceNdkDownload(project, "/base/path") configuredNdkVersion = "27.3.13750724" finalizeDslSlot.captured.invoke(Any()) @@ -2208,6 +2211,8 @@ class FlutterPluginUtilsTest { every { mockExecResult.assertNormalExitValue() } returns mockExecResult every { mockExecSpec.commandLine(any>()) } returns mockExecSpec + val mockAndroidExtension = setUpMockAndroidExtension(project) + every { mockAndroidExtension.ndkVersion } answers { configuredNdkVersion } FlutterPluginUtils.forceNdkDownload(project, "/base/path") configuredNdkVersion = "27.3.13750724" finalizeDslSlot.captured.invoke(Any()) @@ -2236,7 +2241,6 @@ class FlutterPluginUtilsTest { val mockNdkBuildOptions = mockk() val mockDefaultConfig = mockk() val mockBaseExtension = mockk() - every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions every { mockBaseExtension.externalNativeBuild.ndkBuild } returns mockNdkBuildOptions @@ -2248,10 +2252,8 @@ class FlutterPluginUtilsTest { every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" every { project.gradle.startParameter.taskNames } returns emptyList() - val mockAndroidExtension = mockk() - every { project.extensions.findByName("android") } returns mockAndroidExtension - every { mockAndroidExtension.ndkVersion } returns "29.0.13846066" + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "/base/path") finalizeDslSlot.captured.invoke(Any()) @@ -2285,6 +2287,7 @@ class FlutterPluginUtilsTest { every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" every { project.gradle.startParameter.taskNames } returns emptyList() + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "/base/path") finalizeDslSlot.captured.invoke(Any()) @@ -2316,13 +2319,11 @@ class FlutterPluginUtilsTest { every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" every { project.gradle.startParameter.taskNames } returns emptyList() every { project.gradle.startParameter.isOffline } returns false - val mockAndroidExtension = mockk() - every { project.extensions.findByName("android") } returns mockAndroidExtension - every { mockAndroidExtension.ndkVersion } returns "29.0.13846066" every { project.serviceOf() } returns mockExecOperations every { mockExecOperations.exec(any>()) } returns mockExecResult every { mockExecResult.assertNormalExitValue() } returns mockExecResult + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, "/base/path") assertThrows { @@ -2349,6 +2350,7 @@ class FlutterPluginUtilsTest { every { project.gradle.startParameter.taskNames } returns listOf(FlutterPluginUtils.TASK_PRINT_NDK_VERSION) every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk(relaxed = true) + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk(relaxed = true) FlutterPluginUtils.forceNdkDownload(project, "/base/path") verify(exactly = 0) { mockCmakeOptions.path(any()) } @@ -2365,7 +2367,6 @@ class FlutterPluginUtilsTest { val mockDirectoryProperty = mockk() val mockDirectory = mockk() val mockBaseExtension = mockk() - every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions every { mockBaseExtension.externalNativeBuild.ndkBuild } returns mockNdkBuildOptions @@ -2390,6 +2391,7 @@ class FlutterPluginUtilsTest { every { mockBuildType.name } returns "Debug" every { mockBuildType.externalNativeBuild.cmake.arguments(any(), any(), any()) } returns Unit + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, basePath) finalizeDslSlot.captured.invoke(Any()) @@ -2416,7 +2418,6 @@ class FlutterPluginUtilsTest { val mockDirectoryProperty = mockk() val mockDirectory = mockk() val mockBaseExtension = mockk() - every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions every { mockBaseExtension.externalNativeBuild.ndkBuild } returns mockNdkBuildOptions @@ -2442,6 +2443,7 @@ class FlutterPluginUtilsTest { every { mockBuildType.name } returns "Debug" every { mockBuildType.externalNativeBuild.cmake.arguments(any(), any(), any()) } returns Unit + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, basePath) finalizeDslSlot.captured.invoke(Any()) @@ -2466,7 +2468,6 @@ class FlutterPluginUtilsTest { val mockDefaultConfig = mockk() val mockDirectoryProperty = mockk() val mockDirectory = mockk() - every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns null every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns null @@ -2504,6 +2505,7 @@ class FlutterPluginUtilsTest { every { mockBuildType.name } returns "Debug" every { mockBuildType.externalNativeBuild.cmake.arguments(any(), any(), any()) } returns Unit + setUpMockAndroidExtension(project) FlutterPluginUtils.forceNdkDownload(project, basePath) verify(exactly = 1) { From 4dbb5857cef9639a411861055e021486eba18fba Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 11 Aug 2026 21:22:46 +0000 Subject: [PATCH 205/330] Roll Skia from 7817fed2e368 to 339bedab6766 (16 revisions) (#190948) https://skia.googlesource.com/skia.git/+log/7817fed2e368..339bedab6766 2026-08-11 ashkan.hoss@gmail.com [text] Enforce trivial destruction in GlyphType 2026-08-11 jrosengren@microsoft.com [rust_ico] Report ICO size from the directory and succeed on partial data 2026-08-11 jmbetancourt@google.com Revert "[Text] Promote thread local SkStrikeCache API" 2026-08-11 robertphillips@google.com [graphite] Address race condition in PipelineCallbackHandler 2026-08-11 michaelludwig@google.com [ganesh] Access beTex's backend format after creation 2026-08-11 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from bc27f11324cf to 34c46f7241a1 (2 revisions) 2026-08-11 skia-autoroll@skia-public.iam.gserviceaccount.com Roll ANGLE from 7b4238a1ad6c to e8d1a2c13248 (44 revisions) 2026-08-11 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Skia Infra from 262acfdc6d6e to d560ab323cf8 (9 revisions) 2026-08-11 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from 5b79878f746c to 9d6253a28823 (16 revisions) 2026-08-11 skia-autoroll@skia-public.iam.gserviceaccount.com Roll SwiftShader from 26e6a4b84daf to 6b8d31709ad1 (1 revision) 2026-08-11 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from a026d3da2361 to 6f9c650f0c47 2026-08-11 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-11 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-10 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 37e841bbd2f9 to bc27f11324cf (14 revisions) 2026-08-10 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2026-08-10 alexisdavidc@google.com [Text] Promote thread local SkStrikeCache API If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/skia-flutter-autoroll Please CC jimgraham@google.com,jmbetancourt@google.com,kjlubick@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Skia: https://bugs.chromium.org/p/skia/issues/entry To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 37ba85941be89..fa50db80e7ea7 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'dart_ai_rev': '9c96bfe5f091c9451eff5b59c9bffeb2e806b875', - 'skia_revision': '7817fed2e368c34917073c61545ff3ec1137589f', + 'skia_revision': '339bedab6766ec57f2000034a643f84db95d7166', # Do not download the Emscripten SDK by default. # This prevents us from downloading the Emscripten toolchain for builds From 5a28206e59ccccc7a71d478e313eb10409466f09 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Tue, 11 Aug 2026 21:22:47 +0000 Subject: [PATCH 206/330] [flutter_tools] Switch 'flutter analyze' to use dart/workspace/analysis/complete (#190843) This updates the `flutter analyze` implementation for one-shot analysis from relying on the progress notifications to instead using the custom `dart/workspace/analysis/complete` request. This request waits for analysis to complete before returning and handles waiting for server + plugin startup plus any in-progress analysis for both server and plugins. This will avoid `flutter analyze` terminating too early in the case of slow plugin startups or hanging if we debounce progress notifications for short analysis. The `--watch` mode continues to use the progress notifications, since it uses them to report progress to the user and not as a signal for being complete. As part of this, I extracted a mock LSP server to simplify the tests a little, since the implementation of the fake server is a bit more complex now. Fixes [#190839](https://github.com/flutter/flutter/issues/190839) ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/src/commands/analyze_once.dart | 39 +-- .../flutter_tools/lib/src/dart/analysis.dart | 38 +-- .../hermetic/analysis_server_mock.dart | 175 ++++++++++ .../hermetic/analyze_continuously_test.dart | 300 +++++++++--------- .../commands.shard/hermetic/analyze_test.dart | 58 +--- 5 files changed, 350 insertions(+), 260 deletions(-) create mode 100644 packages/flutter_tools/test/commands.shard/hermetic/analysis_server_mock.dart diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart index 940804d64ce15..92b1aaaf14127 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_once.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart @@ -50,7 +50,6 @@ class AnalyzeOnce extends AnalyzeBase { throwToolExit('Nothing to analyze.', exitCode: 0); } - final analysisCompleter = Completer(); final errors = []; final server = AnalysisServer( @@ -68,20 +67,6 @@ class AnalyzeOnce extends AnalyzeBase { Stopwatch? timer; Status? progress; try { - StreamSubscription? subscription; - - void handleAnalysisStatus(bool isAnalyzing) { - if (!isAnalyzing) { - analysisCompleter.complete(); - subscription?.cancel(); - subscription = null; - } - } - - subscription = server.onAnalyzing.listen( - (bool isAnalyzing) => handleAnalysisStatus(isAnalyzing), - ); - void handleAnalysisErrors(FileAnalysisErrors fileErrors) { errors.addAll(fileErrors.errors); } @@ -89,18 +74,18 @@ class AnalyzeOnce extends AnalyzeBase { server.onErrors.listen(handleAnalysisErrors); await server.start(); - // Completing the future in the callback can't fail. + + // Capture if the server exits unexpectedly. + final exitErrorCompleter = Completer(); unawaited( server.onExit.then((int? exitCode) { - if (!analysisCompleter.isCompleted) { - analysisCompleter.completeError( - // Include the last 20 lines of server output in exception message - _AnalysisServerExitException( - 'analysis server exited with code $exitCode and output:\n${server.getLogs(20)}', - exitCode, - ), - ); - } + exitErrorCompleter.completeError( + // Include the last 20 lines of server output in exception message + _AnalysisServerExitException( + 'analysis server exited with code $exitCode and output:\n${server.getLogs(20)}', + exitCode, + ), + ); }), ); @@ -113,8 +98,10 @@ class AnalyzeOnce extends AnalyzeBase { ? logger.startProgress('Analyzing $message...') : null; + // Wait for analysis to complete, or the server to exit and produce + // an error. try { - await analysisCompleter.future; + await Future.any([server.waitForAnalysis(), exitErrorCompleter.future]); } on _AnalysisServerExitException catch (error) { throwToolExit(error.message, exitCode: error.exitCode); } diff --git a/packages/flutter_tools/lib/src/dart/analysis.dart b/packages/flutter_tools/lib/src/dart/analysis.dart index 316c9f1003f70..c137e23261397 100644 --- a/packages/flutter_tools/lib/src/dart/analysis.dart +++ b/packages/flutter_tools/lib/src/dart/analysis.dart @@ -50,34 +50,18 @@ class AnalysisServer { final _errorsController = StreamController.broadcast(); var _didServerErrorOccur = false; - /// Whether the server is currently analyzing. - bool get isAnalyzing => _isAnalyzing; - bool _isAnalyzing = false; - - /// Returns a [Future] that completes when the server is no longer analyzing. + /// Returns a [Future] that completes when the server has completed any + /// in-progress initialization or analysis. /// - /// If [delay] is provided, this method will wait for that duration before - /// checking if the server is analyzing. if the server starts analyzing during - /// that duration, it will wait for analysis to complete. + /// This method will wait for [delay] before calling the server. If not + /// provided, defaults to 100ms. /// /// This is useful to avoid the race condition where analysis hasn't started /// yet after a file change. Future waitForAnalysis({Duration delay = const Duration(milliseconds: 100)}) async { - if (_isAnalyzing) { - await onAnalyzing.firstWhere((bool analyzing) => !analyzing); - } - if (delay != Duration.zero) { - // Wait for analysis to potentially start. - try { - await onAnalyzing.firstWhere((bool analyzing) => analyzing).timeout(delay); - // If analysis started, wait for it to finish. - if (_isAnalyzing) { - await onAnalyzing.firstWhere((bool analyzing) => !analyzing); - } - } on TimeoutException { - // Analysis didn't start within the delay, so we assume it's not going to. - } - } + await Future.delayed(delay); + + await sendRequest('dart/workspace/analysis/complete', {}); } var _id = 0; @@ -146,6 +130,12 @@ class AnalysisServer { bool get didServerErrorOccur => _didServerErrorOccur; + /// A stream of booleans indicating that the server is starting or stopping + /// analysis. + /// + /// These statuses are intended to show progress to a user and not intended + /// as a reliable indicator that all analysis has completed. To know when + /// analysis has definitely completed, use [waitForAnalysis]. Stream get onAnalyzing => _analyzingController.stream; Stream get onErrors => _errorsController.stream; @@ -307,10 +297,8 @@ class AnalysisServer { if (value is Map) { final kind = value['kind'] as String?; if (kind == 'begin') { - _isAnalyzing = true; _analyzingController.add(true); } else if (kind == 'end') { - _isAnalyzing = false; _analyzingController.add(false); } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/analysis_server_mock.dart b/packages/flutter_tools/test/commands.shard/hermetic/analysis_server_mock.dart new file mode 100644 index 0000000000000..1598b188365f0 --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/hermetic/analysis_server_mock.dart @@ -0,0 +1,175 @@ +// 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 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import '../../src/fake_process_manager.dart' as test_process_manager; + +/// A mock LSP server that allows a test to control analysis status, progress +/// notifications and diagnostics. +class MockLspServerProcess extends test_process_manager.FakeProcess { + factory MockLspServerProcess() { + final stdinController = StreamController>(); + final stdinSink = IOSink(stdinController.sink); + final exitCompleter = Completer(); + return MockLspServerProcess._(stdinController, stdin: stdinSink, exitCompleter: exitCompleter); + } + + MockLspServerProcess._( + this._stdinController, { + required super.stdin, + required Completer exitCompleter, + }) : _exitCompleter = exitCompleter, + super(completer: exitCompleter) { + _stdinController.stream.transform(utf8.decoder).listen(_handleStdinChunk); + } + + final StreamController> _stdinController; + final _inputBuffer = StringBuffer(); + final _stdoutController = StreamController>(); + final Completer _exitCompleter; + + Completer? _analysisCompleter; + final _initializeRequestCompleter = Completer>(); + Future> get initializeRequest => _initializeRequestCompleter.future; + + @override + Stream> get stdout => _stdoutController.stream; + + /// Starts simulated analysis on the server, but does not wait for it. + void triggerSimulatedAnalysis({Uri? diagnosticsFor, int diagnosticsCount = 1}) { + unawaited( + runSimulatedAnalysis(diagnosticsFor: diagnosticsFor, diagnosticsCount: diagnosticsCount), + ); + } + + /// Simulates analysis on the server. + /// + /// This will trigger a progress notification indicating that analysis has + /// started, optionally send [diagnosticsCount] diagnostics for + /// [diagnosticsFor], then send a progress notification indicating that anlaysis has ended. + Future runSimulatedAnalysis({Uri? diagnosticsFor, int diagnosticsCount = 1}) async { + _simulateAnalysisStart(); + await Future.delayed(const Duration(milliseconds: 10)); + + if (diagnosticsFor != null) { + _simulateDiagnostics(diagnosticsFor, diagnosticsCount); + await Future.delayed(const Duration(milliseconds: 10)); + } + + _simulateAnalysisEnd(); + } + + /// Causes the process to write a Dart VM Service banner to stdout. + void triggerVmServiceUriBanner() { + _writeRawOutput('The Dart VM service is listening on http://127.0.0.1:65155/ZkxDXuYz2Aw=/\n'); + } + + Future _handleRequest(Map request) async { + switch (request['method']) { + case 'initialize': + _initializeRequestCompleter.complete(request); + _writeAsLspToStdout( + jsonEncode({ + 'jsonrpc': '2.0', + 'id': request['id'], + 'result': {'capabilities': {}}, + }), + ); + case 'dart/workspace/analysis/complete': + await _analysisCompleter?.future; + _sendResponse(request['id'], null); + } + } + + void _handleStdinChunk(String chunk) { + _inputBuffer.write(chunk); + var input = _inputBuffer.toString(); + while (true) { + final int headerEnd = input.indexOf('\r\n\r\n'); + if (headerEnd == -1) { + break; + } + final String header = input.substring(0, headerEnd); + final Match? contentLengthMatch = RegExp(r'Content-Length: (\d+)').firstMatch(header); + if (contentLengthMatch == null) { + throw StateError('LSP request is missing a Content-Length header.'); + } + final int contentLength = int.parse(contentLengthMatch.group(1)!); + final int messageEnd = headerEnd + 4 + contentLength; + if (input.length < messageEnd) { + break; + } + final String message = input.substring(headerEnd + 4, messageEnd); + unawaited(_handleRequest(jsonDecode(message) as Map)); + input = input.substring(messageEnd); + } + _inputBuffer + ..clear() + ..write(input); + } + + void _sendNotification(String method, Map params) { + _writeAsLspToStdout( + jsonEncode({'jsonrpc': '2.0', 'method': method, 'params': params}), + ); + } + + void _sendResponse(Object? id, Map? result) { + _writeAsLspToStdout(jsonEncode({'jsonrpc': '2.0', 'id': id, 'result': result})); + } + + void _simulateAnalysisEnd() { + _analysisCompleter?.complete(); + _analysisCompleter = null; + _sendNotification(r'$/progress', { + 'token': 'analyze', + 'value': {'kind': 'end'}, + }); + } + + @override + bool kill([ProcessSignal signal = ProcessSignal.sigterm]) { + _exitCompleter.complete(); + return super.kill(); + } + + void _simulateAnalysisStart() { + if (_analysisCompleter case null || Completer(isCompleted: true)) { + _analysisCompleter = Completer(); + _sendNotification(r'$/progress', { + 'token': 'analyze', + 'value': {'kind': 'begin'}, + }); + } + } + + void _simulateDiagnostics(Uri targetUri, int count) { + _sendNotification('textDocument/publishDiagnostics', { + 'uri': targetUri.toString(), + 'diagnostics': [ + for (var i = 0; i < count; i++) + { + 'range': { + 'start': {'line': 99 + i, 'character': 4}, + 'end': {'line': 99 + i, 'character': 4}, + }, + 'severity': 2, + 'code': '500', + 'message': "It's an error.", + }, + ], + }); + } + + void _writeAsLspToStdout(String message) { + _stdoutController.add(utf8.encode('Content-Length: ${message.length}\r\n\r\n$message')); + } + + void _writeRawOutput(String output) { + _stdoutController.add(utf8.encode(output)); + } +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart index 5fa535ee5e439..7829ca44953f9 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart @@ -16,13 +16,13 @@ import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/analyze.dart'; import 'package:flutter_tools/src/dart/analysis.dart'; -import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project_validator.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; import '../../src/test_flutter_command_runner.dart'; +import 'analysis_server_mock.dart'; void main() { setUpAll(() { @@ -33,10 +33,12 @@ void main() { late FileSystem fileSystem; late Platform platform; late AnsiTerminal terminal; - late Logger logger; + late BufferLogger logger; setUp(() { - fileSystem = globals.localFileSystem; + fileSystem = MemoryFileSystem.test( + style: const LocalPlatform().isWindows ? FileSystemStyle.windows : FileSystemStyle.posix, + ); platform = const LocalPlatform(); terminal = AnsiTerminal(platform: platform, stdio: Stdio()); logger = BufferLogger(outputPreferences: OutputPreferences.test(), terminal: terminal); @@ -69,74 +71,63 @@ void main() { '''); } - group('analyze --watch', () { - testUsingContext('AnalysisServer success', () async { - final fileSystem = MemoryFileSystem.test(); - final Directory tempDir = fileSystem.systemTempDirectory.createTempSync( - 'flutter_analysis_test.', - ); - createSampleProject(tempDir); + testUsingContext('AnalysisServer success', () async { + final Directory tempDir = fileSystem.systemTempDirectory.createTempSync( + 'flutter_analysis_test.', + ); + createSampleProject(tempDir); - final stdin = StreamController>(); - final processManager = FakeProcessManager.list([ - FakeCommand( - command: const [ - 'Artifact.engineDartSdkPath/bin/dart', - 'language-server', - '--dart-sdk', - 'Artifact.engineDartSdkPath', - '--disable-server-feature-completion', - '--disable-server-feature-search', - '--suppress-analytics', - ], - stdin: IOSink(stdin.sink), - stdout: - 'Content-Length: 36\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{}}' - 'Content-Length: 93\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"begin"}}}' - 'Content-Length: 91\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"end"}}}', - ), - ]); + final process = MockLspServerProcess(); + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + fileSystem.path.join('Artifact.engineDartSdkPath', 'bin', 'dart'), + 'language-server', + '--dart-sdk', + 'Artifact.engineDartSdkPath', + '--disable-server-feature-completion', + '--disable-server-feature-search', + '--suppress-analytics', + ], + process: process, + ), + ]); - final server = AnalysisServer( - 'Artifact.engineDartSdkPath', - [tempDir.path], - fileSystem: fileSystem, - platform: FakePlatform(), - processManager: processManager, - logger: logger, - terminal: terminal, - suppressAnalytics: true, - ); + final server = AnalysisServer( + 'Artifact.engineDartSdkPath', + [tempDir.path], + fileSystem: fileSystem, + platform: FakePlatform(), + processManager: processManager, + logger: logger, + terminal: terminal, + suppressAnalytics: true, + ); - var errorCount = 0; - server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length); + var errorCount = 0; + server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length); - await server.start(); - await server.waitForAnalysis(); + await server.start(); + process.triggerSimulatedAnalysis(); + await server.waitForAnalysis(); - expect(errorCount, 0); + expect(errorCount, 0); - await server.dispose(); - expect(processManager, hasNoRemainingExpectations); - }); + await server.dispose(); + expect(processManager, hasNoRemainingExpectations); }); testUsingContext('AnalysisServer errors', () async { - final fileSystem = MemoryFileSystem.test(); final Directory tempDir = fileSystem.systemTempDirectory.createTempSync( 'flutter_analysis_test.', ); createSampleProject(tempDir, brokenCode: true); - final stdin = StreamController>(); + final process = MockLspServerProcess(); final processManager = FakeProcessManager.list([ FakeCommand( - command: const [ - 'Artifact.engineDartSdkPath/bin/dart', + command: [ + fileSystem.path.join('Artifact.engineDartSdkPath', 'bin', 'dart'), 'language-server', '--dart-sdk', 'Artifact.engineDartSdkPath', @@ -144,20 +135,7 @@ void main() { '--disable-server-feature-search', '--suppress-analytics', ], - stdin: IOSink(stdin.sink), - stdout: - 'Content-Length: 36\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{}}' - 'Content-Length: 93\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"begin"}}}' - 'Content-Length: 249\r\n\r\n' - '{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{' - '"uri":"file:///directoryA/foo","diagnostics":[{"range":{"start":{"line":99,' - '"character":4},"end":{"line":99,"character":4}},"severity":2,"code":"500",' - '"message":"It\'s an error."}]}}' - 'Content-Length: 91\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"end"}}}', + process: process, ), ]); @@ -176,6 +154,7 @@ void main() { server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length); await server.start(); + process.triggerSimulatedAnalysis(diagnosticsFor: Uri.parse('file:///directoryA/foo')); await server.waitForAnalysis(); expect(errorCount, greaterThan(0)); @@ -185,17 +164,16 @@ void main() { }); testUsingContext('Returns no errors when source is error-free', () async { - final fileSystem = MemoryFileSystem.test(); final Directory tempDir = fileSystem.systemTempDirectory.createTempSync( 'flutter_analysis_test.', ); createSampleProject(tempDir); - final stdin = StreamController>(); + final process = MockLspServerProcess(); final processManager = FakeProcessManager.list([ FakeCommand( - command: const [ - 'Artifact.engineDartSdkPath/bin/dart', + command: [ + fileSystem.path.join('Artifact.engineDartSdkPath', 'bin', 'dart'), 'language-server', '--dart-sdk', 'Artifact.engineDartSdkPath', @@ -203,15 +181,7 @@ void main() { '--disable-server-feature-search', '--suppress-analytics', ], - stdin: IOSink(stdin.sink), - stdout: - 'Content-Length: 36\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{}}' - 'Content-Length: 93\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"begin"}}}' - 'Content-Length: 91\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"end"}}}', + process: process, ), ]); @@ -231,6 +201,7 @@ void main() { errorCount += errors.errors.length; }); await server.start(); + process.triggerSimulatedAnalysis(); await server.waitForAnalysis(); expect(errorCount, 0); await server.dispose(); @@ -238,7 +209,7 @@ void main() { }); testUsingContext('Can run AnalysisService without suppressing analytics', () async { - final stdin = StreamController>(); + final process = MockLspServerProcess(); final processManager = FakeProcessManager.list([ FakeCommand( command: const [ @@ -249,9 +220,7 @@ void main() { '--disable-server-feature-completion', '--disable-server-feature-search', ], - stdin: IOSink(stdin.sink), - stdout: - 'Content-Length: 53\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}\r\n', + process: process, ), ]); @@ -259,7 +228,7 @@ void main() { final command = AnalyzeCommand( terminal: Terminal.test(), artifacts: artifacts, - logger: BufferLogger.test(), + logger: logger, platform: FakePlatform(), fileSystem: MemoryFileSystem.test(), processManager: processManager, @@ -270,13 +239,13 @@ void main() { final commandRunner = TestFlutterCommandRunner(); commandRunner.addCommand(command); unawaited(commandRunner.run(['analyze', '--watch'])); - await stdin.stream.first; + await process.initializeRequest; expect(processManager, hasNoRemainingExpectations); }); testUsingContext('Can run AnalysisService with customized cache location', () async { - final stdin = StreamController>(); + final process = MockLspServerProcess(); final processManager = FakeProcessManager.list([ FakeCommand( command: const [ @@ -288,9 +257,7 @@ void main() { '--disable-server-feature-search', '--suppress-analytics', ], - stdin: IOSink(stdin.sink), - stdout: - 'Content-Length: 53\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}\r\n', + process: process, ), ]); @@ -298,7 +265,7 @@ void main() { final command = AnalyzeCommand( terminal: Terminal.test(), artifacts: artifacts, - logger: BufferLogger.test(), + logger: logger, platform: FakePlatform(), fileSystem: MemoryFileSystem.test(), processManager: processManager, @@ -309,28 +276,13 @@ void main() { final commandRunner = TestFlutterCommandRunner(); commandRunner.addCommand(command); unawaited(commandRunner.run(['analyze', '--watch'])); - await stdin.stream.first; + await process.initializeRequest; expect(processManager, hasNoRemainingExpectations); }); testUsingContext('Can run AnalysisService with customized cache location --watch', () async { - // Use Windows style on Windows host so Uri.toFilePath() parses it correctly with drive letters. - final fileSystem = MemoryFileSystem.test( - style: const LocalPlatform().isWindows ? FileSystemStyle.windows : FileSystemStyle.posix, - ); - fileSystem.directory('directoryA').childFile('foo').createSync(recursive: true); - - final logger = BufferLogger.test(); - - final fooUri = fileSystem.path.toUri(fileSystem.path.absolute('directoryA', 'foo')).toString(); - final diagnosticsJson = - '{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{' - '"uri":"$fooUri","diagnostics":[{"range":{"start":{"line":99,' - '"character":4},"end":{"line":99,"character":4}},"severity":2,"code":"500",' - '"message":"It\'s an error."}]}}'; - - final stdin = StreamController>(); + final process = MockLspServerProcess(); final processManager = FakeProcessManager.list([ FakeCommand( command: [ @@ -342,17 +294,7 @@ void main() { '--disable-server-feature-search', '--suppress-analytics', ], - stdin: IOSink(stdin.sink), - stdout: - 'Content-Length: 36\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{}}' - 'Content-Length: 93\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"begin"}}}' - 'Content-Length: ${diagnosticsJson.length}\r\n\r\n' - '$diagnosticsJson' - 'Content-Length: 91\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"end"}}}', + process: process, ), ]); @@ -371,6 +313,13 @@ void main() { final commandRunner = TestFlutterCommandRunner(); commandRunner.addCommand(command); unawaited(commandRunner.run(['analyze', '--watch'])); + await process.initializeRequest; + + // Trigger analysis and diagnostics for an existing file. + final File targetFile = fileSystem.directory('directoryA').childFile('foo') + ..createSync(recursive: true); + final Uri targetUri = fileSystem.path.toUri(targetFile.path); + process.triggerSimulatedAnalysis(diagnosticsFor: targetUri); while (!logger.statusText.contains('analyzed 1 file')) { await Future.delayed(const Duration(milliseconds: 100)); @@ -383,8 +332,7 @@ void main() { }); testUsingContext('AnalysisService --watch skips errors from non-files', () async { - final logger = BufferLogger.test(); - final stdin = StreamController>(); + final process = MockLspServerProcess(); final processManager = FakeProcessManager.list([ FakeCommand( command: const [ @@ -396,20 +344,7 @@ void main() { '--disable-server-feature-search', '--suppress-analytics', ], - stdin: IOSink(stdin.sink), - stdout: - 'Content-Length: 36\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{}}' - 'Content-Length: 93\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"begin"}}}' - 'Content-Length: 249\r\n\r\n' - '{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{' - '"uri":"file:///directoryA/bar","diagnostics":[{"range":{"start":{"line":99,' - '"character":4},"end":{"line":99,"character":4}},"severity":2,"code":"500",' - '"message":"It\'s an error."}]}}' - 'Content-Length: 91\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"end"}}}', + process: process, ), ]); @@ -428,6 +363,12 @@ void main() { final commandRunner = TestFlutterCommandRunner(); commandRunner.addCommand(command); unawaited(commandRunner.run(['analyze', '--watch'])); + await process.initializeRequest; + + // Trigger analysis and diagnostics for a non-existing file. + final File targetFile = fileSystem.directory('directoryA').childFile('foo'); + final Uri targetUri = fileSystem.path.toUri(targetFile.path); + process.triggerSimulatedAnalysis(diagnosticsFor: targetUri); while (!logger.statusText.contains('analyzed 1 file')) { await Future.delayed(const Duration(milliseconds: 100)); @@ -445,8 +386,7 @@ void main() { // invoke json.decode(...) on the VM service message. // // Regression test for https://github.com/flutter/flutter/issues/58391. - final logger = BufferLogger.test(); - final stdin = StreamController>(); + final process = MockLspServerProcess(); final processManager = FakeProcessManager.list([ FakeCommand( command: const [ @@ -458,21 +398,7 @@ void main() { '--disable-server-feature-search', '--suppress-analytics', ], - stdin: IOSink(stdin.sink), - stdout: - 'The Dart VM service is listening on http://127.0.0.1:65155/ZkxDXuYz2Aw=/\n' - 'Content-Length: 36\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{}}' - 'Content-Length: 93\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"begin"}}}' - 'Content-Length: 249\r\n\r\n' - '{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{' - '"uri":"file:///directoryA/bar","diagnostics":[{"range":{"start":{"line":99,' - '"character":4},"end":{"line":99,"character":4}},"severity":2,"code":"500",' - '"message":"It\'s an error."}]}}' - 'Content-Length: 91\r\n\r\n' - r'{"jsonrpc":"2.0","method":"$/progress","params":{"token":"analyze",' - '"value":{"kind":"end"}}}', + process: process, ), ]); @@ -491,6 +417,10 @@ void main() { final commandRunner = TestFlutterCommandRunner(); commandRunner.addCommand(command); unawaited(commandRunner.run(['analyze', '--watch'])); + await process.initializeRequest; + process + ..triggerVmServiceUriBanner() + ..triggerSimulatedAnalysis(diagnosticsFor: Uri.parse('file:///directoryA/foo')); while (!logger.statusText.contains('analyzed 1 file')) { await Future.delayed(const Duration(milliseconds: 100)); @@ -501,4 +431,66 @@ void main() { expect(processManager, hasNoRemainingExpectations); }, ); + + testUsingContext('AnalysisService --watch handles errors coming and going', () async { + final process = MockLspServerProcess(); + final processManager = FakeProcessManager.list([ + FakeCommand( + command: [ + fileSystem.path.join('Artifact.engineDartSdkPath', 'bin', 'dart'), + 'language-server', + '--dart-sdk', + 'Artifact.engineDartSdkPath', + '--disable-server-feature-completion', + '--disable-server-feature-search', + '--suppress-analytics', + ], + process: process, + ), + ]); + + final artifacts = Artifacts.test(); + final command = AnalyzeCommand( + terminal: Terminal.test(), + artifacts: artifacts, + logger: logger, + platform: FakePlatform(), + fileSystem: fileSystem, + processManager: processManager, + allProjectValidators: [], + suppressAnalytics: true, + ); + + final commandRunner = TestFlutterCommandRunner(); + commandRunner.addCommand(command); + unawaited(commandRunner.run(['analyze', '--watch'])); + await process.initializeRequest; + + // Simulate some diagnostics coming and going. The file must exist. + final File targetFile = fileSystem.directory('directoryA').childFile('foo') + ..createSync(recursive: true); + final Uri targetUri = fileSystem.path.toUri(targetFile.path); + + await process.runSimulatedAnalysis(diagnosticsFor: targetUri); // 1 new + await process.runSimulatedAnalysis(diagnosticsFor: targetUri, diagnosticsCount: 0); // 1 fixed + await process.runSimulatedAnalysis(diagnosticsFor: targetUri, diagnosticsCount: 2); // 2 new + await process.runSimulatedAnalysis(diagnosticsFor: targetUri, diagnosticsCount: 0); // 2 fixed + + // Wait for the final line we expect. + while (!logger.statusText.contains('2 fixed')) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + expect( + logger.statusText.split('\n'), + containsAllInOrder([ + contains('1 new'), + contains('1 fixed'), + contains('2 new'), + contains('2 fixed'), + ]), + ); + expect(logger.errorText, isEmpty); + expect(processManager, hasNoRemainingExpectations); + }); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart index f448b6a1bd36e..75806cb7f08ad 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -20,8 +17,8 @@ import 'package:flutter_tools/src/project_validator.dart'; import '../../src/common.dart'; import '../../src/context.dart'; -import '../../src/fake_process_manager.dart' as test_process_manager; import '../../src/test_flutter_command_runner.dart'; +import 'analysis_server_mock.dart'; const _kFlutterRoot = '/data/flutter'; const SIGABRT = -6; @@ -156,10 +153,7 @@ void main() { testUsingContext( '--flutter-repo analyzes everything in the flutterRoot', () async { - final streamController = StreamController>(); - final sink = IOSink(streamController.sink); - final exitCompleter = Completer(); - final process = _CustomLspProcess(stdin: sink, exitCompleter: exitCompleter); + final process = MockLspServerProcess(); processManager.addCommands([ FakeCommand( // artifact paths are from Artifacts.test() and stable @@ -176,41 +170,9 @@ void main() { ), ]); - final buffer = StringBuffer(); - final messageReceived = Completer(); - String? firstMessage; - - streamController.stream.transform(utf8.decoder).listen((String chunk) { - buffer.write(chunk); - final current = buffer.toString(); - if (current.contains('{') && firstMessage == null) { - final int startIndex = current.indexOf('{'); - firstMessage = current.substring(startIndex); - final request = jsonDecode(firstMessage!) as Map; - if (request['method'] == 'initialize') { - process.addResponse( - '{"jsonrpc":"2.0","id":1,"result":' - '{"capabilities":{"window":{"workDoneProgress":true}}}}', - ); - process.addResponse( - r'{"jsonrpc":"2.0","method":"$/progress","params":' - r'{"token":"analyze","value":{"kind":"begin"}}}', - ); - process.addResponse( - r'{"jsonrpc":"2.0","method":"$/progress","params":' - r'{"token":"analyze","value":{"kind":"end"}}}', - ); - exitCompleter.complete(); - messageReceived.complete(); - } - } - }); - await runner.run(['analyze', '--flutter-repo']); - expect(firstMessage, isNotNull); - final request = jsonDecode(firstMessage!) as Map; - expect(request['method'], 'initialize'); + final Map request = await process.initializeRequest; final params = request['params']! as Map; expect( params['workspaceFolders'] as List?, @@ -271,17 +233,3 @@ bool inRepo(List? fileList, FileSystem fileSystem) { } return false; } - -class _CustomLspProcess extends test_process_manager.FakeProcess { - _CustomLspProcess({super.stdin, Completer? exitCompleter}) - : super(completer: exitCompleter); - - final StreamController> _stdoutController = StreamController>(); - - @override - Stream> get stdout => _stdoutController.stream; - - void addResponse(String message) { - _stdoutController.add(utf8.encode('Content-Length: ${message.length}\r\n\r\n$message')); - } -} From 87f3828d0154e1488d52aafd8e55751ebd442b60 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 11 Aug 2026 21:23:37 +0000 Subject: [PATCH 207/330] Reduce web_skwasm_tests subshards from 8 to 2 (#190728) Reduces the web_skwasm_tests subshards from 8 to 2 (Linux web_skwasm_tests_0 and Linux web_skwasm_tests_1). --- .ci.yaml | 137 ---------------------- dev/bots/suite_runners/run_web_tests.dart | 9 +- 2 files changed, 6 insertions(+), 140 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index dbf01153595e4..9c65527a2f71b 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -2255,143 +2255,6 @@ targets: - engine/** - DEPS - - name: Linux web_skwasm_tests_2 - recipe: flutter/flutter_drone - bringup: true - timeout: 45 - properties: - dependencies: >- - [ - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, - {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} - ] - shard: web_skwasm_tests - subshard: "2" - tags: > - ["framework", "hostonly", "shard", "linux"] - test_timeout_secs: "2700" - runIf: - - dev/** - - packages/** - - bin/** - - .ci.yaml - - engine/** - - DEPS - - - name: Linux web_skwasm_tests_3 - recipe: flutter/flutter_drone - bringup: true - timeout: 45 - properties: - dependencies: >- - [ - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, - {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} - ] - shard: web_skwasm_tests - subshard: "3" - tags: > - ["framework", "hostonly", "shard", "linux"] - test_timeout_secs: "2700" - runIf: - - dev/** - - packages/** - - bin/** - - .ci.yaml - - engine/** - - DEPS - - - name: Linux web_skwasm_tests_4 - recipe: flutter/flutter_drone - bringup: true - timeout: 45 - properties: - dependencies: >- - [ - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, - {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} - ] - shard: web_skwasm_tests - subshard: "4" - tags: > - ["framework", "hostonly", "shard", "linux"] - test_timeout_secs: "2700" - runIf: - - dev/** - - packages/** - - bin/** - - .ci.yaml - - engine/** - - DEPS - - - name: Linux web_skwasm_tests_5 - recipe: flutter/flutter_drone - bringup: true - timeout: 45 - properties: - dependencies: >- - [ - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, - {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} - ] - shard: web_skwasm_tests - subshard: "5" - tags: > - ["framework", "hostonly", "shard", "linux"] - test_timeout_secs: "2700" - runIf: - - dev/** - - packages/** - - bin/** - - .ci.yaml - - engine/** - - DEPS - - - name: Linux web_skwasm_tests_6 - recipe: flutter/flutter_drone - bringup: true - timeout: 45 - properties: - dependencies: >- - [ - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, - {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} - ] - shard: web_skwasm_tests - subshard: "6" - tags: > - ["framework", "hostonly", "shard", "linux"] - test_timeout_secs: "2700" - runIf: - - dev/** - - packages/** - - bin/** - - .ci.yaml - - engine/** - - DEPS - - - name: Linux web_skwasm_tests_7_last - recipe: flutter/flutter_drone - bringup: true - timeout: 45 - properties: - dependencies: >- - [ - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, - {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} - ] - shard: web_skwasm_tests - subshard: "7_last" - tags: > - ["framework", "hostonly", "shard", "linux"] - test_timeout_secs: "2700" - runIf: - - dev/** - - packages/** - - bin/** - - .ci.yaml - - engine/** - - DEPS - name: Linux web_tool_tests recipe: flutter/flutter_drone diff --git a/dev/bots/suite_runners/run_web_tests.dart b/dev/bots/suite_runners/run_web_tests.dart index 5551e97b51d56..68ee994e501b5 100644 --- a/dev/bots/suite_runners/run_web_tests.dart +++ b/dev/bots/suite_runners/run_web_tests.dart @@ -250,7 +250,7 @@ class WebTestsSuite { } Future runWebSkwasmUnitTests() { - return _runWebUnitTests(useWasm: true, webShardCount: 8); + return _runWebUnitTests(useWasm: true, webShardCount: 2); } /// Runs one of the `dev/integration_tests/web_e2e_tests` tests. @@ -590,7 +590,7 @@ class WebTestsSuite { // // We make sure the last shard ends in _last so it's easier to catch mismatches // between `.ci.yaml` and `test.dart`. - subshards['${webShardCount - 1}_last'] = () async { + Future lastShardRunner() async { await _runFlutterWebTest( flutterPackageDirectory.path, allTests.sublist((webShardCount - 1) * testsPerShard, allTests.length), @@ -602,7 +602,10 @@ class WebTestsSuite { await _runFlutterWebTest(path.join(flutterRoot, 'packages', 'flutter_driver'), [ path.join('test', 'src', 'web_tests', 'web_extension_test.dart'), ], useWasm); - }; + } + + subshards['${webShardCount - 1}'] = lastShardRunner; + subshards['${webShardCount - 1}_last'] = lastShardRunner; await selectSubshard(subshards); } From 8837b1f600f8a83464885cd6b732318dd4b07b6f Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Tue, 11 Aug 2026 21:23:39 +0000 Subject: [PATCH 208/330] Remove the bringup flag from the linux_arm_host_desktop_engine builder (#190935) This builder was recently created in a refactoring of the Linux arm64 builders (see https://github.com/flutter/flutter/pull/180235) The builder was not being scheduled in the merge queue because it is marked as "bringup: true" (see https://github.com/flutter/flutter/issues/190893) --- engine/src/flutter/.ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/src/flutter/.ci.yaml b/engine/src/flutter/.ci.yaml index 1d8ba4f220fa0..b5406a854e765 100644 --- a/engine/src/flutter/.ci.yaml +++ b/engine/src/flutter/.ci.yaml @@ -222,7 +222,6 @@ targets: - name: Linux linux_arm_host_desktop_engine recipe: engine_v2/engine_v2 timeout: 120 - bringup: true properties: add_recipes_cq: "true" release_build: "true" From 537c9d18bb6eb818cede9911905d413454618269 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 11 Aug 2026 21:24:22 +0000 Subject: [PATCH 209/330] [devicelab] Remove orphaned screenshot test files (#190879) Remove several orphaned screenshot test files and associated code that were left behind after `package:image` was removed. Files deleted: - `dev/devicelab/bin/tasks/integration_ui_screenshot.dart` - `dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart` - `dev/integration_tests/ui/lib/screenshot.dart` Code removed: - `createEndToEndScreenshotTest()` in `dev/devicelab/lib/tasks/integration_tests.dart` Also removed corresponding entries from `TESTOWNERS`. Fixes https://github.com/flutter/flutter/issues/190696 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with \`///\`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- TESTOWNERS | 2 - .../tasks/integration_ui_ios_screenshot.dart | 12 --- .../bin/tasks/integration_ui_screenshot.dart | 12 --- .../lib/tasks/integration_tests.dart | 7 -- dev/integration_tests/ui/lib/screenshot.dart | 74 ------------------- 5 files changed, 107 deletions(-) delete mode 100644 dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart delete mode 100644 dev/devicelab/bin/tasks/integration_ui_screenshot.dart delete mode 100644 dev/integration_tests/ui/lib/screenshot.dart diff --git a/TESTOWNERS b/TESTOWNERS index 185001a65f817..c631152cc7b95 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -156,7 +156,6 @@ /dev/devicelab/bin/tasks/integration_ui_driver.dart @bkonyi @flutter/tool /dev/devicelab/bin/tasks/integration_ui_frame_number.dart @jtmcdole @flutter/engine /dev/devicelab/bin/tasks/integration_ui_keyboard_resize.dart @bkonyi @flutter/tool -/dev/devicelab/bin/tasks/integration_ui_screenshot.dart @bkonyi @flutter/tool /dev/devicelab/bin/tasks/integration_ui_textfield.dart @bkonyi @flutter/tool /dev/devicelab/bin/tasks/microbenchmarks.dart @jtmcdole @flutter/engine /dev/devicelab/bin/tasks/native_assets_android.dart @dcharkes @flutter/android @@ -210,7 +209,6 @@ /dev/devicelab/bin/tasks/integration_ui_ios_driver.dart LongCatIsLooong @flutter/tool /dev/devicelab/bin/tasks/integration_ui_ios_frame_number.dart @jtmcdole @flutter/engine /dev/devicelab/bin/tasks/integration_ui_ios_keyboard_resize.dart @LongCatIsLooong @flutter/engine -/dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart @LongCatIsLooong @flutter/tool /dev/devicelab/bin/tasks/integration_ui_ios_textfield.dart @LongCatIsLooong @flutter/tool /dev/devicelab/bin/tasks/ios_app_with_extensions_test.dart @hellohuanlin @flutter/tool /dev/devicelab/bin/tasks/ios_defines_test.dart @vashworth @flutter/tool diff --git a/dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart b/dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart deleted file mode 100644 index 49bb51d65485c..0000000000000 --- a/dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart +++ /dev/null @@ -1,12 +0,0 @@ -// 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.ios; - await task(createEndToEndScreenshotTest()); -} diff --git a/dev/devicelab/bin/tasks/integration_ui_screenshot.dart b/dev/devicelab/bin/tasks/integration_ui_screenshot.dart deleted file mode 100644 index 4a3c8cabbde08..0000000000000 --- a/dev/devicelab/bin/tasks/integration_ui_screenshot.dart +++ /dev/null @@ -1,12 +0,0 @@ -// 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.android; - await task(createEndToEndScreenshotTest()); -} diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index 279b88fc44be2..295891b810042 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -119,13 +119,6 @@ TaskFunction createEndToEndDriverTest({Map? environment}) { ).call; } -TaskFunction createEndToEndScreenshotTest() { - return DriverTest( - '${flutterDirectory.path}/dev/integration_tests/ui', - 'lib/screenshot.dart', - ).call; -} - TaskFunction createEndToEndKeyboardTextfieldTest() { return DriverTest( '${flutterDirectory.path}/dev/integration_tests/ui', diff --git a/dev/integration_tests/ui/lib/screenshot.dart b/dev/integration_tests/ui/lib/screenshot.dart deleted file mode 100644 index 7172185905833..0000000000000 --- a/dev/integration_tests/ui/lib/screenshot.dart +++ /dev/null @@ -1,74 +0,0 @@ -// 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/material.dart'; -import 'package:flutter_driver/driver_extension.dart'; - -/// This sample application creates a hard to render frame, causing the -/// driver script to race the raster thread. If the driver script wins the -/// race, it will screenshot the previous frame. If the raster thread wins -/// it, it will screenshot the latest frame. -void main() { - enableFlutterDriverExtension(); - - runApp(const Toggler()); -} - -class Toggler extends StatefulWidget { - const Toggler({super.key}); - - @override - State createState() => TogglerState(); -} - -class TogglerState extends State { - bool _visible = false; - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(title: const Text('FlutterDriver test')), - body: Material( - child: Column( - children: [ - TextButton( - key: const ValueKey('toggle'), - child: const Text('Toggle visibility'), - onPressed: () { - setState(() { - _visible = !_visible; - }); - }, - ), - Expanded(child: ListView(children: _buildRows(_visible ? 10 : 0))), - ], - ), - ), - ), - ); - } -} - -List _buildRows(int count) { - return List.generate(count, (int i) { - return Row(children: _buildCells(i / count)); - }); -} - -/// Builds cells that are known to take time to render causing a delay on the -/// raster thread. -List _buildCells(double epsilon) { - return List.generate(15, (int i) { - return Expanded( - child: Material( - // A magic color that the test will be looking for on the screenshot. - color: const Color(0xffff0102), - borderRadius: BorderRadius.all(Radius.circular(i.toDouble() + epsilon)), - elevation: 5.0, - child: const SizedBox(height: 10.0, width: 10.0), - ), - ); - }); -} From d794caaf063281f937a1aa12cb21adca6cccfe34 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 11 Aug 2026 21:24:22 +0000 Subject: [PATCH 210/330] Include the examples cross imports checker in the analzyer. (#190674) When we landed the script, we forgot to include it as part of the analzyer check. --- dev/bots/analyze.dart | 8 ++++++++ dev/bots/check_examples_cross_imports.dart | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index f272a87ae33ed..245357da5a810 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -387,6 +387,14 @@ List _getValidations({ path.join(flutterRoot, 'dev', 'bots', 'check_tests_cross_imports.dart'), ], workingDirectory: flutterRoot), ), + Validation( + 'cross-imports-examples', + 'Examples cross-import test validation...', + () => runCommand(dart, [ + '--enable-asserts', + path.join(flutterRoot, 'dev', 'bots', 'check_examples_cross_imports.dart'), + ], workingDirectory: flutterRoot), + ), ]; } diff --git a/dev/bots/check_examples_cross_imports.dart b/dev/bots/check_examples_cross_imports.dart index 51b2842f52709..1a86515c88496 100644 --- a/dev/bots/check_examples_cross_imports.dart +++ b/dev/bots/check_examples_cross_imports.dart @@ -401,6 +401,14 @@ class ExamplesCrossImportChecker { 'packages/flutter/examples/api/lib/widgets/scrollbar/raw_scrollbar.desktop.0.dart', 'packages/flutter/examples/api/lib/widgets/inherited_notifier/inherited_notifier.0.dart', 'packages/flutter/examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart', + 'packages/flutter/examples/api/lib/widgets/expansible/expansible.0.dart', + 'packages/flutter/examples/api/lib/widgets/selection_container/selection_container.0.dart', + 'packages/flutter/examples/api/lib/widgets/selection_container/selection_container_disabled.0.dart', + 'packages/flutter/examples/api/lib/widgets/platform_menu_bar/platform_menu_bar.0.dart', + 'packages/flutter/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.1.dart', + 'packages/flutter/examples/api/lib/widgets/context_menu/context_menu_controller.0.dart', + 'packages/flutter/examples/api/lib/widgets/context_menu/editable_text_toolbar_builder.0.dart', + 'packages/flutter/examples/api/lib/widgets/selectable_region/selectable_region.0.dart', 'packages/flutter/examples/api/test/widgets/animated_grid/animated_grid.0_test.dart', 'packages/flutter/examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart', 'packages/flutter/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart', @@ -585,10 +593,19 @@ class ExamplesCrossImportChecker { 'packages/flutter/examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart', 'packages/flutter/examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart', 'packages/flutter/examples/api/test/widgets/text_magnifier/text_magnifier.0_test.dart', + 'packages/flutter/examples/api/test/widgets/selection_container/selection_container_disabled.0_test.dart', + 'packages/flutter/examples/api/test/widgets/selection_container/selection_container.0_test.dart', + 'packages/flutter/examples/api/test/widgets/platform_menu_bar/platform_menu_bar.0_test.dart', + 'packages/flutter/examples/api/test/widgets/context_menu/context_menu_controller.0_test.dart', + 'packages/flutter/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.0_test.dart', + 'packages/flutter/examples/api/test/widgets/context_menu/editable_text_toolbar_builder.1_test.dart', + 'packages/flutter/examples/api/test/widgets/selectable_region/selectable_region.0_test.dart', 'examples/texture/lib/main.dart', }; - static final RegExp _examplesPrefix = RegExp(r'packages[/\\]flutter[/\\]examples[/\\]api|examples'); + static final RegExp _examplesPrefix = RegExp( + r'packages[/\\]flutter[/\\]examples[/\\]api|examples', + ); /// Find the `packages/flutter/examples/api/lib` and /// `packages/flutter/examples/api/test` directories which contain the API From db3ac0219f80fe1d9337eb41f25b0c5925027b90 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 11 Aug 2026 21:24:24 +0000 Subject: [PATCH 211/330] [flutter_tools] Add --preset option to flutter test (#190878) Adds support for \`--preset\` (and \`-P\`) to \`flutter test\`, allowing test presets defined in \`dart_test.yaml\` to be passed through to \`package:test\`. Fixes https://github.com/flutter/flutter/issues/189191 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with \`///\`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter_tools/lib/src/commands/test.dart | 9 ++++ .../flutter_tools/lib/src/test/runner.dart | 4 ++ .../commands.shard/hermetic/test_test.dart | 45 +++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 1523a4d85ede1..8ff89cc7a28af 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -115,6 +115,12 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { 'Run only tests that do not have the specified tags. See: https://pub.dev/packages/test#tagging-tests', splitCommas: false, ) + ..addMultiOption( + 'preset', + abbr: 'P', + help: 'The configuration preset(s) to use. Presets are defined in "dart_test.yaml".', + splitCommas: false, + ) ..addFlag( 'start-paused', negatable: false, @@ -432,6 +438,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { final List plainNames = stringsArg('plain-name'); final List tags = stringsArg('tags'); final List excludeTags = stringsArg('exclude-tags'); + final List presets = stringsArg('preset'); final BuildInfo buildInfo = await getBuildInfo( forcedBuildMode: BuildMode.debug, forcedUseLocalCanvasKit: true, @@ -676,6 +683,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { plainNames: plainNames, tags: tags, excludeTags: excludeTags, + presets: presets, machine: outputMachineFormat, updateGoldens: boolArg('update-goldens'), concurrency: jobs, @@ -702,6 +710,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { plainNames: plainNames, tags: tags, excludeTags: excludeTags, + presets: presets, watcher: watcher, enableVmService: collector != null || startPaused || enableVmService, machine: outputMachineFormat, diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart index e3386e3ed198c..8c0510dbd08cd 100644 --- a/packages/flutter_tools/lib/src/test/runner.dart +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -40,6 +40,7 @@ interface class FlutterTestRunner { List plainNames = const [], List tags = const [], List excludeTags = const [], + List presets = const [], bool enableVmService = false, bool machine = false, String? precompiledDillPath, @@ -84,6 +85,7 @@ interface class FlutterTestRunner { if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed', for (final String tag in tags) ...['--tags', tag], for (final String excludeTag in excludeTags) ...['--exclude-tags', excludeTag], + for (final String preset in presets) ...['--preset', preset], if (failFast) '--fail-fast', if (runSkipped) '--run-skipped', if (totalShards != null) '--total-shards=$totalShards', @@ -591,6 +593,7 @@ class SpawnPlugin extends PlatformPlugin { List plainNames = const [], List tags = const [], List excludeTags = const [], + List presets = const [], bool machine = false, bool updateGoldens = false, required int? concurrency, @@ -658,6 +661,7 @@ class SpawnPlugin extends PlatformPlugin { if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed', for (final String tag in tags) ...['--tags', tag], for (final String excludeTag in excludeTags) ...['--exclude-tags', excludeTag], + for (final String preset in presets) ...['--preset', preset], if (failFast) '--fail-fast', if (runSkipped) '--run-skipped', if (totalShards != null) '--total-shards=$totalShards', diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index 07fe32b2a145a..7cb5f8def4e36 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -595,6 +595,46 @@ resolution: workspace Cache: () => Cache.test(processManager: FakeProcessManager.any()), }, ); + + testUsingContext( + 'passes --preset through to package:test', + () async { + final fakePackageTest = FakePackageTest(); + final testCommand = TestCommand(testWrapper: fakePackageTest); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + await commandRunner.run(['test', '--no-pub', '--preset=foo', '--preset=bar']); + expect( + fakePackageTest.lastArgs, + containsAllInOrder(['--preset', 'foo', '--preset', 'bar']), + ); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + }, + ); + + testUsingContext( + 'passes -P through to package:test', + () async { + final fakePackageTest = FakePackageTest(); + final testCommand = TestCommand(testWrapper: fakePackageTest); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + await commandRunner.run(['test', '--no-pub', '-Pfoo', '-Pbar']); + expect( + fakePackageTest.lastArgs, + containsAllInOrder(['--preset', 'foo', '--preset', 'bar']), + ); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + }, + ); }); testUsingContext( @@ -711,6 +751,7 @@ resolution: workspace '--test-randomize-ordering-seed=random', '--tags=tag1', '--exclude-tags=tag2', + '--preset=preset1', '--fail-fast', '--run-skipped', '--total-shards=1', @@ -749,6 +790,8 @@ const List packageTestArgs = [ 'tag1', '--exclude-tags', 'tag2', + '--preset', + 'preset1', '--fail-fast', '--run-skipped', '--total-shards=1', @@ -1738,6 +1781,7 @@ class FakeFlutterTestRunner implements FlutterTestRunner { List plainNames = const [], List tags = const [], List excludeTags = const [], + List presets = const [], bool enableVmService = false, bool ipv6 = false, bool machine = false, @@ -1796,6 +1840,7 @@ class FakeFlutterTestRunner implements FlutterTestRunner { List plainNames = const [], List tags = const [], List excludeTags = const [], + List presets = const [], bool machine = false, bool updateGoldens = false, required int? concurrency, From b55957e725a5b46247099fc8745e57023570f579 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Tue, 11 Aug 2026 17:24:35 -0400 Subject: [PATCH 212/330] Remove PR 1 markdown files from PR 2 --- ...Flutter-Gradle-Plugin-to-AGP-public-API.md | 205 ------------------ docs/platforms/android/website-page-draft.md | 191 ---------------- 2 files changed, 396 deletions(-) delete mode 100644 docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md delete mode 100644 docs/platforms/android/website-page-draft.md diff --git a/docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md b/docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md deleted file mode 100644 index 43a37eea10412..0000000000000 --- a/docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md +++ /dev/null @@ -1,205 +0,0 @@ -# Migrating the Flutter Gradle Plugin to the AGP Public API Surface - -This document is the contributor-facing record of the migration of the Flutter -Gradle Plugin (FGP) off the legacy Android Gradle Plugin (AGP) DSL/Variant API -and AGP internals, onto the public API surface shipped in the -`com.android.tools.build:gradle-api` artifact. - -Umbrella issues: - -- newDsl flip: https://github.com/flutter/flutter/issues/180137 -- Variant API migration: https://github.com/flutter/flutter/issues/166550 - -The user-facing breaking-change page draft lives next to this file in -[`website-page-draft.md`](website-page-draft.md). It must be published to -`docs.flutter.dev/release/breaking-changes/` before the newDsl flip (phase P9) -reaches the beta channel. - -## Why - -AGP 9 (January 2026) deprecated the old DSL and Variant APIs behind the -`android.newDsl=false` escape hatch. AGP 10 (late 2026) removes those APIs -entirely **and** removes access to AGP internals — only the public surface of -the `gradle-api` artifact remains. Today the FGP: - -- compiles against the FULL `com.android.tools.build:gradle` artifact - (`packages/flutter_tools/gradle/build.gradle.kts`); -- uses the legacy variant API (`applicationVariants`, `libraryVariants`, - `variant.outputs`, `assembleProvider`, `packageApplicationProvider`, - `versionCodeOverride`); -- uses the legacy `BaseExtension` - (`FlutterPluginUtils.getLegacyAndroidExtension`); -- imports one internal DSL class (`com.android.build.gradle.internal.dsl.BuildType` - in `plugins/PluginHandler.kt`); -- imports one internal utility - (`com.android.build.gradle.internal.utils.getKotlinAndroidPluginVersion` in - `VersionFetcher.kt`); -- drives `flutter build aar` with legacy dynamic Groovy in - `aar_init_script.gradle`. - -Flutter templates pin AGP 9.1.0 but ship `android.newDsl=false`, and a tool -migrator (`disable_new_dsl_migration.dart`) adds the opt-out to existing -projects. That opt-out dies with AGP 10. - -## End state - -- The FGP uses only public APIs and compiles against `gradle-api`. -- Templates no longer ship `android.newDsl=false`. -- The opt-out **add** migrator is replaced by a **removal** migrator that - deletes only the Flutter-added opt-out lines. -- A fresh `flutter create` app builds with newDsl on. - -## Decision records - -1. **Min AGP floor: out of scope.** A separate in-flight version bump owns the - floor; this migration builds on whatever floor is in effect at landing. - Every replacement API used here was verified public in `gradle-api:8.11.1` - (decompiled jar inspection). If implementation finds a replacement API that - genuinely requires a higher min AGP: document which API and why no - compatible alternative exists in this file, then bump — otherwise version - floors are untouched by this work. -2. **"Public in 8.x" does not mean binary-compatible on 9.x.** - `AgpCommonExtensionWrapper.kt` exists precisely because the public - `CommonExtension` broke between AGP 8 and 9. Mitigation: a CI/test axis - compiling the FGP against gradle-api 9.x is mandatory from phase P2 onward, - plus a bytecode check (javap grep) that no compiled FGP class references - `CommonExtension` as an owner. -3. **`android.builtInKotlin=false` stays out of scope.** Flipping it requires - the separate built-in-Kotlin migration workstream. Users get a second - (smaller) gradle.properties churn later; the breaking-change page states - this explicitly. Corollary: the P9 removal migrator must anchor on the - `android.newDsl` property line — never on marker-comment wording alone — - because the template's builtInKotlin marker comment is nearly identical. -4. **Per-ABI versionCode mechanism.** Do NOT re-implement AGP's flavor-merge - precedence via a `finalizeDsl` snapshot. Preferred mechanism (spiked first - in P6): read-then-set on `VariantOutput.versionCode` inside `onVariants` — - it is seeded with the merged value; set `abiOffset * 1000 + current`, - avoiding a self-referential `.map`. Fall back to a snapshot only if - read-then-set is impossible; record the outcome here. - - *Spike result:* _pending (P6)_. -5. **`buildModeFor` semantics.** Every variant-scope call uses the - `(name, debuggable)` overload with the public `Component.debuggable`. - Name-based inference is confined to the one DSL-scope case with no public - signal (the library-plugin build-type copy in `PluginHandler`). This - preserves add-to-app custom-debuggable matching (a host `staging` - debuggable build type maps to debug engine artifacts). - -## Replacement map - -| Legacy usage | Where | Public replacement | Phase | -| --- | --- | --- | --- | -| `internal.utils.getKotlinAndroidPluginVersion` | `VersionFetcher.kt` | delete; rely on existing fallback chain (`kotlin_version` property → `KotlinAndroidPluginWrapper.pluginVersion` → reflection); null when KGP absent is OK | P1 | -| `compileSdkVersion` string compare (`"android-NN"` substring) | `FlutterPluginUtils.getCompileSdkFromProject`, `PluginHandler` warning | wrapper `compileSdk` / `compileSdkPreview`; numeric compare with defined preview semantics | P1 | -| `BaseExtension.ndkVersion` | `FlutterPluginUtils.getConfiguredNdkVersion` | wrapper `ndkVersion` | P1 | -| `buildModeFor(BuildType)` (legacy model type) | `FlutterPluginUtils.kt` | `buildModeFor(name, debuggable)` overload | P2 | -| `getLegacyAndroidExtension(project).buildTypes` loops | `PluginHandler.kt` | wrapper new-DSL `buildTypes` container | P2 | -| `internal.dsl.BuildType` live aliasing into plugin projects | `PluginHandler.kt` | `initWith`-based copy on new-DSL `BuildType`; app-specific props only when both sides are `ApplicationBuildType` | P3 | -| `BaseExtension` / `getLegacyAndroidExtension` (remaining call sites) | `FlutterPluginUtils.kt` | wrapper accessors incl. `externalNativeBuild` | P4 | -| eager `applicationVariants.configureEach` task creation; mergeAssets/processResources hooks | `FlutterPlugin.kt`, `FlutterPluginUtils.kt` | consolidated `onVariants` block; `CopyFlutterAssetsTask` + `variant.sources.assets.addGeneratedSourceDirectory` | P5 | -| `variant.outputs` + `packageApplicationProvider` + `doLast` APK copy; `versionCodeOverride` | `FlutterPluginUtils.kt` | `CopyFlutterApksTask` (`SingleArtifact.APK` + `BuiltArtifactsLoader`); read-then-set `VariantOutput.versionCode` | P6 | -| `libraryVariants.all` × host `applicationVariants.all` cross-wiring | `FlutterPlugin.kt` (add-to-app) | library-side `onVariants` with `Component.debuggable`; no host-project lookup | P7 | -| dynamic Groovy legacy API in `aar_init_script.gradle` | `aar_init_script.gradle` | `components`-based enumeration; ext-property guard | P8 | -| `android.newDsl=false` template/migrator | templates, `disable_new_dsl_migration.dart` | drop from templates; `RemoveNewDslOptOutMigration` | P9 | -| FULL `gradle` artifact dependency | `build.gradle.kts` | `gradle-api` artifact (compile-time proof of zero internal usage) | P10 | - -## Phase map - -Each phase is one PR-sized change on its own branch. P8 is an independent lane -(Groovy script, disjoint files); P0/P1 are disjoint from each other; everything -else serializes through `FlutterPlugin.kt` / `FlutterPluginUtils.kt`. - -| Phase | Branch | Size | Summary | -| --- | --- | --- | --- | -| P0 | `agp-api-doc` | S | this doc + website page draft | -| P1 | `agp-internal-utils` | S | VersionFetcher internal util removal; numeric compileSdk compare; ndkVersion via wrapper | -| P2 | `agp-buildmode-deps` | M | `buildModeFor` overloads; new-DSL flutter dependencies; 9.x compile axis | -| P3 | `agp-plugin-buildtypes` | M | `initWith` copy for plugin build types; drop internal import; internal-import lint | -| P4 | `agp-ndk-fallback` | S | delete `BaseExtension`; externalNativeBuild via wrapper | -| P5 | `agp-assets-onvariants` | L | lazy task registration (5a) + generated-asset-dir wiring (5b) | -| P6 | `agp-apk-copy-versioncode` | L | `CopyFlutterApksTask`; per-ABI versionCode; app path legacy-free | -| P7 | `agp-add-to-app` | L | library-side `onVariants`; delete host cross-wiring + P5a legacy fork | -| P8 | `agp-aar-script` | M | aar_init_script public-API cleanup | -| P9 | `agp-newdsl-flip` | M | templates drop opt-out; removal migrator; new error handlers | -| P10 | `agp-gradle-api` | M | dependency swap to `gradle-api`; test migration | - -## Cross-cutting rules - -- **R1 Lockstep:** any PR changing FGP-emitted message text updates the - matching `gradle_errors.dart` matcher and its Dart test in the same PR. -- **R2 Revert notes:** each PR description carries "revert-safe until phase X - lands"; once superseded, policy is fix-forward. At least one full post-submit - CI soak between dependent phases (no same-day stacking of P2–P4). -- **R3 9.x axis:** from P2, gradle unit tests additionally compile against - gradle-api 9.x in CI, plus the javap `CommonExtension` bytecode check. -- **R4 Config-cache:** master baseline established first; the per-phase - assertion is "no NEW config-cache violations", not full reuse. -- **R5 Internal-import lint:** once P3 lands, a checked-in test forbids - `com.android.build.gradle.internal.*` imports in `src/main`. -- **R6 Staged newDsl=true axis:** app flows green from end of P6; add-to-app - from P7; aar from P8. The full matrix is the P9 gate. - -## Revert-window table - -| Phase | Revert window | -| --- | --- | -| P0 | always revert-safe | -| P1–P4 | each until the next phase in the chain lands; then fix-forward | -| P5 | until P6 lands | -| P6 / P7 | mutually tolerant (disjoint app/module paths) until P10 | -| P8 | revert-safe even after P10, but not after P9 | -| P9 | cleanly revertible in isolation | -| P10 | cleanly revertible in isolation | - -## Features that must break (tracked; updated as implementation learns) - -1. **User build scripts using legacy APIs** (`applicationVariants.all` - APK-rename recipes) fail under newDsl — the biggest break. Mitigated by new - error handlers (P9) and the website page. -2. **flutter-apk copy**: same names/paths (`app[-abi][-flavor]-.apk`, - byte-matching the current concatenation order), but an UP-TO-DATE-capable - finalizer task replaces the `doLast` block; new task names appear in - `gradlew tasks`. -3. **Per-ABI versionCode**: post-`finalizeDsl` user mutations (`afterEvaluate` - CI patterns) may behave differently; a runtime divergence warning is added. -4. **Custom build types → plugins**: live-aliased instances become `initWith` - copies; library plugins cannot receive `isDebuggable` (no public setter on - `LibraryBuildType`) — plugin-side `BuildConfig.DEBUG`/JNI debuggability may - differ for custom debuggable build types; matching preserved via - `matchingFallbacks`. -5. **Asset merge**: flutter assets become a merged source dir instead of a - post-merge overwrite; collisions resolve by AGP source-set priority. -6. **Add-to-app**: the explicit `:app:mergeAssets.dependsOn` edge and - host-project lookup are removed; `flutter.hostAppProjectName` becomes a - no-op with a deprecation warning naming a removal milestone; ordering - against `copyFlutterAssets` task names may break. -7. **Task realization/type**: flutter tasks become lazy `TaskProvider`s, and - `copyFlutterAssets` changes type from `org.gradle.api.tasks.Copy` to a - custom task class — `tasks.named(..., Copy::class)` casts fail. -8. **`flutter build aar`**: the singleVariant dedup guard becomes an - ext-property/try-catch with a specified error message; variant enumeration - moves from `libraryVariants` to `components` — partial user `singleVariant` - declarations surface differently. -9. **newDsl flip**: new projects lose the opt-out; the removal migrator deletes - only marker-tagged `android.newDsl` lines (template marker "This newDsl flag - was added by the Flutter template"; migrator marker "This newDsl flag was - added automatically by Flutter migrator"), anchored on the property line so - the adjacent builtInKotlin lines are never touched; hand-added opt-outs are - respected. -10. **compileSdk mismatch warning** becomes a numeric compare with defined - preview-vs-numeric semantics; the message keeps a distinctive substring of - the old phrasing for searchability. - -## Verification matrix - -Full matrix at P6, P7, P9, P10; targeted per-phase otherwise. - -1. `cd packages/flutter_tools/gradle && ./gradlew test` (+ the R3 9.x axis) -2. Targeted `integration.shard` tests named in each phase -3. Scratch-app matrix: apk/appbundle × 3 modes; `--flavor`; `--split-per-abi` - (+ apkanalyzer versionCode assertions, including the - flavor-defined-versionCode case); `--deferred-components`; plugin with a - custom build type; `flutter build aar`; add-to-app source & AAR host flows; - `flutter run` / hot restart / `flutter attach`; Windows smoke for the copy - tasks -4. AGP axis: current floor AND 9.1 + `newDsl=false`; staged `newDsl=true` per R6 -5. Config-cache per R4 diff --git a/docs/platforms/android/website-page-draft.md b/docs/platforms/android/website-page-draft.md deleted file mode 100644 index de89c1b5ba0c5..0000000000000 --- a/docs/platforms/android/website-page-draft.md +++ /dev/null @@ -1,191 +0,0 @@ -# Android builds use the new Android Gradle Plugin DSL and Variant APIs - -*Draft breaking-change page for `docs.flutter.dev/release/breaking-changes/`. -This file is the source of truth until the page is published to -flutter/website; publishing must complete before the newDsl flip reaches the -beta channel. Contributor-facing details live in -[Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md](Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md).* - -## Summary - -The Flutter Gradle Plugin now uses only the public Android Gradle Plugin (AGP) -API, and new and migrated Flutter projects build with AGP's new DSL enabled -(`android.newDsl` is no longer set to `false` by Flutter). Gradle build -scripts that use the legacy AGP APIs — most commonly -`android.applicationVariants` — fail to configure and must be migrated to the -AGP Variant API. - -## Background - -AGP 9 deprecated the legacy DSL and Variant APIs behind the -`android.newDsl=false` flag. AGP 10 removes them entirely. Flutter previously -added `android.newDsl=false` to your `gradle.properties` (via the project -templates and an automatic migration) to keep legacy builds working. That -opt-out stops working with AGP 10, so Flutter has migrated its own Gradle -plugin to the public API and removed the opt-out from templates. A migration -now *removes* the opt-out lines that Flutter previously added — it only touches -lines carrying Flutter's marker comments, and prints a message when it does. -Opt-outs you added by hand are left alone. - -`android.builtInKotlin=false` is **not** affected by this change. It is owned -by the separate built-in-Kotlin migration (tracked in ), which means one more (smaller) -`gradle.properties` change later. - -## Migration guide - -### Renaming APKs (`applicationVariants.all`) - -Before: - -```groovy -android { - applicationVariants.all { variant -> - variant.outputs.all { output -> - outputFileName = "myapp-${variant.versionName}.apk" - } - } -} -``` - -After (Variant API, `build.gradle` / `build.gradle.kts`): - -```kotlin -androidComponents { - onVariants(selector().all()) { variant -> - variant.outputs.forEach { output -> - // Use variant.name / output.filters and your own naming scheme. - } - } -} -``` - -For output *file* renames, prefer consuming the built APKs from -`SingleArtifact.APK` with a task wired through -`variant.artifacts.use(...)`, or copy/rename in a finalizer task. Flutter's -own copy step already places APKs at -`build/app/outputs/flutter-apk/app[-abi][-flavor]-.apk` with unchanged -names and paths. - -### Setting per-ABI or per-variant versionCode - -Before: - -```groovy -android.applicationVariants.all { variant -> - variant.outputs.each { output -> - output.versionCodeOverride = abiCodes.get(output.getFilter(OutputFile.ABI)) * 1000 + variant.versionCode - } -} -``` - -After: - -```kotlin -androidComponents { - onVariants(selector().all()) { variant -> - variant.outputs.forEach { output -> - val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier - val base = output.versionCode.get() ?: 1 - output.versionCode.set((abiCodes[abi] ?: 0) * 1000 + base) - } - } -} -``` - -Note: Flutter itself sets per-ABI version codes for `--split-per-abi` inside -`onVariants`. If your CI mutates version codes in `afterEvaluate`, that runs at -a different time than before; Flutter prints a warning when it detects a -divergence between the DSL value and the final output value. - -### Custom build types and plugins - -Flutter copies your app's custom build types onto Flutter plugin projects so -they resolve. With the new DSL these are `initWith` copies rather than live -aliases: - -- Set `matchingFallbacks` on custom build types so dependent Android libraries - resolve, for example: - - ```kotlin - android { - buildTypes { - create("staging") { - initWith(getByName("debug")) - matchingFallbacks += listOf("debug", "release") - } - } - } - ``` - -- Library (plugin) projects cannot be marked debuggable through the public - API, so a plugin's `BuildConfig.DEBUG` and native (JNI) debuggability can - differ from before for custom *debuggable* build types. Variant matching - still works via `matchingFallbacks`. - -### Add-to-app (Flutter module in a host app) - -- Flutter no longer looks up or configures the host `:app` project from the - module. The dependency between your host's asset merging and Flutter's asset - copy is expressed through the Variant API instead of an explicit - `mergeAssets.dependsOn(...)` edge. Build scripts that reference - Flutter's `copyFlutterAssets` tasks by name or type may break: the - tasks are now registered lazily and are no longer of type - `org.gradle.api.tasks.Copy`. -- `flutter.hostAppProjectName` in `gradle.properties` is now a no-op. Flutter - prints a deprecation warning naming the removal milestone. It was only used - for the host-project lookup, which no longer exists. -- Flutter maps host build types to Flutter build modes using the public - "debuggable" flag: `profile` stays `profile`, debuggable build types map to - `debug`, everything else maps to `release`. If your host has no `profile` - build type, add `matchingFallbacks`: - - ```kotlin - create("staging") { - initWith(getByName("debug")) - isDebuggable = true // staging gets debug Flutter artifacts - matchingFallbacks += listOf("debug", "release") - } - ``` - -### Flutter plugin authors - -- Do not read `android.applicationVariants` / `android.libraryVariants` in - plugin build scripts; use `androidComponents.onVariants`. -- Do not assume Flutter's tasks exist at configuration time or have specific - types; look tasks up lazily (`tasks.named`) without a type, or better, wire - through Variant API artifacts. -- Test your plugin's example app with AGP 9+ **without** `android.newDsl=false`. - -### `flutter build aar` - -Variant enumeration for AAR builds now uses the public `components` API. If -your module's build script declares `singleVariant(...)` publishing itself, -Flutter detects the overlap and reports it with an actionable error instead of -failing inside AGP. - -## Escape hatch (temporary) - -If you cannot migrate immediately, add the opt-out by hand to -`android/gradle.properties`: - -```properties -android.newDsl=false -``` - -**This stops working with AGP 10** (removal of the legacy APIs). Treat it as a -short-term unblock only; hand-added opt-outs are never touched by Flutter's -migrator. - -## Timeline - -Landed in version: TBD
-In stable release: TBD - -## References - -- AGP 9 release notes (new DSL): - https://developer.android.com/build/releases/agp-9-0-0-release-notes -- Flutter umbrella issues: - [flutter/flutter#180137](https://github.com/flutter/flutter/issues/180137), - [flutter/flutter#166550](https://github.com/flutter/flutter/issues/166550) From 31223582269154d859a640eabf2671b6aaea8ca9 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:25:51 +0000 Subject: [PATCH 213/330] Always update swift package dependencies (#190886) We originally added the `-skipPackageUpdates` flag when doing `xcodebuild` commands that didn't impact the build (like `xcodebuild -list`) so try to improve performance. However, I suspect this causes packages not to resolve properly during those commands. This PR removed the `-skipPackageUpdates` flag. Speculative fix for https://github.com/flutter/flutter/issues/188265. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter_tools/lib/src/ios/mac.dart | 2 +- .../flutter_tools/lib/src/ios/xcodeproj.dart | 17 ++++++++--------- .../lib/src/macos/build_macos.dart | 2 +- packages/flutter_tools/lib/src/macos/xcode.dart | 4 ++-- .../ios/ios_device_start_nonprebuilt_test.dart | 2 +- .../test/general.shard/ios/mac_test.dart | 2 +- .../test/general.shard/ios/xcodeproj_test.dart | 12 ------------ packages/flutter_tools/test/src/context.dart | 2 +- 8 files changed, 15 insertions(+), 28 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 4d30668e5d174..128003dca1cf5 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -303,7 +303,7 @@ Future buildXcodeProject({ .fetchDependenciesAndGenerateXcodebuildArgs( app.project, globals.fs.directory(buildDirectoryPath), - skipPackageUpdatesAndValidation: false, + skipPackageValidation: false, ); final buildCommands = [...xcodebuildCommandArgs, '-configuration', configuration]; diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 7f14e808d3c88..8fc19ebae4bd4 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -189,7 +189,7 @@ class XcodeProjectInterpreter { Future> fetchDependenciesAndGenerateXcodebuildArgs( XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageUpdatesAndValidation = true, + bool skipPackageValidation = true, }) async { // All `xcodebuild` project commands will download and resolve Swift packages. // We should always prefetch Swift packages before running any `xcodebuild` project command @@ -198,7 +198,7 @@ class XcodeProjectInterpreter { return _xcodebuildProjectCommandArguments( buildDirectory, - skipPackageUpdatesAndValidation: skipPackageUpdatesAndValidation, + skipPackageValidation: skipPackageValidation, ); } @@ -209,11 +209,11 @@ class XcodeProjectInterpreter { /// Returns a list of required arguments for the `xcodebuild` Xcode project command. /// - /// When [skipPackageUpdatesAndValidation] is true, it uses arguments to attempt skipping any - /// Swift package updates and validation. + /// When [skipPackageValidation] is true, it uses arguments to attempt skipping any Swift + /// package validation. List _xcodebuildProjectCommandArguments( Directory buildDirectory, { - bool skipPackageUpdatesAndValidation = true, + bool skipPackageValidation = true, }) { final String cachePath = swiftPackageCachePath(buildDirectory); return [ @@ -221,8 +221,7 @@ class XcodeProjectInterpreter { 'xcodebuild', '-clonedSourcePackagesDirPath', cachePath, - if (skipPackageUpdatesAndValidation) ...[ - '-skipPackageUpdates', + if (skipPackageValidation) ...[ '-skipPackagePluginValidation', '-skipPackageSignatureValidation', ], @@ -395,9 +394,9 @@ class XcodeProjectInterpreter { await xcodeProject.prefetchSwiftPackages( xcodebuildProjectCommandArguments: _xcodebuildProjectCommandArguments( buildDirectory, - // skipPackageUpdatesAndValidation should be false so that when subsequent xcodebuild + // skipPackageValidation should be false so that when subsequent xcodebuild // commands run, packages should already be resolved, downloaded, updated, and validated. - skipPackageUpdatesAndValidation: false, + skipPackageValidation: false, ), processUtils: _processUtils, logger: _logger, diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index 23d183d62406b..916a7c3d36080 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -278,7 +278,7 @@ Future buildMacOS({ .fetchDependenciesAndGenerateXcodebuildArgs( flutterProject.macos, globals.fs.directory(buildDirectoryPath), - skipPackageUpdatesAndValidation: false, + skipPackageValidation: false, ); result = await globals.processUtils.stream( [ diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index edd4fcf671475..fc34cc59e4602 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -235,11 +235,11 @@ class Xcode { Future> fetchDependenciesAndGenerateXcodebuildArgs( XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageUpdatesAndValidation = true, + bool skipPackageValidation = true, }) async => _xcodeProjectInterpreter.fetchDependenciesAndGenerateXcodebuildArgs( xcodeProject, buildDirectory, - skipPackageUpdatesAndValidation: skipPackageUpdatesAndValidation, + skipPackageValidation: skipPackageValidation, ); Future cc(List args) => _run('cc', args); diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index aec6dd0ef5a0b..9f3bbd9397568 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -1544,7 +1544,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete Future> fetchDependenciesAndGenerateXcodebuildArgs( XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageUpdatesAndValidation = true, + bool skipPackageValidation = true, }) async { return ['xcrun', 'xcodebuild']; } diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index c91ede19d924c..f5027b99a9b00 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -1333,7 +1333,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete Future> fetchDependenciesAndGenerateXcodebuildArgs( XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageUpdatesAndValidation = true, + bool skipPackageValidation = true, }) async { return ['xcrun', 'xcodebuild']; } diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index a06c451fd999f..93f18ac1f84ee 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -279,7 +279,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', @@ -325,7 +324,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', @@ -371,7 +369,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', @@ -417,7 +414,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', @@ -463,7 +459,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', @@ -507,7 +502,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', @@ -559,7 +553,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/macos/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', @@ -605,7 +598,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-workspace', @@ -644,7 +636,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-list', @@ -688,7 +679,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-list', @@ -734,7 +724,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-list', @@ -838,7 +827,6 @@ Information about project "Runner": 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-list', diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 8d5ec2b1a98aa..5bde0cd635294 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -413,7 +413,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { Future> fetchDependenciesAndGenerateXcodebuildArgs( XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageUpdatesAndValidation = true, + bool skipPackageValidation = true, }) async { return ['xcrun', 'xcodebuild']; } From dcf10760c401791feabf44119fef783e1468f466 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 11 Aug 2026 21:25:53 +0000 Subject: [PATCH 214/330] [flutter_tools] Replace usages of package:dds/dap.dart with package:dap_adapters/dap_adapters.dart (#190667) Migrate Debug Adapter Protocol (DAP) adapter imports in flutter_tools from package:dds/dap.dart to package:dap_adapters/dap_adapters.dart. --- .../lib/src/debug_adapters/flutter_adapter.dart | 2 +- .../lib/src/debug_adapters/flutter_adapter_args.dart | 2 +- .../lib/src/debug_adapters/flutter_base_adapter.dart | 2 +- .../lib/src/debug_adapters/flutter_test_adapter.dart | 2 +- packages/flutter_tools/lib/src/debug_adapters/server.dart | 2 +- packages/flutter_tools/pubspec.yaml | 4 ++-- .../test/general.shard/dap/flutter_adapter_test.dart | 2 +- packages/flutter_tools/test/general.shard/dap/mocks.dart | 2 +- .../debug_adapter/flutter_adapter_test.dart | 2 +- .../debug_adapter/test_adapter_test.dart | 2 +- .../test/integration.shard/debug_adapter/test_client.dart | 2 +- .../test/integration.shard/debug_adapter/test_server.dart | 2 +- .../integration.shard/debug_adapter/test_support.dart | 2 +- pubspec.lock | 8 ++++++++ 14 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart index 9d315f2f9941e..3c0ad2e6fa3c8 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:dds/dap.dart' hide PidTracker; +import 'package:dap_adapters/dap_adapters.dart' hide PidTracker; import 'package:vm_service/vm_service.dart' as vm; import '../base/io.dart'; diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart index 3be9e9371295e..b01d649648dc6 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:dds/dap.dart'; +import 'package:dap_adapters/dap_adapters.dart'; /// An implementation of [AttachRequestArguments] that includes all fields used by the Flutter debug adapter. /// diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_base_adapter.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_base_adapter.dart index 3b483f105dcbf..b44cff299f9c8 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_base_adapter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_base_adapter.dart @@ -4,7 +4,7 @@ import 'dart:async'; -import 'package:dds/dap.dart' hide PidTracker; +import 'package:dap_adapters/dap_adapters.dart' hide PidTracker; import 'package:vm_service/vm_service.dart' as vm; import '../base/file_system.dart'; diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_test_adapter.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_test_adapter.dart index cb0b36dd526d1..4866223c1b339 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_test_adapter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_test_adapter.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:dds/dap.dart' hide PidTracker; +import 'package:dap_adapters/dap_adapters.dart' hide PidTracker; import '../base/io.dart'; import '../cache.dart'; diff --git a/packages/flutter_tools/lib/src/debug_adapters/server.dart b/packages/flutter_tools/lib/src/debug_adapters/server.dart index 93f55142e85c2..c678bf6bb31a9 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/server.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/server.dart @@ -4,7 +4,7 @@ import 'dart:async'; -import 'package:dds/dap.dart' hide DapServer; +import 'package:dap_adapters/dap_adapters.dart' hide DapServer; import '../base/file_system.dart'; import '../base/platform.dart'; diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 692f020774325..af97b3e3eb3f4 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: analyzer: 10.1.0 archive: 3.6.1 args: 2.7.0 + dap_adapters: 1.0.0 dds: 5.4.0 dwds: 27.1.2 code_builder: 4.11.1 @@ -139,5 +140,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true - -# PUBSPEC CHECKSUM: abnfae +# PUBSPEC CHECKSUM: 1leejj diff --git a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart index b036258118a30..b26ee2e01392a 100644 --- a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:dds/dap.dart'; +import 'package:dap_adapters/dap_adapters.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/platform.dart'; diff --git a/packages/flutter_tools/test/general.shard/dap/mocks.dart b/packages/flutter_tools/test/general.shard/dap/mocks.dart index d9e4c2a78ae45..c77c51c276716 100644 --- a/packages/flutter_tools/test/general.shard/dap/mocks.dart +++ b/packages/flutter_tools/test/general.shard/dap/mocks.dart @@ -4,7 +4,7 @@ import 'dart:async'; -import 'package:dds/dap.dart'; +import 'package:dap_adapters/dap_adapters.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/convert.dart'; diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart index 0e9477c32083f..0cd90768ac938 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart @@ -4,7 +4,7 @@ import 'dart:async'; -import 'package:dds/dap.dart'; +import 'package:dap_adapters/dap_adapters.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/convert.dart'; diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_adapter_test.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_adapter_test.dart index 66f8ddffc2bed..4f046758b234d 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_adapter_test.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_adapter_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:dds/dap.dart'; +import 'package:dap_adapters/dap_adapters.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/cache.dart'; diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart index 04776a23e6fef..9f3cba0ae693e 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart @@ -4,7 +4,7 @@ import 'dart:async'; -import 'package:dds/dap.dart'; +import 'package:dap_adapters/dap_adapters.dart'; import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart'; import 'test_server.dart'; diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart index 3c6a02bf0f248..c9c28c261b221 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart @@ -6,7 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:dds/dap.dart' show Logger; +import 'package:dap_adapters/dap_adapters.dart' show Logger; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/debug_adapters/server.dart'; import 'package:flutter_tools/src/globals.dart' as globals; diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart index 84ac28524032e..9e77a22ca6e76 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:dds/dap.dart'; +import 'package:dap_adapters/dap_adapters.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/convert.dart'; diff --git a/pubspec.lock b/pubspec.lock index 29b29050975ae..8a9581a6957f9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -234,6 +234,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + dap_adapters: + dependency: transitive + description: + name: dap_adapters + sha256: "26f1ae0945bd0e0d015d2c0cb0fa8d1ed84262d7e5db3bf2642fb4c199fa7844" + url: "https://pub.dev" + source: hosted + version: "1.0.0" dart_service_protocol_shared: dependency: transitive description: From 3ec72ae07035d8c84323bdf9f58cb4849556c0c7 Mon Sep 17 00:00:00 2001 From: Kenneth Koh Date: Tue, 11 Aug 2026 21:25:53 +0000 Subject: [PATCH 215/330] Offload blocking work in ProcessTextPlugin to the background (#189823) This PR offloads the method handlers of the ProcessTextPlugin to a background TaskQueue. The offloading of the querying of text actions to a background thread moves the following transactions off the main thread: 1. Binder transaction of querying the package manager for activities with process text intent 2. Disk I/O transactions of resolving the text action labels from the APKs of the individual activities The above transactions occurring on the main thread is an anti-pattern according to https://developer.android.com/topic/performance/anrs/find-unresponsive-thread#common-causes Attached below is a Perfetto trace validating that the querying of text actions now occurs on a background thread. image The processTextAction method handler is also moved to a background thread, with additional handling to ensure that the launching of the text action activity is delegated back to the main UI thread. Attached below is a Perfetto trace validating that the processing of a text action begins on a background thread but is later delegated back to the main thread to launch the activity. image Note that the above Perfetto traces have additional trace logging implemented to better visualize when the method handlers are called. This PR also refactors ProcessTextChannel to use `DartExecutor.getBinaryMessenger()` instead of using `DartExecutor` as the `BinaryMessenger` object directly, so that the deprecated wrapper methods of `BinaryMessenger` in `DartExecutor` are no longer used. https://github.com/flutter/flutter/blob/80cabc33032837357c9b233d693266b890860c12/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java#L203-L264 The unit tests for ProcessTextPlugin have also been refactored as such and the obsolete tags to suppress deprecated warnings have also been cleaned up. Issue: https://github.com/flutter/flutter/issues/189176 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md Co-authored-by: Kenneth Koh --- .../systemchannels/ProcessTextChannel.java | 10 +++- .../plugin/text/ProcessTextPlugin.java | 45 ++++++++++------ .../plugin/text/ProcessTextPluginTest.java | 52 +++++++++++-------- 3 files changed, 70 insertions(+), 37 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java index b4efad084c707..aff81a5a89cf4 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMethodCodec; @@ -87,7 +88,14 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result public ProcessTextChannel( @NonNull DartExecutor dartExecutor, @NonNull PackageManager packageManager) { this.packageManager = packageManager; - channel = new MethodChannel(dartExecutor, CHANNEL_NAME, StandardMethodCodec.INSTANCE); + BinaryMessenger.TaskQueue taskQueue = + dartExecutor.getBinaryMessenger().makeBackgroundTaskQueue(); + channel = + new MethodChannel( + dartExecutor.getBinaryMessenger(), + CHANNEL_NAME, + StandardMethodCodec.INSTANCE, + taskQueue); channel.setMethodCallHandler(parsingMethodHandler); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java index beeebbd0c4cca..1dbb84a4fe19e 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java @@ -11,6 +11,8 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -32,9 +34,14 @@ public class ProcessTextPlugin @NonNull private final ProcessTextChannel processTextChannel; @NonNull private final PackageManager packageManager; - @Nullable private ActivityPluginBinding activityBinding; + @NonNull private final Handler mainThreadHandler; + @Nullable private volatile ActivityPluginBinding activityBinding; + // The use of a Map for this variable assumes that it is only accessed + // serially on the background task queue. private Map resolveInfosById; + // The use of a Map for this variable assumes that it is only accessed + // serially on the main thread. @NonNull private Map requestsByCode = new HashMap(); @@ -42,6 +49,7 @@ public class ProcessTextPlugin public ProcessTextPlugin(@NonNull ProcessTextChannel processTextChannel) { this.processTextChannel = processTextChannel; this.packageManager = processTextChannel.packageManager; + this.mainThreadHandler = new Handler(Looper.getMainLooper()); processTextChannel.setMethodHandler(this); } @@ -81,20 +89,27 @@ public void processTextAction( return; } - Integer requestCode = result.hashCode(); - requestsByCode.put(requestCode, result); - - Intent intent = new Intent(); - intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); - intent.setAction(Intent.ACTION_PROCESS_TEXT); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_PROCESS_TEXT, text); - intent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readOnly); - - // Start the text processing activity. When the activity completes, the onActivityResult - // callback - // is called. - activityBinding.getActivity().startActivityForResult(intent, requestCode); + mainThreadHandler.post( + () -> { + if (activityBinding == null) { + result.error("error", "Plugin not bound to an Activity", null); + return; + } + + final Integer requestCode = result.hashCode(); + requestsByCode.put(requestCode, result); + + Intent intent = new Intent(); + intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); + intent.setAction(Intent.ACTION_PROCESS_TEXT); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_PROCESS_TEXT, text); + intent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readOnly); + + // Start the text processing activity. When the activity completes, the + // onActivityResult callback is called. + activityBinding.getActivity().startActivityForResult(intent, requestCode); + }); } private void cacheResolveInfos() { diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/text/ProcessTextPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/text/ProcessTextPluginTest.java index 3976f9710891c..a15248c845dfc 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/text/ProcessTextPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/text/ProcessTextPluginTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -34,6 +35,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.robolectric.shadows.ShadowLooper; @RunWith(AndroidJUnit4.class) @TargetApi(API_LEVELS.API_24) @@ -48,18 +50,19 @@ private static void sendToBinaryMessageHandler( (ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class)); } - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. @Test public void respondsToProcessTextChannelMessage() { ArgumentCaptor binaryMessageHandlerCaptor = ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + DartExecutor mockDartExecutor = mock(DartExecutor.class); + BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class); + when(mockDartExecutor.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + doReturn(null).when(mockBinaryMessenger).makeBackgroundTaskQueue(); ProcessTextChannel.ProcessTextMethodHandler mockHandler = mock(ProcessTextChannel.ProcessTextMethodHandler.class); PackageManager mockPackageManager = mock(PackageManager.class); ProcessTextChannel processTextChannel = - new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + new ProcessTextChannel(mockDartExecutor, mockPackageManager); processTextChannel.setMethodHandler(mockHandler); @@ -74,14 +77,15 @@ public void respondsToProcessTextChannelMessage() { verify(mockHandler).queryTextActions(); } - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. @Test public void performQueryTextActions() { - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + DartExecutor mockDartExecutor = mock(DartExecutor.class); + BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class); + when(mockDartExecutor.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + doReturn(null).when(mockBinaryMessenger).makeBackgroundTaskQueue(); PackageManager mockPackageManager = mock(PackageManager.class); ProcessTextChannel processTextChannel = - new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + new ProcessTextChannel(mockDartExecutor, mockPackageManager); // Set up mocked result for PackageManager.queryIntentActivities. ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); @@ -100,14 +104,15 @@ public void performQueryTextActions() { assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2")); } - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. @Test public void performProcessTextActionWithNoReturnedValue() { - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + DartExecutor mockDartExecutor = mock(DartExecutor.class); + BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class); + when(mockDartExecutor.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + doReturn(null).when(mockBinaryMessenger).makeBackgroundTaskQueue(); PackageManager mockPackageManager = mock(PackageManager.class); ProcessTextChannel processTextChannel = - new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + new ProcessTextChannel(mockDartExecutor, mockPackageManager); // Set up mocked result for PackageManager.queryIntentActivities. ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); @@ -130,10 +135,11 @@ public void performProcessTextActionWithNoReturnedValue() { when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity); processTextPlugin.onAttachedToActivity(mockActivityPluginBinding); - // Execute th first action. + // Execute the first action. String textToBeProcessed = "Flutter!"; MethodChannel.Result result = mock(MethodChannel.Result.class); processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result); + ShadowLooper.runUiThreadTasks(); // Activity.startActivityForResult should have been called. ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -149,14 +155,15 @@ public void performProcessTextActionWithNoReturnedValue() { verify(result).success(null); } - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. @Test public void performProcessTextActionWithReturnedValue() { - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + DartExecutor mockDartExecutor = mock(DartExecutor.class); + BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class); + when(mockDartExecutor.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + doReturn(null).when(mockBinaryMessenger).makeBackgroundTaskQueue(); PackageManager mockPackageManager = mock(PackageManager.class); ProcessTextChannel processTextChannel = - new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + new ProcessTextChannel(mockDartExecutor, mockPackageManager); // Set up mocked result for PackageManager.queryIntentActivities. ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); @@ -183,6 +190,7 @@ public void performProcessTextActionWithReturnedValue() { String textToBeProcessed = "Flutter!"; MethodChannel.Result result = mock(MethodChannel.Result.class); processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result); + ShadowLooper.runUiThreadTasks(); // Activity.startActivityForResult should have been called. ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -200,14 +208,15 @@ public void performProcessTextActionWithReturnedValue() { verify(result).success(processedText); } - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. @Test public void doNotCrashOnNonRelatedActivityResult() { - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + DartExecutor mockDartExecutor = mock(DartExecutor.class); + BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class); + when(mockDartExecutor.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + doReturn(null).when(mockBinaryMessenger).makeBackgroundTaskQueue(); PackageManager mockPackageManager = mock(PackageManager.class); ProcessTextChannel processTextChannel = - new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + new ProcessTextChannel(mockDartExecutor, mockPackageManager); // Set up mocked result for PackageManager.queryIntentActivities. ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); @@ -234,6 +243,7 @@ public void doNotCrashOnNonRelatedActivityResult() { String textToBeProcessed = "Flutter!"; MethodChannel.Result result = mock(MethodChannel.Result.class); processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result); + ShadowLooper.runUiThreadTasks(); // Activity.startActivityForResult should have been called. ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); From 3f2a8bab75c9d327b78a50ab5090edaee03198b2 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 11 Aug 2026 21:26:30 +0000 Subject: [PATCH 216/330] [tool] Add missing play element in web test index.html to fix warning (#190675) Fixes a warning in web tests when using `package:test`: ``` [BROWSER CONSOLE] [warning]: Null check operator used on a null value host.dart.js 15409:12 main_closure.call$0 host.dart.js 4089:16 StaticClosure._rootRun host.dart.js 10850:39 _CustomZone.run$1$1 host.dart.js 4190:88 Object.runZonedGuarded host.dart.js 7282:9 main host.dart.js 16464:7 host.dart.js 16445:7 host.dart.js 16458:5 dartProgram host.dart.js 16467:3 ``` because it expects a `#play` element. ### Historical Context * The `#play` element was introduced in `package:test`'s browser host script over a decade ago on July 30, 2015 (in dart-lang/test@8fcfc7a1) to display a pause screen in a paused browser. * When `flutter test --platform=chrome` was first introduced in the Flutter repository on May 31, 2019 (in flutter/flutter@4db845fb), the `packages/flutter_tools/static/index.html` file was added without this `#play` element. * As a result, this issue has been happening silently during Flutter web tests for over 7 years without causing tests to fail. * The error was recently surfaced as a visible `[BROWSER CONSOLE]` warning because Flutter (`flutter_tools`) recently added a feature to pipe Chrome's console logs directly to the test output terminal. Since `package:test` migrated to strict null safety in 2021, the `querySelector('#play')!` has been throwing a null check exception that is now finally visible! --- packages/flutter_tools/static/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_tools/static/index.html b/packages/flutter_tools/static/index.html index 85ebd46c4c602..cf505d9063009 100644 --- a/packages/flutter_tools/static/index.html +++ b/packages/flutter_tools/static/index.html @@ -29,6 +29,7 @@

+
From 3820cdf0b73cd0322fb3458e1c55b408dba1cb5e Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:26:30 +0000 Subject: [PATCH 217/330] Remove Xcode environment when building swift tools in Xcode pre-action (#190848) By default, commands run within Xcode build scripts inherit the Xcode build environment. This conflicts with the `swift build` command for some reason with Xcode 27. To mitigate, we ignore the environment by prefixing with `env -i`. Fixes https://github.com/flutter/flutter/issues/190846. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../Scripts/flutter_integration.sh.tmpl | 4 +-- .../swift_package_manager_add2app_test.dart | 27 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/flutter_tools/templates/add_to_app/darwin/Scripts/flutter_integration.sh.tmpl b/packages/flutter_tools/templates/add_to_app/darwin/Scripts/flutter_integration.sh.tmpl index ef30cd2e7d5bb..46011b91c0076 100644 --- a/packages/flutter_tools/templates/add_to_app/darwin/Scripts/flutter_integration.sh.tmpl +++ b/packages/flutter_tools/templates/add_to_app/darwin/Scripts/flutter_integration.sh.tmpl @@ -28,8 +28,8 @@ FLUTTER_TOOL_BUILD_MODE="release" buildTool() { cd "$FLUTTER_NATIVE_TOOLS_PACKAGE_PATH" || exit 1 - xcrun --sdk macosx swift package clean --build-path "$FLUTTER_TOOL_BUILD_PATH" - xcrun --sdk macosx swift build --product $FLUTTER_TOOL --build-path "$FLUTTER_TOOL_BUILD_PATH" -c $FLUTTER_TOOL_BUILD_MODE --disable-sandbox + env -i PATH="$PATH" DEVELOPER_DIR="${DEVELOPER_DIR:-}" xcrun --sdk macosx swift package clean --build-path "$FLUTTER_TOOL_BUILD_PATH" + env -i PATH="$PATH" DEVELOPER_DIR="${DEVELOPER_DIR:-}" xcrun --sdk macosx swift build --product $FLUTTER_TOOL --build-path "$FLUTTER_TOOL_BUILD_PATH" -c $FLUTTER_TOOL_BUILD_MODE --disable-sandbox } # Use prebuilt tool if possible, otherwise re-build it for next time diff --git a/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart b/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart index a5b3a954ef5cb..47649121fd472 100644 --- a/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart +++ b/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart @@ -200,9 +200,10 @@ void main() { ], codesign: codesign, expectedOutput: [ - "Build of product 'flutter-prebuild-tool' complete!", + RegExp(r'Gatekeeper check failed.*\.build\/prebuild\/release\/flutter-prebuild-tool'), + RegExp(r'Gatekeeper check failed.*\.build\/assemble\/release\/flutter-assemble-tool'), + RegExp(r'Build(?: of product .*)? complete!'), 'FlutterPluginRegistrant symlink updated to ./Release', - "Build of product 'flutter-assemble-tool' complete!", 'flutter --verbose assemble', 'release_unpack_${targetPlatform.name.toLowerCase()}: Starting', ], @@ -225,8 +226,9 @@ void main() { 'Verification complete.', ], unexpectedOutput: [ - "Build of product 'flutter-prebuild-tool' complete!", - "Build of product 'flutter-assemble-tool' complete!", + RegExp(r'Gatekeeper check failed.*\.build\/prebuild\/release\/flutter-prebuild-tool'), + RegExp(r'Gatekeeper check failed.*\.build\/assemble\/release\/flutter-assemble-tool'), + RegExp(r'Build(?: of product .*)? complete!'), 'flutter --verbose assemble', 'note: Transfer starting', ], @@ -255,8 +257,9 @@ void main() { 'warning: Alternatively, you can remove FLUTTER_APPLICATION_PATH from your build settings to dismiss this warning.', ], unexpectedOutput: [ - "Build of product 'flutter-prebuild-tool' complete!", - "Build of product 'flutter-assemble-tool' complete!", + RegExp(r'Gatekeeper check failed.*\.build\/prebuild\/release\/flutter-prebuild-tool'), + RegExp(r'Gatekeeper check failed.*\.build\/assemble\/release\/flutter-assemble-tool'), + RegExp(r'Build(?: of product .*)? complete!'), 'flutter --verbose assemble', 'note: Transfer starting', ], @@ -276,8 +279,9 @@ void main() { expectedOutput: ['Verification complete.'], unexpectedOutput: [ 'FlutterPluginRegistrant symlink updated to ./Release', - "Build of product 'flutter-prebuild-tool' complete!", - "Build of product 'flutter-assemble-tool' complete!", + RegExp(r'Gatekeeper check failed.*\.build\/prebuild\/release\/flutter-prebuild-tool'), + RegExp(r'Gatekeeper check failed.*\.build\/assemble\/release\/flutter-assemble-tool'), + RegExp(r'Build(?: of product .*)? complete!'), 'flutter --verbose assemble', 'note: Transfer starting', ], @@ -401,7 +405,8 @@ void main() { 'FLUTTER_APPLICATION_PATH=\$SRCROOT/../$appName', ], expectedOutput: [ - "Build of product 'flutter-assemble-tool' complete!", + RegExp(r'Gatekeeper check failed.*\.build\/assemble\/release\/flutter-assemble-tool'), + RegExp(r'Build(?: of product .*)? complete!'), 'FlutterPluginRegistrant symlink updated to ./Release', 'flutter --verbose assemble', 'release_unpack_${targetPlatform.name.toLowerCase()}: Starting', @@ -452,8 +457,8 @@ Future _buildNativeProject({ required FlutterDarwinPlatform platform, required List buildSettings, bool expectFailure = false, - List expectedOutput = const [], - List unexpectedOutput = const [], + List expectedOutput = const [], + List unexpectedOutput = const [], required bool codesign, }) async { final Map environment = Platform.environment; From e9c600abe9bd9e58823406b6d9eb3e1849e0db72 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:26:30 +0000 Subject: [PATCH 218/330] Add batch3 a11y_assessment for vpat (#189042) related https://github.com/flutter/flutter/issues/184841 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- dev/a11y_assessments/lib/main.dart | 2 +- .../lib/use_cases/bottom_app_bar.dart | 73 +++++++++ .../lib/use_cases/check_box.dart | 55 +++++++ .../lib/use_cases/dropdown_menu.dart | 69 ++++++++ .../lib/use_cases/menu_anchor.dart | 86 ++++++++++ .../lib/use_cases/menu_bar.dart | 113 +++++++++++++ dev/a11y_assessments/lib/use_cases/radio.dart | 88 +++++++++++ .../lib/use_cases/search_bar.dart | 148 ++++++++++++++++++ .../lib/use_cases/switch.dart | 57 +++++++ .../lib/use_cases/text_form_field.dart | 106 +++++++++++++ .../lib/use_cases/use_cases.dart | 19 +++ .../test/accessibility_guideline_test.dart | 20 ++- .../test/bottom_app_bar_test.dart | 23 +++ dev/a11y_assessments/test/check_box_test.dart | 27 ++++ .../test/dropdown_menu_test.dart | 43 +++++ .../test/menu_anchor_test.dart | 53 +++++++ dev/a11y_assessments/test/menu_bar_test.dart | 76 +++++++++ dev/a11y_assessments/test/radio_test.dart | 28 ++++ .../test/search_bar_test.dart | 44 ++++++ dev/a11y_assessments/test/switch_test.dart | 27 ++++ .../test/text_form_field_test.dart | 64 ++++++++ 21 files changed, 1215 insertions(+), 6 deletions(-) create mode 100644 dev/a11y_assessments/lib/use_cases/bottom_app_bar.dart create mode 100644 dev/a11y_assessments/lib/use_cases/check_box.dart create mode 100644 dev/a11y_assessments/lib/use_cases/dropdown_menu.dart create mode 100644 dev/a11y_assessments/lib/use_cases/menu_anchor.dart create mode 100644 dev/a11y_assessments/lib/use_cases/menu_bar.dart create mode 100644 dev/a11y_assessments/lib/use_cases/radio.dart create mode 100644 dev/a11y_assessments/lib/use_cases/search_bar.dart create mode 100644 dev/a11y_assessments/lib/use_cases/switch.dart create mode 100644 dev/a11y_assessments/lib/use_cases/text_form_field.dart create mode 100644 dev/a11y_assessments/test/bottom_app_bar_test.dart create mode 100644 dev/a11y_assessments/test/check_box_test.dart create mode 100644 dev/a11y_assessments/test/dropdown_menu_test.dart create mode 100644 dev/a11y_assessments/test/menu_anchor_test.dart create mode 100644 dev/a11y_assessments/test/menu_bar_test.dart create mode 100644 dev/a11y_assessments/test/radio_test.dart create mode 100644 dev/a11y_assessments/test/search_bar_test.dart create mode 100644 dev/a11y_assessments/test/switch_test.dart create mode 100644 dev/a11y_assessments/test/text_form_field_test.dart diff --git a/dev/a11y_assessments/lib/main.dart b/dev/a11y_assessments/lib/main.dart index 2c03117319764..2f47a8530b3f5 100644 --- a/dev/a11y_assessments/lib/main.dart +++ b/dev/a11y_assessments/lib/main.dart @@ -42,7 +42,7 @@ final ThemeData _highContrastDarkTheme = _buildTheme( ); class App extends StatelessWidget { - const App({super.key, this.initialTags = const {Tag.batch2}}); + const App({super.key, this.initialTags = const {Tag.batch3}}); final Set initialTags; diff --git a/dev/a11y_assessments/lib/use_cases/bottom_app_bar.dart b/dev/a11y_assessments/lib/use_cases/bottom_app_bar.dart new file mode 100644 index 0000000000000..3aaf4b79a571e --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/bottom_app_bar.dart @@ -0,0 +1,73 @@ +// 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/material.dart'; + +import '../utils.dart'; +import 'use_cases.dart'; + +class BottomAppBarUseCase extends UseCase { + BottomAppBarUseCase(); + + @override + String get name => 'BottomAppBar'; + + @override + String get route => '/bottom-app-bar'; + + @override + List get tags => [Tag.batch3, Tag.core]; + + @override + Widget build(BuildContext context) => const MainWidget(); +} + +class MainWidget extends StatefulWidget { + const MainWidget({super.key}); + + @override + State createState() => MainWidgetState(); +} + +class MainWidgetState extends State { + final String pageTitle = getUseCaseName(BottomAppBarUseCase()); + String _selectedAction = 'None'; + + void _onItemTapped(String action) { + setState(() { + _selectedAction = action; + }); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Tapped: $action'))); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Semantics(headingLevel: 1, child: Text('$pageTitle Demo'))), + bottomNavigationBar: BottomAppBar( + child: Row( + children: [ + IconButton( + tooltip: 'Open menu', + icon: const Icon(Icons.menu), + onPressed: () => _onItemTapped('Menu'), + ), + IconButton( + tooltip: 'Search', + icon: const Icon(Icons.search), + onPressed: () => _onItemTapped('Search'), + ), + IconButton( + tooltip: 'Favorite', + icon: const Icon(Icons.favorite), + onPressed: () => _onItemTapped('Favorite'), + ), + ], + ), + ), + body: Center(child: Text('Selected: $_selectedAction')), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/check_box.dart b/dev/a11y_assessments/lib/use_cases/check_box.dart new file mode 100644 index 0000000000000..1864e8b77e0e9 --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/check_box.dart @@ -0,0 +1,55 @@ +// 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/material.dart'; +import '../utils.dart'; +import 'use_cases.dart'; + +class CheckBoxUseCase extends UseCase { + CheckBoxUseCase(); + + @override + String get name => 'CheckBox'; + + @override + String get route => '/check-box'; + + @override + List get tags => [Tag.batch3, Tag.core]; + + @override + Widget build(BuildContext context) => _MainWidget(); +} + +class _MainWidget extends StatefulWidget { + @override + State<_MainWidget> createState() => _MainWidgetState(); +} + +class _MainWidgetState extends State<_MainWidget> { + bool _checked = false; + + String pageTitle = getUseCaseName(CheckBoxUseCase()); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Semantics(headingLevel: 1, child: Text('$pageTitle Demo'))), + body: ListView( + children: [ + Checkbox( + value: _checked, + semanticLabel: 'Enabled checkbox', + onChanged: (bool? value) { + setState(() { + _checked = value!; + }); + }, + ), + const Checkbox(value: false, semanticLabel: 'Disabled checkbox', onChanged: null), + ], + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/dropdown_menu.dart b/dev/a11y_assessments/lib/use_cases/dropdown_menu.dart new file mode 100644 index 0000000000000..2de0c02f8eb7a --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/dropdown_menu.dart @@ -0,0 +1,69 @@ +// 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/material.dart'; +import '../utils.dart'; +import 'use_cases.dart'; + +class DropdownMenuUseCase extends UseCase { + DropdownMenuUseCase(); + + @override + String get name => 'DropdownMenu'; + + @override + String get route => '/dropdown-menu'; + + @override + List get tags => [Tag.batch3, Tag.core]; + + @override + Widget build(BuildContext context) => const _MainWidget(); +} + +class _MainWidget extends StatelessWidget { + const _MainWidget(); + + static const List _kOptions = ['apple', 'banana', 'lemon']; + static final List> _kMenuEntries = _kOptions + .map>( + (String name) => DropdownMenuEntry(value: name, label: name), + ) + .toList(); + + @override + Widget build(BuildContext context) { + final String pageTitle = getUseCaseName(DropdownMenuUseCase()); + return Scaffold( + appBar: AppBar(title: Semantics(headingLevel: 1, child: Text('$pageTitle Demo'))), + body: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + Semantics( + label: 'Enabled dropdown menu', + child: DropdownMenu( + key: const Key('enabled dropdown menu'), + label: const Text('Fruit'), + expandedInsets: EdgeInsets.zero, + initialSelection: _kOptions.first, + dropdownMenuEntries: _kMenuEntries, + ), + ), + const SizedBox(height: 24.0), + Semantics( + label: 'Disabled dropdown menu', + child: DropdownMenu( + key: const Key('disabled dropdown menu'), + label: const Text('Fruit'), + expandedInsets: EdgeInsets.zero, + enabled: false, + initialSelection: _kOptions.first, + dropdownMenuEntries: _kMenuEntries, + ), + ), + ], + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/menu_anchor.dart b/dev/a11y_assessments/lib/use_cases/menu_anchor.dart new file mode 100644 index 0000000000000..e63c3bd9541ec --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/menu_anchor.dart @@ -0,0 +1,86 @@ +// 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/material.dart'; +import '../utils.dart'; +import 'use_cases.dart'; + +class MenuAnchorUseCase extends UseCase { + MenuAnchorUseCase(); + + @override + String get name => 'MenuAnchor'; + + @override + String get route => '/menu-anchor'; + + @override + List get tags => [Tag.core]; + + @override + Widget build(BuildContext context) => const _MainWidget(); +} + +class _MainWidget extends StatefulWidget { + const _MainWidget(); + + @override + State<_MainWidget> createState() => _MainWidgetState(); +} + +class _MainWidgetState extends State<_MainWidget> { + String _lastSelection = 'None'; + + @override + Widget build(BuildContext context) { + final String pageTitle = getUseCaseName(MenuAnchorUseCase()); + return Scaffold( + appBar: AppBar(title: Semantics(headingLevel: 1, child: Text('$pageTitle Demo'))), + body: ListView( + children: [ + Semantics(liveRegion: true, child: Text('Last Selection: $_lastSelection')), + Semantics( + label: 'Enabled menu anchor', + child: MenuAnchor( + key: const Key('enabled menu anchor'), + builder: (BuildContext context, MenuController controller, Widget? child) { + return ElevatedButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Text('Open Menu'), + ); + }, + menuChildren: [ + MenuItemButton( + onPressed: () { + setState(() { + _lastSelection = 'Item 1'; + }); + }, + child: const Text('Item 1'), + ), + const MenuItemButton(child: Text('Disabled Item')), + ], + ), + ), + Semantics( + label: 'Disabled menu anchor', + child: MenuAnchor( + key: const Key('disabled menu anchor'), + builder: (BuildContext context, MenuController controller, Widget? child) { + return const ElevatedButton(onPressed: null, child: Text('Disabled Menu Button')); + }, + menuChildren: const [MenuItemButton(child: Text('Disabled Item'))], + ), + ), + ], + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/menu_bar.dart b/dev/a11y_assessments/lib/use_cases/menu_bar.dart new file mode 100644 index 0000000000000..7cddfd95b4cc0 --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/menu_bar.dart @@ -0,0 +1,113 @@ +// 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/material.dart'; +import '../utils.dart'; +import 'use_cases.dart'; + +class MenuBarUseCase extends UseCase { + MenuBarUseCase(); + + @override + String get name => 'MenuBar'; + + @override + String get route => '/menu-bar'; + + @override + List get tags => [Tag.core]; + + @override + Widget build(BuildContext context) => const _MainWidget(); +} + +class _MainWidget extends StatefulWidget { + const _MainWidget(); + + @override + State<_MainWidget> createState() => _MainWidgetState(); +} + +class _MainWidgetState extends State<_MainWidget> { + String _lastSelection = 'None'; + + @override + Widget build(BuildContext context) { + final String pageTitle = getUseCaseName(MenuBarUseCase()); + return Scaffold( + appBar: AppBar(title: Semantics(headingLevel: 1, child: Text('$pageTitle Demo'))), + body: ListView( + children: [ + Semantics(liveRegion: true, child: Text('Last Selection: $_lastSelection')), + Semantics( + label: 'Enabled menu bar', + child: MenuBar( + key: const Key('enabled menu bar'), + children: [ + SubmenuButton( + menuChildren: [ + MenuItemButton( + onPressed: () { + setState(() { + _lastSelection = 'Save'; + }); + }, + child: const Text('Save'), + ), + const MenuItemButton(child: Text('Disabled Item')), + ], + child: const Text('File'), + ), + SubmenuButton( + menuChildren: [ + MenuItemButton( + onPressed: () { + setState(() { + _lastSelection = 'About'; + }); + }, + child: const Text('About'), + ), + SubmenuButton( + menuChildren: [ + MenuItemButton( + onPressed: () { + setState(() { + _lastSelection = 'Documentation'; + }); + }, + child: const Text('Documentation'), + ), + MenuItemButton( + onPressed: () { + setState(() { + _lastSelection = 'Send Feedback'; + }); + }, + child: const Text('Send Feedback'), + ), + ], + child: const Text('Online Help'), + ), + ], + child: const Text('Help'), + ), + ], + ), + ), + Semantics( + label: 'Disabled menu bar', + child: const MenuBar( + key: Key('disabled menu bar'), + children: [ + SubmenuButton(menuChildren: [], child: Text('Disabled File')), + MenuItemButton(child: Text('Disabled Help')), + ], + ), + ), + ], + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/radio.dart b/dev/a11y_assessments/lib/use_cases/radio.dart new file mode 100644 index 0000000000000..bd39862dfee62 --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/radio.dart @@ -0,0 +1,88 @@ +// 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/material.dart'; +import '../utils.dart'; +import 'use_cases.dart'; + +class RadioUseCase extends UseCase { + RadioUseCase(); + + @override + String get name => 'Radio'; + + @override + String get route => '/radio'; + + @override + List get tags => [Tag.batch3, Tag.core]; + + @override + Widget build(BuildContext context) => _MainWidget(); +} + +enum Option { option1, option2, option3 } + +class _MainWidget extends StatefulWidget { + @override + State<_MainWidget> createState() => _MainWidgetState(); +} + +class _MainWidgetState extends State<_MainWidget> { + Option? _value = Option.option1; + + String pageTitle = getUseCaseName(RadioUseCase()); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Semantics(headingLevel: 1, child: Text('$pageTitle Demo'))), + body: Center( + child: RadioGroup